diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 120000 index 00000000000..ff807266877 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1 @@ +../.github/copilot-instructions.md \ No newline at end of file diff --git a/.eslint-ignore b/.eslint-ignore index c65ccc2baac..4736eb5621d 100644 --- a/.eslint-ignore +++ b/.eslint-ignore @@ -33,7 +33,6 @@ **/src/typings/**/*.d.ts **/src/vs/*/**/*.d.ts **/src/vs/base/test/common/filters.perf.data.js -**/src/vs/loader.js **/test/unit/assert.js **/test/automation/out/** **/typings/** diff --git a/.eslint-plugin-local/code-amd-node-module.ts b/.eslint-plugin-local/code-amd-node-module.ts index b622c98a89a..eb6a40c5e30 100644 --- a/.eslint-plugin-local/code-amd-node-module.ts +++ b/.eslint-plugin-local/code-amd-node-module.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; +import { readFileSync } from 'fs'; import { join } from 'path'; -export = new class ApiProviderNaming implements eslint.Rule.RuleModule { +export default new class ApiProviderNaming implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -21,7 +23,8 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { const modules = new Set(); try { - const { dependencies, optionalDependencies } = require(join(__dirname, '../package.json')); + const packageJson = JSON.parse(readFileSync(join(import.meta.dirname, '../package.json'), 'utf-8')); + const { dependencies, optionalDependencies } = packageJson; const all = Object.keys(dependencies).concat(Object.keys(optionalDependencies)); for (const key of all) { modules.add(key); @@ -33,13 +36,13 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { } - const checkImport = (node: any) => { + const checkImport = (node: ESTree.Literal & { parent?: ESTree.Node & { importKind?: string } }) => { - if (node.type !== 'Literal' || typeof node.value !== 'string') { + if (typeof node.value !== 'string') { return; } - if (node.parent.importKind === 'type') { + if (node.parent?.type === 'ImportDeclaration' && node.parent.importKind === 'type') { return; } diff --git a/.eslint-plugin-local/code-declare-service-brand.ts b/.eslint-plugin-local/code-declare-service-brand.ts index 85cf0671545..a077e7b38c6 100644 --- a/.eslint-plugin-local/code-declare-service-brand.ts +++ b/.eslint-plugin-local/code-declare-service-brand.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class DeclareServiceBrand implements eslint.Rule.RuleModule { +export default new class DeclareServiceBrand implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { fixable: 'code', @@ -14,7 +15,7 @@ export = new class DeclareServiceBrand implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['PropertyDefinition[key.name="_serviceBrand"][value]']: (node: any) => { + ['PropertyDefinition[key.name="_serviceBrand"][value]']: (node: ESTree.PropertyDefinition) => { return context.report({ node, message: `The '_serviceBrand'-property should not have a value`, diff --git a/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts b/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts index c657df9bd30..7f1d20482b8 100644 --- a/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts +++ b/.eslint-plugin-local/code-ensure-no-disposables-leak-in-test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; -import { Node } from 'estree'; +import type * as estree from 'estree'; -export = new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rule.RuleModule { +export default new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { type: 'problem', @@ -18,7 +18,7 @@ export = new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rul }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const config = <{ exclude: string[] }>context.options[0]; + const config = context.options[0] as { exclude: string[] }; const needle = context.getFilename().replace(/\\/g, '/'); if (config.exclude.some((e) => needle.endsWith(e))) { @@ -26,7 +26,7 @@ export = new class EnsureNoDisposablesAreLeakedInTestSuite implements eslint.Rul } return { - [`Program > ExpressionStatement > CallExpression[callee.name='suite']`]: (node: Node) => { + [`Program > ExpressionStatement > CallExpression[callee.name='suite']`]: (node: estree.Node) => { const src = context.getSourceCode().getText(node); if (!src.includes('ensureNoDisposablesAreLeakedInTestSuite(')) { context.report({ diff --git a/.eslint-plugin-local/code-import-patterns.ts b/.eslint-plugin-local/code-import-patterns.ts index e4beb9a4738..419e26d41ac 100644 --- a/.eslint-plugin-local/code-import-patterns.ts +++ b/.eslint-plugin-local/code-import-patterns.ts @@ -7,9 +7,9 @@ import * as eslint from 'eslint'; import { TSESTree } from '@typescript-eslint/utils'; import * as path from 'path'; import minimatch from 'minimatch'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -const REPO_ROOT = path.normalize(path.join(__dirname, '../')); +const REPO_ROOT = path.normalize(path.join(import.meta.dirname, '../')); interface ConditionalPattern { when?: 'hasBrowser' | 'hasNode' | 'hasElectron' | 'test'; @@ -31,7 +31,7 @@ interface LayerAllowRule { type RawOption = RawImportPatternsConfig | LayerAllowRule; function isLayerAllowRule(option: RawOption): option is LayerAllowRule { - return !!((option).when && (option).allow); + return !!((option as LayerAllowRule).when && (option as LayerAllowRule).allow); } interface ImportPatternsConfig { @@ -39,7 +39,7 @@ interface ImportPatternsConfig { restrictions: string[]; } -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -55,7 +55,7 @@ export = new class implements eslint.Rule.RuleModule { }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const options = context.options; + const options = context.options as RawOption[]; const configs = this._processOptions(options); const relativeFilename = getRelativeFilename(context); @@ -217,7 +217,7 @@ export = new class implements eslint.Rule.RuleModule { configs.push(testConfig); } } else { - configs.push({ target, restrictions: restrictions.filter(r => typeof r === 'string') }); + configs.push({ target, restrictions: restrictions.filter(r => typeof r === 'string') as string[] }); } } this._optionsCache.set(options, configs); diff --git a/.eslint-plugin-local/code-layering.ts b/.eslint-plugin-local/code-layering.ts index f8b769a1bf6..ac77eb97cf0 100644 --- a/.eslint-plugin-local/code-layering.ts +++ b/.eslint-plugin-local/code-layering.ts @@ -5,14 +5,14 @@ import * as eslint from 'eslint'; import { join, dirname } from 'path'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; type Config = { allowed: Set; disallowed: Set; }; -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -38,8 +38,7 @@ export = new class implements eslint.Rule.RuleModule { const fileDirname = dirname(context.getFilename()); const parts = fileDirname.split(/\\|\//); - const ruleArgs = >context.options[0]; - + const ruleArgs = context.options[0] as Record; let config: Config | undefined; for (let i = parts.length - 1; i >= 0; i--) { if (ruleArgs[parts[i]]) { @@ -91,4 +90,3 @@ export = new class implements eslint.Rule.RuleModule { }); } }; - diff --git a/.eslint-plugin-local/code-limited-top-functions.ts b/.eslint-plugin-local/code-limited-top-functions.ts index 7b48d02a0fe..8c6abacc9d8 100644 --- a/.eslint-plugin-local/code-limited-top-functions.ts +++ b/.eslint-plugin-local/code-limited-top-functions.ts @@ -6,8 +6,9 @@ import * as eslint from 'eslint'; import { dirname, relative } from 'path'; import minimatch from 'minimatch'; +import type * as ESTree from 'estree'; -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -28,11 +29,11 @@ export = new class implements eslint.Rule.RuleModule { }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - let fileRelativePath = relative(dirname(__dirname), context.getFilename()); + let fileRelativePath = relative(dirname(import.meta.dirname), context.getFilename()); if (!fileRelativePath.endsWith('/')) { fileRelativePath += '/'; } - const ruleArgs = >context.options[0]; + const ruleArgs = context.options[0] as Record; const matchingKey = Object.keys(ruleArgs).find(key => fileRelativePath.startsWith(key) || minimatch(fileRelativePath, key)); if (!matchingKey) { @@ -43,8 +44,8 @@ export = new class implements eslint.Rule.RuleModule { const restrictedFunctions = ruleArgs[matchingKey]; return { - FunctionDeclaration: (node: any) => { - const isTopLevel = node.parent.type === 'Program'; + FunctionDeclaration: (node: ESTree.FunctionDeclaration & { parent?: ESTree.Node }) => { + const isTopLevel = node.parent?.type === 'Program'; const functionName = node.id.name; if (isTopLevel && !restrictedFunctions.includes(node.id.name)) { context.report({ @@ -53,10 +54,10 @@ export = new class implements eslint.Rule.RuleModule { }); } }, - ExportNamedDeclaration(node: any) { + ExportNamedDeclaration(node: ESTree.ExportNamedDeclaration & { parent?: ESTree.Node }) { if (node.declaration && node.declaration.type === 'FunctionDeclaration') { const functionName = node.declaration.id.name; - const isTopLevel = node.parent.type === 'Program'; + const isTopLevel = node.parent?.type === 'Program'; if (isTopLevel && !restrictedFunctions.includes(node.declaration.id.name)) { context.report({ node, diff --git a/.eslint-plugin-local/code-must-use-result.ts b/.eslint-plugin-local/code-must-use-result.ts index e249f36dccf..b97396c7e52 100644 --- a/.eslint-plugin-local/code-must-use-result.ts +++ b/.eslint-plugin-local/code-must-use-result.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; const VALID_USES = new Set([ @@ -11,22 +12,22 @@ const VALID_USES = new Set([ TSESTree.AST_NODE_TYPES.VariableDeclarator, ]); -export = new class MustUseResults implements eslint.Rule.RuleModule { +export default new class MustUseResults implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { schema: false }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const config = <{ message: string; functions: string[] }[]>context.options[0]; + const config = context.options[0] as { message: string; functions: string[] }[]; const listener: eslint.Rule.RuleListener = {}; for (const { message, functions } of config) { for (const fn of functions) { const query = `CallExpression[callee.property.name='${fn}'], CallExpression[callee.name='${fn}']`; - listener[query] = (node: any) => { - const cast: TSESTree.CallExpression = node; - if (!VALID_USES.has(cast.parent?.type)) { + listener[query] = (node: ESTree.Node) => { + const callExpression = node as TSESTree.CallExpression; + if (!VALID_USES.has(callExpression.parent?.type)) { context.report({ node, message }); } }; diff --git a/.eslint-plugin-local/code-must-use-super-dispose.ts b/.eslint-plugin-local/code-must-use-super-dispose.ts index ca776d8a2ad..0213d200957 100644 --- a/.eslint-plugin-local/code-must-use-super-dispose.ts +++ b/.eslint-plugin-local/code-must-use-super-dispose.ts @@ -3,18 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class NoAsyncSuite implements eslint.Rule.RuleModule { +export default new class NoAsyncSuite implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function doesCallSuperDispose(node: any) { + function doesCallSuperDispose(node: TSESTree.MethodDefinition) { if (!node.override) { return; } - const body = context.getSourceCode().getText(node); + const body = context.getSourceCode().getText(node as ESTree.Node); if (body.includes('super.dispose')) { return; diff --git a/.eslint-plugin-local/code-no-any-casts.ts b/.eslint-plugin-local/code-no-any-casts.ts index ec1ea1ec3d5..87c3c9466cd 100644 --- a/.eslint-plugin-local/code-no-any-casts.ts +++ b/.eslint-plugin-local/code-no-any-casts.ts @@ -6,7 +6,7 @@ import * as eslint from 'eslint'; import { TSESTree } from '@typescript-eslint/utils'; -export = new class NoAnyCasts implements eslint.Rule.RuleModule { +export default new class NoAnyCasts implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { diff --git a/.eslint-plugin-local/code-no-dangerous-type-assertions.ts b/.eslint-plugin-local/code-no-dangerous-type-assertions.ts index 6c0fa26ca1a..b2e97943670 100644 --- a/.eslint-plugin-local/code-no-dangerous-type-assertions.ts +++ b/.eslint-plugin-local/code-no-dangerous-type-assertions.ts @@ -4,15 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; -export = new class NoDangerousTypeAssertions implements eslint.Rule.RuleModule { +export default new class NoDangerousTypeAssertions implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { // Disallow type assertions on object literals: { ... } or {} as T - ['TSTypeAssertion > ObjectExpression, TSAsExpression > ObjectExpression']: (node: any) => { - const objectNode = node as TSESTree.Node; + ['TSTypeAssertion > ObjectExpression, TSAsExpression > ObjectExpression']: (node: ESTree.ObjectExpression) => { + const objectNode = node as TSESTree.ObjectExpression; const parent = objectNode.parent as TSESTree.TSTypeAssertion | TSESTree.TSAsExpression; if ( diff --git a/.eslint-plugin-local/code-no-deep-import-of-internal.ts b/.eslint-plugin-local/code-no-deep-import-of-internal.ts index 3f54665b49a..cb2d450d2ee 100644 --- a/.eslint-plugin-local/code-no-deep-import-of-internal.ts +++ b/.eslint-plugin-local/code-no-deep-import-of-internal.ts @@ -5,9 +5,9 @@ import * as eslint from 'eslint'; import { join, dirname } from 'path'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -28,8 +28,8 @@ export = new class implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { const patterns = context.options[0] as Record; - const internalModulePattern = Object.entries(patterns).map(([key, v]) => v ? key : undefined).filter(v => !!v); - const allowedPatterns = Object.entries(patterns).map(([key, v]) => !v ? key : undefined).filter(v => !!v); + const internalModulePattern = Object.entries(patterns).map(([key, v]) => v ? key : undefined).filter((v): v is string => !!v); + const allowedPatterns = Object.entries(patterns).map(([key, v]) => !v ? key : undefined).filter((v): v is string => !!v); return createImportRuleListener((node, path) => { const importerModuleDir = dirname(context.filename); diff --git a/.eslint-plugin-local/code-no-global-document-listener.ts b/.eslint-plugin-local/code-no-global-document-listener.ts index 049426a5a03..ad4ec0da820 100644 --- a/.eslint-plugin-local/code-no-global-document-listener.ts +++ b/.eslint-plugin-local/code-no-global-document-listener.ts @@ -5,7 +5,7 @@ import * as eslint from 'eslint'; -export = new class NoGlobalDocumentListener implements eslint.Rule.RuleModule { +export default new class NoGlobalDocumentListener implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { diff --git a/.eslint-plugin-local/code-no-in-operator.ts b/.eslint-plugin-local/code-no-in-operator.ts index dcfb1afc22e..026a8f5fe7a 100644 --- a/.eslint-plugin-local/code-no-in-operator.ts +++ b/.eslint-plugin-local/code-no-in-operator.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; +import { TSESTree } from '@typescript-eslint/utils'; /** * Disallows the use of the `in` operator in TypeScript code, except within @@ -15,7 +17,7 @@ import * as eslint from 'eslint'; * Exception: Type predicate functions are allowed to use the `in` operator * since they are the standard way to perform runtime type checking. */ -export = new class NoInOperator implements eslint.Rule.RuleModule { +export default new class NoInOperator implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -26,9 +28,10 @@ export = new class NoInOperator implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function checkInOperator(inNode: any) { + function checkInOperator(inNode: ESTree.BinaryExpression) { + const node = inNode as TSESTree.BinaryExpression; // Check if we're inside a type predicate function - const ancestors = context.sourceCode.getAncestors(inNode); + const ancestors = context.sourceCode.getAncestors(node as ESTree.Node); for (const ancestor of ancestors) { if (ancestor.type === 'FunctionDeclaration' || @@ -45,7 +48,7 @@ export = new class NoInOperator implements eslint.Rule.RuleModule { } context.report({ - node: inNode, + node, messageId: 'noInOperator' }); } diff --git a/.eslint-plugin-local/code-no-localization-template-literals.ts b/.eslint-plugin-local/code-no-localization-template-literals.ts new file mode 100644 index 00000000000..30a5de7f364 --- /dev/null +++ b/.eslint-plugin-local/code-no-localization-template-literals.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/utils'; + +/** + * Prevents the use of template literals in localization function calls. + * + * vscode.l10n.t() and nls.localize() cannot handle string templating. + * Use placeholders instead: vscode.l10n.t('Message {0}', value) + * + * Examples: + * ❌ vscode.l10n.t(`Message ${value}`) + * ✅ vscode.l10n.t('Message {0}', value) + * + * ❌ nls.localize('key', `Message ${value}`) + * ✅ nls.localize('key', 'Message {0}', value) + */ +export default new class NoLocalizationTemplateLiterals implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + noTemplateLiteral: 'Template literals cannot be used in localization calls. Use placeholders like {0}, {1} instead.' + }, + docs: { + description: 'Prevents template literals in vscode.l10n.t() and nls.localize() calls', + }, + schema: false, + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + function checkCallExpression(node: TSESTree.CallExpression) { + const callee = node.callee; + let isLocalizationCall = false; + let isNlsLocalize = false; + + // Check for vscode.l10n.t() + if (callee.type === 'MemberExpression') { + const object = callee.object; + const property = callee.property; + + // vscode.l10n.t + if (object.type === 'MemberExpression') { + const outerObject = object.object; + const outerProperty = object.property; + if (outerObject.type === 'Identifier' && outerObject.name === 'vscode' && + outerProperty.type === 'Identifier' && outerProperty.name === 'l10n' && + property.type === 'Identifier' && property.name === 't') { + isLocalizationCall = true; + } + } + + // l10n.t or nls.localize or any *.localize + if (object.type === 'Identifier' && property.type === 'Identifier') { + if (object.name === 'l10n' && property.name === 't') { + isLocalizationCall = true; + } else if (property.name === 'localize') { + isLocalizationCall = true; + isNlsLocalize = true; + } + } + } + + if (!isLocalizationCall) { + return; + } + + // For vscode.l10n.t(message, ...args) - check the first argument (message) + // For nls.localize(key, message, ...args) - check first two arguments (key and message) + const argsToCheck = isNlsLocalize ? 2 : 1; + for (let i = 0; i < argsToCheck && i < node.arguments.length; i++) { + const arg = node.arguments[i]; + if (arg && arg.type === 'TemplateLiteral' && arg.expressions.length > 0) { + context.report({ + node: arg, + messageId: 'noTemplateLiteral' + }); + } + } + } + + return { + CallExpression: (node: any) => checkCallExpression(node as TSESTree.CallExpression) + }; + } +}; diff --git a/.eslint-plugin-local/code-no-localized-model-description.ts b/.eslint-plugin-local/code-no-localized-model-description.ts new file mode 100644 index 00000000000..a624aeb8619 --- /dev/null +++ b/.eslint-plugin-local/code-no-localized-model-description.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TSESTree } from '@typescript-eslint/utils'; +import * as eslint from 'eslint'; +import * as visitorKeys from 'eslint-visitor-keys'; +import type * as ESTree from 'estree'; + +const MESSAGE_ID = 'noLocalizedModelDescription'; +type NodeWithChildren = TSESTree.Node & { + [key: string]: TSESTree.Node | TSESTree.Node[] | null | undefined; +}; +type PropertyKeyNode = TSESTree.Property['key'] | TSESTree.MemberExpression['property']; +type AssignmentTarget = TSESTree.AssignmentExpression['left']; + +export default new class NoLocalizedModelDescriptionRule implements eslint.Rule.RuleModule { + meta: eslint.Rule.RuleMetaData = { + messages: { + [MESSAGE_ID]: 'modelDescription values describe behavior to the language model and must not use localized strings.' + }, + type: 'problem', + schema: false + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + const reportIfLocalized = (expression: TSESTree.Expression | null | undefined) => { + if (expression && containsLocalizedCall(expression)) { + context.report({ node: expression, messageId: MESSAGE_ID }); + } + }; + + return { + Property: (node: ESTree.Property) => { + const propertyNode = node as TSESTree.Property; + if (!isModelDescriptionKey(propertyNode.key, propertyNode.computed)) { + return; + } + reportIfLocalized(propertyNode.value as TSESTree.Expression); + }, + AssignmentExpression: (node: ESTree.AssignmentExpression) => { + const assignment = node as TSESTree.AssignmentExpression; + if (!isModelDescriptionAssignmentTarget(assignment.left)) { + return; + } + reportIfLocalized(assignment.right); + } + }; + } +}; + +function isModelDescriptionKey(key: PropertyKeyNode, computed: boolean | undefined): boolean { + if (!computed && key.type === 'Identifier') { + return key.name === 'modelDescription'; + } + if (key.type === 'Literal' && key.value === 'modelDescription') { + return true; + } + return false; +} + +function isModelDescriptionAssignmentTarget(target: AssignmentTarget): target is TSESTree.MemberExpression { + if (target.type === 'MemberExpression') { + return isModelDescriptionKey(target.property, target.computed); + } + return false; +} + +function containsLocalizedCall(expression: TSESTree.Expression): boolean { + let found = false; + + const visit = (node: TSESTree.Node) => { + if (found) { + return; + } + + if (isLocalizeCall(node)) { + found = true; + return; + } + + for (const key of visitorKeys.KEYS[node.type] ?? []) { + const value = (node as NodeWithChildren)[key]; + if (Array.isArray(value)) { + for (const child of value) { + if (child) { + visit(child); + if (found) { + return; + } + } + } + } else if (value) { + visit(value); + } + } + }; + + visit(expression); + return found; +} + +function isLocalizeCall(node: TSESTree.Node): boolean { + if (node.type === 'CallExpression') { + return isLocalizeCallee(node.callee); + } + if (node.type === 'ChainExpression') { + return isLocalizeCall(node.expression); + } + return false; +} + + +function isLocalizeCallee(callee: TSESTree.CallExpression['callee']): boolean { + if (callee.type === 'Identifier') { + return callee.name === 'localize'; + } + if (callee.type === 'MemberExpression') { + if (!callee.computed && callee.property.type === 'Identifier') { + return callee.property.name === 'localize'; + } + if (callee.property.type === 'Literal' && callee.property.value === 'localize') { + return true; + } + } + return false; +} diff --git a/.eslint-plugin-local/code-no-native-private.ts b/.eslint-plugin-local/code-no-native-private.ts index e2d20694ca8..5d945ec34f7 100644 --- a/.eslint-plugin-local/code-no-native-private.ts +++ b/.eslint-plugin-local/code-no-native-private.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class ApiProviderNaming implements eslint.Rule.RuleModule { +export default new class ApiProviderNaming implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -17,13 +18,13 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['PropertyDefinition PrivateIdentifier']: (node: any) => { + ['PropertyDefinition PrivateIdentifier']: (node: ESTree.Node) => { context.report({ node, messageId: 'slow' }); }, - ['MethodDefinition PrivateIdentifier']: (node: any) => { + ['MethodDefinition PrivateIdentifier']: (node: ESTree.Node) => { context.report({ node, messageId: 'slow' diff --git a/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts b/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts index c0d60985604..2b3896795a8 100644 --- a/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts +++ b/.eslint-plugin-local/code-no-nls-in-standalone-editor.ts @@ -5,9 +5,9 @@ import * as eslint from 'eslint'; import { join } from 'path'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { +export default new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { 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 index 2fa8e0bd9b5..94d3a1b4ead 100644 --- a/.eslint-plugin-local/code-no-observable-get-in-reactive-context.ts +++ b/.eslint-plugin-local/code-no-observable-get-in-reactive-context.ts @@ -6,9 +6,9 @@ import { TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; import * as visitorKeys from 'eslint-visitor-keys'; -import * as ESTree from 'estree'; +import type * as ESTree from 'estree'; -export = new class NoObservableGetInReactiveContext implements eslint.Rule.RuleModule { +export default new class NoObservableGetInReactiveContext implements eslint.Rule.RuleModule { meta: eslint.Rule.RuleMetaData = { type: 'problem', docs: { @@ -19,7 +19,7 @@ export = new class NoObservableGetInReactiveContext implements eslint.Rule.RuleM create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - 'CallExpression': (node: any) => { + 'CallExpression': (node: ESTree.CallExpression) => { const callExpression = node as TSESTree.CallExpression; if (!isReactiveFunctionWithReader(callExpression.callee)) { diff --git a/.eslint-plugin-local/code-no-potentially-unsafe-disposables.ts b/.eslint-plugin-local/code-no-potentially-unsafe-disposables.ts index 077ad081901..bc250df1182 100644 --- a/.eslint-plugin-local/code-no-potentially-unsafe-disposables.ts +++ b/.eslint-plugin-local/code-no-potentially-unsafe-disposables.ts @@ -4,23 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; /** * Checks for potentially unsafe usage of `DisposableStore` / `MutableDisposable`. * * These have been the source of leaks in the past. */ -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function checkVariableDeclaration(inNode: any) { + function checkVariableDeclaration(inNode: ESTree.Node) { context.report({ node: inNode, message: `Use const for 'DisposableStore' to avoid leaks by accidental reassignment.` }); } - function checkProperty(inNode: any) { + function checkProperty(inNode: ESTree.Node) { context.report({ node: inNode, message: `Use readonly for DisposableStore/MutableDisposable to avoid leaks through accidental reassignment.` diff --git a/.eslint-plugin-local/code-no-reader-after-await.ts b/.eslint-plugin-local/code-no-reader-after-await.ts index 545e1fd050a..6d0e8d39b06 100644 --- a/.eslint-plugin-local/code-no-reader-after-await.ts +++ b/.eslint-plugin-local/code-no-reader-after-await.ts @@ -5,11 +5,12 @@ import { TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class NoReaderAfterAwait implements eslint.Rule.RuleModule { +export default new class NoReaderAfterAwait implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - 'CallExpression': (node: any) => { + 'CallExpression': (node: ESTree.CallExpression) => { const callExpression = node as TSESTree.CallExpression; if (!isFunctionWithReader(callExpression.callee)) { diff --git a/.eslint-plugin-local/code-no-runtime-import.ts b/.eslint-plugin-local/code-no-runtime-import.ts index afebe0b0d68..2c53d84f973 100644 --- a/.eslint-plugin-local/code-no-runtime-import.ts +++ b/.eslint-plugin-local/code-no-runtime-import.ts @@ -7,9 +7,9 @@ import { TSESTree } from '@typescript-eslint/typescript-estree'; import * as eslint from 'eslint'; import { dirname, join, relative } from 'path'; import minimatch from 'minimatch'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -30,11 +30,11 @@ export = new class implements eslint.Rule.RuleModule { }; create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - let fileRelativePath = relative(dirname(__dirname), context.getFilename()); + let fileRelativePath = relative(dirname(import.meta.dirname), context.getFilename()); if (!fileRelativePath.endsWith('/')) { fileRelativePath += '/'; } - const ruleArgs = >context.options[0]; + const ruleArgs = context.options[0] as Record; const matchingKey = Object.keys(ruleArgs).find(key => fileRelativePath.startsWith(key) || minimatch(fileRelativePath, key)); if (!matchingKey) { diff --git a/.eslint-plugin-local/code-no-standalone-editor.ts b/.eslint-plugin-local/code-no-standalone-editor.ts index 36bf48b1417..dca4e22bfb0 100644 --- a/.eslint-plugin-local/code-no-standalone-editor.ts +++ b/.eslint-plugin-local/code-no-standalone-editor.ts @@ -5,9 +5,9 @@ import * as eslint from 'eslint'; import { join } from 'path'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -export = new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { +export default new class NoNlsInStandaloneEditorRule implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { diff --git a/.eslint-plugin-local/code-no-static-self-ref.ts b/.eslint-plugin-local/code-no-static-self-ref.ts index 94287b8311c..9a47f87b9c1 100644 --- a/.eslint-plugin-local/code-no-static-self-ref.ts +++ b/.eslint-plugin-local/code-no-static-self-ref.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; -import * as ESTree from 'estree'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; /** * WORKAROUND for https://github.com/evanw/esbuild/issues/3823 */ -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function checkProperty(inNode: any) { + function checkProperty(inNode: TSESTree.PropertyDefinition) { - const classDeclaration = context.sourceCode.getAncestors(inNode).find(node => node.type === 'ClassDeclaration'); - const propertyDefinition = inNode; + const classDeclaration = context.sourceCode.getAncestors(inNode as ESTree.Node).find(node => node.type === 'ClassDeclaration'); + const propertyDefinition = inNode; if (!classDeclaration || !classDeclaration.id?.name) { return; diff --git a/.eslint-plugin-local/code-no-test-async-suite.ts b/.eslint-plugin-local/code-no-test-async-suite.ts index 60a0f2153ab..b53747390b0 100644 --- a/.eslint-plugin-local/code-no-test-async-suite.ts +++ b/.eslint-plugin-local/code-no-test-async-suite.ts @@ -3,8 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; +import { TSESTree } from '@typescript-eslint/utils'; function isCallExpression(node: TSESTree.Node): node is TSESTree.CallExpression { return node.type === 'CallExpression'; @@ -14,13 +15,14 @@ function isFunctionExpression(node: TSESTree.Node): node is TSESTree.FunctionExp return node.type.includes('FunctionExpression'); } -export = new class NoAsyncSuite implements eslint.Rule.RuleModule { +export default new class NoAsyncSuite implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function hasAsyncSuite(node: any) { - if (isCallExpression(node) && node.arguments.length >= 2 && isFunctionExpression(node.arguments[1]) && node.arguments[1].async) { + function hasAsyncSuite(node: ESTree.Node) { + const tsNode = node as TSESTree.Node; + if (isCallExpression(tsNode) && tsNode.arguments.length >= 2 && isFunctionExpression(tsNode.arguments[1]) && tsNode.arguments[1].async) { return context.report({ - node: node, + node: tsNode, message: 'suite factory function should never be async' }); } diff --git a/.eslint-plugin-local/code-no-test-only.ts b/.eslint-plugin-local/code-no-test-only.ts index d4751eef2ee..389d32fe13b 100644 --- a/.eslint-plugin-local/code-no-test-only.ts +++ b/.eslint-plugin-local/code-no-test-only.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class NoTestOnly implements eslint.Rule.RuleModule { +export default new class NoTestOnly implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['MemberExpression[object.name=/^(test|suite)$/][property.name="only"]']: (node: any) => { + ['MemberExpression[object.name=/^(test|suite)$/][property.name="only"]']: (node: ESTree.MemberExpression) => { return context.report({ node, message: 'only is a dev-time tool and CANNOT be pushed' diff --git a/.eslint-plugin-local/code-no-unexternalized-strings.ts b/.eslint-plugin-local/code-no-unexternalized-strings.ts index 7cf7b2f38ee..a7065cb2a0d 100644 --- a/.eslint-plugin-local/code-no-unexternalized-strings.ts +++ b/.eslint-plugin-local/code-no-unexternalized-strings.ts @@ -5,7 +5,7 @@ import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; -import * as ESTree from 'estree'; +import type * as ESTree from 'estree'; function isStringLiteral(node: TSESTree.Node | ESTree.Node | null | undefined): node is TSESTree.StringLiteral { return !!node && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'; @@ -24,7 +24,7 @@ function isDoubleQuoted(node: TSESTree.StringLiteral): boolean { const enableDoubleToSingleQuoteFixes = false; -export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { +export default new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { private static _rNlsKeys = /^[_a-zA-Z0-9][ .\-_a-zA-Z0-9]*$/; @@ -100,9 +100,7 @@ export = new class NoUnexternalizedStrings implements eslint.Rule.RuleModule { function visitL10NCall(node: TSESTree.CallExpression) { // localize(key, message) - const [messageNode] = (node).arguments; - - // remove message-argument from doubleQuoted list and make + const [messageNode] = (node as TSESTree.CallExpression).arguments; // remove message-argument from doubleQuoted list and make // sure it is a string-literal if (isStringLiteral(messageNode)) { doubleQuotedStringLiterals.delete(messageNode); diff --git a/.eslint-plugin-local/code-no-unused-expressions.ts b/.eslint-plugin-local/code-no-unused-expressions.ts index bd632884dbd..c481313a9a2 100644 --- a/.eslint-plugin-local/code-no-unused-expressions.ts +++ b/.eslint-plugin-local/code-no-unused-expressions.ts @@ -11,15 +11,15 @@ * @author Michael Ficarra */ -import * as eslint from 'eslint'; import { TSESTree } from '@typescript-eslint/utils'; -import * as ESTree from 'estree'; +import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -module.exports = { +export default { meta: { type: 'suggestion', @@ -58,7 +58,7 @@ module.exports = { allowTernary = config.allowTernary || false, allowTaggedTemplates = config.allowTaggedTemplates || false; - + /** * @param node any node * @returns whether the given node structurally represents a directive @@ -68,7 +68,7 @@ module.exports = { node.expression.type === 'Literal' && typeof node.expression.value === 'string'; } - + /** * @param predicate ([a] -> Boolean) the function used to make the determination * @param list the input list @@ -83,7 +83,7 @@ module.exports = { return list.slice(); } - + /** * @param node a Program or BlockStatement node * @returns the leading sequence of directive nodes in the given node's body @@ -92,7 +92,7 @@ module.exports = { return takeWhile(looksLikeDirective, node.body); } - + /** * @param node any node * @param ancestors the given node's ancestors @@ -141,8 +141,8 @@ module.exports = { return { ExpressionStatement(node: TSESTree.ExpressionStatement) { - if (!isValidExpression(node.expression) && !isDirective(node, context.sourceCode.getAncestors(node))) { - context.report({ node: node, message: `Expected an assignment or function call and instead saw an expression. ${node.expression}` }); + if (!isValidExpression(node.expression) && !isDirective(node, context.sourceCode.getAncestors(node as ESTree.Node) as TSESTree.Node[])) { + context.report({ node: node as ESTree.Node, message: `Expected an assignment or function call and instead saw an expression. ${node.expression}` }); } } }; diff --git a/.eslint-plugin-local/code-parameter-properties-must-have-explicit-accessibility.ts b/.eslint-plugin-local/code-parameter-properties-must-have-explicit-accessibility.ts index c9837052fa5..f00d3e1435c 100644 --- a/.eslint-plugin-local/code-parameter-properties-must-have-explicit-accessibility.ts +++ b/.eslint-plugin-local/code-parameter-properties-must-have-explicit-accessibility.ts @@ -3,19 +3,18 @@ * 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 eslint from 'eslint'; /** * Enforces that all parameter properties have an explicit access modifier (public, protected, private). * * This catches a common bug where a service is accidentally made public by simply writing: `readonly prop: Foo` */ -export = new class implements eslint.Rule.RuleModule { +export default new class implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function check(inNode: any) { - const node: TSESTree.TSParameterProperty = inNode; + function check(node: TSESTree.TSParameterProperty) { // For now, only apply to injected services const firstDecorator = node.decorators?.at(0); @@ -28,7 +27,7 @@ export = new class implements eslint.Rule.RuleModule { if (!node.accessibility) { context.report({ - node: inNode, + node: node, message: 'Parameter properties must have an explicit access modifier.' }); } diff --git a/.eslint-plugin-local/code-policy-localization-key-match.ts b/.eslint-plugin-local/code-policy-localization-key-match.ts index 6cfc7cbfbc7..10749d5bb00 100644 --- a/.eslint-plugin-local/code-policy-localization-key-match.ts +++ b/.eslint-plugin-local/code-policy-localization-key-match.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; /** * Ensures that localization keys in policy blocks match the keys used in nls.localize() calls. @@ -21,7 +22,7 @@ import * as eslint from 'eslint'; * The key property ('autoApprove2.description') must match the first argument * to nls.localize() ('autoApprove2.description'). */ -export = new class PolicyLocalizationKeyMatch implements eslint.Rule.RuleModule { +export default new class PolicyLocalizationKeyMatch implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -35,11 +36,11 @@ export = new class PolicyLocalizationKeyMatch implements eslint.Rule.RuleModule create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - function checkLocalizationObject(node: any) { + function checkLocalizationObject(node: ESTree.ObjectExpression) { // Look for objects with structure: { key: '...', value: nls.localize('...', '...') } - let keyProperty: any; - let valueProperty: any; + let keyProperty: ESTree.Property | undefined; + let valueProperty: ESTree.Property | undefined; for (const property of node.properties) { if (property.type !== 'Property') { @@ -113,7 +114,7 @@ export = new class PolicyLocalizationKeyMatch implements eslint.Rule.RuleModule } } - function isInPolicyBlock(node: any): boolean { + function isInPolicyBlock(node: ESTree.Node): boolean { // Walk up the AST to see if we're inside a policy object const ancestors = context.sourceCode.getAncestors(node); @@ -131,7 +132,7 @@ export = new class PolicyLocalizationKeyMatch implements eslint.Rule.RuleModule } return { - 'ObjectExpression': (node: any) => { + 'ObjectExpression': (node: ESTree.ObjectExpression) => { // Only check objects inside policy blocks if (!isInPolicyBlock(node)) { return; diff --git a/.eslint-plugin-local/code-translation-remind.ts b/.eslint-plugin-local/code-translation-remind.ts index cceaba4c419..42032321167 100644 --- a/.eslint-plugin-local/code-translation-remind.ts +++ b/.eslint-plugin-local/code-translation-remind.ts @@ -6,10 +6,10 @@ import * as eslint from 'eslint'; import { TSESTree } from '@typescript-eslint/utils'; import { readFileSync } from 'fs'; -import { createImportRuleListener } from './utils'; +import { createImportRuleListener } from './utils.ts'; -export = new class TranslationRemind implements eslint.Rule.RuleModule { +export default new class TranslationRemind implements eslint.Rule.RuleModule { private static NLS_MODULE = 'vs/nls'; diff --git a/.eslint-plugin-local/index.js b/.eslint-plugin-local/index.js deleted file mode 100644 index ad00191fb6f..00000000000 --- a/.eslint-plugin-local/index.js +++ /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. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -const glob = require('glob'); -const path = require('path'); - -require('ts-node').register({ - experimentalResolver: true, - transpileOnly: true, - compilerOptions: { - module: 'nodenext', - moduleResolution: 'nodenext', - } -}); - -// Re-export all .ts files as rules -/** @type {Record} */ -const rules = {}; -glob.sync(`${__dirname}/*.ts`).forEach((file) => { - rules[path.basename(file, '.ts')] = require(file); -}); - -exports.rules = rules; diff --git a/.eslint-plugin-local/index.ts b/.eslint-plugin-local/index.ts new file mode 100644 index 00000000000..101733773f0 --- /dev/null +++ b/.eslint-plugin-local/index.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import type { LooseRuleDefinition } from '@typescript-eslint/utils/ts-eslint'; +import glob from 'glob'; +import { createRequire } from 'module'; +import path from 'path'; + +const require = createRequire(import.meta.url); + +// Re-export all .ts files as rules +const rules: Record = {}; +glob.sync(`${import.meta.dirname}/*.ts`) + .filter(file => !file.endsWith('index.ts') && !file.endsWith('utils.ts')) + .map(file => { + rules[path.basename(file, '.ts')] = require(file).default; + }); + +export { rules }; diff --git a/.eslint-plugin-local/package.json b/.eslint-plugin-local/package.json index a0df0c86778..90e7facf0a0 100644 --- a/.eslint-plugin-local/package.json +++ b/.eslint-plugin-local/package.json @@ -1,3 +1,7 @@ { - "type": "commonjs" + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsgo -p tsconfig.json --noEmit" + } } diff --git a/.eslint-plugin-local/tsconfig.json b/.eslint-plugin-local/tsconfig.json index 7676f59a781..0de6dacc146 100644 --- a/.eslint-plugin-local/tsconfig.json +++ b/.eslint-plugin-local/tsconfig.json @@ -4,23 +4,26 @@ "lib": [ "ES2024" ], - "module": "commonjs", - "esModuleInterop": true, - "alwaysStrict": true, - "allowJs": true, + "rootDir": ".", + "module": "esnext", + "allowImportingTsExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "noEmit": true, "strict": true, "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "noUnusedLocals": true, "noUnusedParameters": true, - "newLine": "lf", - "noEmit": true + "typeRoots": [ + "." + ] }, "include": [ - "**/*.ts", - "**/*.js" + "./**/*.ts", ], "exclude": [ - "node_modules/**" + "node_modules/**", + "./tests/**" ] } diff --git a/.eslint-plugin-local/utils.ts b/.eslint-plugin-local/utils.ts index b7457884f85..e956e679148 100644 --- a/.eslint-plugin-local/utils.ts +++ b/.eslint-plugin-local/utils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; export function createImportRuleListener(validateImport: (node: TSESTree.Literal, value: string) => any): eslint.Rule.RuleListener { @@ -16,24 +17,24 @@ export function createImportRuleListener(validateImport: (node: TSESTree.Literal return { // import ??? from 'module' - ImportDeclaration: (node: any) => { - _checkImport((node).source); + ImportDeclaration: (node: ESTree.ImportDeclaration) => { + _checkImport((node as TSESTree.ImportDeclaration).source); }, // import('module').then(...) OR await import('module') - ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: any) => { + ['CallExpression[callee.type="Import"][arguments.length=1] > Literal']: (node: TSESTree.Literal) => { _checkImport(node); }, // import foo = ... - ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: any) => { + ['TSImportEqualsDeclaration > TSExternalModuleReference > Literal']: (node: TSESTree.Literal) => { _checkImport(node); }, // export ?? from 'module' - ExportAllDeclaration: (node: any) => { - _checkImport((node).source); + ExportAllDeclaration: (node: ESTree.ExportAllDeclaration) => { + _checkImport((node as TSESTree.ExportAllDeclaration).source); }, // export {foo} from 'module' - ExportNamedDeclaration: (node: any) => { - _checkImport((node).source); + ExportNamedDeclaration: (node: ESTree.ExportNamedDeclaration) => { + _checkImport((node as TSESTree.ExportNamedDeclaration).source); }, }; diff --git a/.eslint-plugin-local/vscode-dts-cancellation.ts b/.eslint-plugin-local/vscode-dts-cancellation.ts index 5e8e875af21..dd5ca293727 100644 --- a/.eslint-plugin-local/vscode-dts-cancellation.ts +++ b/.eslint-plugin-local/vscode-dts-cancellation.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as eslint from 'eslint'; import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; +import * as eslint from 'eslint'; -export = new class ApiProviderNaming implements eslint.Rule.RuleModule { +export default new class ApiProviderNaming implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -18,10 +18,10 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature[key.name=/^(provide|resolve).+/]']: (node: any) => { + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature[key.name=/^(provide|resolve).+/]']: (node: TSESTree.Node) => { let found = false; - for (const param of (node).params) { + for (const param of (node as TSESTree.TSMethodSignature).params) { if (param.type === AST_NODE_TYPES.Identifier) { found = found || param.name === 'token'; } diff --git a/.eslint-plugin-local/vscode-dts-create-func.ts b/.eslint-plugin-local/vscode-dts-create-func.ts index 01db244ce76..91589f91584 100644 --- a/.eslint-plugin-local/vscode-dts-create-func.ts +++ b/.eslint-plugin-local/vscode-dts-create-func.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; -export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { +export default new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' }, @@ -17,9 +18,9 @@ export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSDeclareFunction Identifier[name=/create.*/]']: (node: any) => { + ['TSDeclareFunction Identifier[name=/create.*/]']: (node: ESTree.Node) => { - const decl = (node).parent; + const decl = (node as TSESTree.Identifier).parent as TSESTree.FunctionDeclaration; if (decl.returnType?.typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) { return; diff --git a/.eslint-plugin-local/vscode-dts-event-naming.ts b/.eslint-plugin-local/vscode-dts-event-naming.ts index c27d934f4f9..6f75c50ca12 100644 --- a/.eslint-plugin-local/vscode-dts-event-naming.ts +++ b/.eslint-plugin-local/vscode-dts-event-naming.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/utils'; -export = new class ApiEventNaming implements eslint.Rule.RuleModule { +export default new class ApiEventNaming implements eslint.Rule.RuleModule { private static _nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/; @@ -25,14 +26,14 @@ export = new class ApiEventNaming implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const config = <{ allowed: string[]; verbs: string[] }>context.options[0]; + const config = context.options[0] as { allowed: string[]; verbs: string[] }; const allowed = new Set(config.allowed); const verbs = new Set(config.verbs); return { - ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => { + ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: ESTree.Identifier) => { - const def = (node).parent?.parent?.parent; + const def = (node as TSESTree.Identifier).parent?.parent?.parent; const ident = this.getIdent(def); if (!ident) { diff --git a/.eslint-plugin-local/vscode-dts-interface-naming.ts b/.eslint-plugin-local/vscode-dts-interface-naming.ts index 6b33f9c5343..d6591b97d8d 100644 --- a/.eslint-plugin-local/vscode-dts-interface-naming.ts +++ b/.eslint-plugin-local/vscode-dts-interface-naming.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; -export = new class ApiInterfaceNaming implements eslint.Rule.RuleModule { +export default new class ApiInterfaceNaming implements eslint.Rule.RuleModule { private static _nameRegExp = /^I[A-Z]/; @@ -20,9 +21,9 @@ export = new class ApiInterfaceNaming implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSInterfaceDeclaration Identifier']: (node: any) => { + ['TSInterfaceDeclaration Identifier']: (node: ESTree.Identifier) => { - const name = (node).name; + const name = (node as TSESTree.Identifier).name; if (ApiInterfaceNaming._nameRegExp.test(name)) { context.report({ node, diff --git a/.eslint-plugin-local/vscode-dts-literal-or-types.ts b/.eslint-plugin-local/vscode-dts-literal-or-types.ts index 44ef0fd2a7c..0815720cf92 100644 --- a/.eslint-plugin-local/vscode-dts-literal-or-types.ts +++ b/.eslint-plugin-local/vscode-dts-literal-or-types.ts @@ -3,10 +3,10 @@ * 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 eslint from 'eslint'; -export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { +export default new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' }, @@ -16,8 +16,8 @@ export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSTypeAnnotation TSUnionType']: (node: any) => { - if ((node).types.every(value => value.type === 'TSLiteralType')) { + ['TSTypeAnnotation TSUnionType']: (node: TSESTree.TSUnionType) => { + if (node.types.every(value => value.type === 'TSLiteralType')) { context.report({ node: node, messageId: 'useEnum' diff --git a/.eslint-plugin-local/vscode-dts-provider-naming.ts b/.eslint-plugin-local/vscode-dts-provider-naming.ts index 90409bfe058..64e0101a71e 100644 --- a/.eslint-plugin-local/vscode-dts-provider-naming.ts +++ b/.eslint-plugin-local/vscode-dts-provider-naming.ts @@ -3,10 +3,10 @@ * 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 eslint from 'eslint'; -export = new class ApiProviderNaming implements eslint.Rule.RuleModule { +export default new class ApiProviderNaming implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -19,18 +19,18 @@ export = new class ApiProviderNaming implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { - const config = <{ allowed: string[] }>context.options[0]; + const config = context.options[0] as { allowed: string[] }; const allowed = new Set(config.allowed); return { - ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node: any) => { - const interfaceName = ((node).parent?.parent).id.name; + ['TSInterfaceDeclaration[id.name=/.+Provider/] TSMethodSignature']: (node: TSESTree.Node) => { + const interfaceName = ((node as TSESTree.Identifier).parent?.parent as TSESTree.TSInterfaceDeclaration).id.name; if (allowed.has(interfaceName)) { // allowed return; } - const methodName = ((node).key as TSESTree.Identifier).name; + const methodName = ((node as TSESTree.TSMethodSignatureNonComputedName).key as TSESTree.Identifier).name; if (!ApiProviderNaming._providerFunctionNames.test(methodName)) { context.report({ diff --git a/.eslint-plugin-local/vscode-dts-string-type-literals.ts b/.eslint-plugin-local/vscode-dts-string-type-literals.ts index 0f6d711a3db..ee70d663281 100644 --- a/.eslint-plugin-local/vscode-dts-string-type-literals.ts +++ b/.eslint-plugin-local/vscode-dts-string-type-literals.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; import { TSESTree } from '@typescript-eslint/utils'; -export = new class ApiTypeDiscrimination implements eslint.Rule.RuleModule { +export default new class ApiTypeDiscrimination implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines' }, @@ -18,8 +19,8 @@ export = new class ApiTypeDiscrimination implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSPropertySignature[optional=false] TSTypeAnnotation TSLiteralType Literal']: (node: any) => { - const raw = String((node).raw); + ['TSPropertySignature[optional=false] TSTypeAnnotation TSLiteralType Literal']: (node: ESTree.Literal) => { + const raw = String((node as TSESTree.Literal).raw); if (/^('|").*\1$/.test(raw)) { diff --git a/.eslint-plugin-local/vscode-dts-use-export.ts b/.eslint-plugin-local/vscode-dts-use-export.ts index 904feaeec36..798572d4f21 100644 --- a/.eslint-plugin-local/vscode-dts-use-export.ts +++ b/.eslint-plugin-local/vscode-dts-use-export.ts @@ -5,8 +5,9 @@ import { TSESTree } from '@typescript-eslint/utils'; import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class VscodeDtsUseExport implements eslint.Rule.RuleModule { +export default new class VscodeDtsUseExport implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -17,8 +18,8 @@ export = new class VscodeDtsUseExport implements eslint.Rule.RuleModule { create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { return { - ['TSModuleDeclaration :matches(TSInterfaceDeclaration, ClassDeclaration, VariableDeclaration, TSEnumDeclaration, TSTypeAliasDeclaration)']: (node: any) => { - const parent = (node).parent; + ['TSModuleDeclaration :matches(TSInterfaceDeclaration, ClassDeclaration, VariableDeclaration, TSEnumDeclaration, TSTypeAliasDeclaration)']: (node: ESTree.Node) => { + const parent = (node as TSESTree.Node).parent; if (parent && parent.type !== TSESTree.AST_NODE_TYPES.ExportNamedDeclaration) { context.report({ node, diff --git a/.eslint-plugin-local/vscode-dts-use-thenable.ts b/.eslint-plugin-local/vscode-dts-use-thenable.ts index 683394ad115..2c1ff4c9296 100644 --- a/.eslint-plugin-local/vscode-dts-use-thenable.ts +++ b/.eslint-plugin-local/vscode-dts-use-thenable.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class ApiEventNaming implements eslint.Rule.RuleModule { +export default new class ApiEventNaming implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -19,7 +20,7 @@ export = new class ApiEventNaming implements eslint.Rule.RuleModule { return { - ['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node: any) => { + ['TSTypeAnnotation TSTypeReference Identifier[name="Promise"]']: (node: ESTree.Identifier) => { context.report({ node, diff --git a/.eslint-plugin-local/vscode-dts-vscode-in-comments.ts b/.eslint-plugin-local/vscode-dts-vscode-in-comments.ts index 33fd44d8af6..ab3c338096c 100644 --- a/.eslint-plugin-local/vscode-dts-vscode-in-comments.ts +++ b/.eslint-plugin-local/vscode-dts-vscode-in-comments.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as eslint from 'eslint'; +import type * as ESTree from 'estree'; -export = new class ApiVsCodeInComments implements eslint.Rule.RuleModule { +export default new class ApiVsCodeInComments implements eslint.Rule.RuleModule { readonly meta: eslint.Rule.RuleMetaData = { messages: { @@ -19,7 +20,7 @@ export = new class ApiVsCodeInComments implements eslint.Rule.RuleModule { const sourceCode = context.getSourceCode(); return { - ['Program']: (_node: any) => { + ['Program']: (_node: ESTree.Program) => { for (const comment of sourceCode.getAllComments()) { if (comment.type !== 'Block') { diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 4bc73cb365c..dc4ef34cf21 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -5,8 +5,8 @@ src/vs/base/common/glob.ts @bpasero src/vs/base/common/oauth.ts @TylerLeonhardt src/vs/base/common/path.ts @bpasero src/vs/base/common/stream.ts @bpasero +src/vs/base/common/uri.ts @jrieken src/vs/base/browser/domSanitize.ts @mjbvz -src/vs/base/browser/** @bpasero src/vs/base/node/pfs.ts @bpasero src/vs/base/node/unc.ts @bpasero src/vs/base/parts/contextmenu/** @bpasero @@ -40,24 +40,23 @@ src/vs/platform/secrets/** @TylerLeonhardt src/vs/platform/sharedProcess/** @bpasero src/vs/platform/state/** @bpasero src/vs/platform/storage/** @bpasero +src/vs/platform/terminal/electron-main/** @Tyriar +src/vs/platform/terminal/node/** @Tyriar src/vs/platform/utilityProcess/** @bpasero src/vs/platform/window/** @bpasero src/vs/platform/windows/** @bpasero src/vs/platform/workspace/** @bpasero src/vs/platform/workspaces/** @bpasero +src/vs/platform/actions/common/menuService.ts @jrieken +src/vs/platform/instantiation/** @jrieken + +# Editor Core +src/vs/editor/contrib/snippet/** @jrieken +src/vs/editor/contrib/suggest/** @jrieken +src/vs/editor/contrib/format/** @jrieken # Bootstrap -src/bootstrap-cli.ts @bpasero -src/bootstrap-esm.ts @bpasero -src/bootstrap-fork.ts @bpasero -src/bootstrap-import.ts @bpasero -src/bootstrap-meta.ts @bpasero -src/bootstrap-node.ts @bpasero -src/bootstrap-server.ts @bpasero -src/cli.ts @bpasero -src/main.ts @bpasero -src/server-cli.ts @bpasero -src/server-main.ts @bpasero +src/*.ts @bpasero # Electron Main src/vs/code/** @bpasero @deepak1556 @@ -101,14 +100,15 @@ src/vs/workbench/electron-browser/** @bpasero src/vs/workbench/contrib/authentication/** @TylerLeonhardt src/vs/workbench/contrib/files/** @bpasero src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @roblourens -src/vs/workbench/contrib/chat/browser/chatSetup.ts @bpasero -src/vs/workbench/contrib/chat/browser/chatStatus.ts @bpasero -src/vs/workbench/contrib/chat/browser/chatInputPart.ts @bpasero -src/vs/workbench/contrib/chat/browser/chatWidget.ts @bpasero +src/vs/workbench/contrib/chat/browser/chatSetup/** @bpasero +src/vs/workbench/contrib/chat/browser/chatStatus/** @bpasero +src/vs/workbench/contrib/chat/browser/chatViewPane.ts @bpasero +src/vs/workbench/contrib/chat/browser/media/chatViewPane.css @bpasero +src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts @bpasero +src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/media/chatUsageWidget.css @bpasero src/vs/workbench/contrib/chat/browser/agentSessions/** @bpasero -src/vs/workbench/contrib/chat/browser/chatSessions/** @bpasero src/vs/workbench/contrib/localization/** @TylerLeonhardt src/vs/workbench/contrib/quickaccess/browser/commandsQuickAccess.ts @TylerLeonhardt src/vs/workbench/contrib/scm/** @lszomoru @@ -141,5 +141,6 @@ extensions/git/** @lszomoru extensions/git-base/** @lszomoru extensions/github/** @lszomoru -# Chat Editing +# Chat Editing, Inline Chat src/vs/workbench/contrib/chat/browser/chatEditing/** @jrieken +src/vs/workbench/contrib/inlineChat/** @jrieken diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dd38717a038..0b3388f9aeb 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,6 +10,9 @@ .github/workflows/pr.yml @lszomoru @joaomoreno @TylerLeonhardt @rzhao271 .github/workflows/telemetry.yml @lramos15 @lszomoru @joaomoreno +# Ensure those that manage generated policy are aware of changes +build/lib/policies/policyData.jsonc @joshspicer @rebornix @joaomoreno @pwang347 @sandy081 + # VS Code API # Ensure the API team is aware of changes to the vscode-dts file # this is only about the final API, not about proposed API changes diff --git a/.github/agents/data.md b/.github/agents/data.md new file mode 100644 index 00000000000..5809fb06d86 --- /dev/null +++ b/.github/agents/data.md @@ -0,0 +1,43 @@ +--- +name: Data +description: Answer telemetry questions with data queries using Kusto Query Language (KQL) +tools: + ['vscode/extensions', 'execute/runInTerminal', 'read/readFile', 'search', 'web/githubRepo', 'azure-mcp/kusto_query', 'todo'] +--- + +# Role and Objective + +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). + +# Workflow + +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 parameters or queries), stop and inform the user. + +# Kusto Best Practices + +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. + +# Output Format + +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/agents/demonstrate.md b/.github/agents/demonstrate.md index 1a59be8b211..7b2e66cda93 100644 --- a/.github/agents/demonstrate.md +++ b/.github/agents/demonstrate.md @@ -2,7 +2,28 @@ name: Demonstrate description: Agent for demonstrating VS Code features target: github-copilot -tools: ['edit', 'search', 'vscode-playwright-mcp/*', 'github/github-mcp-server/*', 'usages', 'fetch', 'githubRepo', 'todos'] +tools: +- "view" +- "create" +- "edit" +- "glob" +- "grep" +- "bash" +- "read_bash" +- "write_bash" +- "stop_bash" +- "list_bash" +- "report_intent" +- "fetch_documentation" +- "agents" +- "read" +- "search" +- "todo" +- "web" +- "github-mcp-server/*" +- "GitHub/*" +- "github/*" +- "vscode-playwright-mcp/*" --- # Role and Objective diff --git a/.github/classifier.json b/.github/classifier.json index 506dadb4ce0..32b68800113 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -113,7 +113,7 @@ "interactive-window": {"assign": ["amunger", "rebornix"]}, "ipc": {"assign": ["joaomoreno"]}, "issue-bot": {"assign": ["chrmarti"]}, - "issue-reporter": {"assign": ["justschen"]}, + "issue-reporter": {"assign": ["yoyokrazy"]}, "javascript": {"assign": ["mjbvz"]}, "json": {"assign": ["aeschli"]}, "json-sorting": {"assign": ["aiday-mar"]}, @@ -251,7 +251,6 @@ "terminal-sticky-scroll": {"assign": ["anthonykim1"]}, "terminal-suggest": {"assign": ["meganrogge"]}, "terminal-tabs": {"assign": ["meganrogge"]}, - "terminal-winpty": {"assign": ["anthonykim1"]}, "testing": {"assign": ["connor4312"]}, "themes": {"assign": ["aeschli"]}, "timeline": {"assign": ["lramos15"]}, diff --git a/.github/commands.json b/.github/commands.json index e3118602d31..978a0960eab 100644 --- a/.github/commands.json +++ b/.github/commands.json @@ -513,7 +513,7 @@ "action": "updateLabels", "addLabel": "verification-steps-needed", "removeLabel": "~verification-steps-needed", - "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + "comment": "Friendly ping for the VS Code team member who owns this issue- Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." }, { "type": "label", @@ -663,5 +663,12 @@ "addLabel": "agent-behavior", "removeLabel": "~agent-behavior", "comment": "Unfortunately I think you are hitting a AI quality issue that is not actionable enough for us to track a bug. We would recommend that you try other available models and look at the [Tips and tricks for Copilot in VS Code](https://code.visualstudio.com/docs/copilot/copilot-tips-and-tricks) doc page.\n\nWe are constantly improving AI quality in every release, thank you for the feedback! If you believe this is a technical bug, we recommend you report a new issue including logs described on the [Copilot Issues](https://github.com/microsoft/vscode/wiki/Copilot-Issues) wiki page." + }, + { + "type": "label", + "name": "~accessibility-sla", + "addLabel": "accessibility-sla", + "removeLabel": "~accessibility-sla", + "comment": "The Visual Studio and VS Code teams have an agreement with the Accessibility team that 3:1 contrast is enough for inside the editor." } ] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 24500c211f2..3ec839df4ef 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -28,6 +28,7 @@ Visual Studio Code is built with a layered architecture using TypeScript, web AP The core architecture follows these principles: - **Layered architecture** - from `base`, `platform`, `editor`, to `workbench` - **Dependency injection** - Services are injected through constructor parameters + - If non-service parameters are needed, they need to come after the service parameters - **Contribution model** - Features contribute to registries and extension points - **Cross-platform compatibility** - Abstractions separate platform-specific code @@ -130,7 +131,15 @@ function f(x: number, y: string): void { } - Don't add tests to the wrong test suite (e.g., adding to end of file instead of inside relevant suite) - Look for existing test patterns before creating new structures - Use `describe` and `test` consistently with existing patterns +- Prefer regex capture groups with names over numbered capture groups. - If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task -- Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. - Never duplicate imports. Always reuse existing imports if they are present. -- Prefer regex capture groups with names over numbered capture groups. +- Do not use `any` or `unknown` as the type for variables, parameters, or return values unless absolutely necessary. If they need type annotations, they should have proper types or interfaces defined. +- When adding file watching, prefer correlated file watchers (via fileService.createWatcher) to shared ones. +- When adding tooltips to UI elements, prefer the use of IHoverService service. +- Do not duplicate code. Always look for existing utility functions, helpers, or patterns in the codebase before implementing new functionality. Reuse and extend existing code whenever possible. +- You MUST deal with disposables by registering them immediately after creation for later disposal. Use helpers such as `DisposableStore`, `MutableDisposable` or `DisposableMap`. Do NOT register a disposable to the containing class if the object is created within a method that is called repeadedly to avoid leaks. Instead, return a `IDisposable` from such method and let the caller register it. +- You MUST NOT use storage keys of another component only to make changes to that component. You MUST come up with proper API to change another component. + +## Learnings +- Minimize the amount of assertions in tests. Prefer one snapshot-style `assert.deepStrictEqual` over multiple precise assertions, as they are much more difficult to understand and to update. diff --git a/.github/instructions/api-version.instructions.md b/.github/instructions/api-version.instructions.md new file mode 100644 index 00000000000..b6b8ef6018f --- /dev/null +++ b/.github/instructions/api-version.instructions.md @@ -0,0 +1,18 @@ +--- +description: Read this when changing proposed API in vscode.proposed.*.d.ts files. +applyTo: 'src/vscode-dts/**/vscode.proposed.*.d.ts' +--- + +The following is only required for proposed API related to chat and languageModel proposals. It's optional for other proposed API, but recommended. + +When a proposed API is changed in a non-backwards-compatible way, the version number at the top of the file must be incremented. If it doesn't have a version number, we must add one. The format of the number like this: + +``` +// version: 1 +``` + +No semver, just a basic incrementing integer. The corresponding version number in the extension's package.json must be incremented to match (you could remind the user of this if you don't have access to the extension code yourself). + +An example of a non-backwards-compatible change is removing a non-optional property or changing the type to one that is incompatible with the previous type. + +An example of a backwards-compatible change is an additive change or deleting a property that was already optional. diff --git a/.github/instructions/chat.instructions.md b/.github/instructions/chat.instructions.md new file mode 100644 index 00000000000..b40d556da61 --- /dev/null +++ b/.github/instructions/chat.instructions.md @@ -0,0 +1,21 @@ +--- +description: Chat feature area coding guidelines +--- + +## Adding chat/AI-related features + +- When adding a new chat/AI feature like a new surface where chat or agents appear, a new AI command, etc, these features must not show up for users when they've disabled AI features. The best way to do this is to gate the feature on the context key `ChatContextKeys.enabled` via a when clause. +- When doing a code review for code that adds an AI feature, please ensure that the feature is properly gated. + +### Hiding AI Features When Disabled + +When surfacing a UI AI feature, ensure the feature hides when `chat.disableAIFeatures` is set: + +- **UI Hiding**: Use `ChatContextKeys.enabled` in `when` conditions to conditionally show/hide UI elements (commands, views, menu items, etc.) + - Example: `when: ChatContextKeys.enabled` in action/command registration + - This context key is `false` when AI features are disabled +- **Programmatic Hiding**: Check `IChatEntitlementService.sentiment.hidden` to determine if AI features should be hidden + - `sentiment.hidden` is `true` when the user signals no intent in using Chat + - This should not only disable Chat but also hide all of its UI + +This ensures consistency when implementing AI-powered UI functionality across the codebase. diff --git a/.github/instructions/interactive.instructions.md b/.github/instructions/interactive.instructions.md new file mode 100644 index 00000000000..7336e967c3b --- /dev/null +++ b/.github/instructions/interactive.instructions.md @@ -0,0 +1,50 @@ +--- +description: Architecture documentation for VS Code interactive window component. Use when working in `src/vs/workbench/contrib/interactive` +--- + +# Interactive Window + +The interactive window component enables extensions to offer REPL like experience to its users. VS Code provides the user interface and extensions provide the execution environment, code completions, execution results rendering and so on. + +The interactive window consists of notebook editor at the top and regular monaco editor at the bottom of the viewport. Extensions can extend the interactive window by leveraging the notebook editor API and text editor/document APIs: + +* Extensions register notebook controllers for the notebook document in the interactive window through `vscode.notebooks.createNotebookController`. The notebook document has a special notebook view type `interactive`, which is contributed by the core instead of extensions. The registered notebook controller is responsible for execution. +* Extensions register auto complete provider for the bottom text editor through `vscode.languages.registerCompletionItemProvider`. The resource scheme for the text editor is `interactive-input` and the language used in the editor is determined by the notebook controller contributed by extensions. + +Users can type in code in the text editor and after users pressing `Shift+Enter`, we will insert a new code cell into the notebook document with the content from the text editor. Then we will request execution for the newly inserted cell. The notebook controller will handle the execution just like it's in a normal notebook editor. + +## Interactive Window Registration + +Registering a new editor type in the workbench consists of two steps: + +* Register an editor input factory which is responsible for resolving resources with given `glob` patterns. Here we register an `InteractiveEditorInput` for all resources with `vscode-interactive` scheme. +* Register an editor pane factory for the given editor input type. Here we register `InteractiveEditor` for our own editor input `InteractiveEditorInput`. + +The workbench editor service is not aware of how models are resolved in `EditorInput`, neither how `EditorPane`s are rendered. It only cares about the common states and events on `EditorInput` or `EditorPane`, i.e., display name, capabilities (editable), content change, dirty state change. It's `EditorInput`/`EditorPane`'s responsibility to provide the right info and updates to the editor service. One major difference between Interactive Editor and other editor panes is Interactive Window is never dirty so users never see a dot on the editor title bar. + +![Editor Registration](./resources/interactive/interactive.editor.drawio.svg) + +## Interactive Window Editor Model Resolution + +The `Interactive.open` command will manually create an `InteractiveEditorInput` specific for the Interactive Window and resolving that Input will go through the following workflow: + +The `INotebookEditorModelResolverService` is used to resolve the notebook model. The `InteractiveEditorInput` wraps a `NotebookEditorInput` for the notebook document and manages a separate text model for the input editor. + +When the notebook model is resolved, the `INotebookEditorModelResolverService` uses the working copy infrastructure to create a `IResolvedNotebookEditorModel`. The content is passed through a `NotebookSerializer` from the `INotebookService` to construct a `NotebookTextModel`. + +![Model Resolution](./resources/interactive/interactive.model.resolution.drawio.svg) + +The `FileSystem` provider that is registered for `vscode-interactive` schema will always return an empty buffer for any read, and will drop all write requests as nothing is stored on disk for Interactive Window resources. The `NotebookSerializer` that is registered for the `interactive` viewtype knows to return an empty notebook data model when it deserializes an empty buffer when the model is being resolved. + +Restoring the interactive window happens through the editor serializer (`InteractiveEditorSerializer`), where the notebook data is stored, and can be used to repopulate the `InteractiveEditorInput` without needing to go through the full editor model resolution flow. + +## UI/EH Editor/Document Syncing + +`EditorInput` is responsible for resolving models for the given resources but in Interactive Window it's much simpler as we are not resolving models ourselves but delegating to Notebook and TextEditor. `InteractiveEditorInput` does the coordination job: + +- It wraps a `NotebookEditorInput` via `_notebookEditorInput` for the notebook document (history cells) +- It manages a separate text model via `ITextModelService` for the input editor at the bottom +- The `IInteractiveDocumentService` coordinates between these two parts +- The `IInteractiveHistoryService` manages command history for the input editor + +![Architecture](./resources/interactive/interactive.eh.drawio.svg) diff --git a/.github/instructions/learnings.instructions.md b/.github/instructions/learnings.instructions.md index 78a9f52a06e..22fa31ae474 100644 --- a/.github/instructions/learnings.instructions.md +++ b/.github/instructions/learnings.instructions.md @@ -1,5 +1,4 @@ --- -applyTo: ** description: This document describes how to deal with learnings that you make. (meta instruction) --- @@ -8,14 +7,13 @@ 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) +* Prefer `const` over `let` whenever possible +* Avoid `any` type ``` When the user tells you "learn!", you should: @@ -23,10 +21,7 @@ When the user tells you "learn!", you should: * 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 + * reflect over it, maybe it can be generalized? Avoid too specific learnings. * 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/notebook.instructions.md b/.github/instructions/notebook.instructions.md new file mode 100644 index 00000000000..cb351d69b44 --- /dev/null +++ b/.github/instructions/notebook.instructions.md @@ -0,0 +1,108 @@ +--- +description: Architecture documentation for VS Code notebook and interactive window components. Use when working in `src/vs/workbench/contrib/notebook/` +--- + +# Notebook Architecture + +This document describes the internal architecture of VS Code's notebook implementation. + +## Model resolution + +Notebook model resolution is handled by `NotebookService`. It resolves notebook models from the file system or other sources. The notebook model is a tree of cells, where each cell has a type (code or markdown) and a list of outputs. + +## Viewport rendering (virtualization) + +The notebook viewport is virtualized to improve performance. Only visible cells are rendered, and cells outside the viewport are recycled. The viewport rendering is handled by `NotebookCellList` which extends `WorkbenchList`. + +![Viewport Rendering](./resources/notebook/viewport-rendering.drawio.svg) + +The rendering has the following steps: + +1. **Render Viewport** - Layout/render only the cells that are in the visible viewport +2. **Render Template** - Each cell type has a template (code cell, markdown cell) that is instantiated via `CodeCellRenderer` or `MarkupCellRenderer` +3. **Render Element** - The cell content is rendered into the template +4. **Get Dynamic Height** - Cell height is computed dynamically based on content (editor lines, outputs, etc.) +5. **Cell Parts Lifecycle** - Each cell has lifecycle parts that manage focus, selection, and other state + +### Cell resize above viewport + +When a cell above the viewport is resized (e.g., output grows), the viewport needs to be updated to maintain scroll position. This is handled by tracking scroll anchors. + +![Cell Resize Above Viewport](./resources/notebook/cell-resize-above-viewport.drawio.svg) + +## Cell Rendering + +The notebook editor renders cells through a contribution system. Cell parts are organized into two categories via `CellPartsCollection`: + +- **CellContentPart** - Non-floating elements rendered inside a cell synchronously to avoid flickering + - `prepareRenderCell()` - Prepare model (no DOM operations) + - `renderCell()` / `didRenderCell()` - Update DOM for the cell + - `unrenderCell()` - Cleanup when cell leaves viewport + - `updateInternalLayoutNow()` - Update layout per cell layout changes + - `updateState()` - Update per cell state change + - `updateForExecutionState()` - Update per execution state change + +- **CellOverlayPart** - Floating elements rendered on top, may be deferred to next animation frame + +Cell parts are located in `view/cellParts/` and contribute to different aspects: +- **Editor** - The Monaco editor for code cells +- **Outputs** - Rendered outputs from code execution +- **Toolbar** - Cell toolbar with actions +- **Status Bar** - Execution status, language info +- **Decorations** - Fold regions, diagnostics, etc. +- **Context Keys** - Cell-specific context key management +- **Drag and Drop** - Cell reordering via `CellDragAndDropController` + +## Focus Tracking + +Focus in the notebook editor is complex because there are multiple focusable elements: + +1. The notebook list itself (`NotebookCellList`) +2. Individual cell containers +3. Monaco editors within cells +4. Output elements (webviews via `BackLayerWebView`) + +The `NotebookEditorWidget` tracks focus state and provides APIs to manage focus across these components. Context keys like `NOTEBOOK_EDITOR_FOCUSED`, `NOTEBOOK_OUTPUT_FOCUSED`, and `NOTEBOOK_OUTPUT_INPUT_FOCUSED` are used to track focus state. + +## Optimizations + +### Output virtualization + +Large outputs are virtualized similar to cells. Only visible portions of outputs are rendered. + +### Cell DOM recycling + +Cell DOM elements are pooled and recycled to reduce DOM operations. When scrolling, cells that move out of the viewport have their templates returned to the pool. Editor instances are managed via `NotebookCellEditorPool`. + +### Webview reuse + +Output webviews are reused across cells when possible to reduce the overhead of creating new webview contexts. The `BackLayerWebView` manages the webview lifecycle. + +--- + +# Find in Notebook Outputs + +The notebook find feature supports searching in both text models and rendered outputs. The find functionality is implemented via `FindModel` and `CellFindMatchModel` classes. + +## Hybrid Find + +For rendered outputs (HTML, images with alt text, etc.), the find uses a hybrid approach: + +1. **Text model search** - Searches cell source code using standard text search via `FindMatch` +2. **DOM search in webview** - Uses `window.find()` to search rendered output content via `CellWebviewFindMatch` + +![Hybrid Find](./resources/notebook/hybrid-find.drawio.svg) + +The hybrid find works by: + +1. First finding matches in text models (cell inputs) - stored in `contentMatches` +2. Then finding matches in rendered outputs via webview - stored in `webviewMatches` +3. Mixing both match types into a unified result set via `CellFindMatchModel` +4. Navigating between matches reveals the appropriate editor or output + +### Implementation details + +- Uses `window.find()` for DOM searching in webview +- Uses `document.execCommand('hiliteColor')` to highlight matches +- Serializes `document.getSelection()` to get match positions +- Creates ranges for current match highlighting diff --git a/.github/instructions/resources/interactive/interactive.editor.drawio.svg b/.github/instructions/resources/interactive/interactive.editor.drawio.svg new file mode 100644 index 00000000000..c6b94798280 --- /dev/null +++ b/.github/instructions/resources/interactive/interactive.editor.drawio.svg @@ -0,0 +1,331 @@ + + + + + + + + + + + +
+
+
+ Notebook Editor Widget +
+
+
+
+ + Notebook Editor Widget + +
+
+ + + + +
+
+
+ Code Editor Widget +
+
+
+
+ + Code Editor Widget + +
+
+ + + + +
+
+
+ Interactive Editor +
+
+
+
+ + Interactive Editor + +
+
+ + + + + +
+
+
+ NotebookService +
+
+
+
+ + NotebookService + +
+
+ + + + + +
+
+
+ TextModelResolverService +
+
+
+
+ + TextModelResolverService + +
+
+ + + + + + + + + + + +
+
+
+ NotebookTextModel +
+
+
+
+ + NotebookTextModel + +
+
+ + + + +
+
+
+ ITextModel +
+
+
+
+ + ITextModel + +
+
+ + + + +
+
+
+ Interactive Editor Input +
+
+
+
+ + Interactive Editor In... + +
+
+ + + + +
+
+
+ EditorPane +
+
+
+
+ + EditorPane + +
+
+ + + + +
+
+
+ EditorInput +
+
+
+
+ + EditorInput + +
+
+ + + + + + +
+
+
+ Editor Resolver Service +
+
+
+
+ + Editor Resolver Service + +
+
+ + + + +
+
+
+ EditorPane Registry +
+
+
+
+ + EditorPane Registry + +
+
+ + + + + + +
+
+
+ Registry Editor Pane +
+
+
+
+ + Registry Editor Pane + +
+
+ + + + + + + + +
+
+
+ + registerEditor + +
+
+
+
+ + registerEditor + +
+
+ + + + + + +
+
+
+ input: EditorInput +
+
+
+
+ + input: EditorInput + +
+
+ + + + + + +
+
+
+ group: IEditorGroup +
+
+
+
+ + group: IEditorGroup + +
+
+ + + + + + +
+
+
+ getControl: IEditorControl +
+
+
+
+ + getControl: IEditorControl + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/.github/instructions/resources/interactive/interactive.eh.drawio.svg b/.github/instructions/resources/interactive/interactive.eh.drawio.svg new file mode 100644 index 00000000000..5f9f40795c2 --- /dev/null +++ b/.github/instructions/resources/interactive/interactive.eh.drawio.svg @@ -0,0 +1,244 @@ + + + + + + + + + +
+
+
+ + Ext Host + +
+
+
+
+ + Ext Host + +
+
+ + + + +
+
+
+ + UI + +
+
+
+
+ + UI + +
+
+ + + + + + + +
+
+
+ Notebook Editor +
+
+
+
+ + Notebook Editor + +
+
+ + + + +
+
+
+ Text Editor +
+
+
+
+ + Text Editor + +
+
+ + + + +
+
+
+ Interactive Editor +
+
+
+
+ + Interactive Editor + +
+
+ + + + + +
+
+
+ NotebookService +
+
+
+
+ + NotebookService + +
+
+ + + + + + + +
+
+
+ TextModelResolverService +
+
+
+
+ + TextModelResolverService + +
+
+ + + + + + + +
+
+
+ ExthostNotebook +
+
+
+
+ + ExthostNotebook + +
+
+ + + + + + + +
+
+
+ ExthostEditors +
+
+
+
+ + ExthostEditors + +
+
+ + + + + + + + +
+
+
+ ExthostInteractive +
+
+
+
+ + ExthostInteracti... + +
+
+ + + + + + +
+
+
+ NotebookEditor +
+
+
+
+ + NotebookE... + +
+
+ + + + + + +
+
+
+ Input +
+
+
+
+ + Input + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg b/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg new file mode 100644 index 00000000000..61c91b98125 --- /dev/null +++ b/.github/instructions/resources/interactive/interactive.model.resolution.drawio.svg @@ -0,0 +1,352 @@ + + + + + + + + + + +
+
+
+ Read resource +
+
+
+
+ + Read resource + +
+
+ + + + + + +
+
+
+ Deserialize NotebookData +
+
+
+
+ + Deserialize NotebookData + +
+
+ + + + + + +
+
+
+ creates +
+
+
+
+ + creates + +
+
+ + + + + +
+
+
+ creates +
+
+
+
+ + creates + +
+
+ + + + + +
+
+
+ creates +
+
+
+
+ + creates + +
+
+ + + + + +
+
+
+ NotebookEditorModelResolverService +
+
+
+
+ + NotebookEditorMod... + +
+
+ + + + +
+
+
+ + SimpleNotebookEditorModel + +
+
+
+
+ + SimpleNoteboookEd... + +
+
+ + + + +
+
+
+ + NotebookFileWorkingCopyModelFactory + +
+
+
+
+ + NotebookFileWorki... + +
+
+ + + + +
+
+
+ + WorkingCopyManager + +
+
+
+
+ + WorkingCopyManager + +
+
+ + + + + +
+
+
+ creates +
+
+
+
+ + creates + +
+
+ + + + +
+
+
+ NotebookService +
+
+
+
+ + NotebookService + +
+
+ + + + +
+
+
+ FileService +
+
+
+
+ + FileService + +
+
+ + + + +
+
+
+ NotebookTextModel +
+
+
+
+ + NotebookTextModel + +
+
+ + + + + +
+
+
+ register FS provider +
+ for vscode-interactive schema +
+
+
+
+ + register FS provider... + +
+
+ + + + + +
+
+
+ register notebook serializer +
+ for interactive viewtype +
+
+
+
+ + register notebook serializer... + +
+
+ + + + +
+
+
+ interactive.contribution +
+ (startup) +
+
+
+
+ + interactive.contributio... + +
+
+ + + + + + + +
+
+
+ InteractiveEditor +
+
+
+
+ + InteractiveEditor + +
+
+ + + + + + + +
+
+
+ + NotebookEditorInput + +
+
+
+
+ + NotebookEditorInp... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
diff --git a/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg b/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg new file mode 100644 index 00000000000..6a2f80fc98d --- /dev/null +++ b/.github/instructions/resources/notebook/cell-resize-above-viewport.drawio.svg @@ -0,0 +1,654 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + +
+
+
+ Webview top -1000 +
+
+
+
+ + Webview top -1000 + +
+
+ + + + + + +
+
+
+ Viewport +
+
+
+
+ + Viewport + +
+
+ + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ Grow Height by 50 +
+
+
+
+ + Grow Height by 50 + +
+
+ + + + +
+
+
+ scrollTop 1000 +
+
+
+
+ + scrollTop 1000 + +
+
+ + + + +
+
+
+ scrollTop 1000 +
+
+
+
+ + scrollTop 1000 + +
+
+ + + + + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ scrollTop 1050 +
+
+
+
+ + scrollTop 1050 + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ scrollTop 1050 +
+
+
+
+ + scrollTop 1050 + +
+
+ + + + + + +
+
+
+ Adjust top +
+
+
+
+ + Adjust top + +
+
+ + + + + + +
+
+
+ UpdateScrollTop +
+
+
+
+ + UpdateScrollTop + +
+
+ + + + + + +
+
+
+ Webview top -1000 +
+
+
+
+ + Webview top -1000 + +
+
+ + + + +
+
+
+ Webview top -1000 +
+
+
+
+ + Webview top -1000 + +
+
+ + + + +
+
+
+ Webview top -1000 +
+
+
+
+ + Webview top -1000 + +
+
+ + + + + + + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ scrollTop 1050 +
+
+
+
+ + scrollTop 1050 + +
+
+ + + + +
+
+
+ Webview top -950 +
+
+
+
+ + Webview top -950 + +
+
+ + + + + + + + + + + + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + + + + + + + + +
+
+
+ scrollTop 1050 +
+
+
+
+ + scrollTop 1050 + +
+
+ + + + +
+
+
+ Webview top -950 +
+
+
+
+ + Webview top -950 + +
+
+ + + + + + +
+
+
+ Adjust top +
+
+
+
+ + Adjust top + +
+
+ + + + + + + + +
+
+
+ 1 +
+
+
+
+ + 1 + +
+
+ + + + +
+
+
+ 2 +
+
+
+
+ + 2 + +
+
+ + + + +
+
+
+ 3 +
+
+
+
+ + 3 + +
+
+ + + + +
+
+
+ 4 +
+
+
+
+ + 4 + +
+
+ + + + +
+
+
+ 4' +
+
+
+
+ + 4' + +
+
+ + + + +
+
+
+ 5 +
+
+
+
+ + 5 + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
diff --git a/.github/instructions/resources/notebook/hybrid-find.drawio.svg b/.github/instructions/resources/notebook/hybrid-find.drawio.svg new file mode 100644 index 00000000000..a2419b58fce --- /dev/null +++ b/.github/instructions/resources/notebook/hybrid-find.drawio.svg @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ window.find +
+
+
+
+ + window.find + +
+
+ + + + + + +
+
+
+ exec("hiliteColor") +
+ findMatchColor +
+
+
+
+ + exec("hiliteColor")... + +
+
+ + + + +
+
+
+ Serialize +
+ document.getSelection() +
+
+
+
+ + Serialize... + +
+
+ + + + + + + + + + +
+
+
+ Find in Rendered Outputs (Search in DOM) +
+
+
+
+ + Find in Rendered Out... + +
+
+ + + + +
+
+
+ Find +
+
+
+
+ + Find + +
+
+ + + + +
+
+
+ Find in Text Model +
+
+
+
+ + Find in Text Model + +
+
+ + + + +
+
+
+ Mix matches +
+
+
+
+ + Mix matches + +
+
+ + + + +
+
+
+ End of Doc +
+
+
+
+ + End of Doc + +
+
+ + + + + + +
+
+
+ Webview +
+
+
+
+ + Webview + +
+
+ + + + + + +
+
+
+ Find Next +
+
+
+
+ + Find Next + +
+
+ + + + + + +
+
+
+ Is Model Match +
+
+
+
+ + Is Model Match + +
+
+ + + + +
+
+
+ Reveal Editor +
+
+
+
+ + Reveal Editor + +
+
+ + + + +
+
+
+ Y +
+
+
+
+ + Y + +
+
+ + + + + + +
+
+
+ document create range +
+
+
+
+ + document create range + +
+
+ + + + + + +
+
+
+ "hiliteColor" +
+ currentFindMatchColor +
+
+
+
+ + "hiliteColor"... + +
+
+ + + + + + +
+
+
+ Find cell/output container +
+
+
+
+ + Find cell/output con... + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
diff --git a/.github/instructions/resources/notebook/viewport-rendering.drawio.svg b/.github/instructions/resources/notebook/viewport-rendering.drawio.svg new file mode 100644 index 00000000000..6a51b66c723 --- /dev/null +++ b/.github/instructions/resources/notebook/viewport-rendering.drawio.svg @@ -0,0 +1,521 @@ + + + + + + + + + + +
+
+
+ Render Viewport +
+
+
+
+ + Render Viewport + +
+
+ + + + +
+
+
+ Notebook List View +
+
+
+
+ + Notebook List View + +
+
+ + + + + + + + + +
+
+
+ Render Template +
+
+
+
+ + Render Template + +
+
+ + + + + + + + + +
+
+
+ Render Element +
+
+
+
+ + Render Element + +
+
+ + + + + +
+
+
+ Get Dynamic Height +
+
+
+
+ + Get Dynamic Height + +
+
+ + + + + + + + + +
+
+
+ Create Cell Templates/Parts +
+
+
+
+ + Create Cell Templates/Parts + +
+
+ + + + +
+
+
+ Toolbar +
+
+
+
+ + Toolbar + +
+
+ + + + +
+
+
+ Editor +
+
+
+
+ + Editor + +
+
+ + + + +
+
+
+ Statusbar +
+
+
+
+ + Statusbar + +
+
+ + + + + + + +
+
+
+ Code Cell +
+
+
+
+ + Code Cell + +
+
+ + + + + +
+
+
+ Render Cell Parts +
+
+
+
+ + Render Cell Parts + +
+
+ + + + + + + + + + + +
+
+
+ CellPart read DOM +
+
+
+
+ + CellPart read DOM + +
+
+ + + + +
+
+
+ Update layout info +
+
+
+
+ + Update layout info + +
+
+ + + + + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ Toolbar.didRenderCell +
+
+
+
+ + Toolbar.didRenderCell + +
+
+ + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ + Toolbar.prepareLayout + +
+
+
+
+ + Toolbar.prepareLay... + +
+
+ + + + + + + + + + + +
+
+
+ Cell Layout Change +
+
+
+
+ + Cell Layout Change + +
+
+ + + + + +
+
+
+ Cell Part updateInternalLayoutNow +
+
+
+
+ + Cell Part updateInternalLayoutNow + +
+
+ + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ Toolbar.renderCell +
+
+
+
+ + Toolbar.renderCell + +
+
+ + + + +
+
+
+ + Toolbar.updateInternalLayoutNow + +
+
+
+
+ + Toolbar.updateInternalLayout... + +
+
+ + + + +
+
+
+ Next Frame +
+
+
+
+ + Next Frame + +
+
+ + + + +
+
+
+ + DOM Read + +
+
+
+
+ + DOM Read + +
+
+ + + + +
+
+
+ + DOM Write + +
+
+
+
+ + DOM Write + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
diff --git a/.github/prompts/data.prompt.md b/.github/prompts/data.prompt.md deleted file mode 100644 index 4185ebb4141..00000000000 --- a/.github/prompts/data.prompt.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -agent: agent -description: 'Answer telemetry questions with data queries' -tools: ['search', 'runCommands/runInTerminal', 'Azure MCP/kusto_query', 'githubRepo', 'extensions', 'todos'] ---- - - -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/doc-comments.prompt.md b/.github/prompts/doc-comments.prompt.md new file mode 100644 index 00000000000..b3848435901 --- /dev/null +++ b/.github/prompts/doc-comments.prompt.md @@ -0,0 +1,30 @@ +--- +agent: agent +description: 'Update doc comments' +tools: ['edit', 'search', 'new', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'extensions', 'todos', 'runTests'] +--- +# Role + +You are an expert technical documentation editor specializing in public API documentation. + +## Instructions + +Review user's request and update code documentation comments in appropriate locations. + +## Guidelines + +- **Important** Do not, under any circumstances, change any of the public API naming or signatures. +- **Important** Fetch and review relevant code context (i.e. implementation source code) before making changes or adding comments. +- **Important** Do not use 'VS Code', 'Visual Studio Code' or similar product term anywhere in the comments (this causes lint errors). +- Follow American English grammar, orthography, and punctuation. +- Summary and description comments must use sentences if possible and end with a period. +- Use {@link \} where possible **and reasonable** to refer to code symbols. +- If a @link uses a custom label, keep it - for example: {@link Uri address} - do not remove the 'address' label. +- Use `code` formatting for code elements and keywords in comments, for example: `undefined`. +- Limit the maximum line length of comments to 120 characters. + +## Cleanup Mode + +If the user instructed you to "clean up" doc comments (e.g. by passing in "cleanup" as their prompt), +it is **very important** that you limit your changes to only fixing grammar, punctuation, formatting, and spelling mistakes. +**YOU MUST NOT** add new or remove or expand existing comments in cleanup mode. diff --git a/.github/prompts/find-duplicates.prompt.md b/.github/prompts/find-duplicates.prompt.md new file mode 100644 index 00000000000..4537750b16b --- /dev/null +++ b/.github/prompts/find-duplicates.prompt.md @@ -0,0 +1,16 @@ +--- +# NOTE: This prompt is intended for internal use only for now. +agent: agent +argument-hint: Provide a link or issue number to find duplicates for +description: Find duplicates for a VS Code GitHub issue +model: Claude Sonnet 4.5 (copilot) +tools: + - execute/getTerminalOutput + - execute/runInTerminal + - github/* + - agent/runSubagent +--- + +## Your Task +1. Get the file contents of the prompt file https://github.com/microsoft/vscode-engineering/blob/main/.github/prompts/find-duplicates-gh-cli.prompt.md. +2. Follow those instructions PRECISELY to identify potential duplicate issues for a given issue number in the VS Code repository. diff --git a/.github/prompts/find-issue.prompt.md b/.github/prompts/find-issue.prompt.md new file mode 100644 index 00000000000..32edf030e5b --- /dev/null +++ b/.github/prompts/find-issue.prompt.md @@ -0,0 +1,14 @@ +--- +# ⚠️: Internal use only. To onboard, follow instructions at https://github.com/microsoft/vscode-engineering/blob/main/docs/gh-mcp-onboarding.md +agent: agent +model: Claude Sonnet 4.5 (copilot) +argument-hint: Describe your issue. Include relevant keywords or phrases. +description: Search for an existing VS Code GitHub issue +tools: + - github/* + - agent/runSubagent +--- + +## Your Task +1. Get the file contents of the prompt file https://github.com/microsoft/vscode-engineering/blob/main/.github/prompts/find-issue.prompt.md. +2. Follow those instructions PRECISELY to find issues related to the issue description provided. Perform your search in the `vscode` repository. diff --git a/.github/prompts/issue-grouping.prompt.md b/.github/prompts/issue-grouping.prompt.md new file mode 100644 index 00000000000..ec91b12f1bb --- /dev/null +++ b/.github/prompts/issue-grouping.prompt.md @@ -0,0 +1,22 @@ +--- +agent: agent +model: Claude Sonnet 4.5 (copilot) +argument-hint: Give an assignee and or a label/labels. Issues with that assignee and label will be fetched and grouped. +description: Group similar issues. +tools: + - github/search_issues + - agent/runSubagent + - edit/createFile + - edit/editFiles + - read/readFile +--- + +## Your Task +1. Use a subagent to: + a. Using the GitHub MCP server, fetch only one page (50 per page) of the open issues for the given assignee and label in the `vscode` repository. + b. After fetching a single page, look through the issues and see if there are are any good grouping categories.Output the categories as headers to a local file categorized-issues.md. Do NOT fetch more issue pages yet, make sure to write the categories to the file first. +2. Repeat step 1 (sequentially, don't parallelize) until all pages are fetched and categories are written to the file. +3. Use a subagent to Re-fetch only one page of the issues for the given assignee and label in the `vscode` repository. Write each issue into the categorized-issues.md file under the appropriate category header with a link and the number of upvotes. If an issue doesn't fit into any category, put it under an "Other" category. +4. Repeat step 3 (sequentially, don't parallelize) until all pages are fetched and all issues are written to the file. +5. Within each category, sort the issues by number of upvotes in descending order. +6. Show the categorized-issues.md file as the final output. diff --git a/.github/prompts/micro-perf.prompt.md b/.github/prompts/micro-perf.prompt.md new file mode 100644 index 00000000000..dbfad2dfc27 --- /dev/null +++ b/.github/prompts/micro-perf.prompt.md @@ -0,0 +1,26 @@ +--- +agent: agent +description: 'Optimize code performance' +tools: ['edit', 'search', 'new', 'runCommands', 'runTasks', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'openSimpleBrowser', 'fetch', 'githubRepo', 'extensions', 'todos', 'runTests'] +--- +# Role + +You are an expert performance engineer. + +## Instructions + +Review the attached file and find all publicly exported class or functions. +Optimize performance of all exported definitions. +If the user provided explicit list of classes or functions to optimize, scope your work only to those definitions. + +## Guidelines + +1. Make sure to analyze usage and calling patterns for each function you optimize. +2. When you need to change a function or a class, add optimized version of it immediately below the existing definition instead of changing the original. +3. Optimized function or class name should have the same name as original with '_new' suffix. +4. Create a file with '..perf.js' suffix with perf tests. For example if you are using model 'Foo' and optimizing file name utils.ts, you will create file named 'utils.foo.perf.js'. +5. **IMPORTANT**: You should use ESM format for the perf test files (i.e. use 'import' instead of 'require'). +6. The perf tests should contain comprehensive perf tests covering identified scenarios and common cases, and comparing old and new implementations. +7. The results of perf tests and your summary should be placed in another file with '..perf.md' suffix, for example 'utils.foo.perf.md'. +8. The results file must include section per optimized definition with a table with comparison of old vs new implementations with speedup ratios and analysis of results. +9. At the end ask the user if they want to apply the changes and if the answer is yes, replace original implementations with optimized versions but only in cases where there are significant perf gains and no serious regressions. Revert any other changes to the original code. diff --git a/.github/prompts/migrate.prompt.md b/.github/prompts/migrate.prompt.md new file mode 100644 index 00000000000..d404ebf6f4b --- /dev/null +++ b/.github/prompts/migrate.prompt.md @@ -0,0 +1,184 @@ +--- +agent: agent +tools: + [ + "github/add_issue_comment", + "github/get_label", + "github/get_me", + "github/issue_read", + "github/issue_write", + "github/search_issues", + "github/search_pull_requests", + "github/search_repositories", + "github/sub_issue_write", + ] +--- + +# Issue Migration Prompt + +Use this prompt when migrating issues from one GitHub repository to another (e.g., from `microsoft/vscode-copilot` to `microsoft/vscode`). + +## Input Methods + +You can specify which issues to migrate using **any** of these three methods: + +### Option A: GitHub Search Query URL + +Provide a full GitHub issues search URL. **All matching issues will be migrated.** + +``` +https://github.com/microsoft/vscode-copilot/issues?q=is%3Aissue+is%3Aopen+assignee%3Ayoyokrazy +``` + +### Option B: GitHub Search Query Parameters + +Provide search query syntax for a specific repo. **All matching issues will be migrated.** + +``` +repo:microsoft/vscode-copilot is:issue is:open assignee:yoyokrazy +``` + +Common query filters: + +- `is:issue` / `is:pr` - Filter by type +- `is:open` / `is:closed` - Filter by state +- `assignee:USERNAME` - Filter by assignee +- `author:USERNAME` - Filter by author +- `label:LABEL` - Filter by label +- `milestone:MILESTONE` - Filter by milestone + +### Option C: Specific Issue URL + +Provide a direct link to a single issue. **Only this issue will be migrated.** + +``` +https://github.com/microsoft/vscode-copilot/issues/12345 +``` + +## Task + +**Target Repository:** `{TARGET_REPO}` + +Based on the input provided, migrate the issue(s) to the target repository following all requirements below. + +## Requirements + +### 1. Issue Body Format + +Create the new issue with this header format: + +```markdown +_Transferred from {SOURCE_REPO}#{ORIGINAL_ISSUE_NUMBER}_ +_Original author: `@{ORIGINAL_AUTHOR}`_ + +--- + +{ORIGINAL_ISSUE_BODY} +``` + +### 2. Comment Migration + +For each comment on the original issue, add a comment to the new issue: + +```markdown +_`@{COMMENT_AUTHOR}` commented:_ + +--- + +{COMMENT_BODY} +``` + +### 3. CRITICAL: Preventing GitHub Pings + +**ALL `@username` mentions MUST be wrapped in backticks to prevent GitHub from sending notifications.** + +✅ Correct: `` `@username` `` +❌ Wrong: `@username` + +This applies to: + +- The "Original author" line in the issue body +- Any `@mentions` within the issue body content +- The comment author attribution line +- Any `@mentions` within comment content +- Any quoted content that contains `@mentions` + +### 4. CRITICAL: Issue/PR Link Reformatting + +**Issue references like `#12345` are REPO-SPECIFIC.** If you copy `#12345` from the source repo to the target repo, it will incorrectly link to issue 12345 in the _target_ repo instead of the source. + +**Convert ALL `#NUMBER` references to full URLs:** + +✅ Correct: `https://github.com/microsoft/vscode-copilot/issues/12345` +✅ Also OK: `microsoft/vscode-copilot#12345` +❌ Wrong: `#12345` (will link to wrong repo) + +This applies to: + +- Issue references in the body (`#12345` → full URL) +- PR references in the body (`#12345` → full URL) +- References in comments +- References in quoted content +- References in image alt text or links + +**Exception:** References that are _already_ full URLs should be left unchanged. + +### 5. Metadata Preservation + +- Copy all applicable labels to the new issue +- Assign the new issue to the same assignees (if they exist in target repo) +- Preserve the issue title exactly + +### 5. Post-Migration + +After creating the new issue and all comments: + +- Add a comment to the **original** issue linking to the new issue: + ```markdown + Migrated to {TARGET_REPO}#{NEW_ISSUE_NUMBER} + ``` +- Close the original issue as not_planned + +## Example Transformation + +### Original Issue Body (in `microsoft/vscode-copilot`): + +```markdown +I noticed @johndoe had a similar issue in #9999. cc @janedoe for visibility. + +Related to #8888 and microsoft/vscode#12345. + +Steps to reproduce: + +1. Open VS Code +2. ... +``` + +### Migrated Issue Body (in `microsoft/vscode`): + +```markdown +_Transferred from microsoft/vscode-copilot#12345_ +_Original author: `@originalauthor`_ + +--- + +I noticed `@johndoe` had a similar issue in https://github.com/microsoft/vscode-copilot/issues/9999. cc `@janedoe` for visibility. + +Related to https://github.com/microsoft/vscode-copilot/issues/8888 and microsoft/vscode#12345. + +Steps to reproduce: + +1. Open VS Code +2. ... +``` + +Note: The `microsoft/vscode#12345` reference was already a cross-repo link, so it stays unchanged. + +## Checklist Before Migration + +- [ ] Confirm input method (query URL, query params, or specific issue URL) +- [ ] Confirm target repository +- [ ] If using query: verify the query returns the expected issues +- [ ] Verify all `@mentions` are wrapped in backticks +- [ ] Verify all `#NUMBER` references are converted to full URLs +- [ ] Decide whether to close original issues after migration diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 1b0af580378..8cf20c233b0 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -26,7 +26,7 @@ jobs: # If you do not check out your code, Copilot will do this for you. steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -51,11 +51,11 @@ jobs: 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 + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts linux x64 $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .build/node_modules_cache key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" @@ -107,7 +107,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -115,11 +115,11 @@ jobs: run: mkdir -p .build - name: Prepare built-in extensions cache key - run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: enableCrossOsArchive: true path: .build/builtInExtensions @@ -127,7 +127,7 @@ jobs: - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' - run: node build/lib/builtInExtensions.js + run: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -139,7 +139,7 @@ jobs: 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 + if npm exec -- npm-run-all2 -lp "electron x64" "playwright-install"; then echo "Download successful on attempt $i" break fi diff --git a/.github/workflows/monaco-editor.yml b/.github/workflows/monaco-editor.yml index b1d462546ac..822210da8d0 100644 --- a/.github/workflows/monaco-editor.yml +++ b/.github/workflows/monaco-editor.yml @@ -19,7 +19,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: persist-credentials: false @@ -29,10 +29,10 @@ jobs: - name: Compute node modules cache key id: nodeModulesCacheKey - run: echo "value=$(node build/azure-pipelines/common/computeNodeModulesCacheKey.js)" >> $GITHUB_OUTPUT + run: echo "value=$(node build/azure-pipelines/common/computeNodeModulesCacheKey.ts)" >> $GITHUB_OUTPUT - name: Cache node modules id: cacheNodeModules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: "**/node_modules" key: ${{ runner.os }}-cacheNodeModules20-${{ steps.nodeModulesCacheKey.outputs.value }} @@ -43,7 +43,7 @@ jobs: run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT - name: Cache npm directory if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ steps.npmCacheDirPath.outputs.dir }} key: ${{ runner.os }}-npmCacheDir-${{ steps.nodeModulesCacheKey.outputs.value }} diff --git a/.github/workflows/no-engineering-system-changes.yml b/.github/workflows/no-engineering-system-changes.yml new file mode 100644 index 00000000000..45d1ae55f62 --- /dev/null +++ b/.github/workflows/no-engineering-system-changes.yml @@ -0,0 +1,50 @@ +name: Prevent engineering system changes in PRs + +on: pull_request +permissions: {} + +jobs: + main: + name: Prevent engineering system changes in PRs + runs-on: ubuntu-latest + steps: + - name: Get file changes + uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b # v1.2.4 + id: file_changes + - name: Check if engineering systems were modified + id: engineering_systems_check + run: | + if cat $HOME/files.json | jq -e 'any(test("^\\.github\\/workflows\\/|^build\\/|package\\.json$"))' > /dev/null; then + echo "engineering_systems_modified=true" >> $GITHUB_OUTPUT + echo "Engineering systems were modified in this PR" + else + echo "engineering_systems_modified=false" >> $GITHUB_OUTPUT + echo "No engineering systems were modified in this PR" + fi + - name: Prevent Copilot from modifying engineering systems + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login == 'Copilot' }} + run: | + echo "Copilot is not allowed to modify .github/workflows, build folder files, or package.json files." + echo "If you need to update engineering systems, please do so manually or through authorized means." + exit 1 + - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 + id: get_permissions + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} + with: + route: GET /repos/microsoft/vscode/collaborators/${{ github.event.pull_request.user.login }}/permission + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Set control output variable + id: control + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} + run: | + echo "user: ${{ github.event.pull_request.user.login }}" + echo "role: ${{ fromJson(steps.get_permissions.outputs.data).permission }}" + echo "is dependabot: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}" + echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" + echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT + - name: Check for engineering system changes + if: ${{ steps.engineering_systems_check.outputs.engineering_systems_modified == 'true' && steps.control.outputs.should_run == 'true' }} + run: | + echo "Changes to .github/workflows/, build/ folder files, or package.json files aren't allowed in PRs." + exit 1 diff --git a/.github/workflows/no-yarn-lock-changes.yml b/.github/workflows/no-yarn-lock-changes.yml deleted file mode 100644 index 5727d1c511c..00000000000 --- a/.github/workflows/no-yarn-lock-changes.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Prevent yarn.lock changes in PRs - -on: pull_request -permissions: {} - -jobs: - main: - name: Prevent yarn.lock changes in PRs - runs-on: ubuntu-latest - steps: - - name: Get file changes - uses: trilom/file-changes-action@a6ca26c14274c33b15e6499323aac178af06ad4b # v1.2.4 - id: file_changes - - name: Check if lockfiles were modified - id: lockfile_check - run: | - if cat $HOME/files.json | jq -e 'any(test("yarn\\.lock$|Cargo\\.lock$"))' > /dev/null; then - echo "lockfiles_modified=true" >> $GITHUB_OUTPUT - echo "Lockfiles were modified in this PR" - else - echo "lockfiles_modified=false" >> $GITHUB_OUTPUT - echo "No lockfiles were modified in this PR" - fi - - name: Prevent Copilot from modifying lockfiles - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login == 'Copilot' }} - run: | - echo "Copilot is not allowed to modify yarn.lock or Cargo.lock files." - echo "If you need to update dependencies, please do so manually or through authorized means." - exit 1 - - uses: octokit/request-action@dad4362715b7fb2ddedf9772c8670824af564f0d # v2.4.0 - id: get_permissions - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} - with: - route: GET /repos/microsoft/vscode/collaborators/{username}/permission - username: ${{ github.event.pull_request.user.login }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Set control output variable - id: control - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && github.event.pull_request.user.login != 'Copilot' }} - run: | - echo "user: ${{ github.event.pull_request.user.login }}" - echo "role: ${{ fromJson(steps.get_permissions.outputs.data).permission }}" - echo "is dependabot: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }}" - echo "should_run: ${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) }}" - echo "should_run=${{ !contains(fromJson('["admin", "maintain", "write"]'), fromJson(steps.get_permissions.outputs.data).permission) && github.event.pull_request.user.login != 'dependabot[bot]' }}" >> $GITHUB_OUTPUT - - name: Check for lockfile changes - if: ${{ steps.lockfile_check.outputs.lockfiles_modified == 'true' && steps.control.outputs.should_run == 'true' }} - run: | - echo "Changes to yarn.lock/Cargo.lock files aren't allowed in PRs." - exit 1 diff --git a/.github/workflows/pr-darwin-test.yml b/.github/workflows/pr-darwin-test.yml index 6fce35247a6..04446c0f747 100644 --- a/.github/workflows/pr-darwin-test.yml +++ b/.github/workflows/pr-darwin-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: arm64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -32,11 +32,11 @@ jobs: node-version-file: .nvmrc - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .build/node_modules_cache key: "node_modules-macos-${{ hashFiles('.build/packagelockhash') }}" @@ -94,7 +94,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -102,11 +102,11 @@ jobs: run: mkdir -p .build - name: Prepare built-in extensions cache key - run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: enableCrossOsArchive: true path: .build/builtInExtensions @@ -114,7 +114,7 @@ jobs: - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' - run: node build/lib/builtInExtensions.js + run: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -126,7 +126,7 @@ jobs: set -e for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) - if npm exec -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then + if npm exec -- npm-run-all2 -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then echo "Download successful on attempt $i" break fi @@ -229,7 +229,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -240,7 +240,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -249,7 +249,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() continue-on-error: true with: diff --git a/.github/workflows/pr-linux-cli-test.yml b/.github/workflows/pr-linux-cli-test.yml index e317a6ee103..f201aaa9287 100644 --- a/.github/workflows/pr-linux-cli-test.yml +++ b/.github/workflows/pr-linux-cli-test.yml @@ -16,7 +16,7 @@ jobs: RUSTUP_TOOLCHAIN: ${{ inputs.rustup_toolchain }} steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install Rust run: | diff --git a/.github/workflows/pr-linux-test.yml b/.github/workflows/pr-linux-test.yml index f0b593c5913..088602fc4bd 100644 --- a/.github/workflows/pr-linux-test.yml +++ b/.github/workflows/pr-linux-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -51,11 +51,11 @@ jobs: sudo service xvfb start - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .build/node_modules_cache key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" @@ -107,7 +107,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -115,11 +115,11 @@ jobs: run: mkdir -p .build - name: Prepare built-in extensions cache key - run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: enableCrossOsArchive: true path: .build/builtInExtensions @@ -127,7 +127,7 @@ jobs: - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' - run: node build/lib/builtInExtensions.js + run: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -139,7 +139,7 @@ jobs: set -e for i in {1..3}; do # try 3 times (matching retryCountOnTaskFailure: 3) - if npm exec -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then + if npm exec -- npm-run-all2 -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install"; then echo "Download successful on attempt $i" break fi @@ -273,7 +273,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -284,7 +284,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -293,7 +293,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() continue-on-error: true with: diff --git a/.github/workflows/pr-node-modules.yml b/.github/workflows/pr-node-modules.yml index c23cc425266..14336c79b62 100644 --- a/.github/workflows/pr-node-modules.yml +++ b/.github/workflows/pr-node-modules.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -21,11 +21,11 @@ jobs: node-version-file: .nvmrc - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .build/node_modules_cache key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" @@ -60,7 +60,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -68,11 +68,11 @@ jobs: run: | set -e mkdir -p .build - node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache@v4 + uses: actions/cache@v5 with: enableCrossOsArchive: true path: .build/builtInExtensions @@ -80,7 +80,7 @@ jobs: - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' - run: node build/lib/builtInExtensions.js + run: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: ${{ secrets.VSCODE_OSS }} @@ -92,7 +92,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -100,11 +100,11 @@ jobs: node-version-file: .nvmrc - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .build/node_modules_cache key: "node_modules-linux-${{ hashFiles('.build/packagelockhash') }}" @@ -152,7 +152,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -164,7 +164,7 @@ jobs: VSCODE_ARCH: arm64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -172,11 +172,11 @@ jobs: node-version-file: .nvmrc - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: .build/node_modules_cache key: "node_modules-macos-${{ hashFiles('.build/packagelockhash') }}" @@ -213,7 +213,7 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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 @@ -225,7 +225,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -236,10 +236,10 @@ jobs: shell: pwsh run: | mkdir .build -ea 0 - node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash + node build/azure-pipelines/common/computeNodeModulesCacheKey.ts win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache - uses: actions/cache@v4 + uses: actions/cache@v5 id: node-modules-cache with: path: .build/node_modules_cache @@ -280,6 +280,6 @@ jobs: run: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt } + exec { node build/azure-pipelines/common/listNodeModules.ts .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 } diff --git a/.github/workflows/pr-win32-test.yml b/.github/workflows/pr-win32-test.yml index f1b94e18897..6c1ef55ebb0 100644 --- a/.github/workflows/pr-win32-test.yml +++ b/.github/workflows/pr-win32-test.yml @@ -24,7 +24,7 @@ jobs: VSCODE_ARCH: x64 steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -35,10 +35,10 @@ jobs: shell: pwsh run: | mkdir .build -ea 0 - node build/azure-pipelines/common/computeNodeModulesCacheKey.js win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash + node build/azure-pipelines/common/computeNodeModulesCacheKey.ts win32 ${{ env.VSCODE_ARCH }} $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 id: node-modules-cache with: path: .build/node_modules_cache @@ -84,7 +84,7 @@ jobs: run: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt } + exec { node build/azure-pipelines/common/listNodeModules.ts .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 } @@ -94,11 +94,11 @@ jobs: - name: Prepare built-in extensions cache key shell: pwsh - run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + run: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash - name: Restore built-in extensions cache id: cache-builtin-extensions - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: enableCrossOsArchive: true path: .build/builtInExtensions @@ -106,7 +106,7 @@ jobs: - name: Download built-in extensions if: steps.cache-builtin-extensions.outputs.cache-hit != 'true' - run: node build/lib/builtInExtensions.js + run: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -119,7 +119,7 @@ jobs: run: | for ($i = 1; $i -le 3; $i++) { try { - npm exec -- npm-run-all -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install" + npm exec -- npm-run-all2 -lp "electron ${{ env.VSCODE_ARCH }}" "playwright-install" break } catch { @@ -249,7 +249,7 @@ jobs: if: always() - name: Publish Crash Reports - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -260,7 +260,7 @@ jobs: # In order to properly symbolify above crash reports # (if any), we need the compiled native modules too - name: Publish Node Modules - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: failure() continue-on-error: true with: @@ -269,7 +269,7 @@ jobs: if-no-files-found: ignore - name: Publish Log Files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() continue-on-error: true with: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5785a377639..e5fa2f8f938 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout microsoft/vscode - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup Node.js uses: actions/setup-node@v6 @@ -29,11 +29,11 @@ jobs: node-version-file: .nvmrc - name: Prepare node_modules cache key - run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + run: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash - name: Restore node_modules cache id: cache-node-modules - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: .build/node_modules_cache key: "node_modules-compile-${{ hashFiles('.build/packagelockhash') }}" @@ -68,19 +68,16 @@ jobs: if: steps.cache-node-modules.outputs.cache-hit != 'true' run: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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: Compile /build/ folder - run: npm run compile + - name: Type check /build/ scripts + run: npm run typecheck working-directory: build - - name: Check /build/ folder - run: .github/workflows/check-clean-git-state.sh - - name: Compile & Hygiene - run: 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 test-build-scripts + run: npm exec -- npm-run-all2 -lp core-ci extensions-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/telemetry.yml b/.github/workflows/telemetry.yml index cb7d81e551f..e30d3cc8da3 100644 --- a/.github/workflows/telemetry.yml +++ b/.github/workflows/telemetry.yml @@ -7,7 +7,7 @@ jobs: runs-on: 'ubuntu-latest' steps: - - uses: 'actions/checkout@v5' + - uses: 'actions/checkout@v6' with: persist-credentials: false diff --git a/.gitignore b/.gitignore index ef8c9290422..3d97a65e027 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ node_modules/ .build/ .vscode/extensions/**/out/ extensions/**/dist/ -!extensions/open-remote-ssh/dist/ +src/vs/base/browser/ui/codicons/codicon/codicon.ttf /out*/ /extensions/**/out/ build/node_modules @@ -21,11 +21,7 @@ vscode.db /cli/openssl product.overrides.json *.snap.actual +*.tsbuildinfo .vscode-test - -# CortexIDE added these: -.tmp/ -.tmp2/ -.tool-versions -src/vs/workbench/contrib/cortexide/browser/react/out/** -src/vs/workbench/contrib/cortexide/browser/react/src2/** +vscode-telemetry-docs/ +test-output.json diff --git a/.npmrc b/.npmrc index 5c19939fde2..50d910c65e4 100644 --- a/.npmrc +++ b/.npmrc @@ -1,8 +1,7 @@ disturl="https://electronjs.org/headers" -target="37.7.0" -ms_build_id="12597478" +target="39.3.0" +ms_build_id="13168319" runtime="electron" build_from_source="true" legacy-peer-deps="true" timeout=180000 -npm_config_node_gyp="node build/npm/gyp/node_modules/node-gyp/bin/node-gyp.js" diff --git a/.nvmrc b/.nvmrc index 442c7587a99..5767036af0e 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.20.0 +22.21.1 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 85bbd28a4d8..3fb87652c81 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -8,6 +8,7 @@ "ms-vscode.vscode-github-issue-notebooks", "ms-vscode.extension-test-runner", "jrieken.vscode-pr-pinger", - "typescriptteam.native-preview" + "typescriptteam.native-preview", + "ms-vscode.ts-customized-language-service" ] } diff --git a/.vscode/extensions/vscode-selfhost-test-provider/package.json b/.vscode/extensions/vscode-selfhost-test-provider/package.json index ec4ea96389b..6f0db218fb2 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/package.json +++ b/.vscode/extensions/vscode-selfhost-test-provider/package.json @@ -53,7 +53,7 @@ "Other" ], "activationEvents": [ - "workspaceContains:src/vs/loader.js" + "workspaceContains:src/vscode-dts/vscode.d.ts" ], "workspaceTrust": { "request": "onDemand", diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts index 7a54c5c0d32..4210987083f 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/testTree.ts @@ -31,17 +31,24 @@ export const guessWorkspaceFolder = async () => { } for (const folder of vscode.workspace.workspaceFolders) { - try { - await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, 'src/vs/loader.js')); + if (await isVsCodeWorkspaceFolder(folder)) { return folder; - } catch { - // ignored } } return undefined; }; +export async function isVsCodeWorkspaceFolder(folder: vscode.WorkspaceFolder): Promise { + try { + const buffer = await vscode.workspace.fs.readFile(vscode.Uri.joinPath(folder.uri, 'package.json')); + const pkg = JSON.parse(textDecoder.decode(buffer)); + return pkg.name === 'code-oss-dev'; + } catch { + return false; + } +} + export const getContentFromFilesystem: ContentGetter = async uri => { try { const rawContent = await vscode.workspace.fs.readFile(uri); @@ -58,7 +65,7 @@ export class TestFile { constructor( public readonly uri: vscode.Uri, public readonly workspaceFolder: vscode.WorkspaceFolder - ) {} + ) { } public getId() { return this.uri.toString().toLowerCase(); @@ -169,8 +176,8 @@ export abstract class TestConstruct { } } -export class TestSuite extends TestConstruct {} +export class TestSuite extends TestConstruct { } -export class TestCase extends TestConstruct {} +export class TestCase extends TestConstruct { } export type VSCodeTest = TestFile | TestSuite | TestCase; diff --git a/.vscode/launch.json b/.vscode/launch.json index 216afd8b573..a7a15cc31a6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -282,7 +282,7 @@ // To debug observables you also need the extension "ms-vscode.debug-value-editor" "type": "chrome", "request": "launch", - "name": "Launch VS Code Internal (Dev Debug)", + "name": "Launch VS Code Internal (Hot Reload)", "windows": { "runtimeExecutable": "${workspaceFolder}/scripts/code.bat" }, @@ -298,7 +298,10 @@ "VSCODE_EXTHOST_WILL_SEND_SOCKET": null, "VSCODE_SKIP_PRELAUNCH": "1", "VSCODE_DEV_DEBUG": "1", + "VSCODE_DEV_SERVER_URL": "http://localhost:5199/build/vite/workbench-vite-electron.html", + "DEV_WINDOW_SRC": "http://localhost:5199/build/vite/workbench-vite-electron.html", "VSCODE_DEV_DEBUG_OBSERVABLES": "1", + "VSCODE_DEV": "1" }, "cleanUp": "wholeBrowser", "runtimeArgs": [ @@ -322,6 +325,7 @@ "presentation": { "hidden": true, }, + "preLaunchTask": "Launch Monaco Editor Vite" }, { "type": "node", @@ -588,11 +592,33 @@ ] }, { - "name": "Monaco Editor Playground", + "name": "Monaco Editor - Playground", "type": "chrome", "request": "launch", - "url": "http://localhost:5001", - "preLaunchTask": "Launch Http Server", + "url": "https://microsoft.github.io/monaco-editor/playground.html?source=http%3A%2F%2Flocalhost%3A5199%2Fbuild%2Fvite%2Findex.ts%3Fesm#example-creating-the-editor-hello-world", + "preLaunchTask": "Launch Monaco Editor Vite", + "presentation": { + "group": "monaco", + "order": 4 + } + }, + { + "name": "Monaco Editor - Self Contained Diff Editor", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5199/build/vite/index.html", + "preLaunchTask": "Launch Monaco Editor Vite", + "presentation": { + "group": "monaco", + "order": 4 + } + }, + { + "name": "Monaco Editor - Workbench", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5199/build/vite/workbench-vite.html", + "preLaunchTask": "Launch Monaco Editor Vite", "presentation": { "group": "monaco", "order": 4 @@ -616,10 +642,10 @@ } }, { - "name": "VS Code (Debug Observables)", + "name": "VS Code (Hot Reload)", "stopAll": true, "configurations": [ - "Launch VS Code Internal (Dev Debug)", + "Launch VS Code Internal (Hot Reload)", "Attach to Main Process", "Attach to Extension Host", "Attach to Shared Process", diff --git a/.vscode/notebooks/api.github-issues b/.vscode/notebooks/api.github-issues index d466fa1b04b..aca29690dc2 100644 --- a/.vscode/notebooks/api.github-issues +++ b/.vscode/notebooks/api.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"October 2025\"" + "value": "$REPO=repo:microsoft/vscode\n$MILESTONE=milestone:\"January 2026\"" }, { "kind": 1, diff --git a/.vscode/notebooks/endgame.github-issues b/.vscode/notebooks/endgame.github-issues index 8592ecd44c0..3a6635f9e17 100644 --- a/.vscode/notebooks/endgame.github-issues +++ b/.vscode/notebooks/endgame.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"October 2025\"" + "value": "$MILESTONE=milestone:\"January 2026\"" }, { "kind": 1, diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 38ff70547c4..d17509b7758 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -7,12 +7,12 @@ { "kind": 2, "language": "github-issues", - "value": "$MILESTONE=milestone:\"October 2025\"\n\n$MINE=assignee:@me" + "value": "$MILESTONE=milestone:\"January 2026\"\n\n$MINE=assignee:@me" }, { "kind": 2, "language": "github-issues", - "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99" + "value": "$NOT_TEAM_MEMBERS=-author:aeschli -author:alexdima -author:alexr00 -author:AmandaSilver -author:bamurtaugh -author:bpasero -author:chrmarti -author:Chuxel -author:claudiaregio -author:connor4312 -author:dbaeumer -author:deepak1556 -author:devinvalenciano -author:digitarald -author:DonJayamanne -author:egamma -author:fiveisprime -author:ntrogh -author:hediet -author:isidorn -author:joaomoreno -author:jrieken -author:kieferrm -author:lramos15 -author:lszomoru -author:meganrogge -author:misolori -author:mjbvz -author:rebornix -author:roblourens -author:rzhao271 -author:sandy081 -author:sbatten -author:stevencl -author:TylerLeonhardt -author:Tyriar -author:weinand -author:amunger -author:karthiknadig -author:eleanorjboyd -author:Yoyokrazy -author:ulugbekna -author:aiday-mar -author:bhavyaus -author:justschen -author:benibenj -author:luabud -author:anthonykim1 -author:joshspicer -author:osortega -author:hawkticehurst -author:pierceboggan -author:benvillalobos -author:dileepyavan -author:dineshc-msft -author:dmitrivMS -author:eli-w-king -author:jo-oikawa -author:jruales -author:jytjyt05 -author:kycutler -author:mrleemurray -author:pwang347 -author:vijayupadya -author:bryanchen-d -author:cwebster-99 -author:rwoll -author:lostintangent -author:jukasper -author:zhichli" }, { "kind": 1, diff --git a/.vscode/notebooks/my-work.github-issues b/.vscode/notebooks/my-work.github-issues index c56ba69614c..cb001643e15 100644 --- a/.vscode/notebooks/my-work.github-issues +++ b/.vscode/notebooks/my-work.github-issues @@ -7,7 +7,7 @@ { "kind": 2, "language": "github-issues", - "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce\n\n// current milestone name\n$MILESTONE=milestone:\"October 2025\"\n" + "value": "// list of repos we work in\n$REPOS=repo:microsoft/lsprotocol repo:microsoft/monaco-editor repo:microsoft/vscode repo:microsoft/vscode-anycode repo:microsoft/vscode-autopep8 repo:microsoft/vscode-black-formatter repo:microsoft/vscode-copilot repo:microsoft/vscode-copilot-release repo:microsoft/vscode-dev repo:microsoft/vscode-dev-chrome-launcher repo:microsoft/vscode-emmet-helper repo:microsoft/vscode-extension-telemetry repo:microsoft/vscode-flake8 repo:microsoft/vscode-github-issue-notebooks repo:microsoft/vscode-hexeditor repo:microsoft/vscode-internalbacklog repo:microsoft/vscode-isort repo:microsoft/vscode-js-debug repo:microsoft/vscode-jupyter repo:microsoft/vscode-jupyter-internal repo:microsoft/vscode-l10n repo:microsoft/vscode-livepreview repo:microsoft/vscode-markdown-languageservice repo:microsoft/vscode-markdown-tm-grammar repo:microsoft/vscode-mypy repo:microsoft/vscode-pull-request-github repo:microsoft/vscode-pylint repo:microsoft/vscode-python repo:microsoft/vscode-python-debugger repo:microsoft/vscode-python-tools-extension-template repo:microsoft/vscode-references-view repo:microsoft/vscode-remote-release repo:microsoft/vscode-remote-repositories-github repo:microsoft/vscode-remote-tunnels repo:microsoft/vscode-remotehub repo:microsoft/vscode-settings-sync-server repo:microsoft/vscode-unpkg repo:microsoft/vscode-vsce repo:microsoft/vscode-copilot-issues repo:microsoft/vscode-extension-samples\n\n// current milestone name\n$MILESTONE=milestone:\"January 2026\"\n" }, { "kind": 1, diff --git a/.vscode/searches/no-any-casts.code-search b/.vscode/searches/no-any-casts.code-search index ff6b2a40f13..c430ea202fc 100644 --- a/.vscode/searches/no-any-casts.code-search +++ b/.vscode/searches/no-any-casts.code-search @@ -1,230 +1,144 @@ -# Query: // eslint-disable-next-line local/code-no-any-casts +# Query: // eslint-disable-next-line (local/code-no-any-casts|@typescript-eslint/no-explicit-any) +# Flags: RegExp -785 results - 287 files +727 results - 269 files -vscode • extensions/css-language-features/client/src/cssClient.ts: - 86: // eslint-disable-next-line local/code-no-any-casts +.eslint-plugin-local/code-policy-localization-key-match.ts: + 123: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/css-language-features/server/src/cssServer.ts: - 71: // eslint-disable-next-line local/code-no-any-casts - 74: // eslint-disable-next-line local/code-no-any-casts - 171: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/git-base/src/api/api1.ts: - 17: // eslint-disable-next-line local/code-no-any-casts - 38: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/html-language-features/client/src/htmlClient.ts: - 182: // eslint-disable-next-line local/code-no-any-casts +build/gulpfile.reh.ts: + 187: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/html-language-features/server/src/htmlServer.ts: - 137: // eslint-disable-next-line local/code-no-any-casts - 140: // eslint-disable-next-line local/code-no-any-casts - 545: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/ipynb/src/deserializers.ts: - 23: // eslint-disable-next-line local/code-no-any-casts - 294: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/ipynb/src/helper.ts: - 14: // eslint-disable-next-line local/code-no-any-casts - 18: // eslint-disable-next-line local/code-no-any-casts - 20: // eslint-disable-next-line local/code-no-any-casts - 22: // eslint-disable-next-line local/code-no-any-casts - 25: // eslint-disable-next-line local/code-no-any-casts +extensions/html-language-features/server/src/htmlServer.ts: + 544: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/ipynb/src/serializers.ts: - 40: // eslint-disable-next-line local/code-no-any-casts - 61: // eslint-disable-next-line local/code-no-any-casts - 403: // eslint-disable-next-line local/code-no-any-casts - 405: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/ipynb/src/test/notebookModelStoreSync.test.ts: - 40: // eslint-disable-next-line local/code-no-any-casts - 79: // eslint-disable-next-line local/code-no-any-casts - 109: // eslint-disable-next-line local/code-no-any-casts - 141: // eslint-disable-next-line local/code-no-any-casts - 176: // eslint-disable-next-line local/code-no-any-casts - 213: // eslint-disable-next-line local/code-no-any-casts - 251: // eslint-disable-next-line local/code-no-any-casts - 274: // eslint-disable-next-line local/code-no-any-casts - 303: // eslint-disable-next-line local/code-no-any-casts - 347: // eslint-disable-next-line local/code-no-any-casts - 371: // eslint-disable-next-line local/code-no-any-casts - 400: // eslint-disable-next-line local/code-no-any-casts - 424: // eslint-disable-next-line local/code-no-any-casts - 459: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/json-language-features/client/src/jsonClient.ts: - 775: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/json-language-features/server/src/jsonServer.ts: - 144: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/markdown-language-features/notebook/index.ts: +extensions/markdown-language-features/notebook/index.ts: 383: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/markdown-language-features/preview-src/index.ts: +extensions/markdown-language-features/preview-src/index.ts: 26: // eslint-disable-next-line local/code-no-any-casts 253: // eslint-disable-next-line local/code-no-any-casts 444: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/markdown-language-features/src/markdownEngine.ts: +extensions/markdown-language-features/src/markdownEngine.ts: 146: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/markdown-language-features/src/languageFeatures/diagnostics.ts: +extensions/markdown-language-features/src/languageFeatures/diagnostics.ts: 54: // eslint-disable-next-line local/code-no-any-casts -vscode • extensions/notebook-renderers/src/index.ts: - 68: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/notebook-renderers/src/test/notebookRenderer.test.ts: - 130: // eslint-disable-next-line local/code-no-any-casts - 137: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/vscode-api-tests/src/extension.ts: - 10: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts: - 181: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/vscode-api-tests/src/singlefolder-tests/proxy.test.ts: - 59: // eslint-disable-next-line local/code-no-any-casts - 76: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/vscode-api-tests/src/singlefolder-tests/state.test.ts: - 16: // eslint-disable-next-line local/code-no-any-casts - -vscode • extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts: - 18: // eslint-disable-next-line local/code-no-any-casts - -vscode • scripts/playground-server.ts: +scripts/playground-server.ts: 257: // eslint-disable-next-line local/code-no-any-casts 336: // eslint-disable-next-line local/code-no-any-casts 352: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/browser/dom.ts: +src/vs/base/browser/dom.ts: 718: // eslint-disable-next-line local/code-no-any-casts - 1324: // eslint-disable-next-line local/code-no-any-casts - 1519: // eslint-disable-next-line local/code-no-any-casts - 1659: // eslint-disable-next-line local/code-no-any-casts - 2012: // eslint-disable-next-line local/code-no-any-casts - 2115: // eslint-disable-next-line local/code-no-any-casts - 2127: // eslint-disable-next-line local/code-no-any-casts - 2290: // eslint-disable-next-line local/code-no-any-casts - 2296: // eslint-disable-next-line local/code-no-any-casts - 2324: // eslint-disable-next-line local/code-no-any-casts - 2436: // eslint-disable-next-line local/code-no-any-casts - 2443: // eslint-disable-next-line local/code-no-any-casts - 2528: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/browser/mouseEvent.ts: + 1325: // eslint-disable-next-line local/code-no-any-casts + 1520: // eslint-disable-next-line local/code-no-any-casts + 1660: // eslint-disable-next-line local/code-no-any-casts + 2013: // eslint-disable-next-line local/code-no-any-casts + 2116: // eslint-disable-next-line local/code-no-any-casts + 2128: // eslint-disable-next-line local/code-no-any-casts + 2291: // eslint-disable-next-line local/code-no-any-casts + 2297: // eslint-disable-next-line local/code-no-any-casts + 2325: // eslint-disable-next-line local/code-no-any-casts + 2437: // eslint-disable-next-line local/code-no-any-casts + 2444: // eslint-disable-next-line local/code-no-any-casts + 2566: // eslint-disable-next-line local/code-no-any-casts + +src/vs/base/browser/mouseEvent.ts: 100: // eslint-disable-next-line local/code-no-any-casts 138: // eslint-disable-next-line local/code-no-any-casts 155: // eslint-disable-next-line local/code-no-any-casts 157: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/browser/trustedTypes.ts: - 21: // eslint-disable-next-line local/code-no-any-casts - 33: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/browser/webWorkerFactory.ts: - 20: // eslint-disable-next-line local/code-no-any-casts - 22: // eslint-disable-next-line local/code-no-any-casts - 43: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/browser/trustedTypes.ts: + 27: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/base/browser/ui/grid/grid.ts: +src/vs/base/browser/ui/grid/grid.ts: 66: // eslint-disable-next-line local/code-no-any-casts 873: // eslint-disable-next-line local/code-no-any-casts 875: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/browser/ui/grid/gridview.ts: +src/vs/base/browser/ui/grid/gridview.ts: 196: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/browser/ui/sash/sash.ts: +src/vs/base/browser/ui/sash/sash.ts: 491: // eslint-disable-next-line local/code-no-any-casts 497: // eslint-disable-next-line local/code-no-any-casts 503: // eslint-disable-next-line local/code-no-any-casts 505: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/console.ts: +src/vs/base/common/console.ts: 134: // eslint-disable-next-line local/code-no-any-casts 138: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/controlFlow.ts: - 57: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/common/decorators.ts: +src/vs/base/common/decorators.ts: 57: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/errors.ts: +src/vs/base/common/errors.ts: 142: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/hotReload.ts: - 97: // eslint-disable-next-line local/code-no-any-casts - 104: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/common/hotReload.ts: + 102: // eslint-disable-next-line local/code-no-any-casts + 109: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/hotReloadHelpers.ts: +src/vs/base/common/hotReloadHelpers.ts: 39: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/lifecycle.ts: - 236: // eslint-disable-next-line local/code-no-any-casts - 246: // eslint-disable-next-line local/code-no-any-casts - 257: // eslint-disable-next-line local/code-no-any-casts - 317: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/common/lifecycle.ts: + 239: // eslint-disable-next-line local/code-no-any-casts + 249: // eslint-disable-next-line local/code-no-any-casts + 260: // eslint-disable-next-line local/code-no-any-casts + 320: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/marshalling.ts: +src/vs/base/common/marshalling.ts: 53: // eslint-disable-next-line local/code-no-any-casts 55: // eslint-disable-next-line local/code-no-any-casts 57: // eslint-disable-next-line local/code-no-any-casts 65: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/network.ts: - 416: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/common/strings.ts: + 26: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/base/common/skipList.ts: - 38: // eslint-disable-next-line local/code-no-any-casts - 47: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/common/types.ts: +src/vs/base/common/types.ts: 65: // eslint-disable-next-line local/code-no-any-casts 73: // eslint-disable-next-line local/code-no-any-casts 275: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/uriIpc.ts: - 33: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/common/validation.ts: + 149: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 165: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 285: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/base/common/verifier.ts: +src/vs/base/common/verifier.ts: 82: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/changeTracker.ts: +src/vs/base/common/marked/marked.js: + 2344: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/base/common/observableInternal/changeTracker.ts: 34: // eslint-disable-next-line local/code-no-any-casts 42: // eslint-disable-next-line local/code-no-any-casts 69: // eslint-disable-next-line local/code-no-any-casts 80: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/debugLocation.ts: - 19: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/common/observableInternal/debugName.ts: - 106: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/base/common/observableInternal/set.ts: +src/vs/base/common/observableInternal/set.ts: 51: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/experimental/reducer.ts: +src/vs/base/common/observableInternal/experimental/reducer.ts: 39: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts: +src/vs/base/common/observableInternal/logging/consoleObservableLogger.ts: 80: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/logging/debugger/debuggerRpc.ts: +src/vs/base/common/observableInternal/logging/debugger/debuggerRpc.ts: 12: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/logging/debugger/rpc.ts: +src/vs/base/common/observableInternal/logging/debugger/rpc.ts: 94: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/observables/derived.ts: +src/vs/base/common/observableInternal/observables/derived.ts: 38: // eslint-disable-next-line local/code-no-any-casts 40: // eslint-disable-next-line local/code-no-any-casts 124: // eslint-disable-next-line local/code-no-any-casts @@ -232,127 +146,138 @@ vscode • src/vs/base/common/observableInternal/observables/derived.ts: 160: // eslint-disable-next-line local/code-no-any-casts 165: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/observables/derivedImpl.ts: +src/vs/base/common/observableInternal/observables/derivedImpl.ts: 313: // eslint-disable-next-line local/code-no-any-casts - 414: // eslint-disable-next-line local/code-no-any-casts + 412: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/observables/observableFromEvent.ts: +src/vs/base/common/observableInternal/observables/observableFromEvent.ts: 151: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/reactions/autorunImpl.ts: +src/vs/base/common/observableInternal/reactions/autorunImpl.ts: 185: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/common/observableInternal/utils/utilsCancellation.ts: +src/vs/base/common/observableInternal/utils/utilsCancellation.ts: 78: // eslint-disable-next-line local/code-no-any-casts 83: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/parts/ipc/test/node/ipc.net.test.ts: +src/vs/base/common/worker/webWorker.ts: + 430: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/base/parts/ipc/test/node/ipc.net.test.ts: 87: // eslint-disable-next-line local/code-no-any-casts 92: // eslint-disable-next-line local/code-no-any-casts 652: // eslint-disable-next-line local/code-no-any-casts 785: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/buffer.test.ts: - 501: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/test/common/buffer.test.ts: + 515: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/decorators.test.ts: +src/vs/base/test/common/decorators.test.ts: 130: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/filters.test.ts: +src/vs/base/test/common/filters.test.ts: 28: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/glob.test.ts: +src/vs/base/test/common/glob.test.ts: 497: // eslint-disable-next-line local/code-no-any-casts 518: // eslint-disable-next-line local/code-no-any-casts 763: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/json.test.ts: +src/vs/base/test/common/json.test.ts: 52: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/mock.ts: +src/vs/base/test/common/mock.ts: 14: // eslint-disable-next-line local/code-no-any-casts 23: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/oauth.test.ts: +src/vs/base/test/common/oauth.test.ts: 1100: // eslint-disable-next-line local/code-no-any-casts - 1572: // eslint-disable-next-line local/code-no-any-casts + 1743: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/snapshot.ts: +src/vs/base/test/common/snapshot.ts: 123: // eslint-disable-next-line local/code-no-any-casts 125: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/timeTravelScheduler.ts: - 268: // eslint-disable-next-line local/code-no-any-casts - 278: // eslint-disable-next-line local/code-no-any-casts - 311: // eslint-disable-next-line local/code-no-any-casts - 317: // eslint-disable-next-line local/code-no-any-casts - 333: // eslint-disable-next-line local/code-no-any-casts +src/vs/base/test/common/timeTravelScheduler.ts: + 276: // eslint-disable-next-line local/code-no-any-casts + 286: // eslint-disable-next-line local/code-no-any-casts + 319: // eslint-disable-next-line local/code-no-any-casts + 325: // eslint-disable-next-line local/code-no-any-casts + 341: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/base/test/common/troubleshooting.ts: +src/vs/base/test/common/troubleshooting.ts: 50: // eslint-disable-next-line local/code-no-any-casts 55: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/editor.api.ts: - 44: // eslint-disable-next-line local/code-no-any-casts - 46: // eslint-disable-next-line local/code-no-any-casts - 51: // eslint-disable-next-line local/code-no-any-casts - 53: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/editor/browser/config/editorConfiguration.ts: +src/vs/editor/browser/config/editorConfiguration.ts: 147: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/controller/mouseTarget.ts: - 992: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 996: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 1000: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 1043: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 1096: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 1099: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any - 1119: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any +src/vs/editor/browser/controller/mouseTarget.ts: + 993: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 997: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 1001: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 1044: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 1097: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 1100: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 1120: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts: +src/vs/editor/browser/controller/editContext/native/nativeEditContextUtils.ts: 81: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 85: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/gpu/gpuUtils.ts: +src/vs/editor/browser/gpu/gpuUtils.ts: 52: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/gpu/viewGpuContext.ts: +src/vs/editor/browser/gpu/viewGpuContext.ts: 226: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts: +src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts: + 625: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts: 179: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts: - 477: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any +src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts: + 480: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/widget/diffEditor/utils.ts: +src/vs/editor/browser/widget/diffEditor/utils.ts: 184: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 192: // eslint-disable-next-line @typescript-eslint/no-explicit-any 195: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 303: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 310: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts: +src/vs/editor/browser/widget/diffEditor/components/diffEditorEditors.ts: 75: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts: +src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts: 100: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 103: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/common/textModelEditSource.ts: +src/vs/editor/common/textModelEditSource.ts: 59: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 68: // eslint-disable-next-line @typescript-eslint/no-explicit-any 70: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/common/core/edits/stringEdit.ts: - 24: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any +src/vs/editor/common/config/editorOptions.ts: + 6812: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/editor/common/core/edits/edit.ts: + 10: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts: +src/vs/editor/common/core/edits/stringEdit.ts: + 12: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 24: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 193: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts: 26: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 30: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 51: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 56: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 64: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 72: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 80: // eslint-disable-next-line @typescript-eslint/no-explicit-any 99: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 101: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 126: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any @@ -365,90 +290,94 @@ vscode • src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree 177: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any 196: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/editor/contrib/colorPicker/browser/colorDetector.ts: +src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet.ts: + 13: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 30: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/editor/contrib/colorPicker/browser/colorDetector.ts: 100: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts: +src/vs/editor/contrib/documentSymbols/test/browser/outlineModel.test.ts: 200: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/editorState/test/browser/editorState.test.ts: +src/vs/editor/contrib/editorState/test/browser/editorState.test.ts: 97: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/find/browser/findModel.ts: +src/vs/editor/contrib/find/browser/findModel.ts: 556: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/find/test/browser/findController.test.ts: +src/vs/editor/contrib/find/test/browser/findController.test.ts: 79: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts: +src/vs/editor/contrib/inlineCompletions/browser/structuredLogger.ts: 56: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts: - 640: // eslint-disable-next-line local/code-no-any-casts +src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts: + 644: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts: - 274: // eslint-disable-next-line local/code-no-any-casts - 296: // eslint-disable-next-line local/code-no-any-casts +src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts: + 173: // eslint-disable-next-line local/code-no-any-casts + 195: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts: - 346: // eslint-disable-next-line local/code-no-any-casts +src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.ts: + 505: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts: +src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts: 15: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts: +src/vs/editor/contrib/inlineCompletions/test/browser/computeGhostText.test.ts: 23: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts: - 163: // eslint-disable-next-line local/code-no-any-casts +src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts: + 164: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts: - 240: // eslint-disable-next-line local/code-no-any-casts +src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts: + 244: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts: +src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts: 794: // eslint-disable-next-line local/code-no-any-casts 813: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/standalone/browser/standaloneEditor.ts: - 504: // eslint-disable-next-line local/code-no-any-casts - 506: // eslint-disable-next-line local/code-no-any-casts - 508: // eslint-disable-next-line local/code-no-any-casts - 510: // eslint-disable-next-line local/code-no-any-casts - 512: // eslint-disable-next-line local/code-no-any-casts - 514: // eslint-disable-next-line local/code-no-any-casts - 517: // eslint-disable-next-line local/code-no-any-casts - 519: // eslint-disable-next-line local/code-no-any-casts - 521: // eslint-disable-next-line local/code-no-any-casts - 523: // eslint-disable-next-line local/code-no-any-casts - 526: // eslint-disable-next-line local/code-no-any-casts - 528: // eslint-disable-next-line local/code-no-any-casts - 530: // eslint-disable-next-line local/code-no-any-casts - 532: // eslint-disable-next-line local/code-no-any-casts - 535: // eslint-disable-next-line local/code-no-any-casts - 537: // eslint-disable-next-line local/code-no-any-casts - 539: // eslint-disable-next-line local/code-no-any-casts - 541: // eslint-disable-next-line local/code-no-any-casts - 543: // eslint-disable-next-line local/code-no-any-casts - 545: // eslint-disable-next-line local/code-no-any-casts - 549: // eslint-disable-next-line local/code-no-any-casts - 551: // eslint-disable-next-line local/code-no-any-casts - 553: // eslint-disable-next-line local/code-no-any-casts - 555: // eslint-disable-next-line local/code-no-any-casts - 557: // eslint-disable-next-line local/code-no-any-casts - 559: // eslint-disable-next-line local/code-no-any-casts - 561: // eslint-disable-next-line local/code-no-any-casts - 567: // eslint-disable-next-line local/code-no-any-casts - 599: // eslint-disable-next-line local/code-no-any-casts - 601: // eslint-disable-next-line local/code-no-any-casts - 603: // eslint-disable-next-line local/code-no-any-casts - 605: // eslint-disable-next-line local/code-no-any-casts - 607: // eslint-disable-next-line local/code-no-any-casts - 609: // eslint-disable-next-line local/code-no-any-casts - 611: // eslint-disable-next-line local/code-no-any-casts - 614: // eslint-disable-next-line local/code-no-any-casts - 619: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/editor/standalone/browser/standaloneLanguages.ts: +src/vs/editor/standalone/browser/standaloneEditor.ts: + 505: // eslint-disable-next-line local/code-no-any-casts + 507: // eslint-disable-next-line local/code-no-any-casts + 509: // eslint-disable-next-line local/code-no-any-casts + 511: // eslint-disable-next-line local/code-no-any-casts + 513: // eslint-disable-next-line local/code-no-any-casts + 515: // eslint-disable-next-line local/code-no-any-casts + 518: // eslint-disable-next-line local/code-no-any-casts + 520: // eslint-disable-next-line local/code-no-any-casts + 522: // eslint-disable-next-line local/code-no-any-casts + 524: // eslint-disable-next-line local/code-no-any-casts + 527: // eslint-disable-next-line local/code-no-any-casts + 529: // eslint-disable-next-line local/code-no-any-casts + 531: // eslint-disable-next-line local/code-no-any-casts + 533: // eslint-disable-next-line local/code-no-any-casts + 536: // eslint-disable-next-line local/code-no-any-casts + 538: // eslint-disable-next-line local/code-no-any-casts + 540: // eslint-disable-next-line local/code-no-any-casts + 542: // eslint-disable-next-line local/code-no-any-casts + 544: // eslint-disable-next-line local/code-no-any-casts + 546: // eslint-disable-next-line local/code-no-any-casts + 550: // eslint-disable-next-line local/code-no-any-casts + 552: // eslint-disable-next-line local/code-no-any-casts + 554: // eslint-disable-next-line local/code-no-any-casts + 556: // eslint-disable-next-line local/code-no-any-casts + 558: // eslint-disable-next-line local/code-no-any-casts + 560: // eslint-disable-next-line local/code-no-any-casts + 562: // eslint-disable-next-line local/code-no-any-casts + 568: // eslint-disable-next-line local/code-no-any-casts + 600: // eslint-disable-next-line local/code-no-any-casts + 602: // eslint-disable-next-line local/code-no-any-casts + 604: // eslint-disable-next-line local/code-no-any-casts + 606: // eslint-disable-next-line local/code-no-any-casts + 608: // eslint-disable-next-line local/code-no-any-casts + 610: // eslint-disable-next-line local/code-no-any-casts + 612: // eslint-disable-next-line local/code-no-any-casts + 615: // eslint-disable-next-line local/code-no-any-casts + 620: // eslint-disable-next-line local/code-no-any-casts + +src/vs/editor/standalone/browser/standaloneLanguages.ts: 753: // eslint-disable-next-line local/code-no-any-casts 755: // eslint-disable-next-line local/code-no-any-casts 757: // eslint-disable-next-line local/code-no-any-casts @@ -487,157 +416,164 @@ vscode • src/vs/editor/standalone/browser/standaloneLanguages.ts: 849: // eslint-disable-next-line local/code-no-any-casts 851: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/standalone/common/monarch/monarchCompile.ts: +src/vs/editor/standalone/common/monarch/monarchCompile.ts: 461: // eslint-disable-next-line local/code-no-any-casts 539: // eslint-disable-next-line local/code-no-any-casts 556: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/test/browser/testCodeEditor.ts: +src/vs/editor/test/browser/testCodeEditor.ts: 279: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/test/browser/config/editorConfiguration.test.ts: +src/vs/editor/test/browser/config/editorConfiguration.test.ts: 90: // eslint-disable-next-line local/code-no-any-casts 99: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/test/common/model/textModel.test.ts: +src/vs/editor/test/common/model/textModel.test.ts: 1167: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/editor/test/common/model/textModelWithTokens.test.ts: +src/vs/editor/test/common/model/textModelWithTokens.test.ts: 272: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/contextkey/common/contextkey.ts: - 939: // eslint-disable-next-line local/code-no-any-casts - 1213: // eslint-disable-next-line local/code-no-any-casts - 1273: // eslint-disable-next-line local/code-no-any-casts - 1334: // eslint-disable-next-line local/code-no-any-casts - 1395: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/contextkey/common/contextkey.ts: + 939: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/contextkey/test/common/contextkey.test.ts: +src/vs/platform/contextkey/test/common/contextkey.test.ts: 96: // eslint-disable-next-line local/code-no-any-casts 98: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/environment/test/node/argv.test.ts: +src/vs/platform/domWidget/browser/domWidget.ts: + 132: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 152: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/platform/environment/test/node/argv.test.ts: 47: // eslint-disable-next-line local/code-no-any-casts 59: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/extensionManagement/common/extensionManagementIpc.ts: - 243: // eslint-disable-next-line local/code-no-any-casts - 245: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/extensionManagement/common/extensionGalleryManifestServiceIpc.ts: + 37: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts: - 405: // eslint-disable-next-line local/code-no-any-casts - 407: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/extensionManagement/common/extensionManagementIpc.ts: + 64: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 113: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 348: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 353: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/extensionManagement/common/implicitActivationEvents.ts: - 73: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts: + 36: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 41: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/files/browser/htmlFileSystemProvider.ts: +src/vs/platform/files/browser/htmlFileSystemProvider.ts: 311: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/platform/files/test/node/diskFileService.integrationTest.ts: +src/vs/platform/files/test/node/diskFileService.integrationTest.ts: 106: // eslint-disable-next-line local/code-no-any-casts 109: // eslint-disable-next-line local/code-no-any-casts 112: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/instantiation/common/instantiationService.ts: - 328: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/instantiation/common/instantiationService.ts: + 321: // eslint-disable-next-line local/code-no-any-casts + +src/vs/platform/ipc/electron-browser/services.ts: + 13: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/list/browser/listService.ts: +src/vs/platform/list/browser/listService.ts: 877: // eslint-disable-next-line local/code-no-any-casts 918: // eslint-disable-next-line local/code-no-any-casts 965: // eslint-disable-next-line local/code-no-any-casts 1012: // eslint-disable-next-line local/code-no-any-casts 1057: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/observable/common/wrapInHotClass.ts: +src/vs/platform/observable/common/wrapInHotClass.ts: 12: // eslint-disable-next-line local/code-no-any-casts 40: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/observable/common/wrapInReloadableClass.ts: +src/vs/platform/observable/common/wrapInReloadableClass.ts: 31: // eslint-disable-next-line local/code-no-any-casts - 38: // eslint-disable-next-line local/code-no-any-casts - 59: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/platform/policy/node/nativePolicyService.ts: - 47: // eslint-disable-next-line local/code-no-any-casts + 58: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/profiling/common/profilingTelemetrySpec.ts: +src/vs/platform/profiling/common/profilingTelemetrySpec.ts: 73: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/quickinput/browser/tree/quickTree.ts: - 74: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/quickinput/browser/tree/quickTree.ts: + 82: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + +src/vs/platform/quickinput/common/quickAccess.ts: + 172: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/quickinput/test/browser/quickinput.test.ts: +src/vs/platform/quickinput/test/browser/quickinput.test.ts: 69: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/remote/browser/browserSocketFactory.ts: +src/vs/platform/remote/browser/browserSocketFactory.ts: 89: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/remote/common/remoteAgentConnection.ts: - 784: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/remote/common/remoteAgentConnection.ts: + 801: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/remote/test/electron-browser/remoteAuthorityResolverService.test.ts: +src/vs/platform/remote/test/electron-browser/remoteAuthorityResolverService.test.ts: 19: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/request/electron-utility/requestService.ts: +src/vs/platform/request/electron-utility/requestService.ts: 15: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/platform/terminal/node/terminalProcess.ts: - 546: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/storage/electron-main/storageIpc.ts: + 74: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 102: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/platform/userDataSync/common/extensionsSync.ts: - 60: // eslint-disable-next-line local/code-no-any-casts - 64: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/terminal/node/terminalProcess.ts: + 547: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts: +src/vs/platform/webContentExtractor/test/electron-main/cdpAccessibilityDomain.test.ts: 22: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/server/node/extensionHostConnection.ts: - 240: // eslint-disable-next-line local/code-no-any-casts +src/vs/platform/webContentExtractor/test/electron-main/webPageLoader.test.ts: + 95: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/server/node/remoteExtensionHostAgentServer.ts: - 765: // eslint-disable-next-line local/code-no-any-casts +src/vs/server/node/extensionHostConnection.ts: + 243: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + +src/vs/server/node/remoteExtensionHostAgentServer.ts: 767: // eslint-disable-next-line local/code-no-any-casts 769: // eslint-disable-next-line local/code-no-any-casts + 771: // eslint-disable-next-line local/code-no-any-casts + +src/vs/server/node/remoteTerminalChannel.ts: + 112: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/workbench.web.main.internal.ts: - 198: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/workbench.web.main.internal.ts: + 196: // eslint-disable-next-line local/code-no-any-casts + 221: // eslint-disable-next-line local/code-no-any-casts 223: // eslint-disable-next-line local/code-no-any-casts - 225: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/workbench.web.main.ts: +src/vs/workbench/workbench.web.main.ts: 58: // eslint-disable-next-line local/code-no-any-casts 60: // eslint-disable-next-line local/code-no-any-casts 82: // eslint-disable-next-line local/code-no-any-casts 91: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/browser/mainThreadExtensionService.ts: - 57: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/browser/mainThreadExtensionService.ts: + 57: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts: - 1013: // eslint-disable-next-line local/code-no-any-casts - 1024: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts: + 912: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 923: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/api/browser/mainThreadQuickOpen.ts: - 195: // eslint-disable-next-line local/code-no-any-casts - 198: // eslint-disable-next-line local/code-no-any-casts - 203: // eslint-disable-next-line local/code-no-any-casts - 216: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/browser/mainThreadQuickOpen.ts: + 242: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/api/browser/viewsExtensionPoint.ts: - 528: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/browser/viewsExtensionPoint.ts: + 545: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/api/common/extHost.api.impl.ts: - 161: // eslint-disable-next-line local/code-no-any-casts - 315: // eslint-disable-next-line local/code-no-any-casts - 324: // eslint-disable-next-line local/code-no-any-casts - 563: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHost.api.impl.ts: + 162: // eslint-disable-next-line local/code-no-any-casts + 317: // eslint-disable-next-line local/code-no-any-casts + 326: // eslint-disable-next-line local/code-no-any-casts + 565: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHost.protocol.ts: - 2109: // eslint-disable-next-line local/code-no-any-casts - 2111: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHost.protocol.ts: + 2209: // eslint-disable-next-line local/code-no-any-casts + 2211: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostDebugService.ts: +src/vs/workbench/api/common/extHostDebugService.ts: 243: // eslint-disable-next-line local/code-no-any-casts 491: // eslint-disable-next-line local/code-no-any-casts 493: // eslint-disable-next-line local/code-no-any-casts @@ -646,99 +582,91 @@ vscode • src/vs/workbench/api/common/extHostDebugService.ts: 770: // eslint-disable-next-line local/code-no-any-casts 778: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts: - 65: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts: + 114: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/api/common/extHostExtensionActivator.ts: +src/vs/workbench/api/common/extHostExtensionActivator.ts: 405: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostExtensionService.ts: +src/vs/workbench/api/common/extHostExtensionService.ts: 566: // eslint-disable-next-line local/code-no-any-casts 1009: // eslint-disable-next-line local/code-no-any-casts 1050: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostLanguageFeatures.ts: +src/vs/workbench/api/common/extHostLanguageFeatures.ts: 197: // eslint-disable-next-line local/code-no-any-casts 714: // eslint-disable-next-line local/code-no-any-casts 735: // eslint-disable-next-line local/code-no-any-casts 748: // eslint-disable-next-line local/code-no-any-casts 771: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostLanguageModels.ts: - 175: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/api/common/extHostLanguageModelTools.ts: +src/vs/workbench/api/common/extHostLanguageModelTools.ts: 221: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostMcp.ts: - 163: // eslint-disable-next-line local/code-no-any-casts - 165: // eslint-disable-next-line local/code-no-any-casts - 168: // eslint-disable-next-line local/code-no-any-casts - 170: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHostMcp.ts: + 211: // eslint-disable-next-line local/code-no-any-casts + 213: // eslint-disable-next-line local/code-no-any-casts + 216: // eslint-disable-next-line local/code-no-any-casts + 218: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostSearch.ts: +src/vs/workbench/api/common/extHostSearch.ts: 221: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostTerminalService.ts: - 1287: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/api/common/extHostTimeline.ts: +src/vs/workbench/api/common/extHostTimeline.ts: 160: // eslint-disable-next-line local/code-no-any-casts 163: // eslint-disable-next-line local/code-no-any-casts 166: // eslint-disable-next-line local/code-no-any-casts 169: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostTypeConverters.ts: - 463: // eslint-disable-next-line local/code-no-any-casts - 856: // eslint-disable-next-line local/code-no-any-casts - 3173: // eslint-disable-next-line local/code-no-any-casts - 3175: // eslint-disable-next-line local/code-no-any-casts - 3177: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHostTypeConverters.ts: + 465: // eslint-disable-next-line local/code-no-any-casts + 858: // eslint-disable-next-line local/code-no-any-casts 3179: // eslint-disable-next-line local/code-no-any-casts 3181: // eslint-disable-next-line local/code-no-any-casts 3183: // eslint-disable-next-line local/code-no-any-casts 3185: // eslint-disable-next-line local/code-no-any-casts 3187: // eslint-disable-next-line local/code-no-any-casts - 3194: // eslint-disable-next-line local/code-no-any-casts + 3189: // eslint-disable-next-line local/code-no-any-casts + 3191: // eslint-disable-next-line local/code-no-any-casts + 3193: // eslint-disable-next-line local/code-no-any-casts + 3195: // eslint-disable-next-line local/code-no-any-casts + 3202: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/common/extHostTypes.ts: - 3175: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/common/extHostTypes.ts: + 3190: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/node/extensionHostProcess.ts: - 107: // eslint-disable-next-line local/code-no-any-casts - 119: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/node/extensionHostProcess.ts: + 108: // eslint-disable-next-line local/code-no-any-casts + 120: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/node/extHostConsoleForwarder.ts: +src/vs/workbench/api/node/extHostConsoleForwarder.ts: 31: // eslint-disable-next-line local/code-no-any-casts 53: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/node/extHostMcpNode.ts: +src/vs/workbench/api/node/extHostMcpNode.ts: 57: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/node/proxyResolver.ts: - 92: // eslint-disable-next-line local/code-no-any-casts - 95: // eslint-disable-next-line local/code-no-any-casts - 103: // eslint-disable-next-line local/code-no-any-casts - 126: // eslint-disable-next-line local/code-no-any-casts - 129: // eslint-disable-next-line local/code-no-any-casts - 132: // eslint-disable-next-line local/code-no-any-casts - 373: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/node/proxyResolver.ts: + 113: // eslint-disable-next-line local/code-no-any-casts + 136: // eslint-disable-next-line local/code-no-any-casts + 139: // eslint-disable-next-line local/code-no-any-casts + 142: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostApiCommands.test.ts: +src/vs/workbench/api/test/browser/extHostApiCommands.test.ts: 874: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts: +src/vs/workbench/api/test/browser/extHostAuthentication.integrationTest.ts: 75: // eslint-disable-next-line local/code-no-any-casts 164: // eslint-disable-next-line local/code-no-any-casts 173: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostCommands.test.ts: +src/vs/workbench/api/test/browser/extHostCommands.test.ts: 92: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostConfiguration.test.ts: +src/vs/workbench/api/test/browser/extHostConfiguration.test.ts: 750: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostDocumentData.test.ts: +src/vs/workbench/api/test/browser/extHostDocumentData.test.ts: 46: // eslint-disable-next-line local/code-no-any-casts 48: // eslint-disable-next-line local/code-no-any-casts 50: // eslint-disable-next-line local/code-no-any-casts @@ -746,17 +674,17 @@ vscode • src/vs/workbench/api/test/browser/extHostDocumentData.test.ts: 54: // eslint-disable-next-line local/code-no-any-casts 56: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts: +src/vs/workbench/api/test/browser/extHostDocumentSaveParticipant.test.ts: 84: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts: +src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts: 1068: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts: +src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts: 164: // eslint-disable-next-line local/code-no-any-casts 166: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostTelemetry.test.ts: +src/vs/workbench/api/test/browser/extHostTelemetry.test.ts: 107: // eslint-disable-next-line local/code-no-any-casts 109: // eslint-disable-next-line local/code-no-any-casts 111: // eslint-disable-next-line local/code-no-any-casts @@ -764,55 +692,63 @@ vscode • src/vs/workbench/api/test/browser/extHostTelemetry.test.ts: 121: // eslint-disable-next-line local/code-no-any-casts 128: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostTesting.test.ts: +src/vs/workbench/api/test/browser/extHostTesting.test.ts: 640: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostTextEditor.test.ts: +src/vs/workbench/api/test/browser/extHostTextEditor.test.ts: 265: // eslint-disable-next-line local/code-no-any-casts 290: // eslint-disable-next-line local/code-no-any-casts 327: // eslint-disable-next-line local/code-no-any-casts 340: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostTypes.test.ts: +src/vs/workbench/api/test/browser/extHostTypes.test.ts: 87: // eslint-disable-next-line local/code-no-any-casts 89: // eslint-disable-next-line local/code-no-any-casts 91: // eslint-disable-next-line local/code-no-any-casts 209: // eslint-disable-next-line local/code-no-any-casts 211: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/extHostWorkspace.test.ts: +src/vs/workbench/api/test/browser/extHostWorkspace.test.ts: 541: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/mainThreadAuthentication.integrationTest.ts: - 115: // eslint-disable-next-line local/code-no-any-casts - 122: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/test/browser/mainThreadAuthentication.integrationTest.ts: + 119: // eslint-disable-next-line local/code-no-any-casts + 126: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts: +src/vs/workbench/api/test/browser/mainThreadDocumentsAndEditors.test.ts: 86: // eslint-disable-next-line local/code-no-any-casts 93: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/mainThreadEditors.test.ts: - 115: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/api/test/browser/mainThreadEditors.test.ts: + 130: // eslint-disable-next-line local/code-no-any-casts + 137: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts: +src/vs/workbench/api/test/browser/mainThreadTreeViews.test.ts: 60: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/common/extensionHostMain.test.ts: +src/vs/workbench/api/test/common/extensionHostMain.test.ts: 80: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts: +src/vs/workbench/api/test/common/extHostTerminalShellIntegration.test.ts: 86: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/common/testRPCProtocol.ts: +src/vs/workbench/api/test/common/extHostTypeConverters.test.ts: + 34: // eslint-disable-next-line local/code-no-any-casts + 43: // eslint-disable-next-line local/code-no-any-casts + 66: // eslint-disable-next-line local/code-no-any-casts + 78: // eslint-disable-next-line local/code-no-any-casts + 85: // eslint-disable-next-line local/code-no-any-casts + +src/vs/workbench/api/test/common/testRPCProtocol.ts: 36: // eslint-disable-next-line local/code-no-any-casts 163: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/test/node/extHostSearch.test.ts: +src/vs/workbench/api/test/node/extHostSearch.test.ts: 177: // eslint-disable-next-line local/code-no-any-casts 1004: // eslint-disable-next-line local/code-no-any-casts 1050: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/worker/extensionHostWorker.ts: +src/vs/workbench/api/worker/extensionHostWorker.ts: 83: // eslint-disable-next-line local/code-no-any-casts 85: // eslint-disable-next-line local/code-no-any-casts 87: // eslint-disable-next-line local/code-no-any-casts @@ -826,35 +762,117 @@ vscode • src/vs/workbench/api/worker/extensionHostWorker.ts: 106: // eslint-disable-next-line local/code-no-any-casts 158: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/api/worker/extHostConsoleForwarder.ts: +src/vs/workbench/api/worker/extHostConsoleForwarder.ts: 20: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/browser/actions/developerActions.ts: - 781: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any +src/vs/workbench/browser/actions/developerActions.ts: + 762: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 764: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 795: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 798: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/common/configuration.ts: + 63: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts: +src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts: 54: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts: +src/vs/workbench/contrib/bulkEdit/test/browser/bulkCellEdits.test.ts: 29: // eslint-disable-next-line local/code-no-any-casts 39: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts: - 86: // eslint-disable-next-line local/code-no-any-casts - 88: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/chat/browser/chatSessions/common.ts: - 126: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/chat/common/chatModel.ts: - 1214: // eslint-disable-next-line local/code-no-any-casts - 1537: // eslint-disable-next-line local/code-no-any-casts - 1869: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/chat/common/chatServiceImpl.ts: - 437: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts: +src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts: + 923: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/chatWidget.ts: + 174: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts: + 88: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 625: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 646: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 699: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts: + 261: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts: + 56: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 63: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 101: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts: + 85: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 87: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 107: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 111: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/chatSessions/common.ts: + 71: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 112: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 114: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts: + 89: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts: + 180: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 198: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/chatModel.ts: + 672: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 1396: // eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts + 1815: // eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts + 2108: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 2128: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/chatService.ts: + 45: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 277: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 324: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 375: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 860: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 928: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 930: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/chatServiceImpl.ts: + 553: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/chatSessionsService.ts: + 129: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 141: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 182: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts: + 24: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 87: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/languageModels.ts: + 55: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 135: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 143: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 209: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 216: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 223: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 286: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 557: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/languageModelToolsService.ts: + 129: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 150: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 156: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 193: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 198: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 270: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts: + 390: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts: + 33: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 124: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts: 30: // eslint-disable-next-line local/code-no-any-casts 35: // eslint-disable-next-line local/code-no-any-casts 63: // eslint-disable-next-line local/code-no-any-casts @@ -875,325 +893,221 @@ vscode • src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNoteboo 1532: // eslint-disable-next-line local/code-no-any-casts 1537: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts: +src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts: 41: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts: - 35: // eslint-disable-next-line local/code-no-any-casts - 144: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts: - 72: // eslint-disable-next-line local/code-no-any-casts - 84: // eslint-disable-next-line local/code-no-any-casts - 96: // eslint-disable-next-line local/code-no-any-casts - 108: // eslint-disable-next-line local/code-no-any-casts - 120: // eslint-disable-next-line local/code-no-any-casts - 132: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts: + 75: // eslint-disable-next-line local/code-no-any-casts + 87: // eslint-disable-next-line local/code-no-any-casts + 99: // eslint-disable-next-line local/code-no-any-casts + 111: // eslint-disable-next-line local/code-no-any-casts + 123: // eslint-disable-next-line local/code-no-any-casts + 135: // eslint-disable-next-line local/code-no-any-casts 142: // eslint-disable-next-line local/code-no-any-casts 154: // eslint-disable-next-line local/code-no-any-casts - 164: // eslint-disable-next-line local/code-no-any-casts - 176: // eslint-disable-next-line local/code-no-any-casts - 186: // eslint-disable-next-line local/code-no-any-casts - 198: // eslint-disable-next-line local/code-no-any-casts - 208: // eslint-disable-next-line local/code-no-any-casts - 253: // eslint-disable-next-line local/code-no-any-casts - 264: // eslint-disable-next-line local/code-no-any-casts - 275: // eslint-disable-next-line local/code-no-any-casts - 286: // eslint-disable-next-line local/code-no-any-casts - 297: // eslint-disable-next-line local/code-no-any-casts - 308: // eslint-disable-next-line local/code-no-any-casts - 319: // eslint-disable-next-line local/code-no-any-casts - 330: // eslint-disable-next-line local/code-no-any-casts - 346: // eslint-disable-next-line local/code-no-any-casts + 161: // eslint-disable-next-line local/code-no-any-casts + 173: // eslint-disable-next-line local/code-no-any-casts + 181: // eslint-disable-next-line local/code-no-any-casts + 193: // eslint-disable-next-line local/code-no-any-casts + 200: // eslint-disable-next-line local/code-no-any-casts + 245: // eslint-disable-next-line local/code-no-any-casts + 256: // eslint-disable-next-line local/code-no-any-casts + 267: // eslint-disable-next-line local/code-no-any-casts + 278: // eslint-disable-next-line local/code-no-any-casts + 289: // eslint-disable-next-line local/code-no-any-casts + 300: // eslint-disable-next-line local/code-no-any-casts + 311: // eslint-disable-next-line local/code-no-any-casts + 322: // eslint-disable-next-line local/code-no-any-casts + 338: // eslint-disable-next-line local/code-no-any-casts + +src/vs/workbench/contrib/chat/test/common/languageModels.ts: + 53: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts: + 52: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts: + 36: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 38: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 41: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 44: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 47: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 50: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 52: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts: +src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts: 16: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/browser/debugSession.ts: +src/vs/workbench/contrib/debug/browser/debugSession.ts: 1193: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts: +src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts: 450: // eslint-disable-next-line local/code-no-any-casts 466: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts: +src/vs/workbench/contrib/debug/test/browser/debugConfigurationManager.test.ts: 92: // eslint-disable-next-line local/code-no-any-casts 129: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts: +src/vs/workbench/contrib/debug/test/browser/debugMemory.test.ts: 76: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts: +src/vs/workbench/contrib/debug/test/browser/rawDebugSession.test.ts: 28: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/browser/repl.test.ts: +src/vs/workbench/contrib/debug/test/browser/repl.test.ts: 139: // eslint-disable-next-line local/code-no-any-casts 142: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/debug/test/common/debugModel.test.ts: - 70: // eslint-disable-next-line local/code-no-any-casts - 75: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/debug/test/common/debugModel.test.ts: + 72: // eslint-disable-next-line local/code-no-any-casts + 77: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/editTelemetry/browser/helpers/utils.ts: +src/vs/workbench/contrib/editTelemetry/browser/helpers/utils.ts: 15: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts: - 147: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts: + 142: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts: +src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts: 1182: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts: +src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts: 99: // eslint-disable-next-line local/code-no-any-casts 101: // eslint-disable-next-line local/code-no-any-casts 103: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts: +src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts: 100: // eslint-disable-next-line local/code-no-any-casts 102: // eslint-disable-next-line local/code-no-any-casts 104: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts: +src/vs/workbench/contrib/files/test/browser/editorAutoSave.test.ts: 46: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/files/test/browser/explorerView.test.ts: +src/vs/workbench/contrib/files/test/browser/explorerView.test.ts: 94: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts: - 160: // eslint-disable-next-line local/code-no-any-casts - 662: // eslint-disable-next-line local/code-no-any-casts - 711: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts: + 163: // eslint-disable-next-line local/code-no-any-casts + 673: // eslint-disable-next-line local/code-no-any-casts + 722: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts: +src/vs/workbench/contrib/markdown/test/browser/markdownSettingRenderer.test.ts: 72: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/markers/browser/markersTable.ts: - 343: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts: + 145: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts: - 143: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts: + 100: // eslint-disable-next-line @typescript-eslint/no-explicit-any + 116: // eslint-disable-next-line @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/contrib/mergeEditor/browser/utils.ts: +src/vs/workbench/contrib/mergeEditor/browser/utils.ts: 89: // eslint-disable-next-line local/code-no-any-casts 99: // eslint-disable-next-line local/code-no-any-casts 120: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts: +src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts: 69: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts: - 309: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.ts: - 127: // eslint-disable-next-line local/code-no-any-casts - 199: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts: - 74: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts: - 122: // eslint-disable-next-line local/code-no-any-casts - 1462: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts: - 329: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts: - 170: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts: - 75: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts: - 424: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts: + 75: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts: - 575: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts: - 1110: // eslint-disable-next-line local/code-no-any-casts - 1152: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/notebookCellLayoutManager.test.ts: - 50: // eslint-disable-next-line local/code-no-any-casts - 66: // eslint-disable-next-line local/code-no-any-casts - 85: // eslint-disable-next-line local/code-no-any-casts - 96: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts: - 284: // eslint-disable-next-line local/code-no-any-casts - 308: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/NotebookEditorWidgetService.test.ts: - 37: // eslint-disable-next-line local/code-no-any-casts - 91: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts: - 72: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/diff/editorHeightCalculator.test.ts: - 26: // eslint-disable-next-line local/code-no-any-casts - 59: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiff.test.ts: - 652: // eslint-disable-next-line local/code-no-any-casts - 654: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/search/browser/searchActionsFind.ts: +src/vs/workbench/contrib/search/browser/searchActionsFind.ts: 460: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/search/browser/searchMessage.ts: +src/vs/workbench/contrib/search/browser/searchMessage.ts: 51: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts: +src/vs/workbench/contrib/search/browser/searchTreeModel/searchModel.ts: 306: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts: +src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts: 299: // eslint-disable-next-line local/code-no-any-casts 301: // eslint-disable-next-line local/code-no-any-casts 318: // eslint-disable-next-line local/code-no-any-casts 324: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/search/test/browser/searchModel.test.ts: +src/vs/workbench/contrib/search/test/browser/searchModel.test.ts: 201: // eslint-disable-next-line local/code-no-any-casts 229: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts: +src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts: 205: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts: +src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts: 155: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts: +src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts: 328: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts: - 1491: // eslint-disable-next-line local/code-no-any-casts - 1500: // eslint-disable-next-line local/code-no-any-casts - 1539: // eslint-disable-next-line local/code-no-any-casts - 1585: // eslint-disable-next-line local/code-no-any-casts - 1739: // eslint-disable-next-line local/code-no-any-casts - 1786: // eslint-disable-next-line local/code-no-any-casts - 1789: // eslint-disable-next-line local/code-no-any-casts - 2655: // eslint-disable-next-line local/code-no-any-casts - 2836: // eslint-disable-next-line local/code-no-any-casts - 3568: // eslint-disable-next-line local/code-no-any-casts - 3602: // eslint-disable-next-line local/code-no-any-casts - 3608: // eslint-disable-next-line local/code-no-any-casts - 3737: // eslint-disable-next-line local/code-no-any-casts - 3796: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/tasks/common/problemMatcher.ts: +src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts: + 1489: // eslint-disable-next-line local/code-no-any-casts + 1498: // eslint-disable-next-line local/code-no-any-casts + 1537: // eslint-disable-next-line local/code-no-any-casts + 1583: // eslint-disable-next-line local/code-no-any-casts + 1737: // eslint-disable-next-line local/code-no-any-casts + 1784: // eslint-disable-next-line local/code-no-any-casts + 1787: // eslint-disable-next-line local/code-no-any-casts + 2673: // eslint-disable-next-line local/code-no-any-casts + 2854: // eslint-disable-next-line local/code-no-any-casts + 3586: // eslint-disable-next-line local/code-no-any-casts + 3620: // eslint-disable-next-line local/code-no-any-casts + 3626: // eslint-disable-next-line local/code-no-any-casts + 3755: // eslint-disable-next-line local/code-no-any-casts + 3814: // eslint-disable-next-line local/code-no-any-casts + +src/vs/workbench/contrib/tasks/common/problemMatcher.ts: 361: // eslint-disable-next-line local/code-no-any-casts 374: // eslint-disable-next-line local/code-no-any-casts 1015: // eslint-disable-next-line local/code-no-any-casts 1906: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/tasks/common/taskConfiguration.ts: +src/vs/workbench/contrib/tasks/common/taskConfiguration.ts: 1720: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/tasks/common/tasks.ts: +src/vs/workbench/contrib/tasks/common/tasks.ts: 667: // eslint-disable-next-line local/code-no-any-casts 708: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts: +src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts: 84: // eslint-disable-next-line local/code-no-any-casts 86: // eslint-disable-next-line local/code-no-any-casts 89: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts: - 99: // eslint-disable-next-line local/code-no-any-casts - 445: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts: - 55: // eslint-disable-next-line local/code-no-any-casts - 96: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/terminalProfileService.integrationTest.ts: - 104: // eslint-disable-next-line local/code-no-any-casts - 130: // eslint-disable-next-line local/code-no-any-casts - 175: // eslint-disable-next-line local/code-no-any-casts - 230: // eslint-disable-next-line local/code-no-any-casts - 247: // eslint-disable-next-line local/code-no-any-casts - 264: // eslint-disable-next-line local/code-no-any-casts - 278: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/terminalService.test.ts: - 58: // eslint-disable-next-line local/code-no-any-casts - 65: // eslint-disable-next-line local/code-no-any-casts - 75: // eslint-disable-next-line local/code-no-any-casts - 83: // eslint-disable-next-line local/code-no-any-casts - 93: // eslint-disable-next-line local/code-no-any-casts - 105: // eslint-disable-next-line local/code-no-any-casts - 113: // eslint-disable-next-line local/code-no-any-casts - 122: // eslint-disable-next-line local/code-no-any-casts - 129: // eslint-disable-next-line local/code-no-any-casts - 141: // eslint-disable-next-line local/code-no-any-casts - 149: // eslint-disable-next-line local/code-no-any-casts - 158: // eslint-disable-next-line local/code-no-any-casts - 165: // eslint-disable-next-line local/code-no-any-casts - 178: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/capabilities/terminalCapabilityStore.test.ts: - 28: // eslint-disable-next-line local/code-no-any-casts - 34: // eslint-disable-next-line local/code-no-any-casts - 42: // eslint-disable-next-line local/code-no-any-casts - 50: // eslint-disable-next-line local/code-no-any-casts - 53: // eslint-disable-next-line local/code-no-any-casts - 94: // eslint-disable-next-line local/code-no-any-casts - 97: // eslint-disable-next-line local/code-no-any-casts - 105: // eslint-disable-next-line local/code-no-any-casts - 107: // eslint-disable-next-line local/code-no-any-casts - 117: // eslint-disable-next-line local/code-no-any-casts - 120: // eslint-disable-next-line local/code-no-any-casts - 130: // eslint-disable-next-line local/code-no-any-casts - 133: // eslint-disable-next-line local/code-no-any-casts - 135: // eslint-disable-next-line local/code-no-any-casts - 144: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts: - 59: // eslint-disable-next-line local/code-no-any-casts - 67: // eslint-disable-next-line local/code-no-any-casts - 75: // eslint-disable-next-line local/code-no-any-casts - 83: // eslint-disable-next-line local/code-no-any-casts - 91: // eslint-disable-next-line local/code-no-any-casts - 99: // eslint-disable-next-line local/code-no-any-casts - 107: // eslint-disable-next-line local/code-no-any-casts - 165: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts: - 51: // eslint-disable-next-line local/code-no-any-casts - 70: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts: +src/vs/workbench/contrib/terminalContrib/accessibility/test/browser/bufferContentTracker.test.ts: 71: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts: - 40: // eslint-disable-next-line local/code-no-any-casts - 56: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/outputMonitor.test.ts: + 45: // eslint-disable-next-line local/code-no-any-casts + 61: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/browser/runInTerminalTool.test.ts: - 380: // eslint-disable-next-line local/code-no-any-casts - 834: // eslint-disable-next-line local/code-no-any-casts - 858: // eslint-disable-next-line local/code-no-any-casts - 863: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/terminalContrib/chatAgentTools/test/electron-browser/runInTerminalTool.test.ts: + 402: // eslint-disable-next-line local/code-no-any-casts + 924: // eslint-disable-next-line local/code-no-any-casts + 948: // eslint-disable-next-line local/code-no-any-casts + 953: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts: +src/vs/workbench/contrib/terminalContrib/history/test/common/history.test.ts: 102: // eslint-disable-next-line local/code-no-any-casts 108: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts: +src/vs/workbench/contrib/terminalContrib/links/browser/links.ts: + 168: // eslint-disable-next-line @typescript-eslint/no-explicit-any + +src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkHelpers.test.ts: 242: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts: +src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkManager.test.ts: 95: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts: - 150: // eslint-disable-next-line local/code-no-any-casts - 290: // eslint-disable-next-line local/code-no-any-casts - 553: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkOpeners.test.ts: + 151: // eslint-disable-next-line local/code-no-any-casts + 292: // eslint-disable-next-line local/code-no-any-casts + 556: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts: +src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalWordLinkDetector.test.ts: 49: // eslint-disable-next-line local/code-no-any-casts 59: // eslint-disable-next-line local/code-no-any-casts 69: // eslint-disable-next-line local/code-no-any-casts @@ -1205,77 +1119,70 @@ vscode • src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalW 136: // eslint-disable-next-line local/code-no-any-casts 142: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts: - 786: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts: - 39: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/terminalContrib/typeAhead/test/browser/terminalTypeAhead.test.ts: + 40: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts: - 122: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts: + 123: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/contrib/themes/browser/themes.contribution.ts: - 614: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/contrib/themes/browser/themes.contribution.ts: + 614: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts: +src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts: 600: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/authentication/test/browser/authenticationMcpAccessService.test.ts: +src/vs/workbench/services/authentication/test/browser/authenticationMcpAccessService.test.ts: 236: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts: +src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts: 81: // eslint-disable-next-line local/code-no-any-casts 106: // eslint-disable-next-line local/code-no-any-casts 306: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/driver/browser/driver.ts: - 193: // eslint-disable-next-line local/code-no-any-casts - 215: // eslint-disable-next-line local/code-no-any-casts - -vscode • src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts: - 61: // eslint-disable-next-line local/code-no-any-casts - 63: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/services/driver/browser/driver.ts: + 199: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + 222: // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any -vscode • src/vs/workbench/services/extensions/common/extensionsRegistry.ts: +src/vs/workbench/services/extensions/common/extensionsRegistry.ts: 229: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts: +src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts: 49: // eslint-disable-next-line local/code-no-any-casts 54: // eslint-disable-next-line local/code-no-any-casts 97: // eslint-disable-next-line local/code-no-any-casts 102: // eslint-disable-next-line local/code-no-any-casts 107: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts: +src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts: 185: // eslint-disable-next-line local/code-no-any-casts 222: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts: +src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts: 47: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/search/common/search.ts: +src/vs/workbench/services/search/common/search.ts: 628: // eslint-disable-next-line local/code-no-any-casts 631: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/search/node/rawSearchService.ts: +src/vs/workbench/services/search/node/rawSearchService.ts: 438: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts: +src/vs/workbench/services/textfile/test/node/encoding/encoding.test.ts: 44: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/textMate/common/TMGrammarFactory.ts: +src/vs/workbench/services/textMate/common/TMGrammarFactory.ts: 147: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/themes/browser/fileIconThemeData.ts: +src/vs/workbench/services/themes/browser/fileIconThemeData.ts: 122: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/themes/browser/productIconThemeData.ts: +src/vs/workbench/services/themes/browser/productIconThemeData.ts: 123: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/themes/common/colorThemeData.ts: +src/vs/workbench/services/themes/common/colorThemeData.ts: 650: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts: +src/vs/workbench/services/views/test/browser/viewContainerModel.test.ts: 67: // eslint-disable-next-line local/code-no-any-casts 74: // eslint-disable-next-line local/code-no-any-casts 102: // eslint-disable-next-line local/code-no-any-casts @@ -1299,7 +1206,7 @@ vscode • src/vs/workbench/services/views/test/browser/viewContainerModel.test. 755: // eslint-disable-next-line local/code-no-any-casts 833: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts: +src/vs/workbench/services/views/test/browser/viewDescriptorService.test.ts: 25: // eslint-disable-next-line local/code-no-any-casts 27: // eslint-disable-next-line local/code-no-any-casts 335: // eslint-disable-next-line local/code-no-any-casts @@ -1309,54 +1216,54 @@ vscode • src/vs/workbench/services/views/test/browser/viewDescriptorService.te 645: // eslint-disable-next-line local/code-no-any-casts 678: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/part.test.ts: - 133: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/test/browser/part.test.ts: + 135: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/window.test.ts: +src/vs/workbench/test/browser/window.test.ts: 42: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/workbenchTestServices.ts: +src/vs/workbench/test/browser/workbenchTestServices.ts: 305: // eslint-disable-next-line local/code-no-any-casts 698: // eslint-disable-next-line local/code-no-any-casts 1065: // eslint-disable-next-line local/code-no-any-casts - 1968: // eslint-disable-next-line local/code-no-any-casts - 1986: // eslint-disable-next-line local/code-no-any-casts + 1970: // eslint-disable-next-line local/code-no-any-casts + 1988: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/parts/editor/editorInput.test.ts: +src/vs/workbench/test/browser/parts/editor/editorInput.test.ts: 98: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/parts/editor/editorPane.test.ts: +src/vs/workbench/test/browser/parts/editor/editorPane.test.ts: 131: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts: +src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts: 95: // eslint-disable-next-line local/code-no-any-casts 106: // eslint-disable-next-line local/code-no-any-casts 113: // eslint-disable-next-line local/code-no-any-casts 120: // eslint-disable-next-line local/code-no-any-casts 127: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/common/resources.test.ts: +src/vs/workbench/test/common/resources.test.ts: 51: // eslint-disable-next-line local/code-no-any-casts 59: // eslint-disable-next-line local/code-no-any-casts 72: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/common/workbenchTestServices.ts: +src/vs/workbench/test/common/workbenchTestServices.ts: 293: // eslint-disable-next-line local/code-no-any-casts -vscode • src/vs/workbench/test/electron-browser/workbenchTestServices.ts: - 255: // eslint-disable-next-line local/code-no-any-casts +src/vs/workbench/test/electron-browser/workbenchTestServices.ts: + 257: // eslint-disable-next-line local/code-no-any-casts -vscode • test/automation/src/code.ts: - 127: // eslint-disable-next-line local/code-no-any-casts +test/automation/src/code.ts: + 128: // eslint-disable-next-line local/code-no-any-casts -vscode • test/automation/src/terminal.ts: +test/automation/src/terminal.ts: 315: // eslint-disable-next-line local/code-no-any-casts -vscode • test/mcp/src/application.ts: - 309: // eslint-disable-next-line local/code-no-any-casts +test/mcp/src/application.ts: + 250: // eslint-disable-next-line local/code-no-any-casts -vscode • test/mcp/src/playwright.ts: +test/mcp/src/playwright.ts: 17: // eslint-disable-next-line local/code-no-any-casts -vscode • test/mcp/src/automationTools/problems.ts: +test/mcp/src/automationTools/problems.ts: 76: // eslint-disable-next-line local/code-no-any-casts diff --git a/.vscode/settings.json b/.vscode/settings.json index f394c8a5a73..ec8c556838b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,16 @@ { // --- Chat --- "inlineChat.enableV2": true, + "inlineChat.affordance": "gutter", + "inlineChat.renderMode": "hover", "chat.tools.terminal.autoApprove": { - "/^npm (test|lint|run compile)\\b/": true, - "/^npx tsc\\b.*--noEmit/": true, "scripts/test.bat": true, "scripts/test.sh": true, "scripts/test-integration.bat": true, "scripts/test-integration.sh": true, }, + "chat.viewSessions.enabled": true, + "chat.editing.explainChanges.enabled": true, // --- Editor --- "editor.insertSpaces": false, "editor.experimental.asyncTokenization": true, @@ -60,7 +62,6 @@ "**/yarn.lock": true, "**/package-lock.json": true, "**/Cargo.lock": true, - "build/**/*.js": true, "out/**": true, "out-build/**": true, "out-vscode/**": true, @@ -72,12 +73,6 @@ "test/automation/out/**": true, "test/integration/browser/out/**": true }, - "files.readonlyExclude": { - "build/builtin/*.js": true, - "build/monaco/*.js": true, - "build/npm/*.js": true, - "build/*.js": true - }, // --- Search --- "search.exclude": { "**/node_modules": true, @@ -132,6 +127,7 @@ "git.ignoreLimitWarning": true, "git.branchProtection": [ "main", + "main-*", "distro", "release/*" ], @@ -202,17 +198,10 @@ // --- Workbench --- // "application.experimental.rendererProfiling": true, // https://github.com/microsoft/vscode/issues/265654 "editor.aiStats.enabled": true, // Team selfhosting on ai stats - "chat.emptyState.history.enabled": true, - "chat.agentSessionsViewLocation": "view", - "chat.promptFilesRecommendations": { - "plan-fast": true, - "plan-deep": true - }, - // Needed for kusto tool in data.prompt.md "azureMcp.enabledServices": [ - "kusto" + "kusto" // Needed for kusto tool in data.prompt.md ], "azureMcp.serverMode": "all", "azureMcp.readOnly": true, - "chat.tools.terminal.outputLocation": "none" + "debug.breakpointsView.presentation": "tree" } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 51a34c77a57..f601633b570 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -257,7 +257,7 @@ }, { "type": "shell", - "command": "node build/lib/preLaunch.js", + "command": "node build/lib/preLaunch.ts", "label": "Ensure Prelaunch Dependencies", "presentation": { "reveal": "silent", @@ -279,23 +279,30 @@ "detail": "node_modules/tsec/bin/tsec -p src/tsconfig.json --noEmit" }, { - "label": "Launch Http Server", + "label": "Launch Monaco Editor Vite", "type": "shell", - "command": "node_modules/.bin/ts-node -T ./scripts/playground-server", + "command": "npm run dev", + "options": { + "cwd": "./build/vite/" + }, "isBackground": true, "problemMatcher": { + "owner": "vite", + "fileLocation": "absolute", "pattern": { - "regexp": "" + "regexp": "^(.+?):(\\d+):(\\d+):\\s+(error|warning)\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 }, "background": { "activeOnStart": true, - "beginsPattern": "never match", - "endsPattern": ".*" + "beginsPattern": ".*VITE.*", + "endsPattern": "(Local|Network):.*" } - }, - "dependsOn": [ - "Core - Build" - ] + } }, { "label": "Launch MCP Server", diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..d6abb76ab83 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# VS Code Agents Instructions + +This file provides instructions for AI coding agents working with the VS Code codebase. + +For detailed project overview, architecture, coding guidelines, and validation steps, see the [Copilot Instructions](.github/copilot-instructions.md). diff --git a/README.md b/README.md index 70e7749f6ca..6f1e94e4844 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,78 @@ -# Welcome to CortexIDE. +# Visual Studio Code - Open Source ("Code - OSS") +[![Feature Requests](https://img.shields.io/github/issues/microsoft/vscode/feature-request.svg)](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) +[![Bugs](https://img.shields.io/github/issues/microsoft/vscode/bug.svg)](https://github.com/microsoft/vscode/issues?utf8=✓&q=is%3Aissue+is%3Aopen+label%3Abug) +[![Gitter](https://img.shields.io/badge/chat-on%20gitter-yellow.svg)](https://gitter.im/Microsoft/vscode) -
- CortexIDE Welcome -
+## The Repository -CortexIDE is the open-source Cursor alternative. +This repository ("`Code - OSS`") is where we (Microsoft) develop the [Visual Studio Code](https://code.visualstudio.com) product together with the community. Not only do we work on code and issues here, we also publish our [roadmap](https://github.com/microsoft/vscode/wiki/Roadmap), [monthly iteration plans](https://github.com/microsoft/vscode/wiki/Iteration-Plans), and our [endgame plans](https://github.com/microsoft/vscode/wiki/Running-the-Endgame). This source code is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/main/LICENSE.txt). -Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. CortexIDE sends messages directly to providers without retaining your data. +## Visual Studio Code -This repo contains the full sourcecode for CortexIDE. If you're new, welcome! +

+ VS Code in action +

-📊 **See**: [CortexIDE vs Cursor, Void, Antigravity, Continue.dev — Full Comparison](docs/CortexIDE-vs-Other-AI-Editors.md) +[Visual Studio Code](https://code.visualstudio.com) is a distribution of the `Code - OSS` repository with Microsoft-specific customizations released under a traditional [Microsoft product license](https://code.visualstudio.com/License/). -- 🧭 [Website](https://opencortexide.com) +[Visual Studio Code](https://code.visualstudio.com) combines the simplicity of a code editor with what developers need for their core edit-build-debug cycle. It provides comprehensive code editing, navigation, and understanding support along with lightweight debugging, a rich extensibility model, and lightweight integration with existing tools. -- 👋 [Discord](https://discord.gg/NFc3EKPany) +Visual Studio Code is updated monthly with new features and bug fixes. You can download it for Windows, macOS, and Linux on [Visual Studio Code's website](https://code.visualstudio.com/Download). To get the latest releases every day, install the [Insiders build](https://code.visualstudio.com/insiders). -- 🚙 [Project Board](https://github.com/orgs/opencortexide/projects/1) +## Contributing +There are many ways in which you can participate in this project, for example: -## Contributing +* [Submit bugs and feature requests](https://github.com/microsoft/vscode/issues), and help us verify as they are checked in +* Review [source code changes](https://github.com/microsoft/vscode/pulls) +* Review the [documentation](https://github.com/microsoft/vscode-docs) and make pull requests for anything from typos to additional and new content + +If you are interested in fixing issues and contributing directly to the code base, +please see the document [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute), which covers the following: + +* [How to build and run from source](https://github.com/microsoft/vscode/wiki/How-to-Contribute) +* [The development workflow, including debugging and running tests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#debugging) +* [Coding guidelines](https://github.com/microsoft/vscode/wiki/Coding-Guidelines) +* [Submitting pull requests](https://github.com/microsoft/vscode/wiki/How-to-Contribute#pull-requests) +* [Finding an issue to work on](https://github.com/microsoft/vscode/wiki/How-to-Contribute#where-to-contribute) +* [Contributing to translations](https://aka.ms/vscodeloc) + +## Feedback + +* Ask a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/vscode) +* [Request a new feature](CONTRIBUTING.md) +* Upvote [popular feature requests](https://github.com/microsoft/vscode/issues?q=is%3Aopen+is%3Aissue+label%3Afeature-request+sort%3Areactions-%2B1-desc) +* [File an issue](https://github.com/microsoft/vscode/issues) +* Connect with the extension author community on [GitHub Discussions](https://github.com/microsoft/vscode-discussions/discussions) or [Slack](https://aka.ms/vscode-dev-community) +* Follow [@code](https://x.com/code) and let us know what you think! + +See our [wiki](https://github.com/microsoft/vscode/wiki/Feedback-Channels) for a description of each of these channels and information on some other available community-driven channels. + +## Related Projects + +Many of the core components and extensions to VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) repositories are separate from each other. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki). + +## Bundled Extensions + +VS Code includes a set of built-in extensions located in the [extensions](extensions) folder, including grammars and snippets for many languages. Extensions that provide rich language support (inline suggestions, Go to Definition) for a language have the suffix `language-features`. For example, the `json` extension provides coloring for `JSON` and the `json-language-features` extension provides rich language support for `JSON`. + +## Development Container + +This repository includes a Visual Studio Code Dev Containers / GitHub Codespaces development container. -1. To get started working on CortexIDE, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/opencortexide/cortexide/blob/main/HOW_TO_CONTRIBUTE.md). +* For [Dev Containers](https://aka.ms/vscode-remote/download/containers), use the **Dev Containers: Clone Repository in Container Volume...** command which creates a Docker volume for better disk I/O on macOS and Windows. + * If you already have VS Code and Docker installed, you can also click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/microsoft/vscode) to get started. This will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use. -2. Feel free to attend a casual weekly meeting in our [Discord channel](https://discord.gg/NFc3EKPany)! +* For Codespaces, install the [GitHub Codespaces](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces) extension in VS Code, and use the **Codespaces: Create New Codespace** command. +Docker / the Codespace should have at least **4 Cores and 6 GB of RAM (8 GB recommended)** to run a full build. See the [development container README](.devcontainer/README.md) for more information. -## Reference +## Code of Conduct -CortexIDE is a fork of [Void](https://github.com/voideditor/void), which itself is a fork of the [VS Code](https://github.com/microsoft/vscode) repository. We acknowledge and thank both projects for their foundational work. +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -For a guide to the codebase, see [CORTEXIDE_CODEBASE_GUIDE.md](./CORTEXIDE_CODEBASE_GUIDE.md). +## License +Copyright (c) Microsoft Corporation. All rights reserved. -## Support -You can always reach us in our [Discord server](https://discord.gg/NFc3EKPany). +Licensed under the [MIT](LICENSE.txt) license. diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index f47e5a6c50f..896b59001d6 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -684,7 +684,7 @@ more details. --------------------------------------------------------- -go-syntax 0.8.4 - MIT +go-syntax 0.8.5 - MIT https://github.com/worlpaker/go-syntax MIT License @@ -1000,7 +1000,7 @@ SOFTWARE. --------------------------------------------------------- -jlelong/vscode-latex-basics 1.15.0 - MIT +jlelong/vscode-latex-basics 1.16.0 - MIT https://github.com/jlelong/vscode-latex-basics Copyright (c) vscode-latex-basics authors @@ -2675,6 +2675,10 @@ the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. + +--- + +Git Logo by [Jason Long](https://bsky.app/profile/jasonlong.me) is licensed under the [Creative Commons Attribution 3.0 Unported License](https://creativecommons.org/licenses/by/3.0/). --------------------------------------------------------- --------------------------------------------------------- diff --git a/build/.cachesalt b/build/.cachesalt index d55dde3c035..a90b8e833cb 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2025-07-23T19:44:03.051Z +2026-01-23T20:55:53.631Z diff --git a/build/.moduleignore b/build/.moduleignore index 0459b46f743..ed36151130c 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -75,10 +75,10 @@ native-is-elevated/src/** native-is-elevated/deps/** !native-is-elevated/build/Release/*.node -native-watchdog/binding.gyp -native-watchdog/build/** -native-watchdog/src/** -!native-watchdog/build/Release/*.node +@vscode/native-watchdog/binding.gyp +@vscode/native-watchdog/build/** +@vscode/native-watchdog/src/** +!@vscode/native-watchdog/build/Release/*.node @vscode/vsce-sign/** !@vscode/vsce-sign/src/main.d.ts diff --git a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml index d1c6659d197..cc53000a15c 100644 --- a/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml +++ b/build/azure-pipelines/alpine/product-build-alpine-node-modules.yml @@ -29,11 +29,11 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js alpine $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts alpine $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -108,13 +108,13 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts displayName: Mixin distro node modules condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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')) diff --git a/build/azure-pipelines/alpine/product-build-alpine.yml b/build/azure-pipelines/alpine/product-build-alpine.yml index c6d5ba27eda..5c5714e9d5b 100644 --- a/build/azure-pipelines/alpine/product-build-alpine.yml +++ b/build/azure-pipelines/alpine/product-build-alpine.yml @@ -73,11 +73,11 @@ jobs: - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz displayName: Extract compilation output - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js alpine $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts alpine $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -156,19 +156,19 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts displayName: Mixin distro node modules condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: ../common/install-builtin-extensions.yml@self diff --git a/build/azure-pipelines/cli/cli-apply-patches.yml b/build/azure-pipelines/cli/cli-apply-patches.yml index 2815124efb6..e04951f3f56 100644 --- a/build/azure-pipelines/cli/cli-apply-patches.yml +++ b/build/azure-pipelines/cli/cli-apply-patches.yml @@ -1,7 +1,7 @@ steps: - template: ../distro/download-distro.yml@self - - script: node build/azure-pipelines/distro/mixin-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - script: node .build/distro/cli-patches/index.js diff --git a/build/azure-pipelines/cli/cli-compile.yml b/build/azure-pipelines/cli/cli-compile.yml index 769a1153bc1..2abefa7b6a4 100644 --- a/build/azure-pipelines/cli/cli-compile.yml +++ b/build/azure-pipelines/cli/cli-compile.yml @@ -35,7 +35,7 @@ steps: set -e if [ -n "$SYSROOT_ARCH" ]; then export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots - node -e '(async () => { const { getVSCodeSysroot } = require("../build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"], process.env["IS_MUSL"] === "1"); })()' + node -e 'import { getVSCodeSysroot } from "../build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"], process.env["IS_MUSL"] === "1"); })()' if [ "$SYSROOT_ARCH" == "arm64" ]; then if [ -n "$IS_MUSL" ]; then export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$VSCODE_SYSROOT_DIR/output/bin/aarch64-linux-musl-gcc" diff --git a/build/azure-pipelines/common/checkForArtifact.js b/build/azure-pipelines/common/checkForArtifact.js deleted file mode 100644 index 899448f78bd..00000000000 --- a/build/azure-pipelines/common/checkForArtifact.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const publish_1 = require("./publish"); -const retry_1 = require("./retry"); -async function getPipelineArtifacts() { - const result = await (0, publish_1.requestAZDOAPI)('artifacts'); - return result.value.filter(a => !/sbom$/.test(a.name)); -} -async function main([variableName, artifactName]) { - if (!variableName || !artifactName) { - throw new Error(`Usage: node checkForArtifact.js `); - } - try { - const artifacts = await (0, retry_1.retry)(() => getPipelineArtifacts()); - const artifact = artifacts.find(a => a.name === artifactName); - console.log(`##vso[task.setvariable variable=${variableName}]${artifact ? 'true' : 'false'}`); - } - catch (err) { - console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); - console.log(`##vso[task.setvariable variable=${variableName}]false`); - } -} -main(process.argv.slice(2)) - .then(() => { - process.exit(0); -}, err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=checkForArtifact.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/checkForArtifact.ts b/build/azure-pipelines/common/checkForArtifact.ts index e0a1a2ce1d3..21a30552e58 100644 --- a/build/azure-pipelines/common/checkForArtifact.ts +++ b/build/azure-pipelines/common/checkForArtifact.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Artifact, requestAZDOAPI } from './publish'; -import { retry } from './retry'; +import { type Artifact, requestAZDOAPI } from './publish.ts'; +import { retry } from './retry.ts'; async function getPipelineArtifacts(): Promise { const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts'); @@ -13,7 +13,7 @@ async function getPipelineArtifacts(): Promise { async function main([variableName, artifactName]: string[]): Promise { if (!variableName || !artifactName) { - throw new Error(`Usage: node checkForArtifact.js `); + throw new Error(`Usage: node checkForArtifact.ts `); } try { diff --git a/build/azure-pipelines/common/codesign.js b/build/azure-pipelines/common/codesign.js deleted file mode 100644 index e3a8f330dcd..00000000000 --- a/build/azure-pipelines/common/codesign.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.printBanner = printBanner; -exports.streamProcessOutputAndCheckResult = streamProcessOutputAndCheckResult; -exports.spawnCodesignProcess = spawnCodesignProcess; -const zx_1 = require("zx"); -function printBanner(title) { - title = `${title} (${new Date().toISOString()})`; - console.log('\n'); - console.log('#'.repeat(75)); - console.log(`# ${title.padEnd(71)} #`); - console.log('#'.repeat(75)); - console.log('\n'); -} -async function streamProcessOutputAndCheckResult(name, promise) { - const result = await promise.pipe(process.stdout); - if (result.ok) { - console.log(`\n${name} completed successfully. Duration: ${result.duration} ms`); - return; - } - throw new Error(`${name} failed: ${result.stderr}`); -} -function spawnCodesignProcess(esrpCliDLLPath, type, folder, glob) { - return (0, zx_1.$) `node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; -} -//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/codesign.ts b/build/azure-pipelines/common/codesign.ts index 9f26b3924b5..4c27048093b 100644 --- a/build/azure-pipelines/common/codesign.ts +++ b/build/azure-pipelines/common/codesign.ts @@ -26,5 +26,5 @@ export async function streamProcessOutputAndCheckResult(name: string, promise: P } export function spawnCodesignProcess(esrpCliDLLPath: string, type: 'sign-windows' | 'sign-windows-appx' | 'sign-pgp' | 'sign-darwin' | 'notarize-darwin', folder: string, glob: string): ProcessPromise { - return $`node build/azure-pipelines/common/sign ${esrpCliDLLPath} ${type} ${folder} ${glob}`; + return $`node build/azure-pipelines/common/sign.ts ${esrpCliDLLPath} ${type} ${folder} ${glob}`; } diff --git a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js deleted file mode 100644 index 10fa9087454..00000000000 --- a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.js +++ /dev/null @@ -1,19 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const crypto_1 = __importDefault(require("crypto")); -const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../product.json'), 'utf8')); -const shasum = crypto_1.default.createHash('sha256'); -for (const ext of productjson.builtInExtensions) { - shasum.update(`${ext.name}@${ext.version}`); -} -process.stdout.write(shasum.digest('hex')); -//# sourceMappingURL=computeBuiltInDepsCacheKey.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts index 8abaaccb654..8e172ee5ecb 100644 --- a/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts +++ b/build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts @@ -7,7 +7,7 @@ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../../product.json'), 'utf8')); +const productjson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../product.json'), 'utf8')); const shasum = crypto.createHash('sha256'); for (const ext of productjson.builtInExtensions) { diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.js b/build/azure-pipelines/common/computeNodeModulesCacheKey.js deleted file mode 100644 index c09c13be9d4..00000000000 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const crypto_1 = __importDefault(require("crypto")); -const { dirs } = require('../../npm/dirs'); -const ROOT = path_1.default.join(__dirname, '../../../'); -const shasum = crypto_1.default.createHash('sha256'); -shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build/.cachesalt'))); -shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, '.npmrc'))); -shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'build', '.npmrc'))); -shasum.update(fs_1.default.readFileSync(path_1.default.join(ROOT, 'remote', '.npmrc'))); -// Add `package.json` and `package-lock.json` files -for (const dir of dirs) { - const packageJsonPath = path_1.default.join(ROOT, dir, 'package.json'); - const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()); - const relevantPackageJsonSections = { - dependencies: packageJson.dependencies, - devDependencies: packageJson.devDependencies, - optionalDependencies: packageJson.optionalDependencies, - resolutions: packageJson.resolutions, - distro: packageJson.distro - }; - shasum.update(JSON.stringify(relevantPackageJsonSections)); - const packageLockPath = path_1.default.join(ROOT, dir, 'package-lock.json'); - shasum.update(fs_1.default.readFileSync(packageLockPath)); -} -// Add any other command line arguments -for (let i = 2; i < process.argv.length; i++) { - shasum.update(process.argv[i]); -} -process.stdout.write(shasum.digest('hex')); -//# sourceMappingURL=computeNodeModulesCacheKey.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts index 57b35dc78de..e5dbc06aa94 100644 --- a/build/azure-pipelines/common/computeNodeModulesCacheKey.ts +++ b/build/azure-pipelines/common/computeNodeModulesCacheKey.ts @@ -2,13 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; -const { dirs } = require('../../npm/dirs'); +import { dirs } from '../../npm/dirs.ts'; -const ROOT = path.join(__dirname, '../../../'); +const ROOT = path.join(import.meta.dirname, '../../../'); const shasum = crypto.createHash('sha256'); diff --git a/build/azure-pipelines/common/createBuild.js b/build/azure-pipelines/common/createBuild.js deleted file mode 100644 index c605ed6218e..00000000000 --- a/build/azure-pipelines/common/createBuild.js +++ /dev/null @@ -1,55 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const identity_1 = require("@azure/identity"); -const cosmos_1 = require("@azure/cosmos"); -const retry_1 = require("./retry"); -if (process.argv.length !== 3) { - console.error('Usage: node createBuild.js VERSION'); - process.exit(-1); -} -function getEnv(name) { - const result = process.env[name]; - if (typeof result === 'undefined') { - throw new Error('Missing env: ' + name); - } - return result; -} -async function main() { - const [, , _version] = process.argv; - const quality = getEnv('VSCODE_QUALITY'); - const commit = getEnv('BUILD_SOURCEVERSION'); - const queuedBy = getEnv('BUILD_QUEUEDBY'); - const sourceBranch = getEnv('BUILD_SOURCEBRANCH'); - const version = _version + (quality === 'stable' ? '' : `-${quality}`); - console.log('Creating build...'); - console.log('Quality:', quality); - console.log('Version:', version); - console.log('Commit:', commit); - const build = { - id: commit, - timestamp: (new Date()).getTime(), - version, - isReleased: false, - private: process.env['VSCODE_PRIVATE_BUILD']?.toLowerCase() === 'true', - sourceBranch, - queuedBy, - assets: [], - updates: {} - }; - const aadCredentials = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); - const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], aadCredentials }); - const scripts = client.database('builds').container(quality).scripts; - await (0, retry_1.retry)(() => scripts.storedProcedure('createBuild').execute('', [{ ...build, _partitionKey: '' }])); -} -main().then(() => { - console.log('Build successfully created'); - process.exit(0); -}, err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=createBuild.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/createBuild.ts b/build/azure-pipelines/common/createBuild.ts index 6afeb01e6cc..2524e7405a8 100644 --- a/build/azure-pipelines/common/createBuild.ts +++ b/build/azure-pipelines/common/createBuild.ts @@ -5,10 +5,10 @@ import { ClientAssertionCredential } from '@azure/identity'; import { CosmosClient } from '@azure/cosmos'; -import { retry } from './retry'; +import { retry } from './retry.ts'; if (process.argv.length !== 3) { - console.error('Usage: node createBuild.js VERSION'); + console.error('Usage: node createBuild.ts VERSION'); process.exit(-1); } @@ -35,16 +35,21 @@ async function main(): Promise { console.log('Version:', version); console.log('Commit:', commit); + const timestamp = Date.now(); const build = { id: commit, - timestamp: (new Date()).getTime(), + timestamp, version, isReleased: false, private: process.env['VSCODE_PRIVATE_BUILD']?.toLowerCase() === 'true', sourceBranch, queuedBy, assets: [], - updates: {} + updates: {}, + firstReleaseTimestamp: null, + history: [ + { event: 'created', timestamp } + ] }; const aadCredentials = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!)); diff --git a/build/azure-pipelines/common/getPublishAuthTokens.js b/build/azure-pipelines/common/getPublishAuthTokens.js deleted file mode 100644 index 9c22e9ad94b..00000000000 --- a/build/azure-pipelines/common/getPublishAuthTokens.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getAccessToken = getAccessToken; -const msal_node_1 = require("@azure/msal-node"); -function e(name) { - const result = process.env[name]; - if (typeof result !== 'string') { - throw new Error(`Missing env: ${name}`); - } - return result; -} -async function getAccessToken(endpoint, tenantId, clientId, idToken) { - const app = new msal_node_1.ConfidentialClientApplication({ - auth: { - clientId, - authority: `https://login.microsoftonline.com/${tenantId}`, - clientAssertion: idToken - } - }); - const result = await app.acquireTokenByClientCredential({ scopes: [`${endpoint}.default`] }); - if (!result) { - throw new Error('Failed to get access token'); - } - return { - token: result.accessToken, - expiresOnTimestamp: result.expiresOn.getTime(), - refreshAfterTimestamp: result.refreshOn?.getTime() - }; -} -async function main() { - const cosmosDBAccessToken = await getAccessToken(e('AZURE_DOCUMENTDB_ENDPOINT'), e('AZURE_TENANT_ID'), e('AZURE_CLIENT_ID'), e('AZURE_ID_TOKEN')); - const blobServiceAccessToken = await getAccessToken(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], process.env['AZURE_ID_TOKEN']); - console.log(JSON.stringify({ cosmosDBAccessToken, blobServiceAccessToken })); -} -if (require.main === module) { - main().then(() => { - process.exit(0); - }, err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=getPublishAuthTokens.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/getPublishAuthTokens.ts b/build/azure-pipelines/common/getPublishAuthTokens.ts index 68e76de1a83..2293480b306 100644 --- a/build/azure-pipelines/common/getPublishAuthTokens.ts +++ b/build/azure-pipelines/common/getPublishAuthTokens.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccessToken } from '@azure/core-auth'; +import type { AccessToken } from '@azure/core-auth'; import { ConfidentialClientApplication } from '@azure/msal-node'; function e(name: string): string { @@ -44,7 +44,7 @@ async function main() { console.log(JSON.stringify({ cosmosDBAccessToken, blobServiceAccessToken })); } -if (require.main === module) { +if (import.meta.main) { main().then(() => { process.exit(0); }, err => { diff --git a/build/azure-pipelines/common/install-builtin-extensions.yml b/build/azure-pipelines/common/install-builtin-extensions.yml index c1ee18d05b5..f9cbfd4b085 100644 --- a/build/azure-pipelines/common/install-builtin-extensions.yml +++ b/build/azure-pipelines/common/install-builtin-extensions.yml @@ -7,7 +7,7 @@ steps: condition: and(succeeded(), not(contains(variables['Agent.OS'], 'windows'))) displayName: Create .build folder - - script: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.js > .build/builtindepshash + - script: node build/azure-pipelines/common/computeBuiltInDepsCacheKey.ts > .build/builtindepshash displayName: Prepare built-in extensions cache key - task: Cache@2 @@ -17,7 +17,7 @@ steps: cacheHitVar: BUILTIN_EXTENSIONS_RESTORED displayName: Restore built-in extensions cache - - script: node build/lib/builtInExtensions.js + - script: node build/lib/builtInExtensions.ts env: GITHUB_TOKEN: "$(github-distro-mixin-password)" condition: and(succeeded(), ne(variables.BUILTIN_EXTENSIONS_RESTORED, 'true')) diff --git a/build/azure-pipelines/common/listNodeModules.js b/build/azure-pipelines/common/listNodeModules.js deleted file mode 100644 index 301b5f930b6..00000000000 --- a/build/azure-pipelines/common/listNodeModules.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -if (process.argv.length !== 3) { - console.error('Usage: node listNodeModules.js OUTPUT_FILE'); - process.exit(-1); -} -const ROOT = path_1.default.join(__dirname, '../../../'); -function findNodeModulesFiles(location, inNodeModules, result) { - const entries = fs_1.default.readdirSync(path_1.default.join(ROOT, location)); - for (const entry of entries) { - const entryPath = `${location}/${entry}`; - if (/(^\/out)|(^\/src$)|(^\/.git$)|(^\/.build$)/.test(entryPath)) { - continue; - } - let stat; - try { - stat = fs_1.default.statSync(path_1.default.join(ROOT, entryPath)); - } - catch (err) { - continue; - } - if (stat.isDirectory()) { - findNodeModulesFiles(entryPath, inNodeModules || (entry === 'node_modules'), result); - } - else { - if (inNodeModules) { - result.push(entryPath.substr(1)); - } - } - } -} -const result = []; -findNodeModulesFiles('', false, result); -fs_1.default.writeFileSync(process.argv[2], result.join('\n') + '\n'); -//# sourceMappingURL=listNodeModules.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/listNodeModules.ts b/build/azure-pipelines/common/listNodeModules.ts index fb85b25cfd1..5ab955faca4 100644 --- a/build/azure-pipelines/common/listNodeModules.ts +++ b/build/azure-pipelines/common/listNodeModules.ts @@ -7,11 +7,11 @@ import fs from 'fs'; import path from 'path'; if (process.argv.length !== 3) { - console.error('Usage: node listNodeModules.js OUTPUT_FILE'); + console.error('Usage: node listNodeModules.ts OUTPUT_FILE'); process.exit(-1); } -const ROOT = path.join(__dirname, '../../../'); +const ROOT = path.join(import.meta.dirname, '../../../'); function findNodeModulesFiles(location: string, inNodeModules: boolean, result: string[]) { const entries = fs.readdirSync(path.join(ROOT, location)); diff --git a/build/azure-pipelines/common/publish.js b/build/azure-pipelines/common/publish.js deleted file mode 100644 index 49b718344a0..00000000000 --- a/build/azure-pipelines/common/publish.js +++ /dev/null @@ -1,724 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.e = e; -exports.requestAZDOAPI = requestAZDOAPI; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const stream_1 = require("stream"); -const promises_1 = require("node:stream/promises"); -const yauzl_1 = __importDefault(require("yauzl")); -const crypto_1 = __importDefault(require("crypto")); -const retry_1 = require("./retry"); -const cosmos_1 = require("@azure/cosmos"); -const child_process_1 = __importDefault(require("child_process")); -const os_1 = __importDefault(require("os")); -const node_worker_threads_1 = require("node:worker_threads"); -const msal_node_1 = require("@azure/msal-node"); -const storage_blob_1 = require("@azure/storage-blob"); -const jws_1 = __importDefault(require("jws")); -const node_timers_1 = require("node:timers"); -function e(name) { - const result = process.env[name]; - if (typeof result !== 'string') { - throw new Error(`Missing env: ${name}`); - } - return result; -} -function hashStream(hashName, stream) { - return new Promise((c, e) => { - const shasum = crypto_1.default.createHash(hashName); - stream - .on('data', shasum.update.bind(shasum)) - .on('error', e) - .on('close', () => c(shasum.digest())); - }); -} -var StatusCode; -(function (StatusCode) { - StatusCode["Pass"] = "pass"; - StatusCode["Aborted"] = "aborted"; - StatusCode["Inprogress"] = "inprogress"; - StatusCode["FailCanRetry"] = "failCanRetry"; - StatusCode["FailDoNotRetry"] = "failDoNotRetry"; - StatusCode["PendingAnalysis"] = "pendingAnalysis"; - StatusCode["Cancelled"] = "cancelled"; -})(StatusCode || (StatusCode = {})); -function getCertificateBuffer(input) { - return Buffer.from(input.replace(/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n/g, ''), 'base64'); -} -function getThumbprint(input, algorithm) { - const buffer = getCertificateBuffer(input); - return crypto_1.default.createHash(algorithm).update(buffer).digest(); -} -function getKeyFromPFX(pfx) { - const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); - const pemKeyPath = path_1.default.join(os_1.default.tmpdir(), 'key.pem'); - try { - const pfxCertificate = Buffer.from(pfx, 'base64'); - fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); - child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nocerts -nodes -out "${pemKeyPath}" -passin pass:`); - const raw = fs_1.default.readFileSync(pemKeyPath, 'utf-8'); - const result = raw.match(/-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/g)[0]; - return result; - } - finally { - fs_1.default.rmSync(pfxCertificatePath, { force: true }); - fs_1.default.rmSync(pemKeyPath, { force: true }); - } -} -function getCertificatesFromPFX(pfx) { - const pfxCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pfx'); - const pemCertificatePath = path_1.default.join(os_1.default.tmpdir(), 'cert.pem'); - try { - const pfxCertificate = Buffer.from(pfx, 'base64'); - fs_1.default.writeFileSync(pfxCertificatePath, pfxCertificate); - child_process_1.default.execSync(`openssl pkcs12 -in "${pfxCertificatePath}" -nokeys -out "${pemCertificatePath}" -passin pass:`); - const raw = fs_1.default.readFileSync(pemCertificatePath, 'utf-8'); - const matches = raw.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); - return matches ? matches.reverse() : []; - } - finally { - fs_1.default.rmSync(pfxCertificatePath, { force: true }); - fs_1.default.rmSync(pemCertificatePath, { force: true }); - } -} -class ESRPReleaseService { - log; - clientId; - accessToken; - requestSigningCertificates; - requestSigningKey; - containerClient; - stagingSasToken; - static async create(log, tenantId, clientId, authCertificatePfx, requestSigningCertificatePfx, containerClient, stagingSasToken) { - const authKey = getKeyFromPFX(authCertificatePfx); - const authCertificate = getCertificatesFromPFX(authCertificatePfx)[0]; - const requestSigningKey = getKeyFromPFX(requestSigningCertificatePfx); - const requestSigningCertificates = getCertificatesFromPFX(requestSigningCertificatePfx); - const app = new msal_node_1.ConfidentialClientApplication({ - auth: { - clientId, - authority: `https://login.microsoftonline.com/${tenantId}`, - clientCertificate: { - thumbprintSha256: getThumbprint(authCertificate, 'sha256').toString('hex'), - privateKey: authKey, - x5c: authCertificate - } - } - }); - const response = await app.acquireTokenByClientCredential({ - scopes: ['https://api.esrp.microsoft.com/.default'] - }); - return new ESRPReleaseService(log, clientId, response.accessToken, requestSigningCertificates, requestSigningKey, containerClient, stagingSasToken); - } - static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/'; - constructor(log, clientId, accessToken, requestSigningCertificates, requestSigningKey, containerClient, stagingSasToken) { - this.log = log; - this.clientId = clientId; - this.accessToken = accessToken; - this.requestSigningCertificates = requestSigningCertificates; - this.requestSigningKey = requestSigningKey; - this.containerClient = containerClient; - this.stagingSasToken = stagingSasToken; - } - async createRelease(version, filePath, friendlyFileName) { - const correlationId = crypto_1.default.randomUUID(); - const blobClient = this.containerClient.getBlockBlobClient(correlationId); - this.log(`Uploading ${filePath} to ${blobClient.url}`); - await blobClient.uploadFile(filePath); - this.log('Uploaded blob successfully'); - try { - this.log(`Submitting release for ${version}: ${filePath}`); - const submitReleaseResult = await this.submitRelease(version, filePath, friendlyFileName, correlationId, blobClient); - this.log(`Successfully submitted release ${submitReleaseResult.operationId}. Polling for completion...`); - // Poll every 5 seconds, wait 60 minutes max -> poll 60/5*60=720 times - for (let i = 0; i < 720; i++) { - await new Promise(c => setTimeout(c, 5000)); - const releaseStatus = await this.getReleaseStatus(submitReleaseResult.operationId); - if (releaseStatus.status === 'pass') { - break; - } - else if (releaseStatus.status === 'aborted') { - this.log(JSON.stringify(releaseStatus)); - throw new Error(`Release was aborted`); - } - else if (releaseStatus.status !== 'inprogress') { - this.log(JSON.stringify(releaseStatus)); - throw new Error(`Unknown error when polling for release`); - } - } - const releaseDetails = await this.getReleaseDetails(submitReleaseResult.operationId); - if (releaseDetails.status !== 'pass') { - throw new Error(`Timed out waiting for release: ${JSON.stringify(releaseDetails)}`); - } - this.log('Successfully created release:', releaseDetails.files[0].fileDownloadDetails[0].downloadUrl); - return releaseDetails.files[0].fileDownloadDetails[0].downloadUrl; - } - finally { - this.log(`Deleting blob ${blobClient.url}`); - await blobClient.delete(); - this.log('Deleted blob successfully'); - } - } - async submitRelease(version, filePath, friendlyFileName, correlationId, blobClient) { - const size = fs_1.default.statSync(filePath).size; - const hash = await hashStream('sha256', fs_1.default.createReadStream(filePath)); - const blobUrl = `${blobClient.url}?${this.stagingSasToken}`; - const message = { - customerCorrelationId: correlationId, - esrpCorrelationId: correlationId, - driEmail: ['joao.moreno@microsoft.com'], - createdBy: { userPrincipalName: 'jomo@microsoft.com' }, - owners: [{ owner: { userPrincipalName: 'jomo@microsoft.com' } }], - approvers: [{ approver: { userPrincipalName: 'jomo@microsoft.com' }, isAutoApproved: true, isMandatory: false }], - releaseInfo: { - title: 'VS Code', - properties: { - 'ReleaseContentType': 'InstallPackage' - }, - minimumNumberOfApprovers: 1 - }, - productInfo: { - name: 'VS Code', - version, - description: 'VS Code' - }, - accessPermissionsInfo: { - mainPublisher: 'VSCode', - channelDownloadEntityDetails: { - AllDownloadEntities: ['VSCode'] - } - }, - routingInfo: { - intent: 'filedownloadlinkgeneration' - }, - files: [{ - name: path_1.default.basename(filePath), - friendlyFileName, - tenantFileLocation: blobUrl, - tenantFileLocationType: 'AzureBlob', - sourceLocation: { - type: 'azureBlob', - blobUrl - }, - hashType: 'sha256', - hash: Array.from(hash), - sizeInBytes: size - }] - }; - message.jwsToken = await this.generateJwsToken(message); - const res = await fetch(`${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.accessToken}` - }, - body: JSON.stringify(message) - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`Failed to submit release: ${res.statusText}\n${text}`); - } - return await res.json(); - } - async getReleaseStatus(releaseId) { - const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grs/${releaseId}`; - const res = await (0, retry_1.retry)(() => fetch(url, { - headers: { - 'Authorization': `Bearer ${this.accessToken}` - } - })); - if (!res.ok) { - const text = await res.text(); - throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); - } - return await res.json(); - } - async getReleaseDetails(releaseId) { - const url = `${ESRPReleaseService.API_URL}${this.clientId}/workflows/release/operations/grd/${releaseId}`; - const res = await (0, retry_1.retry)(() => fetch(url, { - headers: { - 'Authorization': `Bearer ${this.accessToken}` - } - })); - if (!res.ok) { - const text = await res.text(); - throw new Error(`Failed to get release status: ${res.statusText}\n${text}`); - } - return await res.json(); - } - async generateJwsToken(message) { - // Create header with properly typed properties, then override x5c with the non-standard string format - const header = { - alg: 'RS256', - crit: ['exp', 'x5t'], - // Release service uses ticks, not seconds :roll_eyes: (https://stackoverflow.com/a/7968483) - exp: ((Date.now() + (6 * 60 * 1000)) * 10000) + 621355968000000000, - // Release service uses hex format, not base64url :roll_eyes: - x5t: getThumbprint(this.requestSigningCertificates[0], 'sha1').toString('hex'), - }; - // The Release service expects x5c as a '.' separated string, not the standard array format - header['x5c'] = this.requestSigningCertificates.map(c => getCertificateBuffer(c).toString('base64url')).join('.'); - return jws_1.default.sign({ - header, - payload: message, - privateKey: this.requestSigningKey, - }); - } -} -class State { - statePath; - set = new Set(); - constructor() { - const pipelineWorkspacePath = e('PIPELINE_WORKSPACE'); - const previousState = fs_1.default.readdirSync(pipelineWorkspacePath) - .map(name => /^artifacts_processed_(\d+)$/.exec(name)) - .filter((match) => !!match) - .map(match => ({ name: match[0], attempt: Number(match[1]) })) - .sort((a, b) => b.attempt - a.attempt)[0]; - if (previousState) { - const previousStatePath = path_1.default.join(pipelineWorkspacePath, previousState.name, previousState.name + '.txt'); - fs_1.default.readFileSync(previousStatePath, 'utf8').split(/\n/).filter(name => !!name).forEach(name => this.set.add(name)); - } - const stageAttempt = e('SYSTEM_STAGEATTEMPT'); - this.statePath = path_1.default.join(pipelineWorkspacePath, `artifacts_processed_${stageAttempt}`, `artifacts_processed_${stageAttempt}.txt`); - fs_1.default.mkdirSync(path_1.default.dirname(this.statePath), { recursive: true }); - fs_1.default.writeFileSync(this.statePath, [...this.set.values()].map(name => `${name}\n`).join('')); - } - get size() { - return this.set.size; - } - has(name) { - return this.set.has(name); - } - add(name) { - this.set.add(name); - fs_1.default.appendFileSync(this.statePath, `${name}\n`); - } - [Symbol.iterator]() { - return this.set[Symbol.iterator](); - } -} -const azdoFetchOptions = { - headers: { - // Pretend we're a web browser to avoid download rate limits - 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'en-US,en;q=0.9', - 'Referer': 'https://dev.azure.com', - Authorization: `Bearer ${e('SYSTEM_ACCESSTOKEN')}` - } -}; -async function requestAZDOAPI(path) { - const abortController = new AbortController(); - const timeout = setTimeout(() => abortController.abort(), 2 * 60 * 1000); - try { - const res = await (0, retry_1.retry)(() => fetch(`${e('BUILDS_API_URL')}${path}?api-version=6.0`, { ...azdoFetchOptions, signal: abortController.signal })); - if (!res.ok) { - throw new Error(`Unexpected status code: ${res.status}`); - } - return await res.json(); - } - finally { - clearTimeout(timeout); - } -} -async function getPipelineArtifacts() { - const result = await requestAZDOAPI('artifacts'); - return result.value.filter(a => /^vscode_/.test(a.name) && !/sbom$/.test(a.name)); -} -async function getPipelineTimeline() { - return await requestAZDOAPI('timeline'); -} -async function downloadArtifact(artifact, downloadPath) { - const abortController = new AbortController(); - const timeout = setTimeout(() => abortController.abort(), 4 * 60 * 1000); - try { - const res = await fetch(artifact.resource.downloadUrl, { ...azdoFetchOptions, signal: abortController.signal }); - if (!res.ok) { - throw new Error(`Unexpected status code: ${res.status}`); - } - await (0, promises_1.pipeline)(stream_1.Readable.fromWeb(res.body), fs_1.default.createWriteStream(downloadPath)); - } - finally { - clearTimeout(timeout); - } -} -async function unzip(packagePath, outputPath) { - return new Promise((resolve, reject) => { - yauzl_1.default.open(packagePath, { lazyEntries: true, autoClose: true }, (err, zipfile) => { - if (err) { - return reject(err); - } - const result = []; - zipfile.on('entry', entry => { - if (/\/$/.test(entry.fileName)) { - zipfile.readEntry(); - } - else { - zipfile.openReadStream(entry, (err, istream) => { - if (err) { - return reject(err); - } - const filePath = path_1.default.join(outputPath, entry.fileName); - fs_1.default.mkdirSync(path_1.default.dirname(filePath), { recursive: true }); - const ostream = fs_1.default.createWriteStream(filePath); - ostream.on('finish', () => { - result.push(filePath); - zipfile.readEntry(); - }); - istream?.on('error', err => reject(err)); - istream.pipe(ostream); - }); - } - }); - zipfile.on('close', () => resolve(result)); - zipfile.readEntry(); - }); - }); -} -// Contains all of the logic for mapping details to our actual product names in CosmosDB -function getPlatform(product, os, arch, type) { - switch (os) { - case 'win32': - switch (product) { - case 'client': { - switch (type) { - case 'archive': - return `win32-${arch}-archive`; - case 'setup': - return `win32-${arch}`; - case 'user-setup': - return `win32-${arch}-user`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - } - case 'server': - return `server-win32-${arch}`; - case 'web': - return `server-win32-${arch}-web`; - case 'cli': - return `cli-win32-${arch}`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - case 'alpine': - switch (product) { - case 'server': - return `server-alpine-${arch}`; - case 'web': - return `server-alpine-${arch}-web`; - case 'cli': - return `cli-alpine-${arch}`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - case 'linux': - switch (type) { - case 'snap': - return `linux-snap-${arch}`; - case 'archive-unsigned': - switch (product) { - case 'client': - return `linux-${arch}`; - case 'server': - return `server-linux-${arch}`; - case 'web': - if (arch === 'standalone') { - return 'web-standalone'; - } - return `server-linux-${arch}-web`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - case 'deb-package': - return `linux-deb-${arch}`; - case 'rpm-package': - return `linux-rpm-${arch}`; - case 'cli': - return `cli-linux-${arch}`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - case 'darwin': - switch (product) { - case 'client': - if (arch === 'x64') { - return 'darwin'; - } - return `darwin-${arch}`; - case 'server': - if (arch === 'x64') { - return 'server-darwin'; - } - return `server-darwin-${arch}`; - case 'web': - if (arch === 'x64') { - return 'server-darwin-web'; - } - return `server-darwin-${arch}-web`; - case 'cli': - return `cli-darwin-${arch}`; - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } - default: - throw new Error(`Unrecognized: ${product} ${os} ${arch} ${type}`); - } -} -// Contains all of the logic for mapping types to our actual types in CosmosDB -function getRealType(type) { - switch (type) { - case 'user-setup': - return 'setup'; - case 'deb-package': - case 'rpm-package': - return 'package'; - default: - return type; - } -} -async function withLease(client, fn) { - const lease = client.getBlobLeaseClient(); - for (let i = 0; i < 360; i++) { // Try to get lease for 30 minutes - try { - await client.uploadData(new ArrayBuffer()); // blob needs to exist for lease to be acquired - await lease.acquireLease(60); - try { - const abortController = new AbortController(); - const refresher = new Promise((c, e) => { - abortController.signal.onabort = () => { - (0, node_timers_1.clearInterval)(interval); - c(); - }; - const interval = (0, node_timers_1.setInterval)(() => { - lease.renewLease().catch(err => { - (0, node_timers_1.clearInterval)(interval); - e(new Error('Failed to renew lease ' + err)); - }); - }, 30_000); - }); - const result = await Promise.race([fn(), refresher]); - abortController.abort(); - return result; - } - finally { - await lease.releaseLease(); - } - } - catch (err) { - if (err.statusCode !== 409 && err.statusCode !== 412) { - throw err; - } - await new Promise(c => setTimeout(c, 5000)); - } - } - throw new Error('Failed to acquire lease on blob after 30 minutes'); -} -async function processArtifact(artifact, filePath) { - const log = (...args) => console.log(`[${artifact.name}]`, ...args); - const match = /^vscode_(?[^_]+)_(?[^_]+)(?:_legacy)?_(?[^_]+)_(?[^_]+)$/.exec(artifact.name); - if (!match) { - throw new Error(`Invalid artifact name: ${artifact.name}`); - } - const { cosmosDBAccessToken, blobServiceAccessToken } = JSON.parse(e('PUBLISH_AUTH_TOKENS')); - const quality = e('VSCODE_QUALITY'); - const version = e('BUILD_SOURCEVERSION'); - const friendlyFileName = `${quality}/${version}/${path_1.default.basename(filePath)}`; - const blobServiceClient = new storage_blob_1.BlobServiceClient(`https://${e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')}.blob.core.windows.net/`, { getToken: async () => blobServiceAccessToken }); - const leasesContainerClient = blobServiceClient.getContainerClient('leases'); - await leasesContainerClient.createIfNotExists(); - const leaseBlobClient = leasesContainerClient.getBlockBlobClient(friendlyFileName); - log(`Acquiring lease for: ${friendlyFileName}`); - await withLease(leaseBlobClient, async () => { - log(`Successfully acquired lease for: ${friendlyFileName}`); - const url = `${e('PRSS_CDN_URL')}/${friendlyFileName}`; - const res = await (0, retry_1.retry)(() => fetch(url)); - if (res.status === 200) { - log(`Already released and provisioned: ${url}`); - } - else { - const stagingContainerClient = blobServiceClient.getContainerClient('staging'); - await stagingContainerClient.createIfNotExists(); - const now = new Date().valueOf(); - const oneHour = 60 * 60 * 1000; - const oneHourAgo = new Date(now - oneHour); - const oneHourFromNow = new Date(now + oneHour); - const userDelegationKey = await blobServiceClient.getUserDelegationKey(oneHourAgo, oneHourFromNow); - const sasOptions = { containerName: 'staging', permissions: storage_blob_1.ContainerSASPermissions.from({ read: true }), startsOn: oneHourAgo, expiresOn: oneHourFromNow }; - const stagingSasToken = (0, storage_blob_1.generateBlobSASQueryParameters)(sasOptions, userDelegationKey, e('VSCODE_STAGING_BLOB_STORAGE_ACCOUNT_NAME')).toString(); - const releaseService = await ESRPReleaseService.create(log, e('RELEASE_TENANT_ID'), e('RELEASE_CLIENT_ID'), e('RELEASE_AUTH_CERT'), e('RELEASE_REQUEST_SIGNING_CERT'), stagingContainerClient, stagingSasToken); - await releaseService.createRelease(version, filePath, friendlyFileName); - } - const { product, os, arch, unprocessedType } = match.groups; - const platform = getPlatform(product, os, arch, unprocessedType); - const type = getRealType(unprocessedType); - const size = fs_1.default.statSync(filePath).size; - const stream = fs_1.default.createReadStream(filePath); - const [hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); // CodeQL [SM04514] Using SHA1 only for legacy reasons, we are actually only respecting SHA256 - const asset = { platform, type, url, hash: hash.toString('hex'), sha256hash: sha256hash.toString('hex'), size, supportsFastUpdate: true }; - log('Creating asset...'); - const result = await (0, retry_1.retry)(async (attempt) => { - log(`Creating asset in Cosmos DB (attempt ${attempt})...`); - const client = new cosmos_1.CosmosClient({ endpoint: e('AZURE_DOCUMENTDB_ENDPOINT'), tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); - const scripts = client.database('builds').container(quality).scripts; - const { resource: result } = await scripts.storedProcedure('createAsset').execute('', [version, asset, true]); - return result; - }); - if (result === 'already exists') { - log('Asset already exists!'); - } - else { - log('Asset successfully created: ', JSON.stringify(asset, undefined, 2)); - } - }); - log(`Successfully released lease for: ${friendlyFileName}`); -} -// It is VERY important that we don't download artifacts too much too fast from AZDO. -// AZDO throttles us SEVERELY if we do. Not just that, but they also close open -// sockets, so the whole things turns to a grinding halt. So, downloading and extracting -// happens serially in the main thread, making the downloads are spaced out -// properly. For each extracted artifact, we spawn a worker thread to upload it to -// the CDN and finally update the build in Cosmos DB. -async function main() { - if (!node_worker_threads_1.isMainThread) { - const { artifact, artifactFilePath } = node_worker_threads_1.workerData; - await processArtifact(artifact, artifactFilePath); - return; - } - const done = new State(); - const processing = new Set(); - for (const name of done) { - console.log(`\u2705 ${name}`); - } - const stages = new Set(['Compile']); - if (e('VSCODE_BUILD_STAGE_LINUX') === 'True' || - e('VSCODE_BUILD_STAGE_ALPINE') === 'True' || - e('VSCODE_BUILD_STAGE_MACOS') === 'True' || - e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') { - stages.add('CompileCLI'); - } - if (e('VSCODE_BUILD_STAGE_WINDOWS') === 'True') { - stages.add('Windows'); - } - if (e('VSCODE_BUILD_STAGE_LINUX') === 'True') { - stages.add('Linux'); - } - if (e('VSCODE_BUILD_STAGE_ALPINE') === 'True') { - stages.add('Alpine'); - } - if (e('VSCODE_BUILD_STAGE_MACOS') === 'True') { - stages.add('macOS'); - } - if (e('VSCODE_BUILD_STAGE_WEB') === 'True') { - stages.add('Web'); - } - let timeline; - let artifacts; - let resultPromise = Promise.resolve([]); - const operations = []; - while (true) { - [timeline, artifacts] = await Promise.all([(0, retry_1.retry)(() => getPipelineTimeline()), (0, retry_1.retry)(() => getPipelineArtifacts())]); - const stagesCompleted = new Set(timeline.records.filter(r => r.type === 'Stage' && r.state === 'completed' && stages.has(r.name)).map(r => r.name)); - const stagesInProgress = [...stages].filter(s => !stagesCompleted.has(s)); - const artifactsInProgress = artifacts.filter(a => processing.has(a.name)); - if (stagesInProgress.length === 0 && artifacts.length === done.size + processing.size) { - break; - } - else if (stagesInProgress.length > 0) { - console.log('Stages in progress:', stagesInProgress.join(', ')); - } - else if (artifactsInProgress.length > 0) { - console.log('Artifacts in progress:', artifactsInProgress.map(a => a.name).join(', ')); - } - else { - console.log(`Waiting for a total of ${artifacts.length}, ${done.size} done, ${processing.size} in progress...`); - } - for (const artifact of artifacts) { - if (done.has(artifact.name) || processing.has(artifact.name)) { - continue; - } - console.log(`[${artifact.name}] Found new artifact`); - const artifactZipPath = path_1.default.join(e('AGENT_TEMPDIRECTORY'), `${artifact.name}.zip`); - await (0, retry_1.retry)(async (attempt) => { - const start = Date.now(); - console.log(`[${artifact.name}] Downloading (attempt ${attempt})...`); - await downloadArtifact(artifact, artifactZipPath); - const archiveSize = fs_1.default.statSync(artifactZipPath).size; - const downloadDurationS = (Date.now() - start) / 1000; - const downloadSpeedKBS = Math.round((archiveSize / 1024) / downloadDurationS); - console.log(`[${artifact.name}] Successfully downloaded after ${Math.floor(downloadDurationS)} seconds(${downloadSpeedKBS} KB/s).`); - }); - const artifactFilePaths = await unzip(artifactZipPath, e('AGENT_TEMPDIRECTORY')); - const artifactFilePath = artifactFilePaths.filter(p => !/_manifest/.test(p))[0]; - processing.add(artifact.name); - const promise = new Promise((resolve, reject) => { - const worker = new node_worker_threads_1.Worker(__filename, { workerData: { artifact, artifactFilePath } }); - worker.on('error', reject); - worker.on('exit', code => { - if (code === 0) { - resolve(); - } - else { - reject(new Error(`[${artifact.name}] Worker stopped with exit code ${code}`)); - } - }); - }); - const operation = promise.then(() => { - processing.delete(artifact.name); - done.add(artifact.name); - console.log(`\u2705 ${artifact.name} `); - }); - operations.push({ name: artifact.name, operation }); - resultPromise = Promise.allSettled(operations.map(o => o.operation)); - } - await new Promise(c => setTimeout(c, 10_000)); - } - console.log(`Found all ${done.size + processing.size} artifacts, waiting for ${processing.size} artifacts to finish publishing...`); - const artifactsInProgress = operations.filter(o => processing.has(o.name)); - if (artifactsInProgress.length > 0) { - console.log('Artifacts in progress:', artifactsInProgress.map(a => a.name).join(', ')); - } - const results = await resultPromise; - for (let i = 0; i < operations.length; i++) { - const result = results[i]; - if (result.status === 'rejected') { - console.error(`[${operations[i].name}]`, result.reason); - } - } - // Fail the job if any of the artifacts failed to publish - if (results.some(r => r.status === 'rejected')) { - throw new Error('Some artifacts failed to publish'); - } - // Also fail the job if any of the stages did not succeed - let shouldFail = false; - for (const stage of stages) { - const record = timeline.records.find(r => r.name === stage && r.type === 'Stage'); - if (record.result !== 'succeeded' && record.result !== 'succeededWithIssues') { - shouldFail = true; - console.error(`Stage ${stage} did not succeed: ${record.result}`); - } - } - if (shouldFail) { - throw new Error('Some stages did not succeed'); - } - console.log(`All ${done.size} artifacts published!`); -} -if (require.main === module) { - main().then(() => { - process.exit(0); - }, err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=publish.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/publish.ts b/build/azure-pipelines/common/publish.ts index e8a6776ceb1..12f9ef4ffa8 100644 --- a/build/azure-pipelines/common/publish.ts +++ b/build/azure-pipelines/common/publish.ts @@ -10,7 +10,7 @@ import type { ReadableStream } from 'stream/web'; import { pipeline } from 'node:stream/promises'; import yauzl from 'yauzl'; import crypto from 'crypto'; -import { retry } from './retry'; +import { retry } from './retry.ts'; import { CosmosClient } from '@azure/cosmos'; import cp from 'child_process'; import os from 'os'; @@ -73,15 +73,16 @@ interface ReleaseError { errorMessages: string[]; } -const enum StatusCode { - Pass = 'pass', - Aborted = 'aborted', - Inprogress = 'inprogress', - FailCanRetry = 'failCanRetry', - FailDoNotRetry = 'failDoNotRetry', - PendingAnalysis = 'pendingAnalysis', - Cancelled = 'cancelled' -} +const StatusCode = Object.freeze({ + Pass: 'pass', + Aborted: 'aborted', + Inprogress: 'inprogress', + FailCanRetry: 'failCanRetry', + FailDoNotRetry: 'failDoNotRetry', + PendingAnalysis: 'pendingAnalysis', + Cancelled: 'cancelled' +}); +type StatusCode = typeof StatusCode[keyof typeof StatusCode]; interface ReleaseResultMessage { activities: ReleaseActivityInfo[]; @@ -349,15 +350,31 @@ class ESRPReleaseService { private static API_URL = 'https://api.esrp.microsoft.com/api/v3/releaseservices/clients/'; + private readonly log: (...args: unknown[]) => void; + private readonly clientId: string; + private readonly accessToken: string; + private readonly requestSigningCertificates: string[]; + private readonly requestSigningKey: string; + private readonly containerClient: ContainerClient; + private readonly stagingSasToken: string; + private constructor( - private readonly log: (...args: unknown[]) => void, - private readonly clientId: string, - private readonly accessToken: string, - private readonly requestSigningCertificates: string[], - private readonly requestSigningKey: string, - private readonly containerClient: ContainerClient, - private readonly stagingSasToken: string - ) { } + log: (...args: unknown[]) => void, + clientId: string, + accessToken: string, + requestSigningCertificates: string[], + requestSigningKey: string, + containerClient: ContainerClient, + stagingSasToken: string + ) { + this.log = log; + this.clientId = clientId; + this.accessToken = accessToken; + this.requestSigningCertificates = requestSigningCertificates; + this.requestSigningKey = requestSigningKey; + this.containerClient = containerClient; + this.stagingSasToken = stagingSasToken; + } async createRelease(version: string, filePath: string, friendlyFileName: string) { const correlationId = crypto.randomUUID(); @@ -765,10 +782,16 @@ function getPlatform(product: string, os: string, arch: string, type: string): s case 'darwin': switch (product) { case 'client': - if (arch === 'x64') { - return 'darwin'; + switch (type) { + case 'dmg': + return `darwin-${arch}-dmg`; + case 'archive': + default: + if (arch === 'x64') { + return 'darwin'; + } + return `darwin-${arch}`; } - return `darwin-${arch}`; case 'server': if (arch === 'x64') { return 'server-darwin'; @@ -1009,7 +1032,7 @@ async function main() { processing.add(artifact.name); const promise = new Promise((resolve, reject) => { - const worker = new Worker(__filename, { workerData: { artifact, artifactFilePath } }); + const worker = new Worker(import.meta.filename, { workerData: { artifact, artifactFilePath } }); worker.on('error', reject); worker.on('exit', code => { if (code === 0) { @@ -1075,7 +1098,7 @@ async function main() { console.log(`All ${done.size} artifacts published!`); } -if (require.main === module) { +if (import.meta.main) { main().then(() => { process.exit(0); }, err => { diff --git a/build/azure-pipelines/common/releaseBuild.js b/build/azure-pipelines/common/releaseBuild.js deleted file mode 100644 index b74e2847cbc..00000000000 --- a/build/azure-pipelines/common/releaseBuild.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const cosmos_1 = require("@azure/cosmos"); -const retry_1 = require("./retry"); -function getEnv(name) { - const result = process.env[name]; - if (typeof result === 'undefined') { - throw new Error('Missing env: ' + name); - } - return result; -} -function createDefaultConfig(quality) { - return { - id: quality, - frozen: false - }; -} -async function getConfig(client, quality) { - const query = `SELECT TOP 1 * FROM c WHERE c.id = "${quality}"`; - const res = await client.database('builds').container('config').items.query(query).fetchAll(); - if (res.resources.length === 0) { - return createDefaultConfig(quality); - } - return res.resources[0]; -} -async function main(force) { - const commit = getEnv('BUILD_SOURCEVERSION'); - const quality = getEnv('VSCODE_QUALITY'); - const { cosmosDBAccessToken } = JSON.parse(getEnv('PUBLISH_AUTH_TOKENS')); - const client = new cosmos_1.CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT'], tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); - if (!force) { - const config = await getConfig(client, quality); - console.log('Quality config:', config); - if (config.frozen) { - console.log(`Skipping release because quality ${quality} is frozen.`); - return; - } - } - console.log(`Releasing build ${commit}...`); - const scripts = client.database('builds').container(quality).scripts; - await (0, retry_1.retry)(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); -} -const [, , force] = process.argv; -console.log(process.argv); -main(/^true$/i.test(force)).then(() => { - console.log('Build successfully released'); - process.exit(0); -}, err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=releaseBuild.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/releaseBuild.ts b/build/azure-pipelines/common/releaseBuild.ts index d60701c2fac..37e869a42bc 100644 --- a/build/azure-pipelines/common/releaseBuild.ts +++ b/build/azure-pipelines/common/releaseBuild.ts @@ -4,7 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { CosmosClient } from '@azure/cosmos'; -import { retry } from './retry'; +import path from 'path'; +import fs from 'fs'; +import { retry } from './retry.ts'; +import { type IExtensionManifest, parseApiProposalsFromSource, checkExtensionCompatibility, areAllowlistedApiProposalsMatching } from './versionCompatibility.ts'; + +const root = path.dirname(path.dirname(path.dirname(import.meta.dirname))); function getEnv(name: string): string { const result = process.env[name]; @@ -16,6 +21,109 @@ function getEnv(name: string): string { return result; } +async function fetchLatestExtensionManifest(extensionId: string): Promise { + // Use the vscode-unpkg service to get the latest extension package.json + const [publisher, name] = extensionId.split('.'); + + // First, get the latest version from the gallery endpoint + const galleryUrl = `https://main.vscode-unpkg.net/_gallery/${publisher}/${name}/latest`; + const galleryResponse = await fetch(galleryUrl, { + headers: { 'User-Agent': 'VSCode Build' } + }); + + if (!galleryResponse.ok) { + throw new Error(`Failed to fetch latest version for ${extensionId}: ${galleryResponse.status} ${galleryResponse.statusText}`); + } + + const galleryData = await galleryResponse.json() as { versions: { version: string }[] }; + const version = galleryData.versions[0].version; + + // Now fetch the package.json using the actual version + const url = `https://${publisher}.vscode-unpkg.net/${publisher}/${name}/${version}/extension/package.json`; + + const response = await fetch(url, { + headers: { 'User-Agent': 'VSCode Build' } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch extension ${extensionId} from unpkg: ${response.status} ${response.statusText}`); + } + + return await response.json() as IExtensionManifest; +} + +async function checkCopilotChatCompatibility(): Promise { + const extensionId = 'github.copilot-chat'; + + console.log(`Checking compatibility of ${extensionId}...`); + + // Get product version from package.json + const packageJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); + const productVersion = packageJson.version; + + console.log(`Product version: ${productVersion}`); + + // Get API proposals from the generated file + const apiProposalsPath = path.join(root, 'src/vs/platform/extensions/common/extensionsApiProposals.ts'); + const apiProposalsContent = fs.readFileSync(apiProposalsPath, 'utf8'); + const allApiProposals = parseApiProposalsFromSource(apiProposalsContent); + + const proposalCount = Object.keys(allApiProposals).length; + if (proposalCount === 0) { + throw new Error('Failed to load API proposals from source'); + } + + console.log(`Loaded ${proposalCount} API proposals from source`); + + // Load product.json to check allowlisted API proposals + const productJsonPath = path.join(root, 'product.json'); + let productJson; + try { + productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); + } catch (error) { + throw new Error(`Failed to load or parse product.json: ${error}`); + } + const extensionEnabledApiProposals = productJson?.extensionEnabledApiProposals; + const extensionIdKey = extensionEnabledApiProposals ? Object.keys(extensionEnabledApiProposals).find(key => key.toLowerCase() === extensionId.toLowerCase()) : undefined; + const productAllowlistedProposals = extensionIdKey ? extensionEnabledApiProposals[extensionIdKey] : undefined; + + if (productAllowlistedProposals) { + console.log(`Product.json allowlisted proposals for ${extensionId}:`); + for (const proposal of productAllowlistedProposals) { + console.log(` ${proposal}`); + } + } else { + console.log(`Product.json allowlisted proposals for ${extensionId}: none`); + } + + // Fetch the latest extension manifest + const manifest = await retry(() => fetchLatestExtensionManifest(extensionId)); + + console.log(`Extension ${extensionId}@${manifest.version}:`); + console.log(` engines.vscode: ${manifest.engines.vscode}`); + console.log(` enabledApiProposals:\n ${manifest.enabledApiProposals?.join('\n ') || 'none'}`); + + // Check compatibility + const result = checkExtensionCompatibility(productVersion, allApiProposals, manifest); + if (!result.compatible) { + throw new Error(`Compatibility check failed:\n ${result.errors.join('\n ')}`); + } + + console.log(` ✓ Engine version compatible`); + if (manifest.enabledApiProposals?.length) { + console.log(` ✓ API proposals compatible`); + } + + // Check that product.json allowlist matches package.json declarations + const allowlistResult = areAllowlistedApiProposalsMatching(extensionId, productAllowlistedProposals, manifest.enabledApiProposals); + if (!allowlistResult.compatible) { + throw new Error(`Allowlist check failed:\n ${allowlistResult.errors.join('\n ')}`); + } + + console.log(` ✓ Product.json allowlist matches package.json`); + console.log(`✓ ${extensionId} is compatible with this build`); +} + interface Config { id: string; frozen: boolean; @@ -43,6 +151,12 @@ async function getConfig(client: CosmosClient, quality: string): Promise async function main(force: boolean): Promise { const commit = getEnv('BUILD_SOURCEVERSION'); const quality = getEnv('VSCODE_QUALITY'); + + // Check Copilot Chat compatibility before releasing insider builds + if (quality === 'insider') { + await checkCopilotChatCompatibility(); + } + const { cosmosDBAccessToken } = JSON.parse(getEnv('PUBLISH_AUTH_TOKENS')); const client = new CosmosClient({ endpoint: process.env['AZURE_DOCUMENTDB_ENDPOINT']!, tokenProvider: () => Promise.resolve(`type=aad&ver=1.0&sig=${cosmosDBAccessToken.token}`) }); @@ -59,8 +173,15 @@ async function main(force: boolean): Promise { console.log(`Releasing build ${commit}...`); + let rolloutDurationMs = undefined; + + // If the build is insiders or exploration, start a rollout of 4 hours + if (quality === 'insider') { + rolloutDurationMs = 4 * 60 * 60 * 1000; // 4 hours + } + const scripts = client.database('builds').container(quality).scripts; - await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit])); + await retry(() => scripts.storedProcedure('releaseBuild').execute('', [commit, rolloutDurationMs])); } const [, , force] = process.argv; diff --git a/build/azure-pipelines/common/retry.js b/build/azure-pipelines/common/retry.js deleted file mode 100644 index 91f60bf24b2..00000000000 --- a/build/azure-pipelines/common/retry.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.retry = retry; -async function retry(fn) { - let lastError; - for (let run = 1; run <= 10; run++) { - try { - return await fn(run); - } - catch (err) { - if (!/fetch failed|terminated|aborted|timeout|TimeoutError|Timeout Error|RestError|Client network socket disconnected|socket hang up|ECONNRESET|CredentialUnavailableError|endpoints_resolution_error|Audience validation failed|end of central directory record signature not found/i.test(err.message)) { - throw err; - } - lastError = err; - // maximum delay is 10th retry: ~3 seconds - const millis = Math.floor((Math.random() * 200) + (50 * Math.pow(1.5, run))); - await new Promise(c => setTimeout(c, millis)); - } - } - console.error(`Too many retries, aborting.`); - throw lastError; -} -//# sourceMappingURL=retry.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/sanity-tests.yml b/build/azure-pipelines/common/sanity-tests.yml new file mode 100644 index 00000000000..d95bbe8351a --- /dev/null +++ b/build/azure-pipelines/common/sanity-tests.yml @@ -0,0 +1,129 @@ +parameters: + - name: name + type: string + - name: displayName + type: string + - name: poolName + type: string + - name: os + type: string + default: linux + - name: container + type: string + default: "" + - name: arch + type: string + default: amd64 + - name: baseImage + type: string + default: "" + - name: pageSize + type: string + default: "" + - name: args + type: string + default: "" + +jobs: + - job: ${{ parameters.name }} + displayName: ${{ parameters.displayName }} + pool: + name: ${{ parameters.poolName }} + os: ${{ parameters.os }} + timeoutInMinutes: 30 + variables: + TEST_DIR: $(Build.SourcesDirectory)/test/sanity + LOG_FILE: $(TEST_DIR)/results.xml + DOCKER_CACHE_DIR: $(Pipeline.Workspace)/docker-cache + DOCKER_CACHE_FILE: $(DOCKER_CACHE_DIR)/${{ parameters.container }}.tar + steps: + - checkout: self + fetchDepth: 1 + fetchTags: false + sparseCheckoutDirectories: test/sanity .nvmrc + displayName: Checkout test/sanity + + - task: NodeTool@0 + inputs: + versionSource: fromFile + versionFilePath: .nvmrc + displayName: Install Node.js + + - script: npm config set registry "$(NPM_REGISTRY)" --location=project + workingDirectory: $(TEST_DIR) + displayName: Configure NPM Registry + + - task: npmAuthenticate@0 + inputs: + workingFile: $(TEST_DIR)/.npmrc + displayName: Authenticate with NPM Registry + + - script: npm ci + workingDirectory: $(TEST_DIR) + displayName: Install NPM Dependencies + + - script: npm run compile + workingDirectory: $(TEST_DIR) + displayName: Compile Sanity Tests + + # Windows + - ${{ if eq(parameters.os, 'windows') }}: + - script: $(TEST_DIR)/scripts/run-win32.cmd -c $(BUILD_COMMIT) -q $(BUILD_QUALITY) -t $(LOG_FILE) -v ${{ parameters.args }} + workingDirectory: $(TEST_DIR) + displayName: Run Sanity Tests + + # macOS + - ${{ if eq(parameters.os, 'macOS') }}: + - bash: $(TEST_DIR)/scripts/run-macOS.sh -c $(BUILD_COMMIT) -q $(BUILD_QUALITY) -t $(LOG_FILE) -v ${{ parameters.args }} + workingDirectory: $(TEST_DIR) + displayName: Run Sanity Tests + + # Native Linux host + - ${{ if and(eq(parameters.container, ''), eq(parameters.os, 'linux')) }}: + - bash: $(TEST_DIR)/scripts/run-ubuntu.sh -c $(BUILD_COMMIT) -q $(BUILD_QUALITY) -t $(LOG_FILE) -v ${{ parameters.args }} + workingDirectory: $(TEST_DIR) + displayName: Run Sanity Tests + + # Linux Docker container + - ${{ if ne(parameters.container, '') }}: + - task: Cache@2 + inputs: + key: 'docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "${{ parameters.pageSize }}" | "$(Agent.OS)" | $(TEST_DIR)/containers/${{ parameters.container }}.dockerfile' + path: $(DOCKER_CACHE_DIR) + restoreKeys: docker-v3 | "${{ parameters.container }}" | "${{ parameters.arch }}" | "${{ parameters.pageSize }}" | "$(Agent.OS)" + cacheHitVar: DOCKER_CACHE_HIT + displayName: Download Docker Image + + - bash: | + docker load -i "$(DOCKER_CACHE_FILE)" + rm -f "$(DOCKER_CACHE_FILE)" + condition: eq(variables.DOCKER_CACHE_HIT, 'true') + displayName: Load Docker Image + + - bash: | + $(TEST_DIR)/scripts/run-docker.sh \ + --container "${{ parameters.container }}" \ + --arch "${{ parameters.arch }}" \ + --base-image "${{ parameters.baseImage }}" \ + --page-size "${{ parameters.pageSize }}" \ + --quality "$(BUILD_QUALITY)" \ + --commit "$(BUILD_COMMIT)" \ + --test-results "/root/results.xml" \ + --verbose \ + ${{ parameters.args }} + workingDirectory: $(TEST_DIR) + displayName: Run Sanity Tests + + - bash: | + mkdir -p "$(DOCKER_CACHE_DIR)" + docker save -o "$(DOCKER_CACHE_FILE)" "${{ parameters.container }}" + condition: and(succeeded(), ne(variables.DOCKER_CACHE_HIT, 'true')) + displayName: Save Docker Image + + - task: PublishTestResults@2 + inputs: + testResultsFormat: JUnit + testResultsFiles: $(LOG_FILE) + testRunTitle: ${{ parameters.displayName }} + displayName: Publish Test Results + condition: succeededOrFailed() diff --git a/build/azure-pipelines/common/sign-win32.js b/build/azure-pipelines/common/sign-win32.js deleted file mode 100644 index f4e3f27c1f2..00000000000 --- a/build/azure-pipelines/common/sign-win32.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const sign_1 = require("./sign"); -const path_1 = __importDefault(require("path")); -(0, sign_1.main)([ - process.env['EsrpCliDllPath'], - 'sign-windows', - path_1.default.dirname(process.argv[2]), - path_1.default.basename(process.argv[2]) -]); -//# sourceMappingURL=sign-win32.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/sign-win32.ts b/build/azure-pipelines/common/sign-win32.ts index ad88435b5a3..677c2024b9c 100644 --- a/build/azure-pipelines/common/sign-win32.ts +++ b/build/azure-pipelines/common/sign-win32.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { main } from './sign'; +import { main } from './sign.ts'; import path from 'path'; main([ diff --git a/build/azure-pipelines/common/sign.js b/build/azure-pipelines/common/sign.js deleted file mode 100644 index 47c034dea1c..00000000000 --- a/build/azure-pipelines/common/sign.js +++ /dev/null @@ -1,209 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Temp = void 0; -exports.main = main; -const child_process_1 = __importDefault(require("child_process")); -const fs_1 = __importDefault(require("fs")); -const crypto_1 = __importDefault(require("crypto")); -const path_1 = __importDefault(require("path")); -const os_1 = __importDefault(require("os")); -class Temp { - _files = []; - tmpNameSync() { - const file = path_1.default.join(os_1.default.tmpdir(), crypto_1.default.randomBytes(20).toString('hex')); - this._files.push(file); - return file; - } - dispose() { - for (const file of this._files) { - try { - fs_1.default.unlinkSync(file); - } - catch (err) { - // noop - } - } - } -} -exports.Temp = Temp; -function getParams(type) { - switch (type) { - case 'sign-windows': - return [ - { - keyCode: 'CP-230012', - operationSetCode: 'SigntoolSign', - parameters: [ - { parameterName: 'OpusName', parameterValue: 'VS Code' }, - { parameterName: 'OpusInfo', parameterValue: 'https://code.visualstudio.com/' }, - { parameterName: 'Append', parameterValue: '/as' }, - { parameterName: 'FileDigest', parameterValue: '/fd "SHA256"' }, - { parameterName: 'PageHash', parameterValue: '/NPH' }, - { parameterName: 'TimeStamp', parameterValue: '/tr "http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer" /td sha256' } - ], - toolName: 'sign', - toolVersion: '1.0' - }, - { - keyCode: 'CP-230012', - operationSetCode: 'SigntoolVerify', - parameters: [ - { parameterName: 'VerifyAll', parameterValue: '/all' } - ], - toolName: 'sign', - toolVersion: '1.0' - } - ]; - case 'sign-windows-appx': - return [ - { - keyCode: 'CP-229979', - operationSetCode: 'SigntoolSign', - parameters: [ - { parameterName: 'OpusName', parameterValue: 'VS Code' }, - { parameterName: 'OpusInfo', parameterValue: 'https://code.visualstudio.com/' }, - { parameterName: 'FileDigest', parameterValue: '/fd "SHA256"' }, - { parameterName: 'PageHash', parameterValue: '/NPH' }, - { parameterName: 'TimeStamp', parameterValue: '/tr "http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer" /td sha256' } - ], - toolName: 'sign', - toolVersion: '1.0' - }, - { - keyCode: 'CP-229979', - operationSetCode: 'SigntoolVerify', - parameters: [], - toolName: 'sign', - toolVersion: '1.0' - } - ]; - case 'sign-pgp': - return [{ - keyCode: 'CP-450779-Pgp', - operationSetCode: 'LinuxSign', - parameters: [], - toolName: 'sign', - toolVersion: '1.0' - }]; - case 'sign-darwin': - return [{ - keyCode: 'CP-401337-Apple', - operationSetCode: 'MacAppDeveloperSign', - parameters: [{ parameterName: 'Hardening', parameterValue: '--options=runtime' }], - toolName: 'sign', - toolVersion: '1.0' - }]; - case 'notarize-darwin': - return [{ - keyCode: 'CP-401337-Apple', - operationSetCode: 'MacAppNotarize', - parameters: [], - toolName: 'sign', - toolVersion: '1.0' - }]; - case 'nuget': - return [{ - keyCode: 'CP-401405', - operationSetCode: 'NuGetSign', - parameters: [], - toolName: 'sign', - toolVersion: '1.0' - }, { - keyCode: 'CP-401405', - operationSetCode: 'NuGetVerify', - parameters: [], - toolName: 'sign', - toolVersion: '1.0' - }]; - default: - throw new Error(`Sign type ${type} not found`); - } -} -function main([esrpCliPath, type, folderPath, pattern]) { - const tmp = new Temp(); - process.on('exit', () => tmp.dispose()); - const key = crypto_1.default.randomBytes(32); - const iv = crypto_1.default.randomBytes(16); - const cipher = crypto_1.default.createCipheriv('aes-256-cbc', key, iv); - const encryptedToken = cipher.update(process.env['SYSTEM_ACCESSTOKEN'].trim(), 'utf8', 'hex') + cipher.final('hex'); - const encryptionDetailsPath = tmp.tmpNameSync(); - fs_1.default.writeFileSync(encryptionDetailsPath, JSON.stringify({ key: key.toString('hex'), iv: iv.toString('hex') })); - const encryptedTokenPath = tmp.tmpNameSync(); - fs_1.default.writeFileSync(encryptedTokenPath, encryptedToken); - const patternPath = tmp.tmpNameSync(); - fs_1.default.writeFileSync(patternPath, pattern); - const paramsPath = tmp.tmpNameSync(); - fs_1.default.writeFileSync(paramsPath, JSON.stringify(getParams(type))); - const dotnetVersion = child_process_1.default.execSync('dotnet --version', { encoding: 'utf8' }).trim(); - const adoTaskVersion = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(esrpCliPath))); - const federatedTokenData = { - jobId: process.env['SYSTEM_JOBID'], - planId: process.env['SYSTEM_PLANID'], - projectId: process.env['SYSTEM_TEAMPROJECTID'], - hub: process.env['SYSTEM_HOSTTYPE'], - uri: process.env['SYSTEM_COLLECTIONURI'], - managedIdentityId: process.env['VSCODE_ESRP_CLIENT_ID'], - managedIdentityTenantId: process.env['VSCODE_ESRP_TENANT_ID'], - serviceConnectionId: process.env['VSCODE_ESRP_SERVICE_CONNECTION_ID'], - tempDirectory: os_1.default.tmpdir(), - systemAccessToken: encryptedTokenPath, - encryptionKey: encryptionDetailsPath - }; - const args = [ - esrpCliPath, - 'vsts.sign', - '-a', - process.env['ESRP_CLIENT_ID'], - '-d', - process.env['ESRP_TENANT_ID'], - '-k', JSON.stringify({ akv: 'vscode-esrp' }), - '-z', JSON.stringify({ akv: 'vscode-esrp', cert: 'esrp-sign' }), - '-f', folderPath, - '-p', patternPath, - '-u', 'false', - '-x', 'regularSigning', - '-b', 'input.json', - '-l', 'AzSecPack_PublisherPolicyProd.xml', - '-y', 'inlineSignParams', - '-j', paramsPath, - '-c', '9997', - '-t', '120', - '-g', '10', - '-v', 'Tls12', - '-s', 'https://api.esrp.microsoft.com/api/v1', - '-m', '0', - '-o', 'Microsoft', - '-i', 'https://www.microsoft.com', - '-n', '5', - '-r', 'true', - '-w', dotnetVersion, - '-skipAdoReportAttachment', 'false', - '-pendingAnalysisWaitTimeoutMinutes', '5', - '-adoTaskVersion', adoTaskVersion, - '-resourceUri', 'https://msazurecloud.onmicrosoft.com/api.esrp.microsoft.com', - '-esrpClientId', - process.env['ESRP_CLIENT_ID'], - '-useMSIAuthentication', 'true', - '-federatedTokenData', JSON.stringify(federatedTokenData) - ]; - try { - child_process_1.default.execFileSync('dotnet', args, { stdio: 'inherit' }); - } - catch (err) { - console.error('ESRP failed'); - console.error(err); - process.exit(1); - } -} -if (require.main === module) { - main(process.argv.slice(2)); - process.exit(0); -} -//# sourceMappingURL=sign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/sign.ts b/build/azure-pipelines/common/sign.ts index 19a288483c8..d93f752eeeb 100644 --- a/build/azure-pipelines/common/sign.ts +++ b/build/azure-pipelines/common/sign.ts @@ -216,7 +216,7 @@ export function main([esrpCliPath, type, folderPath, pattern]: string[]) { } } -if (require.main === module) { +if (import.meta.main) { main(process.argv.slice(2)); process.exit(0); } diff --git a/build/azure-pipelines/common/versionCompatibility.ts b/build/azure-pipelines/common/versionCompatibility.ts new file mode 100644 index 00000000000..e4004d78f29 --- /dev/null +++ b/build/azure-pipelines/common/versionCompatibility.ts @@ -0,0 +1,438 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export interface IExtensionManifest { + name: string; + publisher: string; + version: string; + engines: { vscode: string }; + main?: string; + browser?: string; + enabledApiProposals?: string[]; +} + +export function isEngineCompatible(productVersion: string, engineVersion: string): { compatible: boolean; error?: string } { + if (engineVersion === '*') { + return { compatible: true }; + } + + const versionMatch = engineVersion.match(/^(\^|>=)?(\d+)\.(\d+)\.(\d+)/); + if (!versionMatch) { + return { compatible: false, error: `Could not parse engines.vscode value: ${engineVersion}` }; + } + + const [, prefix, major, minor, patch] = versionMatch; + const productMatch = productVersion.match(/^(\d+)\.(\d+)\.(\d+)/); + if (!productMatch) { + return { compatible: false, error: `Could not parse product version: ${productVersion}` }; + } + + const [, prodMajor, prodMinor, prodPatch] = productMatch; + + const reqMajor = parseInt(major); + const reqMinor = parseInt(minor); + const reqPatch = parseInt(patch); + const pMajor = parseInt(prodMajor); + const pMinor = parseInt(prodMinor); + const pPatch = parseInt(prodPatch); + + if (prefix === '>=') { + // Minimum version check + if (pMajor > reqMajor) { return { compatible: true }; } + if (pMajor < reqMajor) { return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; } + if (pMinor > reqMinor) { return { compatible: true }; } + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code >=${engineVersion}, but product version is ${productVersion}` }; + } + + // Caret or exact version check + if (pMajor !== reqMajor) { + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion} (major version mismatch)` }; + } + + if (prefix === '^') { + // Caret: same major, minor and patch must be >= required + if (pMinor > reqMinor) { return { compatible: true }; } + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; + } + + // Exact or default behavior + if (pMinor < reqMinor) { return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; } + if (pMinor > reqMinor) { return { compatible: true }; } + if (pPatch >= reqPatch) { return { compatible: true }; } + return { compatible: false, error: `Extension requires VS Code ${engineVersion}, but product version is ${productVersion}` }; +} + +export function parseApiProposals(enabledApiProposals: string[]): { proposalName: string; version?: number }[] { + return enabledApiProposals.map(proposal => { + const [proposalName, version] = proposal.split('@'); + return { proposalName, version: version ? parseInt(version) : undefined }; + }); +} + +export function areApiProposalsCompatible( + apiProposals: string[], + productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }> +): { compatible: boolean; errors: string[] } { + if (apiProposals.length === 0) { + return { compatible: true, errors: [] }; + } + + const errors: string[] = []; + const parsedProposals = parseApiProposals(apiProposals); + + for (const { proposalName, version } of parsedProposals) { + if (!version) { + continue; + } + const existingProposal = productApiProposals[proposalName]; + if (!existingProposal) { + errors.push(`API proposal '${proposalName}' does not exist in this version of VS Code`); + } else if (existingProposal.version !== version) { + errors.push(`API proposal '${proposalName}' version mismatch: extension requires version ${version}, but VS Code has version ${existingProposal.version ?? 'unversioned'}`); + } + } + + return { compatible: errors.length === 0, errors }; +} + +export function parseApiProposalsFromSource(content: string): { [proposalName: string]: { proposal: string; version?: number } } { + const allApiProposals: { [proposalName: string]: { proposal: string; version?: number } } = {}; + + // Match proposal blocks like: proposalName: {\n\t\tproposal: '...',\n\t\tversion: N\n\t} + // or: proposalName: {\n\t\tproposal: '...',\n\t} + const proposalBlockRegex = /\t(\w+):\s*\{([^}]+)\}/g; + const versionRegex = /version:\s*(\d+)/; + + let match; + while ((match = proposalBlockRegex.exec(content)) !== null) { + const [, name, block] = match; + const versionMatch = versionRegex.exec(block); + allApiProposals[name] = { + proposal: '', + version: versionMatch ? parseInt(versionMatch[1]) : undefined + }; + } + + return allApiProposals; +} + +export function areAllowlistedApiProposalsMatching( + extensionId: string, + productAllowlistedProposals: string[] | undefined, + manifestEnabledProposals: string[] | undefined +): { compatible: boolean; errors: string[] } { + // Normalize undefined to empty arrays for easier comparison + const productProposals = productAllowlistedProposals || []; + const manifestProposals = manifestEnabledProposals || []; + + // If extension doesn't declare any proposals, it's always compatible + // (product.json can allowlist more than the extension uses) + if (manifestProposals.length === 0) { + return { compatible: true, errors: [] }; + } + + // If extension declares API proposals but product.json doesn't allowlist them + if (productProposals.length === 0) { + return { + compatible: false, + errors: [ + `Extension '${extensionId}' declares API proposals in package.json (${manifestProposals.join(', ')}) ` + + `but product.json does not allowlist any API proposals for this extension` + ] + }; + } + + // Check that all proposals in manifest are allowlisted in product.json + // (product.json can have extra proposals that the extension doesn't use) + // Note: Strip version suffixes from manifest proposals (e.g., "chatParticipant@2" -> "chatParticipant") + // because product.json only contains base proposal names + const productSet = new Set(productProposals); + const errors: string[] = []; + + for (const proposal of manifestProposals) { + // Strip version suffix if present (e.g., "chatParticipant@2" -> "chatParticipant") + const proposalName = proposal.split('@')[0]; + if (!productSet.has(proposalName)) { + errors.push(`API proposal '${proposal}' is declared in extension '${extensionId}' package.json but is not allowlisted in product.json`); + } + } + + return { compatible: errors.length === 0, errors }; +} + +export function checkExtensionCompatibility( + productVersion: string, + productApiProposals: Readonly<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>, + manifest: IExtensionManifest +): { compatible: boolean; errors: string[] } { + const errors: string[] = []; + + // Check engine compatibility + const engineResult = isEngineCompatible(productVersion, manifest.engines.vscode); + if (!engineResult.compatible) { + errors.push(engineResult.error!); + } + + // Check API proposals compatibility + if (manifest.enabledApiProposals?.length) { + const apiResult = areApiProposalsCompatible(manifest.enabledApiProposals, productApiProposals); + if (!apiResult.compatible) { + errors.push(...apiResult.errors); + } + } + + return { compatible: errors.length === 0, errors }; +} + +if (import.meta.main) { + console.log('Running version compatibility tests...\n'); + + // isEngineCompatible tests + console.log('Testing isEngineCompatible...'); + + // Wildcard + assert.strictEqual(isEngineCompatible('1.50.0', '*').compatible, true); + + // Invalid engine version + assert.strictEqual(isEngineCompatible('1.50.0', 'invalid').compatible, false); + + // Invalid product version + assert.strictEqual(isEngineCompatible('invalid', '1.50.0').compatible, false); + + // >= prefix + assert.strictEqual(isEngineCompatible('1.50.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('2.0.0', '>=1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '>=1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '>=1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('0.50.0', '>=1.50.0').compatible, false); + + // ^ prefix (caret) + assert.strictEqual(isEngineCompatible('1.50.0', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '^1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '^1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '^1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('2.0.0', '^1.50.0').compatible, false); + + // Exact/default (no prefix) + assert.strictEqual(isEngineCompatible('1.50.0', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.50.1', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.51.0', '1.50.0').compatible, true); + assert.strictEqual(isEngineCompatible('1.49.0', '1.50.0').compatible, false); + assert.strictEqual(isEngineCompatible('1.50.0', '1.50.1').compatible, false); + assert.strictEqual(isEngineCompatible('2.0.0', '1.50.0').compatible, false); + + console.log(' ✓ isEngineCompatible tests passed\n'); + + // parseApiProposals tests + console.log('Testing parseApiProposals...'); + + assert.deepStrictEqual(parseApiProposals([]), []); + assert.deepStrictEqual(parseApiProposals(['proposalA']), [{ proposalName: 'proposalA', version: undefined }]); + assert.deepStrictEqual(parseApiProposals(['proposalA@1']), [{ proposalName: 'proposalA', version: 1 }]); + assert.deepStrictEqual(parseApiProposals(['proposalA@1', 'proposalB', 'proposalC@3']), [ + { proposalName: 'proposalA', version: 1 }, + { proposalName: 'proposalB', version: undefined }, + { proposalName: 'proposalC', version: 3 } + ]); + + console.log(' ✓ parseApiProposals tests passed\n'); + + // areApiProposalsCompatible tests + console.log('Testing areApiProposalsCompatible...'); + + const productProposals = { + proposalA: { proposal: '', version: 1 }, + proposalB: { proposal: '', version: 2 }, + proposalC: { proposal: '' } // unversioned + }; + + // Empty proposals + assert.strictEqual(areApiProposalsCompatible([], productProposals).compatible, true); + + // Unversioned extension proposals (always compatible) + assert.strictEqual(areApiProposalsCompatible(['proposalA', 'proposalB'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['unknownProposal'], productProposals).compatible, true); + + // Versioned proposals - matching + assert.strictEqual(areApiProposalsCompatible(['proposalA@1'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['proposalA@1', 'proposalB@2'], productProposals).compatible, true); + + // Versioned proposals - version mismatch + assert.strictEqual(areApiProposalsCompatible(['proposalA@2'], productProposals).compatible, false); + assert.strictEqual(areApiProposalsCompatible(['proposalB@1'], productProposals).compatible, false); + + // Versioned proposals - missing proposal + assert.strictEqual(areApiProposalsCompatible(['unknownProposal@1'], productProposals).compatible, false); + + // Versioned proposals - product has unversioned + assert.strictEqual(areApiProposalsCompatible(['proposalC@1'], productProposals).compatible, false); + + // Mixed versioned and unversioned + assert.strictEqual(areApiProposalsCompatible(['proposalA@1', 'proposalB'], productProposals).compatible, true); + assert.strictEqual(areApiProposalsCompatible(['proposalA@2', 'proposalB'], productProposals).compatible, false); + + console.log(' ✓ areApiProposalsCompatible tests passed\n'); + + // parseApiProposalsFromSource tests + console.log('Testing parseApiProposalsFromSource...'); + + const sampleSource = ` +export const allApiProposals = { + authSession: { + proposal: 'vscode.proposed.authSession.d.ts', + }, + chatParticipant: { + proposal: 'vscode.proposed.chatParticipant.d.ts', + version: 2 + }, + testProposal: { + proposal: 'vscode.proposed.testProposal.d.ts', + version: 15 + } +}; +`; + + const parsedSource = parseApiProposalsFromSource(sampleSource); + assert.strictEqual(Object.keys(parsedSource).length, 3); + assert.strictEqual(parsedSource['authSession']?.version, undefined); + assert.strictEqual(parsedSource['chatParticipant']?.version, 2); + assert.strictEqual(parsedSource['testProposal']?.version, 15); + + // Empty source + assert.strictEqual(Object.keys(parseApiProposalsFromSource('')).length, 0); + + console.log(' ✓ parseApiProposalsFromSource tests passed\n'); + + // checkExtensionCompatibility tests + console.log('Testing checkExtensionCompatibility...'); + + const testApiProposals = { + authSession: { proposal: '', version: undefined }, + chatParticipant: { proposal: '', version: 2 }, + testProposal: { proposal: '', version: 15 } + }; + + // Compatible extension - matching engine and proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@2'] + }).compatible, true); + + // Compatible - no API proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' } + }).compatible, true); + + // Compatible - unversioned API proposals + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['authSession', 'chatParticipant'] + }).compatible, true); + + // Incompatible - engine version too new + assert.strictEqual(checkExtensionCompatibility('1.89.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@2'] + }).compatible, false); + + // Incompatible - API proposal version mismatch + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@3'] + }).compatible, false); + + // Incompatible - missing API proposal + assert.strictEqual(checkExtensionCompatibility('1.90.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['unknownProposal@1'] + }).compatible, false); + + // Incompatible - both engine and API proposal issues + assert.strictEqual(checkExtensionCompatibility('1.89.0', testApiProposals, { + name: 'test-ext', + publisher: 'test', + version: '1.0.0', + engines: { vscode: '^1.90.0' }, + enabledApiProposals: ['chatParticipant@3'] + }).compatible, false); + + console.log(' ✓ checkExtensionCompatibility tests passed\n'); + + // areAllowlistedApiProposalsMatching tests + console.log('Testing areAllowlistedApiProposalsMatching...'); + + // Both undefined - compatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', undefined, undefined).compatible, true); + + // Both empty arrays - compatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', [], []).compatible, true); + + // Exact match - compatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA', 'proposalB']).compatible, true); + + // Match regardless of order - compatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalB', 'proposalA'], ['proposalA', 'proposalB']).compatible, true); + + // Extension declares but product.json doesn't allowlist - incompatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', undefined, ['proposalA']).compatible, false); + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', [], ['proposalA']).compatible, false); + + // Product.json allowlists but extension doesn't declare - COMPATIBLE (product.json can have extras) + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], undefined).compatible, true); + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], []).compatible, true); + + // Extension declares more than allowlisted - incompatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalA', 'proposalB']).compatible, false); + + // Product.json allowlists more than declared - COMPATIBLE (product.json can have extras) + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA']).compatible, true); + + // Completely different sets - incompatible (manifest has proposals not in allowlist) + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalB']).compatible, false); + + // Product.json has extras and manifest matches subset - compatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB', 'proposalC'], ['proposalA', 'proposalB']).compatible, true); + + // Versioned proposals - should strip version and match base name + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['chatParticipant'], ['chatParticipant@2']).compatible, true); + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA@1', 'proposalB@3']).compatible, true); + + // Versioned proposal not in allowlist - incompatible + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA'], ['proposalB@2']).compatible, false); + + // Mix of versioned and unversioned proposals + assert.strictEqual(areAllowlistedApiProposalsMatching('test.ext', ['proposalA', 'proposalB'], ['proposalA', 'proposalB@2']).compatible, true); + + console.log(' ✓ areAllowlistedApiProposalsMatching tests passed\n'); + + console.log('All tests passed! ✓'); +} diff --git a/build/azure-pipelines/common/waitForArtifacts.js b/build/azure-pipelines/common/waitForArtifacts.js deleted file mode 100644 index b9ffb73962d..00000000000 --- a/build/azure-pipelines/common/waitForArtifacts.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const publish_1 = require("../common/publish"); -const retry_1 = require("../common/retry"); -async function getPipelineArtifacts() { - const result = await (0, publish_1.requestAZDOAPI)('artifacts'); - return result.value.filter(a => !/sbom$/.test(a.name)); -} -async function main(artifacts) { - if (artifacts.length === 0) { - throw new Error(`Usage: node waitForArtifacts.js ...`); - } - // This loop will run for 30 minutes and waits to the x64 and arm64 artifacts - // to be uploaded to the pipeline by the `macOS` and `macOSARM64` jobs. As soon - // as these artifacts are found, the loop completes and the `macOSUnivesrsal` - // job resumes. - for (let index = 0; index < 60; index++) { - try { - console.log(`Waiting for artifacts (${artifacts.join(', ')}) to be uploaded (${index + 1}/60)...`); - const allArtifacts = await (0, retry_1.retry)(() => getPipelineArtifacts()); - console.log(` * Artifacts attached to the pipelines: ${allArtifacts.length > 0 ? allArtifacts.map(a => a.name).join(', ') : 'none'}`); - const foundArtifacts = allArtifacts.filter(a => artifacts.includes(a.name)); - console.log(` * Found artifacts: ${foundArtifacts.length > 0 ? foundArtifacts.map(a => a.name).join(', ') : 'none'}`); - if (foundArtifacts.length === artifacts.length) { - console.log(` * All artifacts were found`); - return; - } - } - catch (err) { - console.error(`ERROR: Failed to get pipeline artifacts: ${err}`); - } - await new Promise(c => setTimeout(c, 30_000)); - } - throw new Error(`ERROR: Artifacts (${artifacts.join(', ')}) were not uploaded within 30 minutes.`); -} -main(process.argv.splice(2)).then(() => { - process.exit(0); -}, err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=waitForArtifacts.js.map \ No newline at end of file diff --git a/build/azure-pipelines/common/waitForArtifacts.ts b/build/azure-pipelines/common/waitForArtifacts.ts index 3fed6cd38d2..1b48a70d994 100644 --- a/build/azure-pipelines/common/waitForArtifacts.ts +++ b/build/azure-pipelines/common/waitForArtifacts.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Artifact, requestAZDOAPI } from '../common/publish'; -import { retry } from '../common/retry'; +import { type Artifact, requestAZDOAPI } from '../common/publish.ts'; +import { retry } from '../common/retry.ts'; async function getPipelineArtifacts(): Promise { const result = await requestAZDOAPI<{ readonly value: Artifact[] }>('artifacts'); @@ -13,7 +13,7 @@ async function getPipelineArtifacts(): Promise { async function main(artifacts: string[]): Promise { if (artifacts.length === 0) { - throw new Error(`Usage: node waitForArtifacts.js ...`); + throw new Error(`Usage: node waitForArtifacts.ts ...`); } // This loop will run for 30 minutes and waits to the x64 and arm64 artifacts diff --git a/build/azure-pipelines/darwin/codesign.js b/build/azure-pipelines/darwin/codesign.js deleted file mode 100644 index 30a3bdc332b..00000000000 --- a/build/azure-pipelines/darwin/codesign.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const codesign_1 = require("../common/codesign"); -const publish_1 = require("../common/publish"); -async function main() { - const arch = (0, publish_1.e)('VSCODE_ARCH'); - const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); - const pipelineWorkspace = (0, publish_1.e)('PIPELINE_WORKSPACE'); - const folder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_archive`; - const glob = `VSCode-darwin-${arch}.zip`; - // Codesign - (0, codesign_1.printBanner)('Codesign'); - const codeSignTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-darwin', folder, glob); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign', codeSignTask); - // Notarize - (0, codesign_1.printBanner)('Notarize'); - const notarizeTask = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'notarize-darwin', folder, glob); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Notarize', notarizeTask); -} -main().then(() => { - process.exit(0); -}, err => { - console.error(`ERROR: ${err}`); - process.exit(1); -}); -//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/darwin/codesign.ts b/build/azure-pipelines/darwin/codesign.ts index e6f6a5ce754..263f96cd2ff 100644 --- a/build/azure-pipelines/darwin/codesign.ts +++ b/build/azure-pipelines/darwin/codesign.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; -import { e } from '../common/publish'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign.ts'; +import { e } from '../common/publish.ts'; async function main() { const arch = e('VSCODE_ARCH'); @@ -12,17 +12,25 @@ async function main() { const pipelineWorkspace = e('PIPELINE_WORKSPACE'); const folder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_archive`; + const dmgFolder = `${pipelineWorkspace}/vscode_client_darwin_${arch}_dmg`; const glob = `VSCode-darwin-${arch}.zip`; + const dmgGlob = `VSCode-darwin-${arch}.dmg`; // Codesign - printBanner('Codesign'); - const codeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', folder, glob); - await streamProcessOutputAndCheckResult('Codesign', codeSignTask); + const archiveCodeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', folder, glob); + const dmgCodeSignTask = spawnCodesignProcess(esrpCliDLLPath, 'sign-darwin', dmgFolder, dmgGlob); + printBanner('Codesign Archive'); + await streamProcessOutputAndCheckResult('Codesign Archive', archiveCodeSignTask); + printBanner('Codesign DMG'); + await streamProcessOutputAndCheckResult('Codesign DMG', dmgCodeSignTask); // Notarize - printBanner('Notarize'); - const notarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', folder, glob); - await streamProcessOutputAndCheckResult('Notarize', notarizeTask); + const archiveNotarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', folder, glob); + const dmgNotarizeTask = spawnCodesignProcess(esrpCliDLLPath, 'notarize-darwin', dmgFolder, dmgGlob); + printBanner('Notarize Archive'); + await streamProcessOutputAndCheckResult('Notarize Archive', archiveNotarizeTask); + printBanner('Notarize DMG'); + await streamProcessOutputAndCheckResult('Notarize DMG', dmgNotarizeTask); } main().then(() => { diff --git a/build/azure-pipelines/darwin/product-build-darwin-ci.yml b/build/azure-pipelines/darwin/product-build-darwin-ci.yml index 3920c4ec799..45af3707590 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-ci.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-ci.yml @@ -7,7 +7,7 @@ parameters: jobs: - job: macOS${{ parameters.VSCODE_TEST_SUITE }} displayName: ${{ parameters.VSCODE_TEST_SUITE }} Tests - timeoutInMinutes: 30 + timeoutInMinutes: 90 variables: VSCODE_ARCH: arm64 templateContext: 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 c26f2ad25c8..94eee5e476c 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli-sign.yml @@ -41,7 +41,7 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - script: node build/setup-npm-registry.js $NPM_REGISTRY build + - script: node build/setup-npm-registry.ts $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry diff --git a/build/azure-pipelines/darwin/product-build-darwin-cli.yml b/build/azure-pipelines/darwin/product-build-darwin-cli.yml index 35a9b3566ce..dc5a5d79c14 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-cli.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-cli.yml @@ -14,6 +14,8 @@ jobs: pool: name: AcesShared os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia variables: # todo@connor4312 to diagnose build flakes MSRUSTUP_LOG: debug diff --git a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml index 4151d30b06c..d70adbee0af 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-node-modules.yml @@ -8,6 +8,8 @@ jobs: pool: name: AcesShared os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia timeoutInMinutes: 60 variables: VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} @@ -28,11 +30,11 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -85,13 +87,13 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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')) diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index 23c85dc714a..ed94a170791 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -14,6 +14,13 @@ jobs: sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" sbomPackageVersion: $(Build.SourceVersion) + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg/VSCode-darwin-$(VSCODE_ARCH).dmg + artifactName: vscode_client_darwin_$(VSCODE_ARCH)_dmg + displayName: Publish client DMG + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) steps: - template: ../common/checkout.yml@self @@ -31,7 +38,7 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password,macos-developer-certificate,macos-developer-certificate-key" - - script: node build/setup-npm-registry.js $NPM_REGISTRY build + - script: node build/setup-npm-registry.ts $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry @@ -67,7 +74,7 @@ jobs: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Install build dependencies - - pwsh: node build/azure-pipelines/common/waitForArtifacts.js unsigned_vscode_client_darwin_x64_archive unsigned_vscode_client_darwin_arm64_archive + - pwsh: node -- build/azure-pipelines/common/waitForArtifacts.ts unsigned_vscode_client_darwin_x64_archive unsigned_vscode_client_darwin_arm64_archive env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Wait for x64 and arm64 artifacts @@ -80,7 +87,7 @@ jobs: artifact: unsigned_vscode_client_darwin_arm64_archive displayName: Download arm64 artifact - - script: node build/azure-pipelines/distro/mixin-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - script: | @@ -88,14 +95,14 @@ jobs: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64 & unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_arm64_archive/VSCode-darwin-arm64.zip -d $(agent.builddirectory)/VSCode-darwin-arm64 & wait - DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory) + DEBUG=* node build/darwin/create-universal-app.ts $(agent.builddirectory) displayName: Create Universal App - script: | set -e APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)" APP_NAME="`ls $APP_ROOT | head -n 1`" - APP_PATH="$APP_ROOT/$APP_NAME" node build/darwin/verify-macho.js universal + APP_PATH="$APP_ROOT/$APP_NAME" node build/darwin/verify-macho.ts universal displayName: Verify arch of Mach-O objects - script: | @@ -107,13 +114,24 @@ jobs: security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign export CODESIGN_IDENTITY=$(security find-identity -v -p codesigning $(agent.tempdirectory)/buildagent.keychain | grep -oEi "([0-9A-F]{40})" | head -n 1) security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain - DEBUG=electron-osx-sign* node build/darwin/sign.js $(agent.builddirectory) + DEBUG=electron-osx-sign* node build/darwin/sign.ts $(agent.builddirectory) displayName: Set Hardened Entitlements - script: | set -e - mkdir -p $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive - pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip * && popd + # Needed for https://github.com/dmgbuild/dmgbuild/blob/main/src/dmgbuild/badge.py + python3 -m pip install pyobjc-framework-Quartz + DMG_OUT="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_dmg" + mkdir -p $DMG_OUT + node build/darwin/create-dmg.ts $(agent.builddirectory) $DMG_OUT + python3 build/darwin/patch-dmg.py $DMG_OUT/VSCode-darwin-$(VSCODE_ARCH).dmg resources/darwin/disk.icns + echo "##vso[task.setvariable variable=DMG_PATH]$DMG_OUT/VSCode-darwin-$(VSCODE_ARCH).dmg" + displayName: Create DMG installer + + - script: | + set -e + mkdir -p $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive + pushd $(agent.builddirectory)/VSCode-darwin-$(VSCODE_ARCH) && zip -r -X -y $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip * && popd displayName: Archive build - task: UseDotNet@2 @@ -132,17 +150,21 @@ jobs: Pattern: noop displayName: 'Install ESRP Tooling' - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: ✍️ Codesign + - pwsh: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $EsrpCodeSigningTool = (gci -directory -filter EsrpCodeSigning_* $(Agent.RootDirectory)/_tasks | Select-Object -last 1).FullName + $Version = (gci -directory $EsrpCodeSigningTool | Select-Object -last 1).FullName + echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" + displayName: Find ESRP CLI - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive VSCode-darwin-$(VSCODE_ARCH).zip + - script: node build/azure-pipelines/darwin/codesign.ts env: + EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) - displayName: ✍️ Notarize + displayName: ✍️ Codesign & Notarize - - script: unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + - script: unzip $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip -d $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) displayName: Extract signed app - script: | @@ -157,5 +179,8 @@ jobs: - script: | set -e mkdir -p $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_archive - mv $(Pipeline.Workspace)/unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip + mv $(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip + + mkdir -p $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg + mv $(DMG_PATH) $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg/VSCode-darwin-$(VSCODE_ARCH).dmg displayName: Move artifact to out directory diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 770a54f7925..37282e92146 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -69,6 +69,13 @@ jobs: sbomBuildDropPath: $(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)-web sbomPackageName: "VS Code macOS $(VSCODE_ARCH) Web" sbomPackageVersion: $(Build.SourceVersion) + - output: pipelineArtifact + targetPath: $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg/VSCode-darwin-$(VSCODE_ARCH).dmg + artifactName: vscode_client_darwin_$(VSCODE_ARCH)_dmg + displayName: Publish client DMG + sbomBuildDropPath: $(Build.ArtifactStagingDirectory)/VSCode-darwin-$(VSCODE_ARCH) + sbomPackageName: "VS Code macOS $(VSCODE_ARCH)" + sbomPackageVersion: $(Build.SourceVersion) steps: - template: ./steps/product-build-darwin-compile.yml@self parameters: diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-cli-sign.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-cli-sign.yml index 883645aec69..1cd0fe2a824 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-cli-sign.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-cli-sign.yml @@ -33,12 +33,12 @@ steps: archiveFilePatterns: $(Build.ArtifactStagingDirectory)/pkg/${{ target }}/*.zip destinationFolder: $(Build.ArtifactStagingDirectory)/sign/${{ target }} - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" + - script: node build/azure-pipelines/common/sign.ts $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll sign-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign - - script: node build/azure-pipelines/common/sign $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" + - script: node build/azure-pipelines/common/sign.ts $(Agent.RootDirectory)/_tasks/EsrpCodeSigning_*/*/net6.0/esrpcli.dll notarize-darwin $(Build.ArtifactStagingDirectory)/pkg "*.zip" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Notarize diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml index d1d431505f6..e20ef8deb01 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-compile.yml @@ -39,11 +39,11 @@ steps: - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz displayName: Extract compilation output - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts darwin $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -100,25 +100,25 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: ../../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - script: node build/lib/policies/policyGenerator build/lib/policies/policyData.jsonc darwin + - script: npm run copy-policy-dto --prefix build && node build/lib/policies/policyGenerator.ts build/lib/policies/policyData.jsonc darwin displayName: Generate policy definitions retryCountOnTaskFailure: 3 @@ -178,8 +178,8 @@ steps: set -e APP_ROOT="$(Agent.BuildDirectory)/VSCode-darwin-$(VSCODE_ARCH)" APP_NAME="`ls $APP_ROOT | head -n 1`" - APP_PATH="$APP_ROOT/$APP_NAME" node build/darwin/verify-macho.js $(VSCODE_ARCH) - APP_PATH="$(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)" node build/darwin/verify-macho.js $(VSCODE_ARCH) + APP_PATH="$APP_ROOT/$APP_NAME" node build/darwin/verify-macho.ts $(VSCODE_ARCH) + APP_PATH="$(Agent.BuildDirectory)/vscode-server-darwin-$(VSCODE_ARCH)" node build/darwin/verify-macho.ts $(VSCODE_ARCH) displayName: Verify arch of Mach-O objects - script: | @@ -191,7 +191,7 @@ steps: condition: eq(variables['BUILT_CLIENT'], 'true') displayName: Package client - - pwsh: node build/azure-pipelines/common/checkForArtifact.js CLIENT_ARCHIVE_UPLOADED unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive + - pwsh: node build/azure-pipelines/common/checkForArtifact.ts CLIENT_ARCHIVE_UPLOADED unsigned_vscode_client_darwin_$(VSCODE_ARCH)_archive env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: Check for client artifact @@ -221,9 +221,21 @@ steps: security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign export CODESIGN_IDENTITY=$(security find-identity -v -p codesigning $(agent.tempdirectory)/buildagent.keychain | grep -oEi "([0-9A-F]{40})" | head -n 1) security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain - DEBUG=electron-osx-sign* node build/darwin/sign.js $(agent.builddirectory) + DEBUG=electron-osx-sign* node build/darwin/sign.ts $(agent.builddirectory) displayName: Set Hardened Entitlements + - script: | + set -e + # Needed for https://github.com/dmgbuild/dmgbuild/blob/main/src/dmgbuild/badge.py + python3 -m pip install pyobjc-framework-Quartz + DMG_OUT="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_dmg" + mkdir -p $DMG_OUT + node build/darwin/create-dmg.ts $(agent.builddirectory) $DMG_OUT + python3 build/darwin/patch-dmg.py $DMG_OUT/VSCode-darwin-$(VSCODE_ARCH).dmg resources/darwin/disk.icns + echo "##vso[task.setvariable variable=DMG_PATH]$DMG_OUT/VSCode-darwin-$(VSCODE_ARCH).dmg" + condition: eq(variables['BUILT_CLIENT'], 'true') + displayName: Create DMG installer + - script: | set -e ARCHIVE_PATH="$(Pipeline.Workspace)/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip" @@ -257,7 +269,7 @@ steps: echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" displayName: Find ESRP CLI - - script: npx deemon --detach --wait node build/azure-pipelines/darwin/codesign.js + - script: npx deemon --detach --wait node build/azure-pipelines/darwin/codesign.ts env: EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) @@ -271,7 +283,7 @@ steps: VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - script: npx deemon --attach node build/azure-pipelines/darwin/codesign.js + - script: npx deemon --attach node build/azure-pipelines/darwin/codesign.ts condition: succeededOrFailed() displayName: "Post-job: ✍️ Codesign & Notarize" @@ -298,6 +310,9 @@ steps: mv $(CLIENT_PATH) $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_archive/VSCode-darwin-$(VSCODE_ARCH).zip fi + mkdir -p $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg + mv $(DMG_PATH) $(Build.ArtifactStagingDirectory)/out/vscode_client_darwin_$(VSCODE_ARCH)_dmg/VSCode-darwin-$(VSCODE_ARCH).dmg + mkdir -p $(Build.ArtifactStagingDirectory)/out/vscode_server_darwin_$(VSCODE_ARCH)_archive mv $(SERVER_PATH) $(Build.ArtifactStagingDirectory)/out/vscode_server_darwin_$(VSCODE_ARCH)_archive/vscode-server-darwin-$(VSCODE_ARCH).zip diff --git a/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml b/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml index 1facd03c6ee..80be5496d97 100644 --- a/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml +++ b/build/azure-pipelines/darwin/steps/product-build-darwin-test.yml @@ -7,7 +7,7 @@ parameters: type: boolean steps: - - script: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright diff --git a/build/azure-pipelines/distro/mixin-npm.js b/build/azure-pipelines/distro/mixin-npm.js deleted file mode 100644 index 87958a5d449..00000000000 --- a/build/azure-pipelines/distro/mixin-npm.js +++ /dev/null @@ -1,38 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const { dirs } = require('../../npm/dirs'); -function log(...args) { - console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); -} -function mixin(mixinPath) { - if (!fs_1.default.existsSync(`${mixinPath}/node_modules`)) { - log(`Skipping distro npm dependencies: ${mixinPath} (no node_modules)`); - return; - } - log(`Mixing in distro npm dependencies: ${mixinPath}`); - const distroPackageJson = JSON.parse(fs_1.default.readFileSync(`${mixinPath}/package.json`, 'utf8')); - const targetPath = path_1.default.relative('.build/distro/npm', mixinPath); - for (const dependency of Object.keys(distroPackageJson.dependencies)) { - fs_1.default.rmSync(`./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true }); - fs_1.default.cpSync(`${mixinPath}/node_modules/${dependency}`, `./${targetPath}/node_modules/${dependency}`, { recursive: true, force: true, dereference: true }); - } - log(`Mixed in distro npm dependencies: ${mixinPath} ✔︎`); -} -function main() { - log(`Mixing in distro npm dependencies...`); - const mixinPaths = dirs.filter(d => /^.build\/distro\/npm/.test(d)); - for (const mixinPath of mixinPaths) { - mixin(mixinPath); - } -} -main(); -//# sourceMappingURL=mixin-npm.js.map \ No newline at end of file diff --git a/build/azure-pipelines/distro/mixin-npm.ts b/build/azure-pipelines/distro/mixin-npm.ts index f98f6e6b55d..d5caa0a9502 100644 --- a/build/azure-pipelines/distro/mixin-npm.ts +++ b/build/azure-pipelines/distro/mixin-npm.ts @@ -5,7 +5,7 @@ import fs from 'fs'; import path from 'path'; -const { dirs } = require('../../npm/dirs') as { dirs: string[] }; +import { dirs } from '../../npm/dirs.ts'; function log(...args: unknown[]): void { console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); diff --git a/build/azure-pipelines/distro/mixin-quality.js b/build/azure-pipelines/distro/mixin-quality.js deleted file mode 100644 index 335f63ca1fc..00000000000 --- a/build/azure-pipelines/distro/mixin-quality.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -function log(...args) { - console.log(`[${new Date().toLocaleTimeString('en', { hour12: false })}]`, '[distro]', ...args); -} -function main() { - const quality = process.env['VSCODE_QUALITY']; - if (!quality) { - throw new Error('Missing VSCODE_QUALITY, skipping mixin'); - } - log(`Mixing in distro quality...`); - const basePath = `.build/distro/mixin/${quality}`; - for (const name of fs_1.default.readdirSync(basePath)) { - const distroPath = path_1.default.join(basePath, name); - const ossPath = path_1.default.relative(basePath, distroPath); - if (ossPath === 'product.json') { - const distro = JSON.parse(fs_1.default.readFileSync(distroPath, 'utf8')); - const oss = JSON.parse(fs_1.default.readFileSync(ossPath, 'utf8')); - let builtInExtensions = oss.builtInExtensions; - if (Array.isArray(distro.builtInExtensions)) { - log('Overwriting built-in extensions:', distro.builtInExtensions.map(e => e.name)); - builtInExtensions = distro.builtInExtensions; - } - else if (distro.builtInExtensions) { - const include = distro.builtInExtensions['include'] ?? []; - const exclude = distro.builtInExtensions['exclude'] ?? []; - log('OSS built-in extensions:', builtInExtensions.map(e => e.name)); - log('Including built-in extensions:', include.map(e => e.name)); - log('Excluding built-in extensions:', exclude); - builtInExtensions = builtInExtensions.filter(ext => !include.find(e => e.name === ext.name) && !exclude.find(name => name === ext.name)); - builtInExtensions = [...builtInExtensions, ...include]; - log('Final built-in extensions:', builtInExtensions.map(e => e.name)); - } - else { - log('Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); - } - const result = { webBuiltInExtensions: oss.webBuiltInExtensions, ...distro, builtInExtensions }; - fs_1.default.writeFileSync(ossPath, JSON.stringify(result, null, '\t'), 'utf8'); - } - else { - fs_1.default.cpSync(distroPath, ossPath, { force: true, recursive: true }); - } - log(distroPath, '✔︎'); - } -} -main(); -//# sourceMappingURL=mixin-quality.js.map \ No newline at end of file diff --git a/build/azure-pipelines/linux/codesign.js b/build/azure-pipelines/linux/codesign.js deleted file mode 100644 index 98b97db5666..00000000000 --- a/build/azure-pipelines/linux/codesign.js +++ /dev/null @@ -1,29 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const codesign_1 = require("../common/codesign"); -const publish_1 = require("../common/publish"); -async function main() { - const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); - // Start the code sign processes in parallel - // 1. Codesign deb package - // 2. Codesign rpm package - const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/deb', '*.deb'); - const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-pgp', '.build/linux/rpm', '*.rpm'); - // Codesign deb package - (0, codesign_1.printBanner)('Codesign deb package'); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign deb package', codesignTask1); - // Codesign rpm package - (0, codesign_1.printBanner)('Codesign rpm package'); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign rpm package', codesignTask2); -} -main().then(() => { - process.exit(0); -}, err => { - console.error(`ERROR: ${err}`); - process.exit(1); -}); -//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/linux/codesign.ts b/build/azure-pipelines/linux/codesign.ts index 1f74cc21ee9..67a34d9e7a1 100644 --- a/build/azure-pipelines/linux/codesign.ts +++ b/build/azure-pipelines/linux/codesign.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; -import { e } from '../common/publish'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign.ts'; +import { e } from '../common/publish.ts'; async function main() { const esrpCliDLLPath = e('EsrpCliDllPath'); diff --git a/build/azure-pipelines/linux/product-build-linux-cli.yml b/build/azure-pipelines/linux/product-build-linux-cli.yml index 9052a29e18e..ef160c2cc38 100644 --- a/build/azure-pipelines/linux/product-build-linux-cli.yml +++ b/build/azure-pipelines/linux/product-build-linux-cli.yml @@ -51,7 +51,7 @@ jobs: tar -xvzf $(Build.ArtifactStagingDirectory)/vscode-internal-openssl-prebuilt-0.0.11.tgz --strip-components=1 --directory=$(Build.ArtifactStagingDirectory)/openssl displayName: Extract openssl prebuilt - - script: node build/setup-npm-registry.js $NPM_REGISTRY build + - script: node build/setup-npm-registry.ts $NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry diff --git a/build/azure-pipelines/linux/product-build-linux-node-modules.yml b/build/azure-pipelines/linux/product-build-linux-node-modules.yml index 3867a81a55f..290a3fe1b29 100644 --- a/build/azure-pipelines/linux/product-build-linux-node-modules.yml +++ b/build/azure-pipelines/linux/product-build-linux-node-modules.yml @@ -48,11 +48,11 @@ jobs: sudo service xvfb start displayName: Setup system services - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -104,7 +104,7 @@ jobs: SYSROOT_ARCH="amd64" fi export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-8.5.0 - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: $(VSCODE_ARCH) GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -116,6 +116,10 @@ jobs: source ./build/azure-pipelines/linux/setup-env.sh + # Run preinstall script before root dependencies are installed + # so that v8 headers are patched correctly for native modules. + node build/npm/preinstall.ts + for i in {1..5}; do # try 5 times npm ci && break if [ $i -eq 5 ]; then @@ -133,13 +137,13 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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')) diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 3c331192467..31eb7c3d466 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -29,6 +29,10 @@ jobs: NPM_ARCH: ${{ parameters.NPM_ARCH }} VSCODE_ARCH: ${{ parameters.VSCODE_ARCH }} templateContext: + sdl: + binskim: + analyzeTargetGlob: '$(Agent.BuildDirectory)/VSCode-linux-$(VSCODE_ARCH)/**/*.node;$(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)/**/*.node;$(Agent.BuildDirectory)/vscode-server-linux-$(VSCODE_ARCH)-web/**/*.node' + preReleaseVersion: '4.3.1' outputParentDirectory: $(Build.ArtifactStagingDirectory)/out outputs: - ${{ if or(eq(parameters.VSCODE_RUN_ELECTRON_TESTS, true), eq(parameters.VSCODE_RUN_BROWSER_TESTS, true), eq(parameters.VSCODE_RUN_REMOTE_TESTS, true)) }}: diff --git a/build/azure-pipelines/linux/setup-env.sh b/build/azure-pipelines/linux/setup-env.sh index b565a498dae..f0d5fe6c412 100755 --- a/build/azure-pipelines/linux/setup-env.sh +++ b/build/azure-pipelines/linux/setup-env.sh @@ -9,30 +9,23 @@ fi export VSCODE_CLIENT_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-10.5.0 export VSCODE_REMOTE_SYSROOT_DIR=$PWD/.build/sysroots/glibc-2.28-gcc-8.5.0 +if [ -d "$VSCODE_CLIENT_SYSROOT_DIR" ]; then + echo "Using cached client sysroot" +else + echo "Downloading client sysroot" + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' +fi -# Check if we should skip sysroot download -# This is needed for alternative architectures where sysroot is not available -if [ "$1" != "--skip-sysroot" ] && [ "$VSCODE_SKIP_SYSROOT" != "1" ]; then - if [ -d "$VSCODE_CLIENT_SYSROOT_DIR" ]; then - echo "Using cached client sysroot" - else - echo "Downloading client sysroot" - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_CLIENT_SYSROOT_DIR" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' - fi - - if [ -d "$VSCODE_REMOTE_SYSROOT_DIR" ]; then - echo "Using cached remote sysroot" - else - echo "Downloading remote sysroot" - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' - fi +if [ -d "$VSCODE_REMOTE_SYSROOT_DIR" ]; then + echo "Using cached remote sysroot" else - echo "Skipping sysroot download (VSCODE_SKIP_SYSROOT=1 or --skip-sysroot flag)" + echo "Downloading remote sysroot" + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_DIR="$VSCODE_REMOTE_SYSROOT_DIR" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' fi if [ "$npm_config_arch" == "x64" ]; then # Download clang based on chromium revision used by vscode - curl -s https://raw.githubusercontent.com/chromium/chromium/138.0.7204.251/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux + curl -s https://raw.githubusercontent.com/chromium/chromium/142.0.7444.265/tools/clang/scripts/update.py | python - --output-dir=$PWD/.build/CR_Clang --host-os=linux # Download libcxx headers and objects from upstream electron releases DEBUG=libcxx-fetcher \ @@ -40,13 +33,13 @@ if [ "$npm_config_arch" == "x64" ]; then VSCODE_LIBCXX_HEADERS_DIR=$PWD/.build/libcxx_headers \ VSCODE_LIBCXXABI_HEADERS_DIR=$PWD/.build/libcxxabi_headers \ VSCODE_ARCH="$npm_config_arch" \ - node build/linux/libcxx-fetcher.js + node build/linux/libcxx-fetcher.ts # Set compiler toolchain # Flags for the client build are based on - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/138.0.7204.251:build/config/arm.gni - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/138.0.7204.251:build/config/compiler/BUILD.gn - # https://source.chromium.org/chromium/chromium/src/+/refs/tags/138.0.7204.251:build/config/c++/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/142.0.7444.265:build/config/arm.gni + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/142.0.7444.265:build/config/compiler/BUILD.gn + # https://source.chromium.org/chromium/chromium/src/+/refs/tags/142.0.7444.265:build/config/c++/BUILD.gn export CC="$PWD/.build/CR_Clang/bin/clang --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXX="$PWD/.build/CR_Clang/bin/clang++ --gcc-toolchain=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu" export CXXFLAGS="-nostdinc++ -D__NO_INLINE__ -DSPDLOG_USE_STD_FORMAT -I$PWD/.build/libcxx_headers -isystem$PWD/.build/libcxx_headers/include -isystem$PWD/.build/libcxxabi_headers/include -fPIC -flto=thin -fsplit-lto-unit -D_LIBCPP_ABI_NAMESPACE=Cr -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE --sysroot=$VSCODE_CLIENT_SYSROOT_DIR/x86_64-linux-gnu/x86_64-linux-gnu/sysroot" diff --git a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml index 53a78c24c95..89199ebbbb1 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-compile.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-compile.yml @@ -61,11 +61,11 @@ steps: sudo service xvfb start displayName: Setup system services - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts linux $VSCODE_ARCH $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -121,7 +121,7 @@ steps: SYSROOT_ARCH="amd64" fi export VSCODE_SYSROOT_DIR=$(Build.SourcesDirectory)/.build/sysroots/glibc-2.28-gcc-8.5.0 - SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e '(async () => { const { getVSCodeSysroot } = require("./build/linux/debian/install-sysroot.js"); await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' + SYSROOT_ARCH="$SYSROOT_ARCH" VSCODE_SYSROOT_PREFIX="-glibc-2.28-gcc-8.5.0" node -e 'import { getVSCodeSysroot } from "./build/linux/debian/install-sysroot.ts"; (async () => { await getVSCodeSysroot(process.env["SYSROOT_ARCH"]); })()' env: VSCODE_ARCH: $(VSCODE_ARCH) GITHUB_TOKEN: "$(github-distro-mixin-password)" @@ -132,6 +132,10 @@ steps: source ./build/azure-pipelines/linux/setup-env.sh + # Run preinstall script before root dependencies are installed + # so that v8 headers are patched correctly for native modules. + node build/npm/preinstall.ts + for i in {1..5}; do # try 5 times npm ci && break if [ $i -eq 5 ]; then @@ -149,25 +153,25 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: ../../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - script: node build/lib/policies/policyGenerator build/lib/policies/policyData.jsonc linux + - script: npm run copy-policy-dto --prefix build && node build/lib/policies/policyGenerator.ts build/lib/policies/policyData.jsonc linux displayName: Generate policy definitions retryCountOnTaskFailure: 3 @@ -361,7 +365,7 @@ steps: echo "##vso[task.setvariable variable=EsrpCliDllPath]$Version/net6.0/esrpcli.dll" displayName: Find ESRP CLI - - script: npx deemon --detach --wait node build/azure-pipelines/linux/codesign.js + - script: npx deemon --detach --wait node build/azure-pipelines/linux/codesign.ts env: EsrpCliDllPath: $(EsrpCliDllPath) SYSTEM_ACCESSTOKEN: $(System.AccessToken) @@ -375,7 +379,7 @@ steps: VSCODE_RUN_REMOTE_TESTS: ${{ parameters.VSCODE_RUN_REMOTE_TESTS }} - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - script: npx deemon --attach node build/azure-pipelines/linux/codesign.js + - script: npx deemon --attach node build/azure-pipelines/linux/codesign.ts condition: succeededOrFailed() displayName: "✍️ Post-job: Codesign deb & rpm" diff --git a/build/azure-pipelines/linux/steps/product-build-linux-test.yml b/build/azure-pipelines/linux/steps/product-build-linux-test.yml index 2b46c9e007f..c91f3e8ec0e 100644 --- a/build/azure-pipelines/linux/steps/product-build-linux-test.yml +++ b/build/azure-pipelines/linux/steps/product-build-linux-test.yml @@ -7,7 +7,7 @@ parameters: type: boolean steps: - - script: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + - script: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright diff --git a/build/azure-pipelines/product-build-macos.yml b/build/azure-pipelines/product-build-macos.yml index 4c14e0b1ed5..cc563953b00 100644 --- a/build/azure-pipelines/product-build-macos.yml +++ b/build/azure-pipelines/product-build-macos.yml @@ -28,6 +28,8 @@ variables: value: ${{ parameters.VSCODE_QUALITY }} - name: VSCODE_CIBUILD value: ${{ in(variables['Build.Reason'], 'IndividualCI', 'BatchedCI') }} + - name: VSCODE_STEP_ON_IT + value: false - name: skipComponentGovernanceDetection value: true - name: ComponentDetection.Timeout @@ -41,7 +43,7 @@ name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.VSCODE_QUALITY }})" resources: repositories: - - repository: 1ESPipelines + - repository: 1esPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release @@ -53,8 +55,6 @@ extends: tsa: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json - binskim: - analyzeTargetGlob: '+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.exe;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.node;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.dll;-:file|$(Build.SourcesDirectory)/.build/**/system-setup/VSCodeSetup*.exe;-:file|$(Build.SourcesDirectory)/.build/**/user-setup/VSCodeUserSetup*.exe' codeql: runSourceLanguagesInSourceAnalysis: true compiled: @@ -73,59 +73,34 @@ extends: image: onebranch.azurecr.io/linux/ubuntu-2004-arm64:latest stages: - stage: Compile + pool: + name: AcesShared + os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia jobs: - - job: Compile - timeoutInMinutes: 90 - pool: - name: ACESLabTest - os: macOS - steps: - - template: build/azure-pipelines/product-compile.yml@self + - template: build/azure-pipelines/product-compile.yml@self - stage: macOS dependsOn: - Compile pool: - name: ACESLabTest + name: AcesShared os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia variables: BUILDSECMON_OPT_IN: true jobs: - - job: macOSElectronTest - displayName: Electron Tests - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: arm64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin.yml@self - parameters: - VSCODE_ARCH: arm64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: electron - VSCODE_RUN_ELECTRON_TESTS: true - - - job: macOSBrowserTest - displayName: Browser Tests - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: arm64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin.yml@self - parameters: - VSCODE_ARCH: arm64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: browser - VSCODE_RUN_BROWSER_TESTS: true - - - job: macOSRemoteTest - displayName: Remote Tests - timeoutInMinutes: 30 - variables: - VSCODE_ARCH: arm64 - steps: - - template: build/azure-pipelines/darwin/product-build-darwin.yml@self - parameters: - VSCODE_ARCH: arm64 - VSCODE_CIBUILD: ${{ variables.VSCODE_CIBUILD }} - VSCODE_TEST_ARTIFACT_NAME: remote - VSCODE_RUN_REMOTE_TESTS: true + - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self + parameters: + VSCODE_CIBUILD: true + VSCODE_TEST_SUITE: Electron + - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self + parameters: + VSCODE_CIBUILD: true + VSCODE_TEST_SUITE: Browser + - template: build/azure-pipelines/darwin/product-build-darwin-ci.yml@self + parameters: + VSCODE_CIBUILD: true + VSCODE_TEST_SUITE: Remote diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index e9c8f74e659..3ff40d1d941 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -6,6 +6,11 @@ schedules: branches: include: - main + - cron: "0 17 * * Mon-Fri" + displayName: Mon-Fri at 19:00 + branches: + include: + - main trigger: batch: true @@ -158,7 +163,7 @@ resources: source: 'VS Code 7PM Kick-Off' trigger: true repositories: - - repository: 1ESPipelines + - repository: 1esPipelines type: git name: 1ESPipelineTemplates/1ESPipelineTemplates ref: refs/tags/release @@ -171,7 +176,7 @@ extends: enabled: true configFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/tsaoptions.json binskim: - analyzeTargetGlob: '+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.exe;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.node;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.dll;-:file|$(Build.SourcesDirectory)/.build/**/system-setup/VSCodeSetup*.exe;-:file|$(Build.SourcesDirectory)/.build/**/user-setup/VSCodeUserSetup*.exe' + analyzeTargetGlob: '+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.exe;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.dll;+:file|$(Agent.BuildDirectory)/VSCode-*/**/*.node;-:file|$(Agent.BuildDirectory)/VSCode-*/**/resources/**/*.node;-:file|$(Build.SourcesDirectory)/.build/**/system-setup/VSCodeSetup*.exe;-:file|$(Build.SourcesDirectory)/.build/**/user-setup/VSCodeUserSetup*.exe' codeql: runSourceLanguagesInSourceAnalysis: true compiled: @@ -190,6 +195,11 @@ extends: image: onebranch.azurecr.io/linux/ubuntu-2004-arm64:latest stages: - stage: Compile + pool: + name: AcesShared + os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia jobs: - template: build/azure-pipelines/product-compile.yml@self @@ -424,6 +434,8 @@ extends: pool: name: AcesShared os: macOS + demands: + - ImageOverride -equals ACES_VM_SharedPool_Sequoia variables: BUILDSECMON_OPT_IN: true jobs: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index e025e84f911..bc13d980df2 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,9 +1,6 @@ jobs: - job: Compile timeoutInMinutes: 60 - pool: - name: AcesShared - os: macOS templateContext: outputs: - output: pipelineArtifact @@ -29,11 +26,11 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js compile $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts compile $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -81,28 +78,45 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: common/install-builtin-extensions.yml@self - - 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 test-build-scripts + - script: npm exec -- npm-run-all2 -lp core-ci extensions-ci hygiene eslint valid-layers-check define-class-fields-check vscode-dts-compile-check tsec-compile-check test-build-scripts env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Compile & Hygiene + - script: | + set -e + + [ -d "out-build" ] || { echo "ERROR: out-build folder is missing" >&2; exit 1; } + [ -n "$(find out-build -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: out-build folder is empty" >&2; exit 1; } + echo "out-build exists and is not empty" + + ls -d out-vscode-* >/dev/null 2>&1 || { echo "ERROR: No out-vscode-* folders found" >&2; exit 1; } + for folder in out-vscode-*; do + [ -d "$folder" ] || { echo "ERROR: $folder is missing" >&2; exit 1; } + [ -n "$(find "$folder" -mindepth 1 2>/dev/null | head -1)" ] || { echo "ERROR: $folder is empty" >&2; exit 1; } + echo "$folder exists and is not empty" + done + + echo "All required compilation folders checked." + displayName: Validate compilation folders + - script: | set -e npm run compile @@ -135,7 +149,7 @@ jobs: AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ - node build/azure-pipelines/upload-sourcemaps + node build/azure-pipelines/upload-sourcemaps.ts displayName: Upload sourcemaps to Azure - script: ./build/azure-pipelines/common/extract-telemetry.sh diff --git a/build/azure-pipelines/product-npm-package-validate.yml b/build/azure-pipelines/product-npm-package-validate.yml index 4979c96edc5..b256107437d 100644 --- a/build/azure-pipelines/product-npm-package-validate.yml +++ b/build/azure-pipelines/product-npm-package-validate.yml @@ -17,7 +17,6 @@ jobs: name: 1es-ubuntu-22.04-x64 os: linux timeoutInMinutes: 40000 - continueOnError: true variables: VSCODE_ARCH: x64 steps: @@ -47,7 +46,7 @@ jobs: fi displayName: Check if package files were modified - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none'), eq(variables['SHOULD_VALIDATE'], 'true')) displayName: Setup NPM Registry @@ -84,11 +83,11 @@ jobs: echo "Attempt $attempt: Running npm ci" if npm i --ignore-scripts; then - if node build/npm/postinstall.js; then + if node build/npm/postinstall.ts; then echo "npm i succeeded on attempt $attempt" exit 0 else - echo "node build/npm/postinstall.js failed on attempt $attempt" + echo "node build/npm/postinstall.ts failed on attempt $attempt" fi else echo "npm i failed on attempt $attempt" @@ -106,6 +105,12 @@ jobs: timeoutInMinutes: 400 condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) + - script: | + set -e + find . -name 'package-lock.json' -exec sed -i "s|$NPM_REGISTRY|https://registry.npmjs.org/|g" {} \; + displayName: Restore registry URLs in package-lock.json + condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none'), eq(variables['SHOULD_VALIDATE'], 'true')) + - script: .github/workflows/check-clean-git-state.sh displayName: Check clean git state condition: and(succeeded(), eq(variables['SHOULD_VALIDATE'], 'true')) diff --git a/build/azure-pipelines/product-publish.yml b/build/azure-pipelines/product-publish.yml index aa0727a1988..1f124e2f871 100644 --- a/build/azure-pipelines/product-publish.yml +++ b/build/azure-pipelines/product-publish.yml @@ -31,6 +31,11 @@ jobs: versionSource: fromFile versionFilePath: .nvmrc + - template: ./distro/download-distro.yml@self + + - script: node build/azure-pipelines/distro/mixin-quality.ts + displayName: Mixin distro quality + - task: AzureKeyVault@2 displayName: "Azure Key Vault: Get Secrets" inputs: @@ -82,7 +87,7 @@ jobs: $VERSION = node -p "require('./package.json').version" Write-Host "Creating build with version: $VERSION" - exec { node build/azure-pipelines/common/createBuild.js $VERSION } + exec { node build/azure-pipelines/common/createBuild.ts $VERSION } env: AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" AZURE_CLIENT_ID: "$(AZURE_CLIENT_ID)" @@ -90,7 +95,7 @@ jobs: displayName: Create build if it hasn't been created before - pwsh: | - $publishAuthTokens = (node build/azure-pipelines/common/getPublishAuthTokens) + $publishAuthTokens = (node build/azure-pipelines/common/getPublishAuthTokens.ts) Write-Host "##vso[task.setvariable variable=PUBLISH_AUTH_TOKENS;issecret=true]$publishAuthTokens" env: AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" @@ -98,7 +103,7 @@ jobs: AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" displayName: Get publish auth tokens - - pwsh: node build/azure-pipelines/common/publish.js + - pwsh: node build/azure-pipelines/common/publish.ts env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) PUBLISH_AUTH_TOKENS: "$(PUBLISH_AUTH_TOKENS)" @@ -110,7 +115,7 @@ jobs: retryCountOnTaskFailure: 3 - ${{ if and(in(parameters.VSCODE_QUALITY, 'insider', 'exploration'), eq(parameters.VSCODE_SCHEDULEDBUILD, true)) }}: - - script: node build/azure-pipelines/common/releaseBuild.js + - script: node build/azure-pipelines/common/releaseBuild.ts env: PUBLISH_AUTH_TOKENS: "$(PUBLISH_AUTH_TOKENS)" displayName: Release build diff --git a/build/azure-pipelines/product-release.yml b/build/azure-pipelines/product-release.yml index bac4d0e53fa..00821eb41a6 100644 --- a/build/azure-pipelines/product-release.yml +++ b/build/azure-pipelines/product-release.yml @@ -10,6 +10,11 @@ steps: versionSource: fromFile versionFilePath: .nvmrc + - template: ./distro/download-distro.yml@self + + - script: node build/azure-pipelines/distro/mixin-quality.ts + displayName: Mixin distro quality + - task: AzureCLI@2 displayName: Fetch secrets inputs: @@ -27,7 +32,7 @@ steps: displayName: Install build dependencies - pwsh: | - $publishAuthTokens = (node build/azure-pipelines/common/getPublishAuthTokens) + $publishAuthTokens = (node build/azure-pipelines/common/getPublishAuthTokens.ts) Write-Host "##vso[task.setvariable variable=PUBLISH_AUTH_TOKENS;issecret=true]$publishAuthTokens" env: AZURE_TENANT_ID: "$(AZURE_TENANT_ID)" @@ -35,7 +40,7 @@ steps: AZURE_ID_TOKEN: "$(AZURE_ID_TOKEN)" displayName: Get publish auth tokens - - script: node build/azure-pipelines/common/releaseBuild.js ${{ parameters.VSCODE_RELEASE }} + - script: node build/azure-pipelines/common/releaseBuild.ts ${{ parameters.VSCODE_RELEASE }} displayName: Release build env: PUBLISH_AUTH_TOKENS: "$(PUBLISH_AUTH_TOKENS)" diff --git a/build/azure-pipelines/product-sanity-tests.yml b/build/azure-pipelines/product-sanity-tests.yml new file mode 100644 index 00000000000..f334ff3ed75 --- /dev/null +++ b/build/azure-pipelines/product-sanity-tests.yml @@ -0,0 +1,325 @@ +pr: none + +trigger: none + +parameters: + - name: buildQuality + displayName: Published Build Quality + type: string + default: insider + values: + - exploration + - insider + - stable + + - name: buildCommit + displayName: Published Build Commit + type: string + + - name: npmRegistry + displayName: Custom NPM Registry URL + type: string + default: https://pkgs.dev.azure.com/monacotools/Monaco/_packaging/vscode/npm/registry/ + +variables: + - name: skipComponentGovernanceDetection + value: true + - name: Codeql.SkipTaskAutoInjection + value: true + - name: BUILD_COMMIT + value: ${{ parameters.buildCommit }} + - name: BUILD_QUALITY + value: ${{ parameters.buildQuality }} + - name: NPM_REGISTRY + value: ${{ parameters.npmRegistry }} + +name: "$(Date:yyyyMMdd).$(Rev:r) (${{ parameters.buildQuality }} ${{ parameters.buildCommit }})" + +resources: + repositories: + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + sdl: + tsa: + enabled: false + codeql: + compiled: + enabled: false + justificationForDisabling: Sanity tests only, no code compilation + credscan: + suppressionsFile: $(Build.SourcesDirectory)/build/azure-pipelines/config/CredScanSuppressions.json + eslint: + enabled: false + sourceAnalysisPool: 1es-windows-2022-x64 + createAdoIssuesForJustificationsForDisablement: false + stages: + - stage: sanity_tests + displayName: Sanity Tests + jobs: + # macOS + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: macos_x64 + displayName: MacOS x64 (no runtime) + poolName: AcesShared + os: macOS + args: --no-detection --grep "darwin-x64" + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: macos_arm64 + displayName: MacOS arm64 + poolName: AcesShared + os: macOS + + # Windows + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: windows_x64 + displayName: Windows x64 + poolName: 1es-windows-2022-x64 + os: windows + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: windows_arm64 + displayName: Windows arm64 + poolName: 1es-windows-2022-arm64 + os: windows + + # Alpine 3.22 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: alpine_amd64 + displayName: Alpine 3.22 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: alpine + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: alpine_arm64 + displayName: Alpine 3.22 arm64 + poolName: 1es-azure-linux-3-arm64 + container: alpine + arch: arm64 + + # CentOS Stream 9 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: centos_stream9_amd64 + displayName: CentOS Stream 9 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: centos + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: centos_stream9_arm64 + displayName: CentOS Stream 9 arm64 + poolName: 1es-azure-linux-3-arm64 + container: centos + arch: arm64 + + # Debian 10 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_10_amd64 + displayName: Debian 10 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: debian-10 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_10_arm32 + displayName: Debian 10 arm32 + poolName: 1es-azure-linux-3-arm64 + container: debian-10 + arch: arm + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_10_arm64 + displayName: Debian 10 arm64 + poolName: 1es-azure-linux-3-arm64 + container: debian-10 + arch: arm64 + + # Debian 12 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_12_amd64 + displayName: Debian 12 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: debian-12 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_12_arm32 + displayName: Debian 12 arm32 + poolName: 1es-azure-linux-3-arm64 + container: debian-12 + arch: arm + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: debian_12_arm64 + displayName: Debian 12 arm64 + poolName: 1es-azure-linux-3-arm64 + container: debian-12 + arch: arm64 + + # Fedora 36 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: fedora_36_amd64 + displayName: Fedora 36 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: fedora + baseImage: fedora:36 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: fedora_36_arm64 + displayName: Fedora 36 arm64 + poolName: 1es-azure-linux-3-arm64 + container: fedora + baseImage: fedora:36 + arch: arm64 + + # Fedora 40 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: fedora_40_amd64 + displayName: Fedora 40 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: fedora + baseImage: fedora:40 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: fedora_40_arm64 + displayName: Fedora 40 arm64 + poolName: 1es-azure-linux-3-arm64 + container: fedora + baseImage: fedora:40 + arch: arm64 + + # openSUSE Leap 16.0 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: opensuse_leap_amd64 + displayName: openSUSE Leap 16.0 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: opensuse + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: opensuse_leap_arm64 + displayName: openSUSE Leap 16.0 arm64 + poolName: 1es-azure-linux-3-arm64 + container: opensuse + arch: arm64 + + # Red Hat UBI 9 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: redhat_ubi9_amd64 + displayName: Red Hat UBI 9 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: redhat + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: redhat_ubi9_arm64 + displayName: Red Hat UBI 9 arm64 + poolName: 1es-azure-linux-3-arm64 + container: redhat + arch: arm64 + + # Ubuntu 22.04 Native (Snap coverage) + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_native_x64 + displayName: Ubuntu 22.04 x64 Native + poolName: 1es-ubuntu-22.04-x64 + os: linux + + # Ubuntu 22.04 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_22_04_amd64 + displayName: Ubuntu 22.04 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: ubuntu + baseImage: ubuntu:22.04 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_22_04_arm32 + displayName: Ubuntu 22.04 arm32 + poolName: 1es-azure-linux-3-arm64 + container: ubuntu + baseImage: ubuntu:22.04 + arch: arm + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_22_04_arm64 + displayName: Ubuntu 22.04 arm64 + poolName: 1es-azure-linux-3-arm64 + container: ubuntu + baseImage: ubuntu:22.04 + arch: arm64 + + # Ubuntu 24.04 + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_24_04_amd64 + displayName: Ubuntu 24.04 amd64 + poolName: 1es-ubuntu-22.04-x64 + container: ubuntu + baseImage: ubuntu:24.04 + arch: amd64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_24_04_arm32 + displayName: Ubuntu 24.04 arm32 + poolName: 1es-azure-linux-3-arm64 + container: ubuntu + baseImage: ubuntu:24.04 + arch: arm + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_24_04_arm64 + displayName: Ubuntu 24.04 arm64 + poolName: 1es-azure-linux-3-arm64 + container: ubuntu + baseImage: ubuntu:24.04 + arch: arm64 + + - template: build/azure-pipelines/common/sanity-tests.yml@self + parameters: + name: ubuntu_24_04_arm64_64k + displayName: Ubuntu 24.04 arm64 (64K page) + poolName: 1es-ubuntu-22.04-x64 + container: ubuntu + baseImage: ubuntu:24.04 + arch: arm64 + pageSize: 64k + args: --grep "desktop-linux-arm64" diff --git a/build/azure-pipelines/publish-types/check-version.js b/build/azure-pipelines/publish-types/check-version.js deleted file mode 100644 index 5bd80a69bbf..00000000000 --- a/build/azure-pipelines/publish-types/check-version.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const child_process_1 = __importDefault(require("child_process")); -let tag = ''; -try { - tag = child_process_1.default - .execSync('git describe --tags `git rev-list --tags --max-count=1`') - .toString() - .trim(); - if (!isValidTag(tag)) { - throw Error(`Invalid tag ${tag}`); - } -} -catch (err) { - console.error(err); - console.error('Failed to update types'); - process.exit(1); -} -function isValidTag(t) { - if (t.split('.').length !== 3) { - return false; - } - const [major, minor, bug] = t.split('.'); - // Only release for tags like 1.34.0 - if (bug !== '0') { - return false; - } - if (isNaN(parseInt(major, 10)) || isNaN(parseInt(minor, 10))) { - return false; - } - return true; -} -//# sourceMappingURL=check-version.js.map \ No newline at end of file diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 65882ce1971..25dbf1f185a 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -34,7 +34,7 @@ steps: - bash: | # Install build dependencies (cd build && npm ci) - node build/azure-pipelines/publish-types/check-version.js + node build/azure-pipelines/publish-types/check-version.ts displayName: Check version - bash: | @@ -42,7 +42,7 @@ steps: git config --global user.name "VSCode" git clone https://$(GITHUB_TOKEN)@github.com/DefinitelyTyped/DefinitelyTyped.git --depth=1 - node build/azure-pipelines/publish-types/update-types.js + node build/azure-pipelines/publish-types/update-types.ts TAG_VERSION=$(git describe --tags `git rev-list --tags --max-count=1`) diff --git a/build/azure-pipelines/publish-types/update-types.js b/build/azure-pipelines/publish-types/update-types.js deleted file mode 100644 index 29f9bfcf66e..00000000000 --- a/build/azure-pipelines/publish-types/update-types.js +++ /dev/null @@ -1,76 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const child_process_1 = __importDefault(require("child_process")); -const path_1 = __importDefault(require("path")); -let tag = ''; -try { - tag = child_process_1.default - .execSync('git describe --tags `git rev-list --tags --max-count=1`') - .toString() - .trim(); - const dtsUri = `https://raw.githubusercontent.com/microsoft/vscode/${tag}/src/vscode-dts/vscode.d.ts`; - const outPath = path_1.default.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); - child_process_1.default.execSync(`curl ${dtsUri} --output ${outPath}`); - updateDTSFile(outPath, tag); - console.log(`Done updating vscode.d.ts at ${outPath}`); -} -catch (err) { - console.error(err); - console.error('Failed to update types'); - process.exit(1); -} -function updateDTSFile(outPath, tag) { - const oldContent = fs_1.default.readFileSync(outPath, 'utf-8'); - const newContent = getNewFileContent(oldContent, tag); - fs_1.default.writeFileSync(outPath, newContent); -} -function repeat(str, times) { - const result = new Array(times); - for (let i = 0; i < times; i++) { - result[i] = str; - } - return result.join(''); -} -function convertTabsToSpaces(str) { - return str.replace(/\t/gm, value => repeat(' ', value.length)); -} -function getNewFileContent(content, tag) { - const oldheader = [ - `/*---------------------------------------------------------------------------------------------`, - ` * Copyright (c) Microsoft Corporation. All rights reserved.`, - ` * Licensed under the MIT License. See License.txt in the project root for license information.`, - ` *--------------------------------------------------------------------------------------------*/` - ].join('\n'); - return convertTabsToSpaces(getNewFileHeader(tag) + content.slice(oldheader.length)); -} -function getNewFileHeader(tag) { - const [major, minor] = tag.split('.'); - const shorttag = `${major}.${minor}`; - const header = [ - `// Type definitions for Visual Studio Code ${shorttag}`, - `// Project: https://github.com/microsoft/vscode`, - `// Definitions by: Visual Studio Code Team, Microsoft `, - `// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped`, - ``, - `/*---------------------------------------------------------------------------------------------`, - ` * Copyright (c) Microsoft Corporation. All rights reserved.`, - ` * Licensed under the MIT License.`, - ` * See https://github.com/microsoft/vscode/blob/main/LICENSE.txt for license information.`, - ` *--------------------------------------------------------------------------------------------*/`, - ``, - `/**`, - ` * Type Definition for Visual Studio Code ${shorttag} Extension API`, - ` * See https://code.visualstudio.com/api for more information`, - ` */` - ].join('\n'); - return header; -} -//# sourceMappingURL=update-types.js.map \ No newline at end of file diff --git a/build/azure-pipelines/publish-types/update-types.ts b/build/azure-pipelines/publish-types/update-types.ts index 0f99b07cf9a..05482ab452e 100644 --- a/build/azure-pipelines/publish-types/update-types.ts +++ b/build/azure-pipelines/publish-types/update-types.ts @@ -14,22 +14,30 @@ try { .toString() .trim(); + const [major, minor] = tag.split('.'); + const shorttag = `${major}.${minor}`; + const dtsUri = `https://raw.githubusercontent.com/microsoft/vscode/${tag}/src/vscode-dts/vscode.d.ts`; - const outPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); - cp.execSync(`curl ${dtsUri} --output ${outPath}`); + const outDtsPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/index.d.ts'); + cp.execSync(`curl ${dtsUri} --output ${outDtsPath}`); + + updateDTSFile(outDtsPath, shorttag); - updateDTSFile(outPath, tag); + const outPackageJsonPath = path.resolve(process.cwd(), 'DefinitelyTyped/types/vscode/package.json'); + const packageJson = JSON.parse(fs.readFileSync(outPackageJsonPath, 'utf-8')); + packageJson.version = shorttag + '.9999'; + fs.writeFileSync(outPackageJsonPath, JSON.stringify(packageJson, null, 2) + '\n'); - console.log(`Done updating vscode.d.ts at ${outPath}`); + console.log(`Done updating vscode.d.ts at ${outDtsPath} and package.json to version ${packageJson.version}`); } catch (err) { console.error(err); console.error('Failed to update types'); process.exit(1); } -function updateDTSFile(outPath: string, tag: string) { +function updateDTSFile(outPath: string, shorttag: string) { const oldContent = fs.readFileSync(outPath, 'utf-8'); - const newContent = getNewFileContent(oldContent, tag); + const newContent = getNewFileContent(oldContent, shorttag); fs.writeFileSync(outPath, newContent); } @@ -46,7 +54,7 @@ function convertTabsToSpaces(str: string): string { return str.replace(/\t/gm, value => repeat(' ', value.length)); } -function getNewFileContent(content: string, tag: string) { +function getNewFileContent(content: string, shorttag: string) { const oldheader = [ `/*---------------------------------------------------------------------------------------------`, ` * Copyright (c) Microsoft Corporation. All rights reserved.`, @@ -54,13 +62,10 @@ function getNewFileContent(content: string, tag: string) { ` *--------------------------------------------------------------------------------------------*/` ].join('\n'); - return convertTabsToSpaces(getNewFileHeader(tag) + content.slice(oldheader.length)); + return convertTabsToSpaces(getNewFileHeader(shorttag) + content.slice(oldheader.length)); } -function getNewFileHeader(tag: string) { - const [major, minor] = tag.split('.'); - const shorttag = `${major}.${minor}`; - +function getNewFileHeader(shorttag: string) { const header = [ `// Type definitions for Visual Studio Code ${shorttag}`, `// Project: https://github.com/microsoft/vscode`, diff --git a/build/azure-pipelines/upload-cdn.js b/build/azure-pipelines/upload-cdn.js deleted file mode 100644 index adff2c9401d..00000000000 --- a/build/azure-pipelines/upload-cdn.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const event_stream_1 = __importDefault(require("event-stream")); -const vinyl_1 = __importDefault(require("vinyl")); -const vinyl_fs_1 = __importDefault(require("vinyl-fs")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const gulp_gzip_1 = __importDefault(require("gulp-gzip")); -const mime_1 = __importDefault(require("mime")); -const identity_1 = require("@azure/identity"); -const util_1 = require("../lib/util"); -const azure = require('gulp-azure-storage'); -const commit = process.env['BUILD_SOURCEVERSION']; -const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); -mime_1.default.define({ - 'application/typescript': ['ts'], - 'application/json': ['code-snippets'], -}); -// From default AFD configuration -const MimeTypesToCompress = new Set([ - 'application/eot', - 'application/font', - 'application/font-sfnt', - 'application/javascript', - 'application/json', - 'application/opentype', - 'application/otf', - 'application/pkcs7-mime', - 'application/truetype', - 'application/ttf', - 'application/typescript', - 'application/vnd.ms-fontobject', - 'application/xhtml+xml', - 'application/xml', - 'application/xml+rss', - 'application/x-font-opentype', - 'application/x-font-truetype', - 'application/x-font-ttf', - 'application/x-httpd-cgi', - 'application/x-javascript', - 'application/x-mpegurl', - 'application/x-opentype', - 'application/x-otf', - 'application/x-perl', - 'application/x-ttf', - 'font/eot', - 'font/ttf', - 'font/otf', - 'font/opentype', - 'image/svg+xml', - 'text/css', - 'text/csv', - 'text/html', - 'text/javascript', - 'text/js', - 'text/markdown', - 'text/plain', - 'text/richtext', - 'text/tab-separated-values', - 'text/xml', - 'text/x-script', - 'text/x-component', - 'text/x-java-source' -]); -function wait(stream) { - return new Promise((c, e) => { - stream.on('end', () => c()); - stream.on('error', (err) => e(err)); - }); -} -async function main() { - const files = []; - const options = (compressed) => ({ - account: process.env.AZURE_STORAGE_ACCOUNT, - credential, - container: '$web', - prefix: `${process.env.VSCODE_QUALITY}/${commit}/`, - contentSettings: { - contentEncoding: compressed ? 'gzip' : undefined, - cacheControl: 'max-age=31536000, public' - } - }); - const all = vinyl_fs_1.default.src('**', { cwd: '../vscode-web', base: '../vscode-web', dot: true }) - .pipe((0, gulp_filter_1.default)(f => !f.isDirectory())); - const compressed = all - .pipe((0, gulp_filter_1.default)(f => MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) - .pipe((0, gulp_gzip_1.default)({ append: false })) - .pipe(azure.upload(options(true))); - const uncompressed = all - .pipe((0, gulp_filter_1.default)(f => !MimeTypesToCompress.has(mime_1.default.lookup(f.path)))) - .pipe(azure.upload(options(false))); - const out = event_stream_1.default.merge(compressed, uncompressed) - .pipe(event_stream_1.default.through(function (f) { - console.log('Uploaded:', f.relative); - files.push(f.relative); - this.emit('data', f); - })); - console.log(`Uploading files to CDN...`); // debug - await wait(out); - const listing = new vinyl_1.default({ - path: 'files.txt', - contents: Buffer.from(files.join('\n')), - stat: new util_1.VinylStat({ mode: 0o666 }) - }); - const filesOut = event_stream_1.default.readArray([listing]) - .pipe((0, gulp_gzip_1.default)({ append: false })) - .pipe(azure.upload(options(true))); - console.log(`Uploading: files.txt (${files.length} files)`); // debug - await wait(filesOut); -} -main().catch(err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=upload-cdn.js.map \ No newline at end of file diff --git a/build/azure-pipelines/upload-cdn.ts b/build/azure-pipelines/upload-cdn.ts index ead60d4b6cc..e3a715b4e53 100644 --- a/build/azure-pipelines/upload-cdn.ts +++ b/build/azure-pipelines/upload-cdn.ts @@ -10,8 +10,8 @@ import filter from 'gulp-filter'; import gzip from 'gulp-gzip'; import mime from 'mime'; import { ClientAssertionCredential } from '@azure/identity'; -import { VinylStat } from '../lib/util'; -const azure = require('gulp-azure-storage'); +import { VinylStat } from '../lib/util.ts'; +import azure from 'gulp-azure-storage'; const commit = process.env['BUILD_SOURCEVERSION']; const credential = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!)); @@ -71,7 +71,7 @@ const MimeTypesToCompress = new Set([ function wait(stream: es.ThroughStream): Promise { return new Promise((c, e) => { stream.on('end', () => c()); - stream.on('error', (err: any) => e(err)); + stream.on('error', (err) => e(err)); }); } diff --git a/build/azure-pipelines/upload-nlsmetadata.js b/build/azure-pipelines/upload-nlsmetadata.js deleted file mode 100644 index e89a6497d70..00000000000 --- a/build/azure-pipelines/upload-nlsmetadata.js +++ /dev/null @@ -1,127 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const event_stream_1 = __importDefault(require("event-stream")); -const vinyl_fs_1 = __importDefault(require("vinyl-fs")); -const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); -const gulp_gzip_1 = __importDefault(require("gulp-gzip")); -const identity_1 = require("@azure/identity"); -const path = require("path"); -const fs_1 = require("fs"); -const azure = require('gulp-azure-storage'); -const commit = process.env['BUILD_SOURCEVERSION']; -const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); -function main() { - return new Promise((c, e) => { - const combinedMetadataJson = event_stream_1.default.merge( - // vscode: we are not using `out-build/nls.metadata.json` here because - // it includes metadata for translators for `keys`. but for our purpose - // we want only the `keys` and `messages` as `string`. - event_stream_1.default.merge(vinyl_fs_1.default.src('out-build/nls.keys.json', { base: 'out-build' }), vinyl_fs_1.default.src('out-build/nls.messages.json', { base: 'out-build' })) - .pipe((0, gulp_merge_json_1.default)({ - fileName: 'vscode.json', - jsonSpace: '', - concatArrays: true, - edit: (parsedJson, file) => { - if (file.base === 'out-build') { - if (file.basename === 'nls.keys.json') { - return { keys: parsedJson }; - } - else { - return { messages: parsedJson }; - } - } - } - })), - // extensions - vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/nls.metadata.header.json', { base: '.build/extensions' }), vinyl_fs_1.default.src('.build/extensions/**/package.nls.json', { base: '.build/extensions' })).pipe((0, gulp_merge_json_1.default)({ - fileName: 'combined.nls.metadata.json', - jsonSpace: '', - concatArrays: true, - edit: (parsedJson, file) => { - if (file.basename === 'vscode.json') { - return { vscode: parsedJson }; - } - // Handle extensions and follow the same structure as the Core nls file. - switch (file.basename) { - case 'package.nls.json': - // put package.nls.json content in Core NlsMetadata format - // language packs use the key "package" to specify that - // translations are for the package.json file - parsedJson = { - messages: { - package: Object.values(parsedJson) - }, - keys: { - package: Object.keys(parsedJson) - }, - bundles: { - main: ['package'] - } - }; - break; - case 'nls.metadata.header.json': - parsedJson = { header: parsedJson }; - break; - case 'nls.metadata.json': { - // put nls.metadata.json content in Core NlsMetadata format - const modules = Object.keys(parsedJson); - const json = { - keys: {}, - messages: {}, - bundles: { - main: [] - } - }; - for (const module of modules) { - json.messages[module] = parsedJson[module].messages; - json.keys[module] = parsedJson[module].keys; - json.bundles.main.push(module); - } - parsedJson = json; - break; - } - } - // Get extension id and use that as the key - const folderPath = path.join(file.base, file.relative.split('/')[0]); - const manifest = (0, fs_1.readFileSync)(path.join(folderPath, 'package.json'), 'utf-8'); - const manifestJson = JSON.parse(manifest); - const key = manifestJson.publisher + '.' + manifestJson.name; - return { [key]: parsedJson }; - }, - })); - const nlsMessagesJs = vinyl_fs_1.default.src('out-build/nls.messages.js', { base: 'out-build' }); - event_stream_1.default.merge(combinedMetadataJson, nlsMessagesJs) - .pipe((0, gulp_gzip_1.default)({ append: false })) - .pipe(vinyl_fs_1.default.dest('./nlsMetadata')) - .pipe(event_stream_1.default.through(function (data) { - console.log(`Uploading ${data.path}`); - // trigger artifact upload - console.log(`##vso[artifact.upload containerfolder=nlsmetadata;artifactname=${data.basename}]${data.path}`); - this.emit('data', data); - })) - .pipe(azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - credential, - container: '$web', - prefix: `nlsmetadata/${commit}/`, - contentSettings: { - contentEncoding: 'gzip', - cacheControl: 'max-age=31536000, public' - } - })) - .on('end', () => c()) - .on('error', (err) => e(err)); - }); -} -main().catch(err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=upload-nlsmetadata.js.map \ No newline at end of file diff --git a/build/azure-pipelines/upload-nlsmetadata.ts b/build/azure-pipelines/upload-nlsmetadata.ts index 468a9341a7b..9d6a803e169 100644 --- a/build/azure-pipelines/upload-nlsmetadata.ts +++ b/build/azure-pipelines/upload-nlsmetadata.ts @@ -9,9 +9,9 @@ import vfs from 'vinyl-fs'; import merge from 'gulp-merge-json'; import gzip from 'gulp-gzip'; import { ClientAssertionCredential } from '@azure/identity'; -import path = require('path'); +import path from 'path'; import { readFileSync } from 'fs'; -const azure = require('gulp-azure-storage'); +import azure from 'gulp-azure-storage'; const commit = process.env['BUILD_SOURCEVERSION']; const credential = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!)); diff --git a/build/azure-pipelines/upload-sourcemaps.js b/build/azure-pipelines/upload-sourcemaps.js deleted file mode 100644 index cac1ae3caf2..00000000000 --- a/build/azure-pipelines/upload-sourcemaps.js +++ /dev/null @@ -1,101 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __importDefault(require("path")); -const event_stream_1 = __importDefault(require("event-stream")); -const vinyl_fs_1 = __importDefault(require("vinyl-fs")); -const util = __importStar(require("../lib/util")); -const dependencies_1 = require("../lib/dependencies"); -const identity_1 = require("@azure/identity"); -const azure = require('gulp-azure-storage'); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const commit = process.env['BUILD_SOURCEVERSION']; -const credential = new identity_1.ClientAssertionCredential(process.env['AZURE_TENANT_ID'], process.env['AZURE_CLIENT_ID'], () => Promise.resolve(process.env['AZURE_ID_TOKEN'])); -// optionally allow to pass in explicit base/maps to upload -const [, , base, maps] = process.argv; -function src(base, maps = `${base}/**/*.map`) { - return vinyl_fs_1.default.src(maps, { base }) - .pipe(event_stream_1.default.mapSync((f) => { - f.path = `${f.base}/core/${f.relative}`; - return f; - })); -} -function main() { - const sources = []; - // vscode client maps (default) - if (!base) { - const vs = src('out-vscode-min'); // client source-maps only - sources.push(vs); - const productionDependencies = (0, dependencies_1.getProductionDependencies)(root); - const productionDependenciesSrc = productionDependencies.map((d) => path_1.default.relative(root, d)).map((d) => `./${d}/**/*.map`); - const nodeModules = vinyl_fs_1.default.src(productionDependenciesSrc, { base: '.' }) - .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) - .pipe(util.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`))); - sources.push(nodeModules); - const extensionsOut = vinyl_fs_1.default.src(['.build/extensions/**/*.js.map', '!**/node_modules/**'], { base: '.build' }); - sources.push(extensionsOut); - } - // specific client base/maps - else { - sources.push(src(base, maps)); - } - return new Promise((c, e) => { - event_stream_1.default.merge(...sources) - .pipe(event_stream_1.default.through(function (data) { - console.log('Uploading Sourcemap', data.relative); // debug - this.emit('data', data); - })) - .pipe(azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - credential, - container: '$web', - prefix: `sourcemaps/${commit}/` - })) - .on('end', () => c()) - .on('error', (err) => e(err)); - }); -} -main().catch(err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=upload-sourcemaps.js.map \ No newline at end of file diff --git a/build/azure-pipelines/upload-sourcemaps.ts b/build/azure-pipelines/upload-sourcemaps.ts index 612a57c9da2..d5a72de54bf 100644 --- a/build/azure-pipelines/upload-sourcemaps.ts +++ b/build/azure-pipelines/upload-sourcemaps.ts @@ -7,13 +7,13 @@ import path from 'path'; import es from 'event-stream'; import Vinyl from 'vinyl'; import vfs from 'vinyl-fs'; -import * as util from '../lib/util'; -import { getProductionDependencies } from '../lib/dependencies'; +import * as util from '../lib/util.ts'; +import { getProductionDependencies } from '../lib/dependencies.ts'; import { ClientAssertionCredential } from '@azure/identity'; import Stream from 'stream'; -const azure = require('gulp-azure-storage'); +import azure from 'gulp-azure-storage'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); const commit = process.env['BUILD_SOURCEVERSION']; const credential = new ClientAssertionCredential(process.env['AZURE_TENANT_ID']!, process.env['AZURE_CLIENT_ID']!, () => Promise.resolve(process.env['AZURE_ID_TOKEN']!)); @@ -65,7 +65,7 @@ function main(): Promise { prefix: `sourcemaps/${commit}/` })) .on('end', () => c()) - .on('error', (err: any) => e(err)); + .on('error', (err) => e(err)); }); } diff --git a/build/azure-pipelines/web/product-build-web-node-modules.yml b/build/azure-pipelines/web/product-build-web-node-modules.yml index 1aea6719de3..75a0cc6cd6e 100644 --- a/build/azure-pipelines/web/product-build-web-node-modules.yml +++ b/build/azure-pipelines/web/product-build-web-node-modules.yml @@ -22,11 +22,11 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js web $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts web $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -77,13 +77,13 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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')) diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index d4f1af2d0e0..71932745be7 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -42,11 +42,11 @@ jobs: - script: tar -xzf $(Build.ArtifactStagingDirectory)/compilation.tar.gz displayName: Extract compilation output - - script: node build/setup-npm-registry.js $NPM_REGISTRY + - script: node build/setup-npm-registry.ts $NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry - - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.js web $(node -p process.arch) > .build/packagelockhash + - script: mkdir -p .build && node build/azure-pipelines/common/computeNodeModulesCacheKey.ts web $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -101,19 +101,19 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Mixin distro node modules - script: | set -e - node build/azure-pipelines/common/listNodeModules.js .build/node_modules_list.txt + node build/azure-pipelines/common/listNodeModules.ts .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-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: ../common/install-builtin-extensions.yml@self @@ -147,7 +147,7 @@ jobs: AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ - node build/azure-pipelines/upload-cdn + node build/azure-pipelines/upload-cdn.ts displayName: Upload to CDN - script: | @@ -156,8 +156,8 @@ jobs: AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ - node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.js.map - displayName: Upload sourcemaps (Web Main) + node build/azure-pipelines/upload-sourcemaps.ts out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.internal.js.map + displayName: Upload sourcemaps (Web) - script: | set -e @@ -165,14 +165,5 @@ jobs: AZURE_TENANT_ID="$(AZURE_TENANT_ID)" \ AZURE_CLIENT_ID="$(AZURE_CLIENT_ID)" \ AZURE_ID_TOKEN="$(AZURE_ID_TOKEN)" \ - node build/azure-pipelines/upload-sourcemaps out-vscode-web-min out-vscode-web-min/vs/workbench/workbench.web.main.internal.js.map - displayName: Upload sourcemaps (Web Internal) - - - 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-nlsmetadata + node build/azure-pipelines/upload-nlsmetadata.ts displayName: Upload NLS Metadata diff --git a/build/azure-pipelines/win32/codesign.js b/build/azure-pipelines/win32/codesign.js deleted file mode 100644 index 630f9a64ba1..00000000000 --- a/build/azure-pipelines/win32/codesign.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const zx_1 = require("zx"); -const codesign_1 = require("../common/codesign"); -const publish_1 = require("../common/publish"); -async function main() { - (0, zx_1.usePwsh)(); - const arch = (0, publish_1.e)('VSCODE_ARCH'); - const esrpCliDLLPath = (0, publish_1.e)('EsrpCliDllPath'); - const codeSigningFolderPath = (0, publish_1.e)('CodeSigningFolderPath'); - // Start the code sign processes in parallel - // 1. Codesign executables and shared libraries - // 2. Codesign Powershell scripts - // 3. Codesign context menu appx package (insiders only) - const codesignTask1 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); - const codesignTask2 = (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); - const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' - ? (0, codesign_1.spawnCodesignProcess)(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') - : undefined; - // Codesign executables and shared libraries - (0, codesign_1.printBanner)('Codesign executables and shared libraries'); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign executables and shared libraries', codesignTask1); - // Codesign Powershell scripts - (0, codesign_1.printBanner)('Codesign Powershell scripts'); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign Powershell scripts', codesignTask2); - if (codesignTask3) { - // Codesign context menu appx package - (0, codesign_1.printBanner)('Codesign context menu appx package'); - await (0, codesign_1.streamProcessOutputAndCheckResult)('Codesign context menu appx package', codesignTask3); - } - // Create build artifact directory - await (0, zx_1.$) `New-Item -ItemType Directory -Path .build/win32-${arch} -Force`; - // Package client - if (process.env['BUILT_CLIENT']) { - // Product version - const version = await (0, zx_1.$) `node -p "require('../VSCode-win32-${arch}/resources/app/package.json').version"`; - (0, codesign_1.printBanner)('Package client'); - const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}-${version}.zip`; - await (0, zx_1.$) `7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); - await (0, zx_1.$) `7z.exe l ${clientArchivePath}`.pipe(process.stdout); - } - // Package server - if (process.env['BUILT_SERVER']) { - (0, codesign_1.printBanner)('Package server'); - const serverArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}.zip`; - await (0, zx_1.$) `7z.exe a -tzip ${serverArchivePath} ../vscode-server-win32-${arch}`.pipe(process.stdout); - await (0, zx_1.$) `7z.exe l ${serverArchivePath}`.pipe(process.stdout); - } - // Package server (web) - if (process.env['BUILT_WEB']) { - (0, codesign_1.printBanner)('Package server (web)'); - const webArchivePath = `.build/win32-${arch}/vscode-server-win32-${arch}-web.zip`; - await (0, zx_1.$) `7z.exe a -tzip ${webArchivePath} ../vscode-server-win32-${arch}-web`.pipe(process.stdout); - await (0, zx_1.$) `7z.exe l ${webArchivePath}`.pipe(process.stdout); - } - // Sign setup - if (process.env['BUILT_CLIENT']) { - (0, codesign_1.printBanner)('Sign setup packages (system, user)'); - const task = (0, zx_1.$) `npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; - await (0, codesign_1.streamProcessOutputAndCheckResult)('Sign setup packages (system, user)', task); - } -} -main().then(() => { - process.exit(0); -}, err => { - console.error(`ERROR: ${err}`); - process.exit(1); -}); -//# sourceMappingURL=codesign.js.map \ No newline at end of file diff --git a/build/azure-pipelines/win32/codesign.ts b/build/azure-pipelines/win32/codesign.ts index 7e7170709b5..dce5e55b840 100644 --- a/build/azure-pipelines/win32/codesign.ts +++ b/build/azure-pipelines/win32/codesign.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { $, usePwsh } from 'zx'; -import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign'; -import { e } from '../common/publish'; +import { printBanner, spawnCodesignProcess, streamProcessOutputAndCheckResult } from '../common/codesign.ts'; +import { e } from '../common/publish.ts'; async function main() { usePwsh(); @@ -20,7 +20,7 @@ async function main() { // 3. Codesign context menu appx package (insiders only) const codesignTask1 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows', codeSigningFolderPath, '*.dll,*.exe,*.node'); const codesignTask2 = spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.ps1'); - const codesignTask3 = process.env['VSCODE_QUALITY'] === 'insider' + const codesignTask3 = process.env['VSCODE_QUALITY'] !== 'exploration' ? spawnCodesignProcess(esrpCliDLLPath, 'sign-windows-appx', codeSigningFolderPath, '*.appx') : undefined; @@ -43,11 +43,8 @@ async function main() { // Package client if (process.env['BUILT_CLIENT']) { - // Product version - const version = await $`node -p "require('../VSCode-win32-${arch}/resources/app/package.json').version"`; - printBanner('Package client'); - const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}-${version}.zip`; + const clientArchivePath = `.build/win32-${arch}/VSCode-win32-${arch}.zip`; await $`7z.exe a -tzip ${clientArchivePath} ../VSCode-win32-${arch}/* "-xr!CodeSignSummary*.md"`.pipe(process.stdout); await $`7z.exe l ${clientArchivePath}`.pipe(process.stdout); } @@ -71,7 +68,7 @@ async function main() { // Sign setup if (process.env['BUILT_CLIENT']) { printBanner('Sign setup packages (system, user)'); - const task = $`npm exec -- npm-run-all -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; + const task = $`npm exec -- npm-run-all2 -lp "gulp vscode-win32-${arch}-system-setup -- --sign" "gulp vscode-win32-${arch}-user-setup -- --sign"`; await streamProcessOutputAndCheckResult('Sign setup packages (system, user)', task); } } 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 2b6fe1439b9..fa1328d99e2 100644 --- a/build/azure-pipelines/win32/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/product-build-win32-cli-sign.yml @@ -42,7 +42,7 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY build + - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY build condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry diff --git a/build/azure-pipelines/win32/product-build-win32-node-modules.yml b/build/azure-pipelines/win32/product-build-win32-node-modules.yml index ba30f67ee33..6780073f57a 100644 --- a/build/azure-pipelines/win32/product-build-win32-node-modules.yml +++ b/build/azure-pipelines/win32/product-build-win32-node-modules.yml @@ -33,13 +33,13 @@ jobs: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY + - powershell: node build/setup-npm-registry.ts $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 + node build/azure-pipelines/common/computeNodeModulesCacheKey.ts win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -81,14 +81,14 @@ jobs: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - powershell: node build/azure-pipelines/distro/mixin-npm + - powershell: node build/azure-pipelines/distro/mixin-npm.ts 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 { node build/azure-pipelines/common/listNodeModules.ts .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')) diff --git a/build/azure-pipelines/win32/sdl-scan-win32.yml b/build/azure-pipelines/win32/sdl-scan-win32.yml index dba656eff53..e3356effa95 100644 --- a/build/azure-pipelines/win32/sdl-scan-win32.yml +++ b/build/azure-pipelines/win32/sdl-scan-win32.yml @@ -26,7 +26,7 @@ steps: KeyVaultName: vscode-build-secrets SecretsFilter: "github-distro-mixin-password" - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY + - powershell: node build/setup-npm-registry.ts $env:NPM_REGISTRY condition: and(succeeded(), ne(variables['NPM_REGISTRY'], 'none')) displayName: Setup NPM Registry @@ -92,10 +92,10 @@ steps: retryCountOnTaskFailure: 5 displayName: Install dependencies - - script: node build/azure-pipelines/distro/mixin-npm + - script: node build/azure-pipelines/distro/mixin-npm.ts displayName: Mixin distro node modules - - script: node build/azure-pipelines/distro/mixin-quality + - script: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality env: VSCODE_QUALITY: ${{ parameters.VSCODE_QUALITY }} @@ -115,6 +115,16 @@ steps: Get-ChildItem '$(Agent.BuildDirectory)\scanbin' -Recurse -Filter "*.pdb" displayName: List files + - task: PublishSymbols@2 + displayName: 'Publish Symbols to Artifacts' + inputs: + SymbolsFolder: '$(Agent.BuildDirectory)\scanbin' + SearchPattern: '**/*.pdb' + IndexSources: false + PublishSymbols: true + SymbolServerType: 'TeamServices' + SymbolsProduct: 'vscode-client' + - task: CopyFiles@2 displayName: 'Collect Symbols for API Scan' inputs: diff --git a/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml b/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml index e75581bea77..0caba3d1a2b 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-cli-sign.yml @@ -41,7 +41,7 @@ steps: archiveFilePatterns: $(Build.BinariesDirectory)/pkg/${{ target }}/*.zip destinationFolder: $(Build.BinariesDirectory)/sign/${{ target }} - - powershell: node build\azure-pipelines\common\sign $env:EsrpCliDllPath sign-windows $(Build.BinariesDirectory)/sign "*.exe" + - powershell: node build\azure-pipelines\common\sign.ts $env:EsrpCliDllPath sign-windows $(Build.BinariesDirectory)/sign "*.exe" env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign diff --git a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml index bdc807fdae5..d6412c23420 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-compile.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-compile.yml @@ -49,13 +49,13 @@ steps: archiveFilePatterns: "$(Build.ArtifactStagingDirectory)/compilation.tar.gz" cleanDestinationFolder: false - - powershell: node build/setup-npm-registry.js $env:NPM_REGISTRY + - powershell: node build/setup-npm-registry.ts $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 + node build/azure-pipelines/common/computeNodeModulesCacheKey.ts win32 $(VSCODE_ARCH) $(node -p process.arch) > .build/packagelockhash displayName: Prepare node_modules cache key - task: Cache@2 @@ -101,31 +101,33 @@ steps: displayName: Install dependencies condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) - - powershell: node build/azure-pipelines/distro/mixin-npm + - powershell: node build/azure-pipelines/distro/mixin-npm.ts 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 { node build/azure-pipelines/common/listNodeModules.ts .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 + - powershell: node build/azure-pipelines/distro/mixin-quality.ts displayName: Mixin distro quality - template: ../../common/install-builtin-extensions.yml@self - ${{ if ne(parameters.VSCODE_CIBUILD, true) }}: - - powershell: node build\lib\policies\policyGenerator build\lib\policies\policyData.jsonc win32 + - powershell: | + npm run copy-policy-dto --prefix build + node build\lib\policies\policyGenerator.ts build\lib\policies\policyData.jsonc 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 + - powershell: node build/win32/explorer-dll-fetcher.ts .build/win32/appx displayName: Download Explorer dll - powershell: | @@ -173,6 +175,7 @@ steps: 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" + echo "##vso[task.setvariable variable=CodeSigningFolderPath]$(CodeSigningFolderPath),$(Agent.BuildDirectory)/vscode-server-win32-$(VSCODE_ARCH)-web" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Build server (web) @@ -190,7 +193,8 @@ steps: $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 + $ProductJsonPath = (Get-ChildItem -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)" -Name "product.json" -Recurse | Select-Object -First 1) + $AppProductJson = Get-Content -Raw -Path "$(Agent.BuildDirectory)\VSCode-win32-$(VSCODE_ARCH)\$ProductJsonPath" | 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" @@ -223,7 +227,7 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.js } + exec { npx deemon --detach --wait -- npx zx build/azure-pipelines/win32/codesign.ts } env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) displayName: ✍️ Codesign @@ -240,14 +244,15 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.js } + exec { npx deemon --attach -- npx zx build/azure-pipelines/win32/codesign.ts } condition: succeededOrFailed() displayName: "✍️ Post-job: Codesign" - powershell: | $ErrorActionPreference = "Stop" - $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\resources\app\package.json | ConvertFrom-Json + $PackageJsonPath = (Get-ChildItem -Path "..\VSCode-win32-$(VSCODE_ARCH)" -Name "package.json" -Recurse | Select-Object -First 1) + $PackageJson = Get-Content -Raw -Path ..\VSCode-win32-$(VSCODE_ARCH)\$PackageJsonPath | ConvertFrom-Json $Version = $PackageJson.version mkdir $(Build.ArtifactStagingDirectory)\out\system-setup -Force @@ -257,7 +262,7 @@ steps: mv .build\win32-$(VSCODE_ARCH)\user-setup\VSCodeSetup.exe $(Build.ArtifactStagingDirectory)\out\user-setup\VSCodeUserSetup-$(VSCODE_ARCH)-$Version.exe mkdir $(Build.ArtifactStagingDirectory)\out\archive -Force - mv .build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH)-$Version.zip $(Build.ArtifactStagingDirectory)\out\archive\VSCode-win32-$(VSCODE_ARCH)-$Version.zip + mv .build\win32-$(VSCODE_ARCH)\VSCode-win32-$(VSCODE_ARCH).zip $(Build.ArtifactStagingDirectory)\out\archive\VSCode-win32-$(VSCODE_ARCH)-$Version.zip mkdir $(Build.ArtifactStagingDirectory)\out\server -Force mv .build\win32-$(VSCODE_ARCH)\vscode-server-win32-$(VSCODE_ARCH).zip $(Build.ArtifactStagingDirectory)\out\server\vscode-server-win32-$(VSCODE_ARCH).zip diff --git a/build/azure-pipelines/win32/steps/product-build-win32-test.yml b/build/azure-pipelines/win32/steps/product-build-win32-test.yml index 034cbb8f44b..f8b4eb57440 100644 --- a/build/azure-pipelines/win32/steps/product-build-win32-test.yml +++ b/build/azure-pipelines/win32/steps/product-build-win32-test.yml @@ -9,7 +9,7 @@ parameters: type: boolean steps: - - powershell: npm exec -- npm-run-all -lp "electron $(VSCODE_ARCH)" "playwright-install" + - powershell: npm exec -- npm-run-all2 -lp "electron $(VSCODE_ARCH)" "playwright-install" env: GITHUB_TOKEN: "$(github-distro-mixin-password)" displayName: Download Electron and Playwright @@ -76,7 +76,8 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $AppRoot = "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $ProductJsonPath = (Get-ChildItem -Path "$AppRoot" -Name "product.json" -Recurse | Select-Object -First 1) + $AppProductJson = Get-Content -Raw -Path "$AppRoot\$ProductJsonPath" | ConvertFrom-Json $AppNameShort = $AppProductJson.nameShort $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)" @@ -98,7 +99,8 @@ steps: . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" $AppRoot = "$(agent.builddirectory)\test\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $ProductJsonPath = (Get-ChildItem -Path "$AppRoot" -Name "product.json" -Recurse | Select-Object -First 1) + $AppProductJson = Get-Content -Raw -Path "$AppRoot\$ProductJsonPath" | ConvertFrom-Json $AppNameShort = $AppProductJson.nameShort $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe" $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\test\vscode-server-win32-$(VSCODE_ARCH)" diff --git a/build/buildfile.js b/build/buildfile.js deleted file mode 100644 index 83f84563275..00000000000 --- a/build/buildfile.js +++ /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. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -/** - * @param {string} name - * @returns {import('./lib/bundle').IEntryPoint} - */ -function createModuleDescription(name) { - return { - name - }; -} - -exports.workerEditor = createModuleDescription('vs/editor/common/services/editorWebWorkerMain'); -exports.workerExtensionHost = createModuleDescription('vs/workbench/api/worker/extensionHostWorkerMain'); -exports.workerNotebook = createModuleDescription('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain'); -exports.workerLanguageDetection = createModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain'); -exports.workerLocalFileSearch = createModuleDescription('vs/workbench/services/search/worker/localFileSearchMain'); -exports.workerProfileAnalysis = createModuleDescription('vs/platform/profiling/electron-browser/profileAnalysisWorkerMain'); -exports.workerOutputLinks = createModuleDescription('vs/workbench/contrib/output/common/outputLinkComputerMain'); -exports.workerBackgroundTokenization = createModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain'); - -exports.workbenchDesktop = [ - createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), - createModuleDescription('vs/platform/files/node/watcher/watcherMain'), - createModuleDescription('vs/platform/terminal/node/ptyHostMain'), - createModuleDescription('vs/workbench/api/node/extensionHostProcess'), - createModuleDescription('vs/workbench/workbench.desktop.main') -]; - -exports.workbenchWeb = createModuleDescription('vs/workbench/workbench.web.main'); - -exports.keyboardMaps = [ - createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'), - createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'), - createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win') -]; - -exports.code = [ - // 'vs/code/electron-main/main' is not included here because it comes in via ./src/main.js - // 'vs/code/node/cli' is not included here because it comes in via ./src/cli.js - createModuleDescription('vs/code/node/cliProcessMain'), - createModuleDescription('vs/code/electron-utility/sharedProcess/sharedProcessMain'), - createModuleDescription('vs/code/electron-browser/workbench/workbench'), -]; - -exports.codeWeb = createModuleDescription('vs/code/browser/workbench/workbench'); - -exports.codeServer = [ - // 'vs/server/node/server.main' is not included here because it gets inlined via ./src/server-main.js - // 'vs/server/node/server.cli' is not included here because it gets inlined via ./src/server-cli.js - createModuleDescription('vs/workbench/api/node/extensionHostProcess'), - createModuleDescription('vs/platform/files/node/watcher/watcherMain'), - createModuleDescription('vs/platform/terminal/node/ptyHostMain') -]; - -exports.entrypoint = createModuleDescription; diff --git a/build/buildfile.ts b/build/buildfile.ts new file mode 100644 index 00000000000..168539f4cae --- /dev/null +++ b/build/buildfile.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IEntryPoint } from './lib/bundle.ts'; + +function createModuleDescription(name: string): IEntryPoint { + return { + name + }; +} + +export const workerEditor = createModuleDescription('vs/editor/common/services/editorWebWorkerMain'); +export const workerExtensionHost = createModuleDescription('vs/workbench/api/worker/extensionHostWorkerMain'); +export const workerNotebook = createModuleDescription('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain'); +export const workerLanguageDetection = createModuleDescription('vs/workbench/services/languageDetection/browser/languageDetectionWebWorkerMain'); +export const workerLocalFileSearch = createModuleDescription('vs/workbench/services/search/worker/localFileSearchMain'); +export const workerProfileAnalysis = createModuleDescription('vs/platform/profiling/electron-browser/profileAnalysisWorkerMain'); +export const workerOutputLinks = createModuleDescription('vs/workbench/contrib/output/common/outputLinkComputerMain'); +export const workerBackgroundTokenization = createModuleDescription('vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.workerMain'); + +export const workbenchDesktop = [ + createModuleDescription('vs/workbench/contrib/debug/node/telemetryApp'), + createModuleDescription('vs/platform/files/node/watcher/watcherMain'), + createModuleDescription('vs/platform/terminal/node/ptyHostMain'), + createModuleDescription('vs/workbench/api/node/extensionHostProcess'), + createModuleDescription('vs/workbench/workbench.desktop.main') +]; + +export const workbenchWeb = createModuleDescription('vs/workbench/workbench.web.main.internal'); + +export const keyboardMaps = [ + createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.linux'), + createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.darwin'), + createModuleDescription('vs/workbench/services/keybinding/browser/keyboardLayouts/layout.contribution.win') +]; + +export const code = [ + // 'vs/code/electron-main/main' is not included here because it comes in via ./src/main.js + // 'vs/code/node/cli' is not included here because it comes in via ./src/cli.js + createModuleDescription('vs/code/node/cliProcessMain'), + createModuleDescription('vs/code/electron-utility/sharedProcess/sharedProcessMain'), + createModuleDescription('vs/code/electron-browser/workbench/workbench'), +]; + +export const codeWeb = createModuleDescription('vs/code/browser/workbench/workbench'); + +export const codeServer = [ + // 'vs/server/node/server.main' is not included here because it gets inlined via ./src/server-main.js + // 'vs/server/node/server.cli' is not included here because it gets inlined via ./src/server-cli.js + createModuleDescription('vs/workbench/api/node/extensionHostProcess'), + createModuleDescription('vs/platform/files/node/watcher/watcherMain'), + createModuleDescription('vs/platform/terminal/node/ptyHostMain') +]; + +export const entrypoint = createModuleDescription; + +const buildfile = { + workerEditor, + workerExtensionHost, + workerNotebook, + workerLanguageDetection, + workerLocalFileSearch, + workerProfileAnalysis, + workerOutputLinks, + workerBackgroundTokenization, + workbenchDesktop, + workbenchWeb, + keyboardMaps, + code, + codeWeb, + codeServer, + entrypoint: createModuleDescription +}; + +export default buildfile; diff --git a/build/checker/layersChecker.js b/build/checker/layersChecker.js deleted file mode 100644 index ae84e8ffeb9..00000000000 --- a/build/checker/layersChecker.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const typescript_1 = __importDefault(require("typescript")); -const fs_1 = require("fs"); -const path_1 = require("path"); -const minimatch_1 = require("minimatch"); -// -// ############################################################################################# -// -// A custom typescript checker for the specific task of detecting the use of certain types in a -// layer that does not allow such use. -// -// Make changes to below RULES to lift certain files from these checks only if absolutely needed -// -// NOTE: Most layer checks are done via tsconfig..json files. -// -// ############################################################################################# -// -// Types that are defined in a common layer but are known to be only -// available in native environments should not be allowed in browser -const NATIVE_TYPES = [ - 'NativeParsedArgs', - 'INativeEnvironmentService', - 'AbstractNativeEnvironmentService', - 'INativeWindowConfiguration', - 'ICommonNativeHostService', - 'INativeHostService', - 'IMainProcessService', - 'INativeBrowserElementsService', -]; -const RULES = [ - // Tests: skip - { - target: '**/vs/**/test/**', - skip: true // -> skip all test files - }, - // Common: vs/platform services that can access native types - { - target: `**/vs/platform/{${[ - 'environment/common/*.ts', - 'window/common/window.ts', - 'native/common/native.ts', - 'native/common/nativeHostService.ts', - 'browserElements/common/browserElements.ts', - 'browserElements/common/nativeBrowserElementsService.ts' - ].join(',')}}`, - disallowedTypes: [ /* Ignore native types that are defined from here */ /* Ignore native types that are defined from here */], - }, - // Common: vs/base/parts/sandbox/electron-browser/preload{,-aux}.ts - { - target: '**/vs/base/parts/sandbox/electron-browser/preload{,-aux}.ts', - disallowedTypes: NATIVE_TYPES, - }, - // Common - { - target: '**/vs/**/common/**', - disallowedTypes: NATIVE_TYPES, - }, - // Common - { - target: '**/vs/**/worker/**', - disallowedTypes: NATIVE_TYPES, - }, - // Browser - { - target: '**/vs/**/browser/**', - disallowedTypes: NATIVE_TYPES, - }, - // Electron (main, utility) - { - target: '**/vs/**/{electron-main,electron-utility}/**', - disallowedTypes: [ - 'ipcMain' // not allowed, use validatedIpcMain instead - ] - } -]; -const TS_CONFIG_PATH = (0, path_1.join)(__dirname, '../../', 'src', 'tsconfig.json'); -let hasErrors = false; -function checkFile(program, sourceFile, rule) { - checkNode(sourceFile); - function checkNode(node) { - if (node.kind !== typescript_1.default.SyntaxKind.Identifier) { - return typescript_1.default.forEachChild(node, checkNode); // recurse down - } - const checker = program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - if (!symbol) { - return; - } - let text = symbol.getName(); - let _parentSymbol = symbol; - while (_parentSymbol.parent) { - _parentSymbol = _parentSymbol.parent; - } - const parentSymbol = _parentSymbol; - text = parentSymbol.getName(); - if (rule.disallowedTypes?.some(disallowed => disallowed === text)) { - const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); - console.log(`[build/checker/layersChecker.ts]: Reference to type '${text}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1}). Learn more about our source code organization at https://github.com/microsoft/vscode/wiki/Source-Code-Organization.`); - hasErrors = true; - return; - } - } -} -function createProgram(tsconfigPath) { - const tsConfig = typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile); - const configHostParser = { fileExists: fs_1.existsSync, readDirectory: typescript_1.default.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; - const tsConfigParsed = typescript_1.default.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); - const compilerHost = typescript_1.default.createCompilerHost(tsConfigParsed.options, true); - return typescript_1.default.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); -} -// -// Create program and start checking -// -const program = createProgram(TS_CONFIG_PATH); -for (const sourceFile of program.getSourceFiles()) { - for (const rule of RULES) { - if ((0, minimatch_1.match)([sourceFile.fileName], rule.target).length > 0) { - if (!rule.skip) { - checkFile(program, sourceFile, rule); - } - break; - } - } -} -if (hasErrors) { - process.exit(1); -} -//# sourceMappingURL=layersChecker.js.map \ No newline at end of file diff --git a/build/checker/layersChecker.ts b/build/checker/layersChecker.ts index 68e12e61c40..87341dcffd0 100644 --- a/build/checker/layersChecker.ts +++ b/build/checker/layersChecker.ts @@ -6,7 +6,7 @@ import ts from 'typescript'; import { readFileSync, existsSync } from 'fs'; import { resolve, dirname, join } from 'path'; -import { match } from 'minimatch'; +import minimatch from 'minimatch'; // // ############################################################################################# @@ -88,7 +88,7 @@ const RULES: IRule[] = [ } ]; -const TS_CONFIG_PATH = join(__dirname, '../../', 'src', 'tsconfig.json'); +const TS_CONFIG_PATH = join(import.meta.dirname, '../../', 'src', 'tsconfig.json'); interface IRule { target: string; @@ -151,7 +151,7 @@ const program = createProgram(TS_CONFIG_PATH); for (const sourceFile of program.getSourceFiles()) { for (const rule of RULES) { - if (match([sourceFile.fileName], rule.target).length > 0) { + if (minimatch.match([sourceFile.fileName], rule.target).length > 0) { if (!rule.skip) { checkFile(program, sourceFile, rule); } diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index ab1540038a3..4cebc1a16bd 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,75 +1,75 @@ -766c6904e9825e3342a28ddd0210204d42e40377d2ab7ee17f5f954fbebab8ae *chromedriver-v37.7.0-darwin-arm64.zip -4b24cf1e4c00ef73411c4b715a7c1198d0186d5dcac7a64137759fd987836931 *chromedriver-v37.7.0-darwin-x64.zip -d3635fdbd2a09e23fa9be99e954bd910e8cd2a06265d011b0ed42c9f947a4572 *chromedriver-v37.7.0-linux-arm64.zip -aca613941e5412ea016307a9e40c4b78e8caacc0451fb83166b67ed9da617a91 *chromedriver-v37.7.0-linux-armv7l.zip -22570d8f0f89f22b6175bba8ead9421ffe7316829a08d4b987d7c3adb4c2f153 *chromedriver-v37.7.0-linux-x64.zip -2a4a7f0a43cd611db609b8217687083cf7d88f0e10ab24116a205169b05d75c4 *chromedriver-v37.7.0-mas-arm64.zip -1cd0ec52b319b43ca08f5db0ecb4848b35b2babf1187757aea2904e3bc217dd1 *chromedriver-v37.7.0-mas-x64.zip -d7ee42b443c9b83efaaf0a75b3e5b50cdb3cb04540a57a8398babcfffeaadc2f *chromedriver-v37.7.0-win32-arm64.zip -55199f114621ecbb2365fe0e22b9b188bc013c2c0d7ff66f43f814ebebe38739 *chromedriver-v37.7.0-win32-ia32.zip -18c5e32bfd075a9b497c7e514a83dc18257cbc5119530c0372d61451138bdc78 *chromedriver-v37.7.0-win32-x64.zip -a6d46dfbbd5a7e0c31272eabe1068e8570f18df1559209e8ec239d0eeb0ee38f *electron-api.json -d05484feef95627bc407f1ef6c86dc7076c568f763afd162c718c65e736645e2 *electron-v37.7.0-darwin-arm64-dsym-snapshot.zip -59cc95edb7599fb24b2cc65b9313583465faeeb95a8822cb74b170ffe43d9aee *electron-v37.7.0-darwin-arm64-dsym.zip -e09febc22a7635fc2ca7de0bbeefb5aaba9fe91e6e15da3cb90ec12a2a0cd822 *electron-v37.7.0-darwin-arm64-symbols.zip -c962552e6de47f5eebf1c2932f21d66d556c351d90d70ccbae68a9d22ac17ba9 *electron-v37.7.0-darwin-arm64.zip -c083a859932a2a070cfc0c110f03c7573c1f83f6aef624aacac34fe16c0fb0c9 *electron-v37.7.0-darwin-x64-dsym-snapshot.zip -43758f15ef737505edc544aca5ae4a06636930ca5d95a32030d1c742acc07141 *electron-v37.7.0-darwin-x64-dsym.zip -c65b761a0481ee97c75f62add721b3427d0bde61c5545ebfd3a059b11cb8055a *electron-v37.7.0-darwin-x64-symbols.zip -4aebc43ef4de09086bf4487d1bc491de27f6aa1a2e8dd32622e8bf1deaf9a1ae *electron-v37.7.0-darwin-x64.zip -690855692f644997420b08b79cfa4a2b852f705982f754afb32877e55642b58a *electron-v37.7.0-linux-arm64-debug.zip -6cceaeabd2e7517d8562da6fc9a17a73fc0be5b0bb05b3732ff5b5ec2a08745e *electron-v37.7.0-linux-arm64-symbols.zip -c72c2e963dcdea65d500b23fff3f22806a2518a86b52236dceb825a1d194cd7e *electron-v37.7.0-linux-arm64.zip -eb3303f9d335e5bd518f91ceee7832348ed2943fecf965ab4312ac40e91644f1 *electron-v37.7.0-linux-armv7l-debug.zip -839b0c8c4c637aeb968fdd85cdfedf7e83398aa756724b83ea482b65f8f80e83 *electron-v37.7.0-linux-armv7l-symbols.zip -eecc89703c24b019fad14e3e8341a6e2bcd995c473c9c5e56bf1a4664d8630f7 *electron-v37.7.0-linux-armv7l.zip -42cb0ba3e380f6a10ab6ec56e9e9a2f55c7f71621daf605e94a0eba21c3c9f6c *electron-v37.7.0-linux-x64-debug.zip -03c3761ea84447022f5acb171f34b0129b44c3b54d8882addb67cac3572e1368 *electron-v37.7.0-linux-x64-symbols.zip -4ae04d20d0ea25bf3451e3d0eaa757190b5813fa58d17bbe3be4332836da6b25 *electron-v37.7.0-linux-x64.zip -23bf51bc222cf1384594ac2dba294899e0170f5695125b156b0d5c213eb81af6 *electron-v37.7.0-mas-arm64-dsym-snapshot.zip -a0799acdf988e48c45778c5a797bcece64531c2c0070ab14b066b477df52e837 *electron-v37.7.0-mas-arm64-dsym.zip -dcd1be8cf58bc07813e34d93903e8bf1f268ff02d1214b01d02298e6d83f01b2 *electron-v37.7.0-mas-arm64-symbols.zip -da5d2aaac129d90f36e65d091b630528ac0aff81ca0b9895089878c02a59d8fb *electron-v37.7.0-mas-arm64.zip -a4f489fe0aec2ab13605189ba80ca49042d11249a00c79a6d5456689288e3479 *electron-v37.7.0-mas-x64-dsym-snapshot.zip -1051763877e03d0d80fe5af3f6fdd50bf158f25c7d058a4755ca83fc036f9f69 *electron-v37.7.0-mas-x64-dsym.zip -95fe16c43a57ae8be98d4bb4dfce4b2c3e2f6c6ed1415ca757a1ee15727e476f *electron-v37.7.0-mas-x64-symbols.zip -8e59bf3fa4704a01cdeb3b38ec3beb4de118743af6974b41689b567f80c4939e *electron-v37.7.0-mas-x64.zip -52b7825a8fc4529f622665d46f9cceeacf50a7d75659c38b99c84a35d5d08f41 *electron-v37.7.0-win32-arm64-pdb.zip -9c9c6ce62bd1974f2bd703106fac45127270fcfc629b233572692b3870fcd733 *electron-v37.7.0-win32-arm64-symbols.zip -74e88ea46bb62f4d8698b1058963568b8ccd9debbdd5d755dfdf4303874446d5 *electron-v37.7.0-win32-arm64-toolchain-profile.zip -875ff30aa3148665fc028abb762cf265e5f0a79ed98d1cceec2441afa17b76ea *electron-v37.7.0-win32-arm64.zip -744ead04becabbceaef15a80f6e45539472f20ffb1651c9238a68daed3d4587d *electron-v37.7.0-win32-ia32-pdb.zip -dc8ab512983cecf68d7662fc05c20d46c73646996f2a8b1b53642a27b3b7ebb7 *electron-v37.7.0-win32-ia32-symbols.zip -74e88ea46bb62f4d8698b1058963568b8ccd9debbdd5d755dfdf4303874446d5 *electron-v37.7.0-win32-ia32-toolchain-profile.zip -d79cf6cc733691bce26b4c8830bc927ce028fbf6174aa17041559f5f71d69452 *electron-v37.7.0-win32-ia32.zip -4313294bf3de78ef12b3948a32990d6b2c5ce270f3ba7b6d81c582c02d4127e1 *electron-v37.7.0-win32-x64-pdb.zip -8f3ea7630b0945d2d26aec9c2236560050ea46d61f225ffeed25c5381a131aef *electron-v37.7.0-win32-x64-symbols.zip -74e88ea46bb62f4d8698b1058963568b8ccd9debbdd5d755dfdf4303874446d5 *electron-v37.7.0-win32-x64-toolchain-profile.zip -875cea08076dfa433189aa7e82263cff7f0aa3795a69172baeec4a85fb57bc05 *electron-v37.7.0-win32-x64.zip -a136e010d8757d8a61f06ba9d8fbd3ad057ab04d7d386ff3b0e1ba56ec4a5b64 *electron.d.ts -39d5a5663e491f4e5e4a60ded8d6361a8f4d905a220aa681adfabac1fa90d06f *ffmpeg-v37.7.0-darwin-arm64.zip -210e095fc7c629b411caf90f00958aa004ac33f2f6dd1291780a670c46f028cf *ffmpeg-v37.7.0-darwin-x64.zip -f0792bdd28ac2231e2d10bdb89da0221e9b15149a4761448e6bfd50ba8e76895 *ffmpeg-v37.7.0-linux-arm64.zip -5bd4adf23596c09bbb671952b73427f6701a7e9aee647979925e9cc4ff973045 *ffmpeg-v37.7.0-linux-armv7l.zip -561a7685536c133d2072e2e2b5a64ca3897bb8c71624158a6fe8e07cae9116c9 *ffmpeg-v37.7.0-linux-x64.zip -39d5a5663e491f4e5e4a60ded8d6361a8f4d905a220aa681adfabac1fa90d06f *ffmpeg-v37.7.0-mas-arm64.zip -210e095fc7c629b411caf90f00958aa004ac33f2f6dd1291780a670c46f028cf *ffmpeg-v37.7.0-mas-x64.zip -cb73c4eb1c68b7c1bc8d6962165a4f9f26a92560770d33b27945df2e778a5c37 *ffmpeg-v37.7.0-win32-arm64.zip -0663b6c70171b7abe2cf47a5d0f102da6f10fda69744ec6bc96bc32fde253cbd *ffmpeg-v37.7.0-win32-ia32.zip -9fa33053350c6c9d4420739f82083895dbb4a1d2904a1965ee94a83ab9507a3c *ffmpeg-v37.7.0-win32-x64.zip -bd6cbcc9cb6d9fc4b219b42104cfeaa69260cc7830234150056f0a929358681c *hunspell_dictionaries.zip -7032d6827a0bfe2f382742591241feb238c4fba5ee5db325987f55f56d1ac1e2 *libcxx-objects-v37.7.0-linux-arm64.zip -9a1beec72b821269bdab123025eb7ba54f31e2875b1cd97198952544bfb40ddd *libcxx-objects-v37.7.0-linux-armv7l.zip -f756deeb30afecfa2753ea7cd24e10bb922e3d64d40da5b64189a7d196815330 *libcxx-objects-v37.7.0-linux-x64.zip -1a664a8739a67b1b445ad11da1770d18ecb24a3038a70c2356ed653605175a19 *libcxx_headers.zip -c46a23338c31d465ddb4005e870b955da8886146e5ee92b2bab1c8bccf876a85 *libcxxabi_headers.zip -94e9f968e8de57aebfe0933db9a57dd8645690d8adf731fe2c4eb6b67a2b6534 *mksnapshot-v37.7.0-darwin-arm64.zip -868bd0900566cc32b3414ffe8dd5c229fb0e4953ccc8832c23eb3645c70e5a72 *mksnapshot-v37.7.0-darwin-x64.zip -69e896e2600368244f8f14f2b35857b2c571aabbffbb2277479a709852bba4a2 *mksnapshot-v37.7.0-linux-arm64-x64.zip -dc447c6a9b13ca8b7cf63ad9da9007b19398b6e234bc4b9538bb9e541dd4b57f *mksnapshot-v37.7.0-linux-armv7l-x64.zip -4cb3fe3e176173002b6085b8a87eb51bb6bdefd04ff4ff67a5aba26fce94655a *mksnapshot-v37.7.0-linux-x64.zip -5eef6889b798793eff86b9a5496c9b0ade49266ef885941817bf4520d6e48783 *mksnapshot-v37.7.0-mas-arm64.zip -22d7f4ce4eb6399fc5dc6e6468ed6c27ce6b9f2d434cab45d4646ec7e710589c *mksnapshot-v37.7.0-mas-x64.zip -8185f50df97b2f894560b739889a9d6c219cfa53ef64315fca23481e810edfc5 *mksnapshot-v37.7.0-win32-arm64-x64.zip -51679e701842079ac04fce0c52d3531c13fd4a42fa0cc481ae2a0f8310605ad9 *mksnapshot-v37.7.0-win32-ia32.zip -8415c984500f66c69b195fb31606e29c07e92544e2ed3a9093993f47cb5cb15d *mksnapshot-v37.7.0-win32-x64.zip +3d5ef8b78ce4f35320a76a241bbc67f7a1922cedbfe338de111e5fd617677ca8 *chromedriver-v39.3.0-darwin-arm64.zip +ed1951ecc55949c65452e1f8cd29b2348c8fd7932c2b1e11279987525b4658bc *chromedriver-v39.3.0-darwin-x64.zip +e0f1fa67b9f1ba8f15b2fc0aa0570e80d68261286a19862638a4f26c6966ecf2 *chromedriver-v39.3.0-linux-arm64.zip +8a4f46da6a51c97e3296c13aa480a7714e7bd5e8fa46876526ece89c8ae5b182 *chromedriver-v39.3.0-linux-armv7l.zip +44e15ac6c421e7bc7b36c55721085e5ccd4996c0770785dc78c55baaf4f73322 *chromedriver-v39.3.0-linux-x64.zip +9cef14d9e758f697a4fc32047ae115ac3b43674075eade59c6e57d655f3d3850 *chromedriver-v39.3.0-mas-arm64.zip +7de66aecd14699f331604820742f61201759d7d144b03254de420d8186fc098d *chromedriver-v39.3.0-mas-x64.zip +c5821d83fe3c301399c7d8363fdda006fdb708c1ba382f294580f70612c4299e *chromedriver-v39.3.0-win32-arm64.zip +21b810154777168f9dc571b68f4682ccf5c1ae20192b625ca1b25a9d84b00bf5 *chromedriver-v39.3.0-win32-ia32.zip +232b2d1ebfcf7c4a9e2ad0f6d2cce189ef123d2bc875ee761f819658d0f56a57 *chromedriver-v39.3.0-win32-x64.zip +49337014dfd1da81d62b4ee2a111f552a74795357cf10ba62c563fcba311523e *electron-api.json +9486acdcf73a0d0e0655abb8aed6d5abd9685c15096184ecf6c3aa95d1aca9e6 *electron-v39.3.0-darwin-arm64-dsym-snapshot.zip +e536221ba1d408173676a33b622dc3606c1db8d1d0bb4d30e1effbb2860cb7c2 *electron-v39.3.0-darwin-arm64-dsym.zip +14cdbd45c3fe9649402bee3908b091075a0c89d9242544ce6a2d5e60714e97f9 *electron-v39.3.0-darwin-arm64-symbols.zip +064edf951e0ab546809e217a417401023fd7e3b662de8be0316e8173f6f3db6d *electron-v39.3.0-darwin-arm64.zip +451d63cee049b31a73c3fc221fac5d9007fd4566a09beb30b5f5c71d6e4dc9aa *electron-v39.3.0-darwin-x64-dsym-snapshot.zip +61def766f6ff4ccb826e534b45b8b011f40b9bc3c10a70e4efcfdfb28b44546b *electron-v39.3.0-darwin-x64-dsym.zip +6e8ef61fae54a3ac829dbdf193d2d8f98f87d07fe490e4fa0297328210a775a8 *electron-v39.3.0-darwin-x64-symbols.zip +484c7f39235ea6c2c87b2ce5149436daa4eec97c9a6b11dc662f01ec1b81969e *electron-v39.3.0-darwin-x64.zip +755f67294f8fe02af53f34cb2a0c9308675ecea21f7c07044bd9794212ae1e25 *electron-v39.3.0-linux-arm64-debug.zip +abc44b6fc71ea483a1ac40a6dc6647d13c096810d9efb4f0bded96c2374ef931 *electron-v39.3.0-linux-arm64-symbols.zip +468d3096630953a52fc051abd48714004b58ac550f9c7e798c256c774811b0f2 *electron-v39.3.0-linux-arm64.zip +0481b5cb89883a4964d8b69dcbfe028d2cc4334e286d3c1b50629fa68373b8e3 *electron-v39.3.0-linux-armv7l-debug.zip +a6250c680625e52c4f25b52f1c5e4927a736d7b628317b3981cc8f20db30abdf *electron-v39.3.0-linux-armv7l-symbols.zip +37a889a488e7a64d86961ad774f20e3cbb5dd965d76a6efd98c83488c33964e0 *electron-v39.3.0-linux-armv7l.zip +001f269f544a745df1f426891d3b46a9d4cfdf4c09564aab5a0d34ed447b2b86 *electron-v39.3.0-linux-x64-debug.zip +cc3eb13935c3defbb1e6a9f9b92d2ee5c28d7a4cd6095f7ffa7cc8f124682df1 *electron-v39.3.0-linux-x64-symbols.zip +a676357322bdf28153ba3ad67e8558dd54d76757fe2b1ad48c53f4a5e20614c6 *electron-v39.3.0-linux-x64.zip +04c05cac5e957787b4890a5c24e1c2028e84058a6e0a0cb2b1737793d6aeee9a *electron-v39.3.0-mas-arm64-dsym-snapshot.zip +b8cbf72450cf031a95d6157c3519371c02dbba13cabff2e07778c70eaed30edc *electron-v39.3.0-mas-arm64-dsym.zip +7a2080fa0a3f46495761dc8b1d428756b0f50e8db4ded7db162a2d6cee2f64a0 *electron-v39.3.0-mas-arm64-symbols.zip +c0c7a4454267bcefd94d002a78e846a9f5b50c85d8b5e32700cea03a7418d7b9 *electron-v39.3.0-mas-arm64.zip +ddf14a1f735b6404c5b67666d7d79f707fbd3192fa98c05eac2ac8044dbb30ac *electron-v39.3.0-mas-x64-dsym-snapshot.zip +c0cd9afa7e40e3faf5b2944025876fd6d6b40ecd4b72542e0a792cc5139e854d *electron-v39.3.0-mas-x64-dsym.zip +f30ded8f8392ce8369aa8b1abfa98bd07a6c40b5eb114ec68c25f653242c8f1a *electron-v39.3.0-mas-x64-symbols.zip +d38a8a0d21f4c97a41086ce8bee96b18f980e86585e942067e79f7bfcb26b57f *electron-v39.3.0-mas-x64.zip +d4f5cb793e19c2c1ce0753307e02ca5a9a61f8c00244a0f9c56651df29a10c9b *electron-v39.3.0-win32-arm64-pdb.zip +4683273c81fda42bba609dd240a252ce4c20bd99e56bad89c35f6bd46cdeb212 *electron-v39.3.0-win32-arm64-symbols.zip +ed66f333ff7b385b2f40845178dc2dc4f25cc887510d766433392733fdd272a3 *electron-v39.3.0-win32-arm64-toolchain-profile.zip +31fa0ab2c9a10654e9659badc838281ea9e403c08638cedd508dfa5c75b9ee04 *electron-v39.3.0-win32-arm64.zip +689dbac4dcbfbbf01469b31f213bdeadc5b3af44103ebf81a19167e93c912730 *electron-v39.3.0-win32-ia32-pdb.zip +6f7cdea2b2b0166d77149b458776e39880237a7d63f7f3cb9782aec86d50b3aa *electron-v39.3.0-win32-ia32-symbols.zip +ed66f333ff7b385b2f40845178dc2dc4f25cc887510d766433392733fdd272a3 *electron-v39.3.0-win32-ia32-toolchain-profile.zip +e8650adc82b2c32624428754d971f498c605de48c9aca74206f71506c3444c88 *electron-v39.3.0-win32-ia32.zip +cc4cb73f613408a760570fac04a04d9c941966ec07561ab8d61200af91a0a7fc *electron-v39.3.0-win32-x64-pdb.zip +ba837de8e030ed4aa3bff16bf4ac4aac02fc58872bbb1d690c3f525066ad27c0 *electron-v39.3.0-win32-x64-symbols.zip +ed66f333ff7b385b2f40845178dc2dc4f25cc887510d766433392733fdd272a3 *electron-v39.3.0-win32-x64-toolchain-profile.zip +6f7c3ddaba8d7d829f8ec259a8549c5e8f41d1b6d77bcb1c94ce5f4f85922158 *electron-v39.3.0-win32-x64.zip +d23b65eaafa9aaa3b452fddf2d1f3c060bef3c5f2171ec48c1d149e0589c6997 *electron.d.ts +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.3.0-darwin-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.3.0-darwin-x64.zip +52ae6eccbdb4a9403a6c3eb46b356a28940ec25958b6b9181fb2f38e612e40ed *ffmpeg-v39.3.0-linux-arm64.zip +622cb781fb1e3b9617e7e60c36384427f7b0d9b5ad888e9bc356a83b050e13f1 *ffmpeg-v39.3.0-linux-armv7l.zip +ba441851788008362f013bf2983b22b0042af8df31bf90123328f928cc067492 *ffmpeg-v39.3.0-linux-x64.zip +27cf8e375bc22ceea6b3d42132f2927ea544edac2b8b2c5dc3c10b5df8dfb027 *ffmpeg-v39.3.0-mas-arm64.zip +321d9c07f74c6cf77027ec07d888fb7b634d6589207e3c9e016c43e277ca9944 *ffmpeg-v39.3.0-mas-x64.zip +eeb6919ce2218d1ead2cb565aab1b192244e86d7444eee94c10fcd92074d0b9c *ffmpeg-v39.3.0-win32-arm64.zip +ab82eec0cb820ff0c29b80401dd168780cde80c00ed3ed0551910f94758b5ece *ffmpeg-v39.3.0-win32-ia32.zip +0eefb7877ed0205b9f7d82bd97d8dd50c1b25e8559af38fff263582801126ea6 *ffmpeg-v39.3.0-win32-x64.zip +ea022d26bd2a83c69e45c3d63a8658aeb707d6ba5a1cc57079f430ca6511c76f *hunspell_dictionaries.zip +8b334785a1c0bcfe011871af3346a5059e7ed3726d3f38900b2f8213fd31fd8f *libcxx-objects-v39.3.0-linux-arm64.zip +471b647164fa385a01b4fc8f49e965b8d3cc47e707eb0d7cafd4423b644cf0af *libcxx-objects-v39.3.0-linux-armv7l.zip +02d84914526debacae40d82c06f55655c7297cd925edf96de642cf9b8a4681e8 *libcxx-objects-v39.3.0-linux-x64.zip +377f4549aaeb154d68fb8a07a55cee6d6e188db028df3cca3e0b15557f6e4dc4 *libcxx_headers.zip +06659d8c13cf63ef52ee06be71be0e4d83612c577539f630c97274cbe1ec9ad2 *libcxxabi_headers.zip +2245e51837bb0e8b1a2acca1a43021c92d5d966e16d6cef7207adef2a00031a6 *mksnapshot-v39.3.0-darwin-arm64.zip +eaf4f47036704f0d742a4e5fd864f1eb30ace16afbcc289ba535d4cde075c05f *mksnapshot-v39.3.0-darwin-x64.zip +e94b20999118d812c63633d1988cb0c34798616195db2b0002d589f74c44347c *mksnapshot-v39.3.0-linux-arm64-x64.zip +8b1e0b65fd19f3682f3cd194c3a58ba084ac2b6c0daa9a5c8f30eb8ac58bf7d1 *mksnapshot-v39.3.0-linux-armv7l-x64.zip +2695157678a126eb58dde6575c2d1076755d6d03d3952cf0be91acf034a3f636 *mksnapshot-v39.3.0-linux-x64.zip +bed97fce94eb42d5f3fa1b5f76da3cfea464df62a9ce0465e5e49e9e8a06fe15 *mksnapshot-v39.3.0-mas-arm64.zip +d60038daff4a9e2f932738ef32c4b989953f6d3f2cc167adf14570fe2e6d5b08 *mksnapshot-v39.3.0-mas-x64.zip +10267f4198cc94fc165b63ee9d92946f94e9d13069f96d33c099a73fefdcadae *mksnapshot-v39.3.0-win32-arm64-x64.zip +0494944e5349d20b56f64ac9b11c2c3df56828dcfdf7cffe9a3413ef1d66ce92 *mksnapshot-v39.3.0-win32-ia32.zip +0b6fbe361ddd170fbffe8f7ed32b3e79e78886f2402edb5b18c9cc5b3167cbd0 *mksnapshot-v39.3.0-win32-x64.zip diff --git a/build/checksums/explorer-dll.txt b/build/checksums/explorer-dll.txt index fb8ad756847..141446c6de4 100644 --- a/build/checksums/explorer-dll.txt +++ b/build/checksums/explorer-dll.txt @@ -1,4 +1,4 @@ -11b36db4f244693381e52316261ce61678286f6bdfe2614c6352f6fecf3f060d code_explorer_command_arm64.dll -bfab3719038ca46bcd8afb9249a00f851dd08aa3cc8d13d01a917111a2a6d7c2 code_explorer_command_x64.dll -b5cd79c1e91390bdeefaf35cc5c62a6022220832e145781e5609913fac706ad9 code_insider_explorer_command_arm64.dll -f04335cc6fbe8425bd5516e6acbfa05ca706fd7566799a1e22fca1344c25351f code_insider_explorer_command_x64.dll +5e57386c660318e015e1690f2ea00123ce3a87fef880da2b28e3675bdfe1dc55 code_explorer_command_arm64.dll +903df87c586a0a686783b0560467c4156699827030422dbe4df2c2c1aa69367a code_explorer_command_x64.dll +2cb1613b35dd4eecb31d0e4999e833528a88ffa82155c15aa0bc3467b7ce3f88 code_insider_explorer_command_arm64.dll +8943a8dc5c4d69bee728aec5df894be77f97ec2d97f8de4ec1804c6ae14a3a28 code_insider_explorer_command_x64.dll diff --git a/build/checksums/nodejs.txt b/build/checksums/nodejs.txt index 7c35de5be61..43aace217e9 100644 --- a/build/checksums/nodejs.txt +++ b/build/checksums/nodejs.txt @@ -1,7 +1,7 @@ -cc04a76a09f79290194c0646f48fec40354d88969bec467789a5d55dd097f949 node-v22.20.0-darwin-arm64.tar.gz -00df9c5df3e4ec6848c26b70fb47bf96492f342f4bed6b17f12d99b3a45eeecc node-v22.20.0-darwin-x64.tar.gz -4181609e03dcb9880e7e5bf956061ecc0503c77a480c6631d868cb1f65a2c7dd node-v22.20.0-linux-arm64.tar.gz -607380e96e1543c5ca6dc8a9f5575181b2855b8769fb31d646ef9cf27224f300 node-v22.20.0-linux-armv7l.tar.gz -eeaccb0378b79406f2208e8b37a62479c70595e20be6b659125eb77dd1ab2a29 node-v22.20.0-linux-x64.tar.gz -f7dd5b44ef1bcd751107f89cc2e27f17b012be5e21b5f11f923eff84bb52a3e0 win-arm64/node.exe -fdddbf4581e046b8102815d56208d6a248950bb554570b81519a8a5dacfee95d win-x64/node.exe +c170d6554fba83d41d25a76cdbad85487c077e51fa73519e41ac885aa429d8af node-v22.21.1-darwin-arm64.tar.gz +8e3dc89614debe66c2a6ad2313a1adb06eb37db6cd6c40d7de6f7d987f7d1afd node-v22.21.1-darwin-x64.tar.gz +c86830dedf77f8941faa6c5a9c863bdfdd1927a336a46943decc06a38f80bfb2 node-v22.21.1-linux-arm64.tar.gz +40d3d09aee556abc297dd782864fcc6b9e60acd438ff0660ba9ddcd569c00920 node-v22.21.1-linux-armv7l.tar.gz +219a152ea859861d75adea578bdec3dce8143853c13c5187f40c40e77b0143b2 node-v22.21.1-linux-x64.tar.gz +707bbc8a9e615299ecdbff9040f88f59f20033ff1af923beee749b885cbd565d win-arm64/node.exe +471961cb355311c9a9dd8ba417eca8269ead32a2231653084112554cda52e8b3 win-x64/node.exe diff --git a/build/darwin/create-dmg.ts b/build/darwin/create-dmg.ts new file mode 100644 index 00000000000..dcfb8001a8e --- /dev/null +++ b/build/darwin/create-dmg.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import fs from 'fs'; +import path from 'path'; +import { spawn } from '@malept/cross-spawn-promise'; + +const root = path.dirname(path.dirname(import.meta.dirname)); +const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); + +interface DmgBuildSettings { + title: string; + icon?: string | null; + 'badge-icon'?: string | null; + background?: string; + 'background-color'?: string; + 'icon-size'?: number; + 'text-size'?: number; + format?: string; + window?: { + position?: { x: number; y: number }; + size?: { width: number; height: number }; + }; + contents: Array<{ + path: string; + x: number; + y: number; + type: 'file' | 'link'; + name?: string; + }>; +} + +function getDmgBuilderPath(): string { + return path.join(import.meta.dirname, '..', 'node_modules', 'dmg-builder'); +} + +function getDmgBuilderVendorPath(): string { + return path.join(getDmgBuilderPath(), 'vendor'); +} + +async function runDmgBuild(settingsFile: string, volumeName: string, artifactPath: string): Promise { + const vendorDir = getDmgBuilderVendorPath(); + const scriptPath = path.join(vendorDir, 'run_dmgbuild.py'); + await spawn('python3', [scriptPath, '-s', settingsFile, volumeName, artifactPath], { + cwd: vendorDir, + stdio: 'inherit' + }); +} + +async function main(buildDir?: string, outDir?: string): Promise { + const arch = process.env['VSCODE_ARCH']; + const quality = process.env['VSCODE_QUALITY']; + + if (!buildDir) { + throw new Error('Build directory argument is required'); + } + + if (!arch) { + throw new Error('$VSCODE_ARCH not set'); + } + + if (!outDir) { + throw new Error('Output directory argument is required'); + } + + const appRoot = path.join(buildDir, `VSCode-darwin-${arch}`); + const appName = product.nameLong + '.app'; + const appPath = path.join(appRoot, appName); + const dmgName = `VSCode-darwin-${arch}`; + const artifactPath = path.join(outDir, `${dmgName}.dmg`); + const backgroundPath = path.join(import.meta.dirname, `dmg-background-${quality}.tiff`); + const diskIconPath = path.join(root, 'resources', 'darwin', 'code.icns'); + let title = 'Code OSS'; + switch (quality) { + case 'stable': + title = 'VS Code'; + break; + case 'insider': + title = 'VS Code Insiders'; + break; + case 'exploration': + title = 'VS Code Exploration'; + break; + } + + if (!fs.existsSync(appPath)) { + throw new Error(`App path does not exist: ${appPath}`); + } + + console.log(`Creating DMG for ${product.nameLong}...`); + console.log(` App path: ${appPath}`); + console.log(` Output directory: ${outDir}`); + console.log(` DMG name: ${dmgName}`); + + if (fs.existsSync(artifactPath)) { + fs.unlinkSync(artifactPath); + } + + const settings: DmgBuildSettings = { + title, + 'badge-icon': diskIconPath, + background: backgroundPath, + format: 'ULMO', + 'text-size': 12, + window: { + position: { x: 100, y: 400 }, + size: { width: 480, height: 352 } + }, + contents: [ + { + path: appPath, + x: 120, + y: 160, + type: 'file' + }, + { + path: '/Applications', + x: 360, + y: 160, + type: 'link' + } + ] + }; + + const settingsFile = path.join(outDir, '.dmg-settings.json'); + fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2)); + + try { + await runDmgBuild(settingsFile, dmgName, artifactPath); + } finally { + if (fs.existsSync(settingsFile)) { + fs.unlinkSync(settingsFile); + } + } + + if (!fs.existsSync(artifactPath)) { + throw new Error(`DMG was not created at expected path: ${artifactPath}`); + } + + const stats = fs.statSync(artifactPath); + console.log(`Successfully created DMG: ${artifactPath} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`); +} + +if (import.meta.main) { + main(process.argv[2], process.argv[3]).catch(err => { + console.error('Failed to create DMG:', err); + process.exit(1); + }); +} diff --git a/build/darwin/create-universal-app.js b/build/darwin/create-universal-app.js deleted file mode 100644 index 98e14ef2160..00000000000 --- a/build/darwin/create-universal-app.js +++ /dev/null @@ -1,63 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const minimatch_1 = __importDefault(require("minimatch")); -const vscode_universal_bundler_1 = require("vscode-universal-bundler"); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -async function main(buildDir) { - const arch = process.env['VSCODE_ARCH']; - if (!buildDir) { - throw new Error('Build dir not provided'); - } - const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); - const appName = product.nameLong + '.app'; - const x64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-x64', appName); - const arm64AppPath = path_1.default.join(buildDir, 'VSCode-darwin-arm64', appName); - const asarRelativePath = path_1.default.join('Contents', 'Resources', 'app', 'node_modules.asar'); - const outAppPath = path_1.default.join(buildDir, `VSCode-darwin-${arch}`, appName); - const productJsonPath = path_1.default.resolve(outAppPath, 'Contents', 'Resources', 'app', 'product.json'); - const filesToSkip = [ - '**/CodeResources', - '**/Credits.rtf', - '**/policies/{*.mobileconfig,**/*.plist}', - // TODO: Should we consider expanding this to other files in this area? - '**/node_modules/@parcel/node-addon-api/nothing.target.mk', - ]; - await (0, vscode_universal_bundler_1.makeUniversalApp)({ - x64AppPath, - arm64AppPath, - asarPath: asarRelativePath, - outAppPath, - force: true, - mergeASARs: true, - x64ArchFiles: '{*/kerberos.node,**/extensions/microsoft-authentication/dist/libmsalruntime.dylib,**/extensions/microsoft-authentication/dist/msal-node-runtime.node}', - filesToSkipComparison: (file) => { - for (const expected of filesToSkip) { - if ((0, minimatch_1.default)(file, expected)) { - return true; - } - } - return false; - } - }); - const productJson = JSON.parse(fs_1.default.readFileSync(productJsonPath, 'utf8')); - Object.assign(productJson, { - darwinUniversalAssetId: 'darwin-universal' - }); - fs_1.default.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); -} -if (require.main === module) { - main(process.argv[2]).catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=create-universal-app.js.map \ No newline at end of file diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 41bae77cd12..26aead0ca19 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -8,7 +8,7 @@ import fs from 'fs'; import minimatch from 'minimatch'; import { makeUniversalApp } from 'vscode-universal-bundler'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); async function main(buildDir?: string) { const arch = process.env['VSCODE_ARCH']; @@ -29,8 +29,6 @@ async function main(buildDir?: string) { '**/CodeResources', '**/Credits.rtf', '**/policies/{*.mobileconfig,**/*.plist}', - // TODO: Should we consider expanding this to other files in this area? - '**/node_modules/@parcel/node-addon-api/nothing.target.mk', ]; await makeUniversalApp({ @@ -58,7 +56,7 @@ async function main(buildDir?: string) { fs.writeFileSync(productJsonPath, JSON.stringify(productJson, null, '\t')); } -if (require.main === module) { +if (import.meta.main) { main(process.argv[2]).catch(err => { console.error(err); process.exit(1); diff --git a/build/darwin/dmg-background-exploration.tiff b/build/darwin/dmg-background-exploration.tiff new file mode 100644 index 00000000000..935d70eeda6 Binary files /dev/null and b/build/darwin/dmg-background-exploration.tiff differ diff --git a/build/darwin/dmg-background-insider.tiff b/build/darwin/dmg-background-insider.tiff new file mode 100644 index 00000000000..039d53873bc Binary files /dev/null and b/build/darwin/dmg-background-insider.tiff differ diff --git a/build/darwin/dmg-background-stable.tiff b/build/darwin/dmg-background-stable.tiff new file mode 100644 index 00000000000..d66d8958cdd Binary files /dev/null and b/build/darwin/dmg-background-stable.tiff differ diff --git a/build/darwin/patch-dmg.py b/build/darwin/patch-dmg.py new file mode 100644 index 00000000000..ac6ed0436e8 --- /dev/null +++ b/build/darwin/patch-dmg.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +import subprocess +import shutil +import tempfile +import plistlib +import os + +def patch_dmg_icon(dmg_path, new_icon_path): + """Replace the volume icon in an existing DMG.""" + + # 1. Convert to read-write format + temp_rw = tempfile.NamedTemporaryFile(suffix=".dmg", delete=False) + temp_rw.close() + + subprocess.run([ + "hdiutil", "convert", dmg_path, + "-format", "UDRW", # Read-write + "-o", temp_rw.name, + "-ov" # Overwrite + ], check=True) + + # 2. Attach the writable DMG + result = subprocess.run( + ["hdiutil", "attach", "-nobrowse", "-plist", temp_rw.name], + capture_output=True, check=True + ) + plist = plistlib.loads(result.stdout) + + mount_point = None + device = None + for entity in plist["system-entities"]: + if "mount-point" in entity: + mount_point = entity["mount-point"] + device = entity["dev-entry"] + break + + try: + # 3. Copy custom icon + icon_target = os.path.join(mount_point, ".VolumeIcon.icns") + shutil.copyfile(new_icon_path, icon_target) + + # 4. Set the custom icon attribute on the volume + subprocess.run(["/usr/bin/SetFile", "-a", "C", mount_point], check=True) + + # Sync before detach + subprocess.run(["sync", "--file-system", mount_point], check=True) + + finally: + # 5. Detach + subprocess.run(["hdiutil", "detach", device], check=True) + + # 6. Convert back to compressed format (ULMO = lzma) + subprocess.run([ + "hdiutil", "convert", temp_rw.name, + "-format", "ULMO", + "-o", dmg_path, + "-ov" + ], check=True) + + # Cleanup temp file + os.unlink(temp_rw.name) + print(f"Successfully patched {dmg_path} with new icon") + + +if __name__ == "__main__": + import sys + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + patch_dmg_icon(sys.argv[1], sys.argv[2]) diff --git a/build/darwin/sign.js b/build/darwin/sign.js deleted file mode 100644 index d640e94fbf5..00000000000 --- a/build/darwin/sign.js +++ /dev/null @@ -1,128 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const osx_sign_1 = require("@electron/osx-sign"); -const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const baseDir = path_1.default.dirname(__dirname); -const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); -const helperAppBaseName = product.nameShort; -const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; -const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; -const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; -function getElectronVersion() { - const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); - const target = /^target="(.*)"$/m.exec(npmrc)[1]; - return target; -} -function getEntitlementsForFile(filePath) { - if (filePath.includes(gpuHelperAppName)) { - return path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'); - } - else if (filePath.includes(rendererHelperAppName)) { - return path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'); - } - else if (filePath.includes(pluginHelperAppName)) { - return path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'); - } - return path_1.default.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'); -} -async function retrySignOnKeychainError(fn, maxRetries = 3) { - let lastError; - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } - catch (error) { - lastError = error; - // Check if this is the specific keychain error we want to retry - const errorMessage = error instanceof Error ? error.message : String(error); - const isKeychainError = errorMessage.includes('The specified item could not be found in the keychain.'); - if (!isKeychainError || attempt === maxRetries) { - throw error; - } - console.log(`Signing attempt ${attempt} failed with keychain error, retrying...`); - console.log(`Error: ${errorMessage}`); - const delay = 1000 * Math.pow(2, attempt - 1); - console.log(`Waiting ${Math.round(delay)}ms before retry ${attempt}/${maxRetries}...`); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - throw lastError; -} -async function main(buildDir) { - const tempDir = process.env['AGENT_TEMPDIRECTORY']; - const arch = process.env['VSCODE_ARCH']; - const identity = process.env['CODESIGN_IDENTITY']; - if (!buildDir) { - throw new Error('$AGENT_BUILDDIRECTORY not set'); - } - if (!tempDir) { - throw new Error('$AGENT_TEMPDIRECTORY not set'); - } - const appRoot = path_1.default.join(buildDir, `VSCode-darwin-${arch}`); - const appName = product.nameLong + '.app'; - const infoPlistPath = path_1.default.resolve(appRoot, appName, 'Contents', 'Info.plist'); - const appOpts = { - app: path_1.default.join(appRoot, appName), - platform: 'darwin', - optionsForFile: (filePath) => ({ - entitlements: getEntitlementsForFile(filePath), - hardenedRuntime: true, - }), - preAutoEntitlements: false, - preEmbedProvisioningProfile: false, - keychain: path_1.default.join(tempDir, 'buildagent.keychain'), - version: getElectronVersion(), - identity, - }; - // Only overwrite plist entries for x64 and arm64 builds, - // universal will get its copy from the x64 build. - if (arch !== 'universal') { - await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-insert', - 'NSAppleEventsUsageDescription', - '-string', - 'An application in Visual Studio Code wants to use AppleScript.', - `${infoPlistPath}` - ]); - await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-replace', - 'NSMicrophoneUsageDescription', - '-string', - 'An application in Visual Studio Code wants to use the Microphone.', - `${infoPlistPath}` - ]); - await (0, cross_spawn_promise_1.spawn)('plutil', [ - '-replace', - 'NSCameraUsageDescription', - '-string', - 'An application in Visual Studio Code wants to use the Camera.', - `${infoPlistPath}` - ]); - } - await retrySignOnKeychainError(() => (0, osx_sign_1.sign)(appOpts)); -} -if (require.main === module) { - main(process.argv[2]).catch(async err => { - console.error(err); - const tempDir = process.env['AGENT_TEMPDIRECTORY']; - if (tempDir) { - const keychain = path_1.default.join(tempDir, 'buildagent.keychain'); - const identities = await (0, cross_spawn_promise_1.spawn)('security', ['find-identity', '-p', 'codesigning', '-v', keychain]); - console.error(`Available identities:\n${identities}`); - const dump = await (0, cross_spawn_promise_1.spawn)('security', ['dump-keychain', keychain]); - console.error(`Keychain dump:\n${dump}`); - } - process.exit(1); - }); -} -//# sourceMappingURL=sign.js.map \ No newline at end of file diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index ca3ced9138a..fcdcb2b2d45 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -5,11 +5,11 @@ import fs from 'fs'; import path from 'path'; -import { sign, SignOptions } from '@electron/osx-sign'; +import { sign, type SignOptions } from '@electron/osx-sign'; import { spawn } from '@malept/cross-spawn-promise'; -const root = path.dirname(path.dirname(__dirname)); -const baseDir = path.dirname(__dirname); +const root = path.dirname(path.dirname(import.meta.dirname)); +const baseDir = path.dirname(import.meta.dirname); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const helperAppBaseName = product.nameShort; const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; @@ -122,7 +122,7 @@ async function main(buildDir?: string): Promise { await retrySignOnKeychainError(() => sign(appOpts)); } -if (require.main === module) { +if (import.meta.main) { main(process.argv[2]).catch(async err => { console.error(err); const tempDir = process.env['AGENT_TEMPDIRECTORY']; diff --git a/build/darwin/verify-macho.js b/build/darwin/verify-macho.js deleted file mode 100644 index 8202e8d7b76..00000000000 --- a/build/darwin/verify-macho.js +++ /dev/null @@ -1,136 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const path_1 = __importDefault(require("path")); -const promises_1 = require("fs/promises"); -const cross_spawn_promise_1 = require("@malept/cross-spawn-promise"); -const minimatch_1 = __importDefault(require("minimatch")); -const MACHO_PREFIX = 'Mach-O '; -const MACHO_64_MAGIC_LE = 0xfeedfacf; -const MACHO_UNIVERSAL_MAGIC_LE = 0xbebafeca; -const MACHO_ARM64_CPU_TYPE = new Set([ - 0x0c000001, - 0x0100000c, -]); -const MACHO_X86_64_CPU_TYPE = new Set([ - 0x07000001, - 0x01000007, -]); -// Files to skip during architecture validation -const FILES_TO_SKIP = [ - // MSAL runtime files are only present in ARM64 builds - '**/extensions/microsoft-authentication/dist/libmsalruntime.dylib', - '**/extensions/microsoft-authentication/dist/msal-node-runtime.node', -]; -function isFileSkipped(file) { - return FILES_TO_SKIP.some(pattern => (0, minimatch_1.default)(file, pattern)); -} -async function read(file, buf, offset, length, position) { - let filehandle; - try { - filehandle = await (0, promises_1.open)(file); - await filehandle.read(buf, offset, length, position); - } - finally { - await filehandle?.close(); - } -} -async function checkMachOFiles(appPath, arch) { - const visited = new Set(); - const invalidFiles = []; - const header = Buffer.alloc(8); - const file_header_entry_size = 20; - const checkx86_64Arch = (arch === 'x64'); - const checkArm64Arch = (arch === 'arm64'); - const checkUniversalArch = (arch === 'universal'); - const traverse = async (p) => { - p = await (0, promises_1.realpath)(p); - if (visited.has(p)) { - return; - } - visited.add(p); - const info = await (0, promises_1.stat)(p); - if (info.isSymbolicLink()) { - return; - } - if (info.isFile()) { - let fileOutput = ''; - try { - fileOutput = await (0, cross_spawn_promise_1.spawn)('file', ['--brief', '--no-pad', p]); - } - catch (e) { - if (e instanceof cross_spawn_promise_1.ExitCodeError) { - /* silently accept error codes from "file" */ - } - else { - throw e; - } - } - if (fileOutput.startsWith(MACHO_PREFIX)) { - console.log(`Verifying architecture of ${p}`); - read(p, header, 0, 8, 0).then(_ => { - const header_magic = header.readUInt32LE(); - if (header_magic === MACHO_64_MAGIC_LE) { - const cpu_type = header.readUInt32LE(4); - if (checkUniversalArch) { - invalidFiles.push(p); - } - else if (checkArm64Arch && !MACHO_ARM64_CPU_TYPE.has(cpu_type)) { - invalidFiles.push(p); - } - else if (checkx86_64Arch && !MACHO_X86_64_CPU_TYPE.has(cpu_type)) { - invalidFiles.push(p); - } - } - else if (header_magic === MACHO_UNIVERSAL_MAGIC_LE) { - const num_binaries = header.readUInt32BE(4); - assert_1.default.equal(num_binaries, 2); - const file_entries_size = file_header_entry_size * num_binaries; - const file_entries = Buffer.alloc(file_entries_size); - read(p, file_entries, 0, file_entries_size, 8).then(_ => { - for (let i = 0; i < num_binaries; i++) { - const cpu_type = file_entries.readUInt32LE(file_header_entry_size * i); - if (!MACHO_ARM64_CPU_TYPE.has(cpu_type) && !MACHO_X86_64_CPU_TYPE.has(cpu_type)) { - invalidFiles.push(p); - } - } - }); - } - }); - } - } - if (info.isDirectory()) { - for (const child of await (0, promises_1.readdir)(p)) { - await traverse(path_1.default.resolve(p, child)); - } - } - }; - await traverse(appPath); - return invalidFiles; -} -const archToCheck = process.argv[2]; -(0, assert_1.default)(process.env['APP_PATH'], 'APP_PATH not set'); -(0, assert_1.default)(archToCheck === 'x64' || archToCheck === 'arm64' || archToCheck === 'universal', `Invalid architecture ${archToCheck} to check`); -checkMachOFiles(process.env['APP_PATH'], archToCheck).then(invalidFiles => { - // Filter out files that should be skipped - const actualInvalidFiles = invalidFiles.filter(file => !isFileSkipped(file)); - if (actualInvalidFiles.length > 0) { - console.error('\x1b[31mThese files are built for the wrong architecture:\x1b[0m'); - actualInvalidFiles.forEach(file => console.error(`\x1b[31m${file}\x1b[0m`)); - process.exit(1); - } - else { - console.log('\x1b[32mAll files are valid\x1b[0m'); - } -}).catch(err => { - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=verify-macho.js.map \ No newline at end of file diff --git a/build/eslint.js b/build/eslint.js deleted file mode 100644 index 22e976555a5..00000000000 --- a/build/eslint.js +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -const es = require('event-stream'); -const vfs = require('vinyl-fs'); -const { eslintFilter } = require('./filters'); - -function eslint() { - const eslint = require('./gulp-eslint'); - return vfs - .src(eslintFilter, { base: '.', follow: true, allowEmpty: true }) - .pipe( - eslint((results) => { - if (results.warningCount > 0 || results.errorCount > 0) { - throw new Error(`eslint failed with ${results.warningCount + results.errorCount} warnings and/or errors`); - } - }) - ).pipe(es.through(function () { /* noop, important for the stream to end */ })); -} - -if (require.main === module) { - eslint().on('error', (err) => { - console.error(); - console.error(err); - process.exit(1); - }); -} diff --git a/build/eslint.ts b/build/eslint.ts new file mode 100644 index 00000000000..a2ef396a16c --- /dev/null +++ b/build/eslint.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import eventStream from 'event-stream'; +import vfs from 'vinyl-fs'; +import { eslintFilter } from './filters.ts'; +import gulpEslint from './gulp-eslint.ts'; + +function eslint(): NodeJS.ReadWriteStream { + return vfs + .src(Array.from(eslintFilter), { base: '.', follow: true, allowEmpty: true }) + .pipe( + gulpEslint((results) => { + if (results.warningCount > 0 || results.errorCount > 0) { + throw new Error(`eslint failed with ${results.warningCount + results.errorCount} warnings and/or errors`); + } + }) + ).pipe(eventStream.through(function () { /* noop, important for the stream to end */ })); +} + +if (import.meta.main) { + eslint().on('error', (err) => { + console.error(); + console.error(err); + process.exit(1); + }); +} diff --git a/build/filters.js b/build/filters.js deleted file mode 100644 index 9b2390bf4e8..00000000000 --- a/build/filters.js +++ /dev/null @@ -1,228 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -/** - * Hygiene works by creating cascading subsets of all our files and - * passing them through a sequence of checks. Here are the current subsets, - * named according to the checks performed on them. Each subset contains - * the following one, as described in mathematical notation: - * - * all ⊃ eol ⊇ indentation ⊃ copyright ⊃ typescript - */ - -const { readFileSync } = require('fs'); -const { join } = require('path'); - -module.exports.all = [ - '*', - 'build/**/*', - 'extensions/**/*', - 'scripts/**/*', - 'src/**/*', - 'test/**/*', - '!cli/**/*', - '!out*/**', - '!extensions/**/out*/**', - '!test/**/out/**', - '!**/node_modules/**', - '!**/*.js.map', -]; - -module.exports.unicodeFilter = [ - '**', - - '!**/ThirdPartyNotices.txt', - '!**/ThirdPartyNotices.cli.txt', - '!**/LICENSE.{txt,rtf}', - '!LICENSES.chromium.html', - '!**/LICENSE', - - '!**/*.{dll,exe,png,bmp,jpg,scpt,cur,ttf,woff,eot,template,ico,icns,opus,wasm}', - '!**/test/**', - '!**/*.test.ts', - '!**/*.{d.ts,json,md}', - '!**/*.mp3', - - '!build/win32/**', - '!extensions/markdown-language-features/notebook-out/*.js', - '!extensions/markdown-math/notebook-out/**', - '!extensions/ipynb/notebook-out/**', - '!extensions/notebook-renderers/renderer-out/**', - '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', - '!extensions/terminal-suggest/src/completions/upstream/**', - '!extensions/typescript-language-features/test-workspace/**', - '!extensions/vscode-api-tests/testWorkspace/**', - '!extensions/vscode-api-tests/testWorkspace2/**', - '!extensions/**/dist/**', - '!extensions/**/out/**', - '!extensions/**/snippets/**', - '!extensions/**/colorize-fixtures/**', - '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', - - '!src/vs/base/browser/dompurify/**', - '!src/vs/workbench/services/keybinding/browser/keyboardLayouts/**', - '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', -]; - -module.exports.indentationFilter = [ - '**', - - // except specific files - '!**/ThirdPartyNotices.txt', - '!**/ThirdPartyNotices.cli.txt', - '!**/LICENSE.{txt,rtf}', - '!LICENSES.chromium.html', - '!**/LICENSE', - '!**/*.mp3', - '!src/vs/loader.js', - '!src/vs/base/browser/dompurify/*', - '!src/vs/base/common/marked/marked.js', - '!src/vs/base/common/semver/semver.js', - '!src/vs/base/node/terminateProcess.sh', - '!src/vs/base/node/cpuUsage.sh', - '!src/vs/editor/common/languages/highlights/*.scm', - '!src/vs/editor/common/languages/injections/*.scm', - '!test/unit/assert.js', - '!resources/linux/snap/electron-launch', - '!build/ext.js', - '!build/npm/gyp/patches/gyp_spectre_mitigation_support.patch', - '!product.overrides.json', - - // except specific folders - '!test/automation/out/**', - '!test/monaco/out/**', - '!test/smoke/out/**', - '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', - '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', - '!extensions/terminal-suggest/src/completions/upstream/**', - '!extensions/typescript-language-features/test-workspace/**', - '!extensions/typescript-language-features/resources/walkthroughs/**', - '!extensions/typescript-language-features/package-manager/node-maintainer/**', - '!extensions/markdown-math/notebook-out/**', - '!extensions/ipynb/notebook-out/**', - '!extensions/vscode-api-tests/testWorkspace/**', - '!extensions/vscode-api-tests/testWorkspace2/**', - '!build/monaco/**', - '!build/win32/**', - '!build/checker/**', - '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', - '!src/vs/workbench/contrib/cortexide/**', - - // except multiple specific files - '!**/package.json', - '!**/package-lock.json', - - // except multiple specific folders - '!**/codicon/**', - '!**/fixtures/**', - '!**/lib/**', - '!extensions/**/dist/**', - '!extensions/**/out/**', - '!extensions/**/snippets/**', - '!extensions/**/syntaxes/**', - '!extensions/**/themes/**', - '!extensions/**/colorize-fixtures/**', - - // except specific file types - '!src/vs/*/**/*.d.ts', - '!src/typings/**/*.d.ts', - '!extensions/**/*.d.ts', - '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', - '!build/{lib,download,linux,darwin}/**/*.js', - '!build/**/*.sh', - '!build/azure-pipelines/**/*.js', - '!build/azure-pipelines/**/*.config', - '!**/Dockerfile', - '!**/Dockerfile.*', - '!**/*.Dockerfile', - '!**/*.dockerfile', - - // except for built files - '!extensions/markdown-language-features/media/*.js', - '!extensions/markdown-language-features/notebook-out/*.js', - '!extensions/markdown-math/notebook-out/*.js', - '!extensions/ipynb/notebook-out/**', - '!extensions/notebook-renderers/renderer-out/*.js', - '!extensions/simple-browser/media/*.js', -]; - -module.exports.copyrightFilter = [ - '**', - '!**/*.desktop', - '!**/*.json', - '!**/*.html', - '!**/*.template', - '!**/*.md', - '!**/*.bat', - '!**/*.cmd', - '!**/*.ico', - '!**/*.opus', - '!**/*.mp3', - '!**/*.icns', - '!**/*.xml', - '!**/*.sh', - '!**/*.zsh', - '!**/*.fish', - '!**/*.txt', - '!**/*.xpm', - '!**/*.opts', - '!**/*.disabled', - '!**/*.code-workspace', - '!**/*.js.map', - '!**/*.wasm', - '!build/**/*.init', - '!build/linux/libcxx-fetcher.*', - '!resources/linux/snap/snapcraft.yaml', - '!resources/win32/bin/code.js', - '!resources/completions/**', - '!extensions/configuration-editing/build/inline-allOf.ts', - '!extensions/markdown-language-features/media/highlight.css', - '!extensions/markdown-math/notebook-out/**', - '!extensions/ipynb/notebook-out/**', - '!extensions/simple-browser/media/codicon.css', - '!extensions/terminal-suggest/src/completions/upstream/**', - '!extensions/typescript-language-features/node-maintainer/**', - '!extensions/html-language-features/server/src/modes/typescript/*', - '!extensions/*/server/bin/*', - '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', - '!src/vs/workbench/contrib/cortexide/**', -]; - -module.exports.tsFormattingFilter = [ - 'src/**/*.ts', - 'test/**/*.ts', - 'extensions/**/*.ts', - '!src/vs/*/**/*.d.ts', - '!src/typings/**/*.d.ts', - '!extensions/**/*.d.ts', - '!**/fixtures/**', - '!**/typings/**', - '!**/node_modules/**', - '!extensions/**/colorize-fixtures/**', - '!extensions/vscode-api-tests/testWorkspace/**', - '!extensions/vscode-api-tests/testWorkspace2/**', - '!extensions/**/*.test.ts', - '!extensions/html-language-features/server/lib/jquery.d.ts', - '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', - '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', -]; - -module.exports.eslintFilter = [ - '**/*.js', - '**/*.cjs', - '**/*.mjs', - '**/*.ts', - '.eslint-plugin-local/**/*.ts', - ...readFileSync(join(__dirname, '..', '.eslint-ignore')) - .toString() - .split(/\r\n|\n/) - .filter(line => line && !line.startsWith('#')) - .map(line => line.startsWith('!') ? line.slice(1) : `!${line}`) -]; - -module.exports.stylelintFilter = [ - 'src/**/*.css' -]; diff --git a/build/filters.ts b/build/filters.ts new file mode 100644 index 00000000000..62178455d81 --- /dev/null +++ b/build/filters.ts @@ -0,0 +1,235 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Hygiene works by creating cascading subsets of all our files and + * passing them through a sequence of checks. Here are the current subsets, + * named according to the checks performed on them. Each subset contains + * the following one, as described in mathematical notation: + * + * all ⊃ eol ⊇ indentation ⊃ copyright ⊃ typescript + */ + +export const all = Object.freeze([ + '*', + 'build/**/*', + 'extensions/**/*', + 'scripts/**/*', + 'src/**/*', + 'test/**/*', + '!cli/**/*', + '!out*/**', + '!extensions/**/out*/**', + '!test/**/out/**', + '!**/node_modules/**', + '!**/*.js.map', +]); + +export const unicodeFilter = Object.freeze([ + '**', + + '!**/ThirdPartyNotices.txt', + '!**/ThirdPartyNotices.cli.txt', + '!**/LICENSE.{txt,rtf}', + '!LICENSES.chromium.html', + '!**/LICENSE', + + '!**/*.{dll,exe,png,bmp,jpg,scpt,cur,ttf,woff,eot,template,ico,icns,opus,wasm}', + '!**/test/**', + '!**/*.test.ts', + '!**/*.{d.ts,json,md}', + '!**/*.mp3', + '!**/*.tiff', + + '!build/win32/**', + '!extensions/markdown-language-features/notebook-out/*.js', + '!extensions/markdown-math/notebook-out/**', + '!extensions/mermaid-chat-features/chat-webview-out/**', + '!extensions/ipynb/notebook-out/**', + '!extensions/notebook-renderers/renderer-out/**', + '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', + '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/typescript-language-features/test-workspace/**', + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', + '!extensions/**/dist/**', + '!extensions/**/out/**', + '!extensions/**/snippets/**', + '!extensions/**/colorize-fixtures/**', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', + + '!src/vs/base/browser/dompurify/**', + '!src/vs/workbench/services/keybinding/browser/keyboardLayouts/**', + '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', +]); + +export const indentationFilter = Object.freeze([ + '**', + + // except specific files + '!**/ThirdPartyNotices.txt', + '!**/ThirdPartyNotices.cli.txt', + '!**/LICENSE.{txt,rtf}', + '!LICENSES.chromium.html', + '!**/LICENSE', + '!**/*.mp3', + '!src/vs/base/browser/dompurify/*', + '!src/vs/base/common/marked/marked.js', + '!src/vs/base/common/semver/semver.js', + '!src/vs/base/node/terminateProcess.sh', + '!src/vs/base/node/cpuUsage.sh', + '!src/vs/editor/common/languages/highlights/*.scm', + '!src/vs/editor/common/languages/injections/*.scm', + '!test/unit/assert.js', + '!resources/linux/snap/electron-launch', + '!build/ext.js', + '!build/darwin/patch-dmg.py', + '!build/npm/gyp/patches/gyp_spectre_mitigation_support.patch', + '!product.overrides.json', + + // except specific folders + '!test/automation/out/**', + '!test/monaco/out/**', + '!test/smoke/out/**', + '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', + '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/typescript-language-features/test-workspace/**', + '!extensions/typescript-language-features/resources/walkthroughs/**', + '!extensions/typescript-language-features/package-manager/node-maintainer/**', + '!extensions/markdown-math/notebook-out/**', + '!extensions/ipynb/notebook-out/**', + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', + '!build/monaco/**', + '!build/win32/**', + '!build/checker/**', + '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', + '!src/vs/workbench/contrib/cortexide/**', + + // except multiple specific files + '!**/package.json', + '!**/package-lock.json', + + // except multiple specific folders + '!**/codicon/**', + '!**/fixtures/**', + '!**/lib/**', + '!extensions/**/dist/**', + '!extensions/**/out/**', + '!extensions/**/snippets/**', + '!extensions/**/syntaxes/**', + '!extensions/**/themes/**', + '!extensions/**/colorize-fixtures/**', + + // except specific file types + '!src/vs/*/**/*.d.ts', + '!src/typings/**/*.d.ts', + '!extensions/**/*.d.ts', + '!**/*.{svg,exe,png,bmp,jpg,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,psm1,template,yaml,yml,d.ts.recipe,ico,icns,plist,opus,admx,adml,wasm}', + '!build/{lib,download,linux,darwin}/**/*.js', + '!build/**/*.sh', + '!build/azure-pipelines/**/*.js', + '!build/azure-pipelines/**/*.config', + '!build/npm/gyp/custom-headers/*.patch', + '!**/Dockerfile', + '!**/Dockerfile.*', + '!**/*.Dockerfile', + '!**/*.dockerfile', + '!**/*.tiff', + + // except for built files + '!extensions/mermaid-chat-features/chat-webview-out/*.js', + '!extensions/markdown-language-features/media/*.js', + '!extensions/markdown-language-features/notebook-out/*.js', + '!extensions/markdown-math/notebook-out/*.js', + '!extensions/ipynb/notebook-out/**', + '!extensions/notebook-renderers/renderer-out/*.js', + '!extensions/simple-browser/media/*.js', +]); + +export const copyrightFilter = Object.freeze([ + '**', + '!**/*.desktop', + '!**/*.json', + '!**/*.html', + '!**/*.template', + '!**/*.md', + '!**/*.bat', + '!**/*.cmd', + '!**/*.ico', + '!**/*.opus', + '!**/*.mp3', + '!**/*.icns', + '!**/*.xml', + '!**/*.sh', + '!**/*.zsh', + '!**/*.fish', + '!**/*.txt', + '!**/*.xpm', + '!**/*.opts', + '!**/*.disabled', + '!**/*.code-workspace', + '!**/*.js.map', + '!**/*.wasm', + '!**/*.tiff', + '!build/**/*.init', + '!build/darwin/patch-dmg.py', + '!build/linux/libcxx-fetcher.*', + '!build/npm/gyp/custom-headers/*.patch', + '!resources/linux/snap/snapcraft.yaml', + '!resources/win32/bin/code.js', + '!resources/completions/**', + '!extensions/configuration-editing/build/inline-allOf.ts', + '!extensions/markdown-language-features/media/highlight.css', + '!extensions/markdown-math/notebook-out/**', + '!extensions/ipynb/notebook-out/**', + '!extensions/simple-browser/media/codicon.css', + '!extensions/terminal-suggest/src/completions/upstream/**', + '!extensions/typescript-language-features/node-maintainer/**', + '!extensions/html-language-features/server/src/modes/typescript/*', + '!extensions/*/server/bin/*', + '!src/vs/workbench/contrib/terminal/common/scripts/psreadline/**', + '!src/vs/workbench/contrib/cortexide/**', +]); + +export const tsFormattingFilter = Object.freeze([ + 'src/**/*.ts', + 'test/**/*.ts', + 'extensions/**/*.ts', + '!src/vs/*/**/*.d.ts', + '!src/typings/**/*.d.ts', + '!extensions/**/*.d.ts', + '!**/fixtures/**', + '!**/typings/**', + '!**/node_modules/**', + '!extensions/**/colorize-fixtures/**', + '!extensions/vscode-api-tests/testWorkspace/**', + '!extensions/vscode-api-tests/testWorkspace2/**', + '!extensions/**/*.test.ts', + '!extensions/html-language-features/server/lib/jquery.d.ts', + '!extensions/terminal-suggest/src/shell/zshBuiltinsCache.ts', + '!extensions/terminal-suggest/src/shell/fishBuiltinsCache.ts', +]); + +export const eslintFilter = Object.freeze([ + '**/*.js', + '**/*.cjs', + '**/*.mjs', + '**/*.ts', + '.eslint-plugin-local/**/*.ts', + ...readFileSync(join(import.meta.dirname, '..', '.eslint-ignore')) + .toString() + .split(/\r\n|\n/) + .filter(line => line && !line.startsWith('#')) + .map(line => line.startsWith('!') ? line.slice(1) : `!${line}`) +]); + +export const stylelintFilter = Object.freeze([ + 'src/**/*.css' +]); diff --git a/build/gulp-eslint.js b/build/gulp-eslint.js deleted file mode 100644 index 793c16c2f30..00000000000 --- a/build/gulp-eslint.js +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const { ESLint } = require('eslint'); -const { Transform, default: Stream } = require('stream'); -const { relative } = require('path'); -const fancyLog = require('fancy-log'); - -/** - * @typedef {ESLint.LintResult[] & { errorCount: number, warningCount: number}} ESLintResults - */ - -/** - * @param {(results: ESLintResults) => void} action - A function to handle all ESLint results - */ -function eslint(action) { - const linter = new ESLint({}); - const formatter = linter.loadFormatter('compact'); - - /** @type {ESLintResults} results */ - const results = []; - results.errorCount = 0; - results.warningCount = 0; - - return transform( - async (file, _enc, cb) => { - const filePath = relative(process.cwd(), file.path); - - if (file.isNull()) { - cb(null, file); - return; - } - - if (file.isStream()) { - cb(new Error('vinyl files with Stream contents are not supported')); - return; - } - - try { - // TODO: Should this be checked? - if (await linter.isPathIgnored(filePath)) { - cb(null, file); - return; - } - - const result = (await linter.lintText(file.contents.toString(), { filePath }))[0]; - results.push(result); - results.errorCount += result.errorCount; - results.warningCount += result.warningCount; - - const message = (await formatter).format([result]); - if (message) { - fancyLog(message); - } - cb(null, file); - } catch (error) { - cb(error); - } - }, - (done) => { - try { - action(results); - done(); - } catch (error) { - done(error); - } - }); -} - -/** - * @param {Stream.TransformOptions['transform']} transform - * @param {Stream.TransformOptions['flush']} flush - */ -function transform(transform, flush) { - return new Transform({ - objectMode: true, - transform, - flush - }); -} - -module.exports = eslint; diff --git a/build/gulp-eslint.ts b/build/gulp-eslint.ts new file mode 100644 index 00000000000..1e953cdba7b --- /dev/null +++ b/build/gulp-eslint.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ESLint } from 'eslint'; +import fancyLog from 'fancy-log'; +import { relative } from 'path'; +import { Transform, type TransformOptions } from 'stream'; + +interface ESLintResults extends Array { + errorCount: number; + warningCount: number; +} + +interface EslintAction { + (results: ESLintResults): void; +} + +export default function eslint(action: EslintAction) { + const linter = new ESLint({}); + const formatter = linter.loadFormatter('compact'); + + const results: ESLintResults = Object.assign([], { errorCount: 0, warningCount: 0 }); + + return createTransform( + async (file, _enc, cb) => { + const filePath = relative(process.cwd(), file.path); + + if (file.isNull()) { + cb(null, file); + return; + } + + if (file.isStream()) { + cb(new Error('vinyl files with Stream contents are not supported')); + return; + } + + try { + // TODO: Should this be checked? + if (await linter.isPathIgnored(filePath)) { + cb(null, file); + return; + } + + const result = (await linter.lintText(file.contents.toString(), { filePath }))[0]; + results.push(result); + results.errorCount += result.errorCount; + results.warningCount += result.warningCount; + + const message = (await formatter).format([result]); + if (message) { + fancyLog(message); + } + cb(null, file); + } catch (error) { + cb(error); + } + }, + (done) => { + try { + action(results); + done(); + } catch (error) { + done(error); + } + }); +} + +function createTransform( + transform: TransformOptions['transform'], + flush: TransformOptions['flush'] +): Transform { + return new Transform({ + objectMode: true, + transform, + flush + }); +} diff --git a/build/gulpfile.cli.js b/build/gulpfile.cli.js deleted file mode 100644 index 63e0ae0b847..00000000000 --- a/build/gulpfile.cli.js +++ /dev/null @@ -1,154 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const es = require('event-stream'); -const gulp = require('gulp'); -const path = require('path'); -const fancyLog = require('fancy-log'); -const ansiColors = require('ansi-colors'); -const cp = require('child_process'); -const { tmpdir } = require('os'); -const { existsSync, mkdirSync, rmSync } = require('fs'); - -const task = require('./lib/task'); -const watcher = require('./lib/watch'); -const { debounce } = require('./lib/util'); -const createReporter = require('./lib/reporter').createReporter; - -const root = 'cli'; -const rootAbs = path.resolve(__dirname, '..', root); -const src = `${root}/src`; - -const platformOpensslDirName = - process.platform === 'win32' ? ( - process.arch === 'arm64' - ? 'arm64-windows-static-md' - : 'x64-windows-static-md') - : process.platform === 'darwin' ? ( - process.arch === 'arm64' - ? 'arm64-osx' - : 'x64-osx') - : (process.arch === 'arm64' - ? 'arm64-linux' - : process.arch === 'arm' - ? 'arm-linux' - : 'x64-linux'); -const platformOpensslDir = path.join(rootAbs, 'openssl', 'package', 'out', platformOpensslDirName); - -const hasLocalRust = (() => { - /** @type boolean | undefined */ - let result = undefined; - return () => { - if (result !== undefined) { - return result; - } - - try { - const r = cp.spawnSync('cargo', ['--version']); - result = r.status === 0; - } catch (e) { - result = false; - } - - return result; - }; -})(); - -const compileFromSources = (callback) => { - const proc = cp.spawn('cargo', ['--color', 'always', 'build'], { - cwd: root, - stdio: ['ignore', 'pipe', 'pipe'], - env: existsSync(platformOpensslDir) ? { OPENSSL_DIR: platformOpensslDir, ...process.env } : process.env - }); - - /** @type Buffer[] */ - const stdoutErr = []; - proc.stdout.on('data', d => stdoutErr.push(d)); - proc.stderr.on('data', d => stdoutErr.push(d)); - proc.on('error', callback); - proc.on('exit', code => { - if (code !== 0) { - callback(Buffer.concat(stdoutErr).toString()); - } else { - callback(); - } - }); -}; - -const acquireBuiltOpenSSL = (callback) => { - const untar = require('gulp-untar'); - const gunzip = require('gulp-gunzip'); - const dir = path.join(tmpdir(), 'vscode-openssl-download'); - mkdirSync(dir, { recursive: true }); - - cp.spawnSync( - process.platform === 'win32' ? 'npm.cmd' : 'npm', - ['pack', '@vscode/openssl-prebuilt'], - { stdio: ['ignore', 'ignore', 'inherit'], cwd: dir } - ); - - gulp.src('*.tgz', { cwd: dir }) - .pipe(gunzip()) - .pipe(untar()) - .pipe(gulp.dest(`${root}/openssl`)) - .on('error', callback) - .on('end', () => { - rmSync(dir, { recursive: true, force: true }); - callback(); - }); -}; - -const compileWithOpenSSLCheck = (/** @type import('./lib/reporter').IReporter */ reporter) => es.map((_, callback) => { - compileFromSources(err => { - if (!err) { - // no-op - } else if (err.toString().includes('Could not find directory of OpenSSL installation') && !existsSync(platformOpensslDir)) { - fancyLog(ansiColors.yellow(`[cli]`), 'OpenSSL libraries not found, acquiring prebuilt bits...'); - acquireBuiltOpenSSL(err => { - if (err) { - callback(err); - } else { - compileFromSources(err => { - if (err) { - reporter(err.toString()); - } - callback(null, ''); - }); - } - }); - } else { - reporter(err.toString()); - } - - callback(null, ''); - }); -}); - -const warnIfRustNotInstalled = () => { - if (!hasLocalRust()) { - fancyLog(ansiColors.yellow(`[cli]`), 'No local Rust install detected, compilation may fail.'); - fancyLog(ansiColors.yellow(`[cli]`), 'Get rust from: https://rustup.rs/'); - } -}; - -const compileCliTask = task.define('compile-cli', () => { - warnIfRustNotInstalled(); - const reporter = createReporter('cli'); - return gulp.src(`${root}/Cargo.toml`) - .pipe(compileWithOpenSSLCheck(reporter)) - .pipe(reporter.end(true)); -}); - - -const watchCliTask = task.define('watch-cli', () => { - warnIfRustNotInstalled(); - return watcher(`${src}/**`, { read: false }) - .pipe(debounce(compileCliTask)); -}); - -gulp.task(compileCliTask); -gulp.task(watchCliTask); diff --git a/build/gulpfile.cli.ts b/build/gulpfile.cli.ts new file mode 100644 index 00000000000..974cf892e4f --- /dev/null +++ b/build/gulpfile.cli.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import es from 'event-stream'; +import gulp from 'gulp'; +import * as path from 'path'; +import fancyLog from 'fancy-log'; +import ansiColors from 'ansi-colors'; +import * as cp from 'child_process'; +import { tmpdir } from 'os'; +import { existsSync, mkdirSync, rmSync } from 'fs'; +import * as task from './lib/task.ts'; +import watcher from './lib/watch/index.ts'; +import { debounce, untar } from './lib/util.ts'; +import { createReporter } from './lib/reporter.ts'; +import gunzip from 'gulp-gunzip'; + +const root = 'cli'; +const rootAbs = path.resolve(import.meta.dirname, '..', root); +const src = `${root}/src`; + +const platformOpensslDirName = + process.platform === 'win32' ? ( + process.arch === 'arm64' + ? 'arm64-windows-static-md' + : 'x64-windows-static-md') + : process.platform === 'darwin' ? ( + process.arch === 'arm64' + ? 'arm64-osx' + : 'x64-osx') + : (process.arch === 'arm64' + ? 'arm64-linux' + : process.arch === 'arm' + ? 'arm-linux' + : 'x64-linux'); +const platformOpensslDir = path.join(rootAbs, 'openssl', 'package', 'out', platformOpensslDirName); + +const hasLocalRust = (() => { + let result: boolean | undefined = undefined; + return () => { + if (result !== undefined) { + return result; + } + + try { + const r = cp.spawnSync('cargo', ['--version']); + result = r.status === 0; + } catch (e) { + result = false; + } + + return result; + }; +})(); + +const compileFromSources = (callback: (err?: string) => void) => { + const proc = cp.spawn('cargo', ['--color', 'always', 'build'], { + cwd: root, + stdio: ['ignore', 'pipe', 'pipe'], + env: existsSync(platformOpensslDir) ? { OPENSSL_DIR: platformOpensslDir, ...process.env } : process.env + }); + + const stdoutErr: Buffer[] = []; + proc.stdout.on('data', d => stdoutErr.push(d)); + proc.stderr.on('data', d => stdoutErr.push(d)); + proc.on('error', callback); + proc.on('exit', code => { + if (code !== 0) { + callback(Buffer.concat(stdoutErr).toString()); + } else { + callback(); + } + }); +}; + +const acquireBuiltOpenSSL = (callback: (err?: unknown) => void) => { + const dir = path.join(tmpdir(), 'vscode-openssl-download'); + mkdirSync(dir, { recursive: true }); + + cp.spawnSync( + process.platform === 'win32' ? 'npm.cmd' : 'npm', + ['pack', '@vscode/openssl-prebuilt'], + { stdio: ['ignore', 'ignore', 'inherit'], cwd: dir } + ); + + gulp.src('*.tgz', { cwd: dir }) + .pipe(gunzip()) + .pipe(untar()) + .pipe(gulp.dest(`${root}/openssl`)) + .on('error', callback) + .on('end', () => { + rmSync(dir, { recursive: true, force: true }); + callback(); + }); +}; + +const compileWithOpenSSLCheck = (reporter: import('./lib/reporter.ts').IReporter) => es.map((_, callback) => { + compileFromSources(err => { + if (!err) { + // no-op + } else if (err.toString().includes('Could not find directory of OpenSSL installation') && !existsSync(platformOpensslDir)) { + fancyLog(ansiColors.yellow(`[cli]`), 'OpenSSL libraries not found, acquiring prebuilt bits...'); + acquireBuiltOpenSSL(err => { + if (err) { + callback(err as Error); + } else { + compileFromSources(err => { + if (err) { + reporter(err.toString()); + } + callback(undefined, ''); + }); + } + }); + } else { + reporter(err.toString()); + } + callback(undefined, ''); + }); +}); + +const warnIfRustNotInstalled = () => { + if (!hasLocalRust()) { + fancyLog(ansiColors.yellow(`[cli]`), 'No local Rust install detected, compilation may fail.'); + fancyLog(ansiColors.yellow(`[cli]`), 'Get rust from: https://rustup.rs/'); + } +}; + +const compileCliTask = task.define('compile-cli', () => { + warnIfRustNotInstalled(); + const reporter = createReporter('cli'); + return gulp.src(`${root}/Cargo.toml`) + .pipe(compileWithOpenSSLCheck(reporter)) + .pipe(reporter.end(true)); +}); + + +const watchCliTask = task.define('watch-cli', () => { + warnIfRustNotInstalled(); + return watcher(`${src}/**`, { read: false }) + .pipe(debounce(compileCliTask as task.StreamTask)); +}); + +gulp.task(compileCliTask); +gulp.task(watchCliTask); diff --git a/build/gulpfile.compile.js b/build/gulpfile.compile.js deleted file mode 100644 index 0c0a024c8fc..00000000000 --- a/build/gulpfile.compile.js +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check -'use strict'; - -const gulp = require('gulp'); -const util = require('./lib/util'); -const date = require('./lib/date'); -const task = require('./lib/task'); -const compilation = require('./lib/compilation'); - -/** - * @param {boolean} disableMangle - */ -function makeCompileBuildTask(disableMangle) { - return task.series( - util.rimraf('out-build'), - date.writeISODate('out-build'), - compilation.compileApiProposalNamesTask, - compilation.compileTask('src', 'out-build', true, { disableMangle }) - ); -} - -// Local/PR compile, including nls and inline sources in sourcemaps, minification, no mangling -const compileBuildWithoutManglingTask = task.define('compile-build-without-mangling', makeCompileBuildTask(true)); -gulp.task(compileBuildWithoutManglingTask); -exports.compileBuildWithoutManglingTask = compileBuildWithoutManglingTask; - -// CI compile, including nls and inline sources in sourcemaps, mangling, minification, for build -const compileBuildWithManglingTask = task.define('compile-build-with-mangling', makeCompileBuildTask(false)); -gulp.task(compileBuildWithManglingTask); -exports.compileBuildWithManglingTask = compileBuildWithManglingTask; diff --git a/build/gulpfile.compile.ts b/build/gulpfile.compile.ts new file mode 100644 index 00000000000..76f04ae179f --- /dev/null +++ b/build/gulpfile.compile.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import * as util from './lib/util.ts'; +import * as date from './lib/date.ts'; +import * as task from './lib/task.ts'; +import * as compilation from './lib/compilation.ts'; + +function makeCompileBuildTask(disableMangle: boolean) { + return task.series( + util.rimraf('out-build'), + date.writeISODate('out-build'), + compilation.compileApiProposalNamesTask, + compilation.compileTask('src', 'out-build', true, { disableMangle }) + ); +} + +// Local/PR compile, including nls and inline sources in sourcemaps, minification, no mangling +export const compileBuildWithoutManglingTask = task.define('compile-build-without-mangling', task.series(compilation.copyCodiconsTask, makeCompileBuildTask(true))); +gulp.task(compileBuildWithoutManglingTask); + +// CI compile, including nls and inline sources in sourcemaps, mangling, minification, for build +export const compileBuildWithManglingTask = task.define('compile-build-with-mangling', task.series(compilation.copyCodiconsTask, makeCompileBuildTask(false))); +gulp.task(compileBuildWithManglingTask); diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js deleted file mode 100644 index 5d8d47677a6..00000000000 --- a/build/gulpfile.editor.js +++ /dev/null @@ -1,303 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -const gulp = require('gulp'); -const path = require('path'); -const util = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const task = require('./lib/task'); -const es = require('event-stream'); -const File = require('vinyl'); -const i18n = require('./lib/i18n'); -const standalone = require('./lib/standalone'); -const cp = require('child_process'); -const compilation = require('./lib/compilation'); -const monacoapi = require('./lib/monaco-api'); -const fs = require('fs'); -const filter = require('gulp-filter'); - -const root = path.dirname(__dirname); -const sha1 = getVersion(root); -const semver = require('./monaco/package.json').version; -const headerVersion = semver + '(' + sha1 + ')'; - -const BUNDLED_FILE_HEADER = [ - '/*!-----------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' * Version: ' + headerVersion, - ' * Released under the MIT license', - ' * https://github.com/microsoft/vscode/blob/main/LICENSE.txt', - ' *-----------------------------------------------------------*/', - '' -].join('\n'); - -const extractEditorSrcTask = task.define('extract-editor-src', () => { - const apiusages = monacoapi.execute().usageContent; - const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); - standalone.extractEditor({ - sourcesRoot: path.join(root, 'src'), - entryPoints: [ - 'vs/editor/editor.main.ts', - 'vs/editor/editor.worker.start.ts', - 'vs/editor/common/services/editorWebWorkerMain.ts', - ], - inlineEntryPoints: [ - apiusages, - extrausages - ], - typings: [], - additionalFilesToCopyOut: [ - 'vs/base/browser/dompurify/dompurify.js', - 'vs/base/common/marked/marked.js', - ], - shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers - importIgnorePattern: /\.css$/, - destRoot: path.join(root, 'out-editor-src'), - tsOutDir: '../out-monaco-editor-core/esm/vs', - }); -}); - -const compileEditorESMTask = task.define('compile-editor-esm', () => { - - const src = 'out-editor-src'; - const out = 'out-monaco-editor-core/esm'; - - const compile = compilation.createCompile(src, { build: true, emitError: true, transpileOnly: false, preserveEnglish: true }); - const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); - - return ( - srcPipe - .pipe(compile()) - .pipe(i18n.processNlsFiles({ - out, - fileHeader: BUNDLED_FILE_HEADER, - languages: [...i18n.defaultLanguages, ...i18n.extraLanguages], - })) - .pipe(filter(['**', '!**/inlineEntryPoint*', '!**/tsconfig.json', '!**/loader.js'])) - .pipe(gulp.dest(out)) - ); -}); - -/** - * @param {string} contents - */ -function toExternalDTS(contents) { - const lines = contents.split(/\r\n|\r|\n/); - let killNextCloseCurlyBrace = false; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (killNextCloseCurlyBrace) { - if ('}' === line) { - lines[i] = ''; - killNextCloseCurlyBrace = false; - continue; - } - - if (line.indexOf(' ') === 0) { - lines[i] = line.substr(4); - } else if (line.charAt(0) === '\t') { - lines[i] = line.substr(1); - } - - continue; - } - - if ('declare namespace monaco {' === line) { - lines[i] = ''; - killNextCloseCurlyBrace = true; - continue; - } - - if (line.indexOf('declare namespace monaco.') === 0) { - lines[i] = line.replace('declare namespace monaco.', 'export namespace '); - } - - if (line.indexOf('declare var MonacoEnvironment') === 0) { - lines[i] = `declare global {\n var MonacoEnvironment: Environment | undefined;\n}`; - } - } - return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); -} - -const finalEditorResourcesTask = task.define('final-editor-resources', () => { - return es.merge( - // other assets - es.merge( - gulp.src('build/monaco/LICENSE'), - gulp.src('build/monaco/ThirdPartyNotices.txt'), - gulp.src('src/vs/monaco.d.ts') - ).pipe(gulp.dest('out-monaco-editor-core')), - - // place the .d.ts in the esm folder - gulp.src('src/vs/monaco.d.ts') - .pipe(es.through(function (data) { - this.emit('data', new File({ - path: data.path.replace(/monaco\.d\.ts/, 'editor.api.d.ts'), - base: data.base, - contents: Buffer.from(toExternalDTS(data.contents.toString())) - })); - })) - .pipe(gulp.dest('out-monaco-editor-core/esm/vs/editor')), - - // package.json - gulp.src('build/monaco/package.json') - .pipe(es.through(function (data) { - const json = JSON.parse(data.contents.toString()); - json.private = false; - - let markedVersion; - let dompurifyVersion; - try { - const markedManifestPath = path.join(root, 'src/vs/base/common/marked/cgmanifest.json'); - const dompurifyManifestPath = path.join(root, 'src/vs/base/browser/dompurify/cgmanifest.json'); - - const markedManifest = JSON.parse(fs.readFileSync(markedManifestPath, 'utf8')); - const dompurifyManifest = JSON.parse(fs.readFileSync(dompurifyManifestPath, 'utf8')); - - markedVersion = markedManifest.registrations[0].version; - dompurifyVersion = dompurifyManifest.registrations[0].version; - - if (!markedVersion || !dompurifyVersion) { - throw new Error('Unable to read versions from cgmanifest.json files'); - } - } catch (error) { - throw new Error(`Failed to read cgmanifest.json files for monaco-editor-core dependencies: ${error.message}`); - } - - setUnsetField(json, 'dependencies', { - 'marked': markedVersion, - 'dompurify': dompurifyVersion - }); - - data.contents = Buffer.from(JSON.stringify(json, null, ' ')); - this.emit('data', data); - })) - .pipe(gulp.dest('out-monaco-editor-core')), - - // version.txt - gulp.src('build/monaco/version.txt') - .pipe(es.through(function (data) { - data.contents = Buffer.from(`monaco-editor-core: https://github.com/microsoft/vscode/tree/${sha1}`); - this.emit('data', data); - })) - .pipe(gulp.dest('out-monaco-editor-core')), - - // README.md - gulp.src('build/monaco/README-npm.md') - .pipe(es.through(function (data) { - this.emit('data', new File({ - path: data.path.replace(/README-npm\.md/, 'README.md'), - base: data.base, - contents: data.contents - })); - })) - .pipe(gulp.dest('out-monaco-editor-core')), - ); -}); - -gulp.task('extract-editor-src', - task.series( - util.rimraf('out-editor-src'), - extractEditorSrcTask - ) -); - -gulp.task('editor-distro', - task.series( - task.parallel( - util.rimraf('out-editor-src'), - util.rimraf('out-monaco-editor-core'), - ), - extractEditorSrcTask, - compileEditorESMTask, - finalEditorResourcesTask - ) -); - -gulp.task('monacodts', task.define('monacodts', () => { - const result = monacoapi.execute(); - fs.writeFileSync(result.filePath, result.content); - fs.writeFileSync(path.join(root, 'src/vs/editor/common/standalone/standaloneEnums.ts'), result.enums); - return Promise.resolve(true); -})); - -//#region monaco type checking - -/** - * @param {boolean} watch - */ -function createTscCompileTask(watch) { - return () => { - const createReporter = require('./lib/reporter').createReporter; - - return new Promise((resolve, reject) => { - const args = ['./node_modules/.bin/tsc', '-p', './src/tsconfig.monaco.json', '--noEmit']; - if (watch) { - args.push('-w'); - } - const child = cp.spawn(`node`, args, { - cwd: path.join(__dirname, '..'), - // stdio: [null, 'pipe', 'inherit'] - }); - const errors = []; - const reporter = createReporter('monaco'); - - /** @type {NodeJS.ReadWriteStream | undefined} */ - let report; - const magic = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; // https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings - - child.stdout.on('data', data => { - let str = String(data); - str = str.replace(magic, '').trim(); - if (str.indexOf('Starting compilation') >= 0 || str.indexOf('File change detected') >= 0) { - errors.length = 0; - report = reporter.end(false); - - } else if (str.indexOf('Compilation complete') >= 0) { - // @ts-ignore - report.end(); - - } else if (str) { - const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(str); - if (match) { - // trying to massage the message so that it matches the gulp-tsb error messages - // e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'. - const fullpath = path.join(root, match[1]); - const message = match[3]; - reporter(fullpath + message); - } else { - reporter(str); - } - } - }); - child.on('exit', resolve); - child.on('error', reject); - }); - }; -} - -const monacoTypecheckWatchTask = task.define('monaco-typecheck-watch', createTscCompileTask(true)); -exports.monacoTypecheckWatchTask = monacoTypecheckWatchTask; - -const monacoTypecheckTask = task.define('monaco-typecheck', createTscCompileTask(false)); -exports.monacoTypecheckTask = monacoTypecheckTask; - -//#endregion - -/** - * Sets a field on an object only if it's not already set, otherwise throws an error - * @param {any} obj - The object to modify - * @param {string} field - The field name to set - * @param {any} value - The value to set - */ -function setUnsetField(obj, field, value) { - if (obj[field] !== undefined) { - throw new Error(`Field "${field}" is already set (but was expected to not be).`); - } - obj[field] = value; -} diff --git a/build/gulpfile.editor.ts b/build/gulpfile.editor.ts new file mode 100644 index 00000000000..338c678b7de --- /dev/null +++ b/build/gulpfile.editor.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import path from 'path'; +import * as util from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import es from 'event-stream'; +import File from 'vinyl'; +import * as i18n from './lib/i18n.ts'; +import * as standalone from './lib/standalone.ts'; +import * as cp from 'child_process'; +import * as compilation from './lib/compilation.ts'; +import * as monacoapi from './lib/monaco-api.ts'; +import * as fs from 'fs'; +import filter from 'gulp-filter'; +import { createReporter } from './lib/reporter.ts'; +import monacoPackage from './monaco/package.json' with { type: 'json' }; + +const root = path.dirname(import.meta.dirname); +const sha1 = getVersion(root); +const semver = monacoPackage.version; +const headerVersion = semver + '(' + sha1 + ')'; + +const BUNDLED_FILE_HEADER = [ + '/*!-----------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' * Version: ' + headerVersion, + ' * Released under the MIT license', + ' * https://github.com/microsoft/vscode/blob/main/LICENSE.txt', + ' *-----------------------------------------------------------*/', + '' +].join('\n'); + +const extractEditorSrcTask = task.define('extract-editor-src', () => { + // Ensure codicon.ttf is copied from node_modules (needed when node_modules is cached and postinstall doesn't run) + const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf'); + const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf'); + if (fs.existsSync(codiconSource)) { + fs.mkdirSync(path.dirname(codiconDest), { recursive: true }); + fs.copyFileSync(codiconSource, codiconDest); + } + + const apiusages = monacoapi.execute().usageContent; + const extrausages = fs.readFileSync(path.join(root, 'build', 'monaco', 'monaco.usage.recipe')).toString(); + standalone.extractEditor({ + sourcesRoot: path.join(root, 'src'), + entryPoints: [ + 'vs/editor/editor.main.ts', + 'vs/editor/editor.worker.start.ts', + 'vs/editor/common/services/editorWebWorkerMain.ts', + ], + inlineEntryPoints: [ + apiusages, + extrausages + ], + typings: [], + additionalFilesToCopyOut: [ + 'vs/base/browser/dompurify/dompurify.js', + 'vs/base/common/marked/marked.js', + ], + shakeLevel: 2, // 0-Files, 1-InnerFile, 2-ClassMembers + importIgnorePattern: /\.css$/, + destRoot: path.join(root, 'out-editor-src'), + tsOutDir: '../out-monaco-editor-core/esm/vs', + }); +}); + +const compileEditorESMTask = task.define('compile-editor-esm', () => { + + const src = 'out-editor-src'; + const out = 'out-monaco-editor-core/esm'; + + const compile = compilation.createCompile(src, { build: true, emitError: true, transpileOnly: false, preserveEnglish: true }); + const srcPipe = gulp.src(`${src}/**`, { base: `${src}` }); + + return ( + srcPipe + .pipe(compile()) + .pipe(i18n.processNlsFiles({ + out, + fileHeader: BUNDLED_FILE_HEADER, + languages: [...i18n.defaultLanguages, ...i18n.extraLanguages], + })) + .pipe(filter(['**', '!**/inlineEntryPoint*', '!**/tsconfig.json'])) + .pipe(gulp.dest(out)) + ); +}); + +function toExternalDTS(contents: string) { + const lines = contents.split(/\r\n|\r|\n/); + let killNextCloseCurlyBrace = false; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (killNextCloseCurlyBrace) { + if ('}' === line) { + lines[i] = ''; + killNextCloseCurlyBrace = false; + continue; + } + + if (line.indexOf(' ') === 0) { + lines[i] = line.substr(4); + } else if (line.charAt(0) === '\t') { + lines[i] = line.substr(1); + } + + continue; + } + + if ('declare namespace monaco {' === line) { + lines[i] = ''; + killNextCloseCurlyBrace = true; + continue; + } + + if (line.indexOf('declare namespace monaco.') === 0) { + lines[i] = line.replace('declare namespace monaco.', 'export namespace '); + } + + if (line.indexOf('declare var MonacoEnvironment') === 0) { + lines[i] = `declare global {\n var MonacoEnvironment: Environment | undefined;\n}`; + } + } + return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); +} + +const finalEditorResourcesTask = task.define('final-editor-resources', () => { + return es.merge( + // other assets + es.merge( + gulp.src('build/monaco/LICENSE'), + gulp.src('build/monaco/ThirdPartyNotices.txt'), + gulp.src('src/vs/monaco.d.ts') + ).pipe(gulp.dest('out-monaco-editor-core')), + + // place the .d.ts in the esm folder + gulp.src('src/vs/monaco.d.ts') + .pipe(es.through(function (data) { + this.emit('data', new File({ + path: data.path.replace(/monaco\.d\.ts/, 'editor.api.d.ts'), + base: data.base, + contents: Buffer.from(toExternalDTS(data.contents.toString())) + })); + })) + .pipe(gulp.dest('out-monaco-editor-core/esm/vs/editor')), + + // package.json + gulp.src('build/monaco/package.json') + .pipe(es.through(function (data) { + const json = JSON.parse(data.contents.toString()); + json.private = false; + + let markedVersion; + let dompurifyVersion; + try { + const markedManifestPath = path.join(root, 'src/vs/base/common/marked/cgmanifest.json'); + const dompurifyManifestPath = path.join(root, 'src/vs/base/browser/dompurify/cgmanifest.json'); + + const markedManifest = JSON.parse(fs.readFileSync(markedManifestPath, 'utf8')); + const dompurifyManifest = JSON.parse(fs.readFileSync(dompurifyManifestPath, 'utf8')); + + markedVersion = markedManifest.registrations[0].version; + dompurifyVersion = dompurifyManifest.registrations[0].version; + + if (!markedVersion || !dompurifyVersion) { + throw new Error('Unable to read versions from cgmanifest.json files'); + } + } catch (error) { + throw new Error(`Failed to read cgmanifest.json files for monaco-editor-core dependencies: ${error.message}`); + } + + setUnsetField(json, 'dependencies', { + 'marked': markedVersion, + 'dompurify': dompurifyVersion + }); + + data.contents = Buffer.from(JSON.stringify(json, null, ' ')); + this.emit('data', data); + })) + .pipe(gulp.dest('out-monaco-editor-core')), + + // version.txt + gulp.src('build/monaco/version.txt') + .pipe(es.through(function (data) { + data.contents = Buffer.from(`monaco-editor-core: https://github.com/microsoft/vscode/tree/${sha1}`); + this.emit('data', data); + })) + .pipe(gulp.dest('out-monaco-editor-core')), + + // README.md + gulp.src('build/monaco/README-npm.md') + .pipe(es.through(function (data) { + this.emit('data', new File({ + path: data.path.replace(/README-npm\.md/, 'README.md'), + base: data.base, + contents: data.contents + })); + })) + .pipe(gulp.dest('out-monaco-editor-core')), + ); +}); + +gulp.task('extract-editor-src', + task.series( + util.rimraf('out-editor-src'), + extractEditorSrcTask + ) +); + +gulp.task('editor-distro', + task.series( + task.parallel( + util.rimraf('out-editor-src'), + util.rimraf('out-monaco-editor-core'), + ), + extractEditorSrcTask, + compileEditorESMTask, + finalEditorResourcesTask + ) +); + +gulp.task('monacodts', task.define('monacodts', () => { + const result = monacoapi.execute(); + fs.writeFileSync(result.filePath, result.content); + fs.writeFileSync(path.join(root, 'src/vs/editor/common/standalone/standaloneEnums.ts'), result.enums); + return Promise.resolve(true); +})); + +//#region monaco type checking + +function createTscCompileTask(watch: boolean) { + return () => { + return new Promise((resolve, reject) => { + const args = ['./node_modules/.bin/tsc', '-p', './src/tsconfig.monaco.json', '--noEmit']; + if (watch) { + args.push('-w'); + } + const child = cp.spawn(`node`, args, { + cwd: path.join(import.meta.dirname, '..'), + // stdio: [null, 'pipe', 'inherit'] + }); + const errors: string[] = []; + const reporter = createReporter('monaco'); + + let report: NodeJS.ReadWriteStream | undefined; + const magic = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; // https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings + + child.stdout.on('data', data => { + let str = String(data); + str = str.replace(magic, '').trim(); + if (str.indexOf('Starting compilation') >= 0 || str.indexOf('File change detected') >= 0) { + errors.length = 0; + report = reporter.end(false); + + } else if (str.indexOf('Compilation complete') >= 0) { + // @ts-ignore + report.end(); + + } else if (str) { + const match = /(.*\(\d+,\d+\): )(.*: )(.*)/.exec(str); + if (match) { + // trying to massage the message so that it matches the gulp-tsb error messages + // e.g. src/vs/base/common/strings.ts(663,5): error TS2322: Type '1234' is not assignable to type 'string'. + const fullpath = path.join(root, match[1]); + const message = match[3]; + reporter(fullpath + message); + } else { + reporter(str); + } + } + }); + child.on('exit', resolve); + child.on('error', reject); + }); + }; +} + +export const monacoTypecheckWatchTask = task.define('monaco-typecheck-watch', createTscCompileTask(true)); + +export const monacoTypecheckTask = task.define('monaco-typecheck', createTscCompileTask(false)); + +//#endregion +/** + * Sets a field on an object only if it's not already set, otherwise throws an error + * @param obj The object to modify + * @param field The field name to set + * @param value The value to set + */ +function setUnsetField(obj: Record, field: string, value: unknown) { + if (obj[field] !== undefined) { + throw new Error(`Field "${field}" is already set (but was expected to not be).`); + } + obj[field] = value; +} diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js deleted file mode 100644 index ab2bd26f345..00000000000 --- a/build/gulpfile.extensions.js +++ /dev/null @@ -1,300 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Increase max listeners for event emitters -require('events').EventEmitter.defaultMaxListeners = 100; - -const gulp = require('gulp'); -const path = require('path'); -const nodeUtil = require('util'); -const es = require('event-stream'); -const filter = require('gulp-filter'); -const util = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const task = require('./lib/task'); -const watcher = require('./lib/watch'); -const createReporter = require('./lib/reporter').createReporter; -const glob = require('glob'); -const root = path.dirname(__dirname); -const commit = getVersion(root); -const plumber = require('gulp-plumber'); -const ext = require('./lib/extensions'); - -// To save 250ms for each gulp startup, we are caching the result here -// const compilations = glob.sync('**/tsconfig.json', { -// cwd: extensionsPath, -// ignore: ['**/out/**', '**/node_modules/**'] -// }); -const compilations = [ - 'extensions/configuration-editing/tsconfig.json', - 'extensions/css-language-features/client/tsconfig.json', - 'extensions/css-language-features/server/tsconfig.json', - 'extensions/debug-auto-launch/tsconfig.json', - 'extensions/debug-server-ready/tsconfig.json', - 'extensions/emmet/tsconfig.json', - 'extensions/extension-editing/tsconfig.json', - 'extensions/git/tsconfig.json', - 'extensions/git-base/tsconfig.json', - 'extensions/github/tsconfig.json', - 'extensions/github-authentication/tsconfig.json', - 'extensions/grunt/tsconfig.json', - 'extensions/gulp/tsconfig.json', - 'extensions/html-language-features/client/tsconfig.json', - 'extensions/html-language-features/server/tsconfig.json', - 'extensions/ipynb/tsconfig.json', - 'extensions/jake/tsconfig.json', - 'extensions/json-language-features/client/tsconfig.json', - 'extensions/json-language-features/server/tsconfig.json', - 'extensions/markdown-language-features/tsconfig.json', - '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', - 'extensions/npm/tsconfig.json', - 'extensions/open-remote-ssh/tsconfig.json', - 'extensions/php-language-features/tsconfig.json', - 'extensions/references-view/tsconfig.json', - 'extensions/search-result/tsconfig.json', - 'extensions/simple-browser/tsconfig.json', - 'extensions/tunnel-forwarding/tsconfig.json', - 'extensions/typescript-language-features/web/tsconfig.json', - 'extensions/typescript-language-features/tsconfig.json', - 'extensions/vscode-api-tests/tsconfig.json', - 'extensions/vscode-colorize-tests/tsconfig.json', - 'extensions/vscode-colorize-perf-tests/tsconfig.json', - 'extensions/vscode-test-resolver/tsconfig.json', - - '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', - '.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json', -]; - -const getBaseUrl = out => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; - -const tasks = compilations.map(function (tsconfigFile) { - const absolutePath = path.join(root, tsconfigFile); - const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, '')); - - const overrideOptions = {}; - overrideOptions.sourceMap = true; - - const name = relativeDirname.replace(/\//g, '-'); - - const srcRoot = path.dirname(tsconfigFile); - const srcBase = path.join(srcRoot, 'src'); - const src = path.join(srcBase, '**'); - const srcOpts = { cwd: root, base: srcBase, dot: true }; - - const out = path.join(srcRoot, 'out'); - const baseUrl = getBaseUrl(out); - - function createPipeline(build, emitError, transpileOnly) { - const tsb = require('./lib/tsb'); - const sourcemaps = require('gulp-sourcemaps'); - - const reporter = createReporter('extensions'); - - overrideOptions.inlineSources = Boolean(build); - overrideOptions.base = path.dirname(absolutePath); - - const compilation = tsb.create(absolutePath, overrideOptions, { verbose: false, transpileOnly, transpileOnlyIncludesDts: transpileOnly, transpileWithEsbuild: true }, err => reporter(err.toString())); - - const pipeline = function () { - const input = es.through(); - const tsFilter = filter(['**/*.ts', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true, dot: true }); - const output = input - .pipe(plumber({ - errorHandler: function (err) { - if (err && !err.__reporter__) { - reporter(err); - } - } - })) - .pipe(tsFilter) - .pipe(util.loadSourcemaps()) - .pipe(compilation()) - .pipe(build ? util.stripSourceMappingURL() : es.through()) - .pipe(sourcemaps.write('.', { - sourceMappingURL: !build ? null : f => `${baseUrl}/${f.relative}.map`, - addComment: !!build, - includeContent: !!build, - // note: trailing slash is important, else the source URLs in V8's file coverage are incorrect - sourceRoot: '../src/', - })) - .pipe(tsFilter.restore) - .pipe(reporter.end(emitError)); - - return es.duplex(input, output); - }; - - // add src-stream for project files - pipeline.tsProjectSrc = () => { - return compilation.src(srcOpts); - }; - return pipeline; - } - - const cleanTask = task.define(`clean-extension-${name}`, util.rimraf(out)); - - const transpileTask = task.define(`transpile-extension:${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(false, true, true); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); - - return input - .pipe(pipeline()) - .pipe(gulp.dest(out)); - })); - - const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(false, true); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); - - return input - .pipe(pipeline()) - .pipe(gulp.dest(out)); - })); - - const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(false); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); - const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } }); - - return watchInput - .pipe(util.incremental(pipeline, input)) - .pipe(gulp.dest(out)); - })); - - const compileBuildTask = task.define(`compile-build-extension-${name}`, task.series(cleanTask, () => { - const pipeline = createPipeline(true, true); - const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); - const input = es.merge(nonts, pipeline.tsProjectSrc()); - - return input - .pipe(pipeline()) - .pipe(gulp.dest(out)); - })); - - // Tasks - gulp.task(transpileTask); - gulp.task(compileTask); - gulp.task(watchTask); - - return { transpileTask, compileTask, watchTask, compileBuildTask }; -}); - -const transpileExtensionsTask = task.define('transpile-extensions', task.parallel(...tasks.map(t => t.transpileTask))); -gulp.task(transpileExtensionsTask); - -const compileExtensionsTask = task.define('compile-extensions', task.parallel(...tasks.map(t => t.compileTask))); -gulp.task(compileExtensionsTask); -exports.compileExtensionsTask = compileExtensionsTask; - -const watchExtensionsTask = task.define('watch-extensions', task.parallel(...tasks.map(t => t.watchTask))); -gulp.task(watchExtensionsTask); -exports.watchExtensionsTask = watchExtensionsTask; - -const compileExtensionsBuildLegacyTask = task.define('compile-extensions-build-legacy', task.parallel(...tasks.map(t => t.compileBuildTask))); -gulp.task(compileExtensionsBuildLegacyTask); - -//#region Extension media - -const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false)); -gulp.task(compileExtensionMediaTask); -exports.compileExtensionMediaTask = compileExtensionMediaTask; - -const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true)); -gulp.task(watchExtensionMedia); -exports.watchExtensionMedia = watchExtensionMedia; - -const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions')); -gulp.task(compileExtensionMediaBuildTask); -exports.compileExtensionMediaBuildTask = compileExtensionMediaBuildTask; - -//#endregion - -//#region Azure Pipelines - -/** - * Cleans the build directory for extensions - */ -const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions')); -exports.cleanExtensionsBuildTask = cleanExtensionsBuildTask; - -/** - * brings in the marketplace extensions for the build - */ -const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))); - -/** - * Compiles the non-native extensions for the build - * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. - */ -const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series( - bundleMarketplaceExtensionsBuildTask, - task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))) -)); -gulp.task(compileNonNativeExtensionsBuildTask); -exports.compileNonNativeExtensionsBuildTask = compileNonNativeExtensionsBuildTask; - -/** - * Compiles the native extensions for the build - * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. - */ -const compileNativeExtensionsBuildTask = task.define('compile-native-extensions-build', () => ext.packageNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))); -gulp.task(compileNativeExtensionsBuildTask); -exports.compileNativeExtensionsBuildTask = compileNativeExtensionsBuildTask; - -/** - * Compiles the extensions for the build. - * This is essentially a helper task that combines {@link cleanExtensionsBuildTask}, {@link compileNonNativeExtensionsBuildTask} and {@link compileNativeExtensionsBuildTask} - */ -const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series( - cleanExtensionsBuildTask, - bundleMarketplaceExtensionsBuildTask, - task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))), -)); -gulp.task(compileAllExtensionsBuildTask); -exports.compileAllExtensionsBuildTask = compileAllExtensionsBuildTask; - -// This task is run in the compilation stage of the CI pipeline. We only compile the non-native extensions since those can be fully built regardless of platform. -// This defers the native extensions to the platform specific stage of the CI pipeline. -gulp.task(task.define('extensions-ci', task.series(compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask))); - -const compileExtensionsBuildPullRequestTask = task.define('compile-extensions-build-pr', task.series( - cleanExtensionsBuildTask, - bundleMarketplaceExtensionsBuildTask, - task.define('bundle-extensions-build-pr', () => ext.packageAllLocalExtensionsStream(false, true).pipe(gulp.dest('.build'))), -)); -gulp.task(compileExtensionsBuildPullRequestTask); - -// This task is run in the compilation stage of the PR pipeline. We compile all extensions in it to verify compilation. -gulp.task(task.define('extensions-ci-pr', task.series(compileExtensionsBuildPullRequestTask, compileExtensionMediaBuildTask))); - -//#endregion - -const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false)); -gulp.task(compileWebExtensionsTask); -exports.compileWebExtensionsTask = compileWebExtensionsTask; - -const watchWebExtensionsTask = task.define('watch-web', () => buildWebExtensions(true)); -gulp.task(watchWebExtensionsTask); -exports.watchWebExtensionsTask = watchWebExtensionsTask; - -/** - * @param {boolean} isWatch - */ -async function buildWebExtensions(isWatch) { - const extensionsPath = path.join(root, 'extensions'); - const webpackConfigLocations = await nodeUtil.promisify(glob)( - path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), - { ignore: ['**/node_modules'] } - ); - return ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); -} diff --git a/build/gulpfile.extensions.ts b/build/gulpfile.extensions.ts new file mode 100644 index 00000000000..992e6f21b2d --- /dev/null +++ b/build/gulpfile.extensions.ts @@ -0,0 +1,274 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Increase max listeners for event emitters +import { EventEmitter } from 'events'; +EventEmitter.defaultMaxListeners = 100; + +import gulp from 'gulp'; +import * as path from 'path'; +import * as nodeUtil from 'util'; +import es from 'event-stream'; +import filter from 'gulp-filter'; +import * as util from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import watcher from './lib/watch/index.ts'; +import { createReporter } from './lib/reporter.ts'; +import glob from 'glob'; +import plumber from 'gulp-plumber'; +import * as ext from './lib/extensions.ts'; +import * as tsb from './lib/tsb/index.ts'; +import sourcemaps from 'gulp-sourcemaps'; + +const root = path.dirname(import.meta.dirname); +const commit = getVersion(root); + +// To save 250ms for each gulp startup, we are caching the result here +// const compilations = glob.sync('**/tsconfig.json', { +// cwd: extensionsPath, +// ignore: ['**/out/**', '**/node_modules/**'] +// }); +const compilations = [ + 'extensions/configuration-editing/tsconfig.json', + 'extensions/css-language-features/client/tsconfig.json', + 'extensions/css-language-features/server/tsconfig.json', + 'extensions/debug-auto-launch/tsconfig.json', + 'extensions/debug-server-ready/tsconfig.json', + 'extensions/emmet/tsconfig.json', + 'extensions/extension-editing/tsconfig.json', + 'extensions/git/tsconfig.json', + 'extensions/git-base/tsconfig.json', + 'extensions/github/tsconfig.json', + 'extensions/github-authentication/tsconfig.json', + 'extensions/grunt/tsconfig.json', + 'extensions/gulp/tsconfig.json', + 'extensions/html-language-features/client/tsconfig.json', + 'extensions/html-language-features/server/tsconfig.json', + 'extensions/ipynb/tsconfig.json', + 'extensions/jake/tsconfig.json', + 'extensions/json-language-features/client/tsconfig.json', + 'extensions/json-language-features/server/tsconfig.json', + 'extensions/markdown-language-features/tsconfig.json', + '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', + 'extensions/npm/tsconfig.json', + 'extensions/open-remote-ssh/tsconfig.json', + 'extensions/php-language-features/tsconfig.json', + 'extensions/references-view/tsconfig.json', + 'extensions/search-result/tsconfig.json', + 'extensions/simple-browser/tsconfig.json', + 'extensions/tunnel-forwarding/tsconfig.json', + 'extensions/typescript-language-features/web/tsconfig.json', + 'extensions/typescript-language-features/tsconfig.json', + 'extensions/vscode-api-tests/tsconfig.json', + 'extensions/vscode-colorize-tests/tsconfig.json', + 'extensions/vscode-colorize-perf-tests/tsconfig.json', + 'extensions/vscode-test-resolver/tsconfig.json', + + '.vscode/extensions/vscode-selfhost-test-provider/tsconfig.json', + '.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json', +]; + +const getBaseUrl = (out: string) => `https://main.vscode-cdn.net/sourcemaps/${commit}/${out}`; + +const tasks = compilations.map(function (tsconfigFile) { + const absolutePath = path.join(root, tsconfigFile); + const relativeDirname = path.dirname(tsconfigFile.replace(/^(.*\/)?extensions\//i, '')); + + const overrideOptions: { sourceMap?: boolean; inlineSources?: boolean; base?: string } = {}; + overrideOptions.sourceMap = true; + + const name = relativeDirname.replace(/\//g, '-'); + + const srcRoot = path.dirname(tsconfigFile); + const srcBase = path.join(srcRoot, 'src'); + const src = path.join(srcBase, '**'); + const srcOpts = { cwd: root, base: srcBase, dot: true }; + + const out = path.join(srcRoot, 'out'); + const baseUrl = getBaseUrl(out); + + function createPipeline(build: boolean, emitError?: boolean, transpileOnly?: boolean) { + const reporter = createReporter('extensions'); + + overrideOptions.inlineSources = Boolean(build); + overrideOptions.base = path.dirname(absolutePath); + + const compilation = tsb.create(absolutePath, overrideOptions, { verbose: false, transpileOnly, transpileOnlyIncludesDts: transpileOnly, transpileWithEsbuild: true }, err => reporter(err.toString())); + + const pipeline = function () { + const input = es.through(); + const tsFilter = filter(['**/*.ts', '!**/lib/lib*.d.ts', '!**/node_modules/**'], { restore: true, dot: true }); + const output = input + .pipe(plumber({ + errorHandler: function (err) { + if (err && !err.__reporter__) { + reporter(err); + } + } + })) + .pipe(tsFilter) + .pipe(util.loadSourcemaps()) + .pipe(compilation()) + .pipe(build ? util.stripSourceMappingURL() : es.through()) + .pipe(sourcemaps.write('.', { + sourceMappingURL: !build ? undefined : f => `${baseUrl}/${f.relative}.map`, + addComment: !!build, + includeContent: !!build, + // note: trailing slash is important, else the source URLs in V8's file coverage are incorrect + sourceRoot: '../src/', + })) + .pipe(tsFilter.restore) + .pipe(reporter.end(!!emitError)); + + return es.duplex(input, output); + }; + + // add src-stream for project files + pipeline.tsProjectSrc = () => { + return compilation.src(srcOpts); + }; + return pipeline; + } + + const cleanTask = task.define(`clean-extension-${name}`, util.rimraf(out)); + + const transpileTask = task.define(`transpile-extension:${name}`, task.series(cleanTask, () => { + const pipeline = createPipeline(false, true, true); + const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); + const input = es.merge(nonts, pipeline.tsProjectSrc()); + + return input + .pipe(pipeline()) + .pipe(gulp.dest(out)); + })); + + const compileTask = task.define(`compile-extension:${name}`, task.series(cleanTask, () => { + const pipeline = createPipeline(false, true); + const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); + const input = es.merge(nonts, pipeline.tsProjectSrc()); + + return input + .pipe(pipeline()) + .pipe(gulp.dest(out)); + })); + + const watchTask = task.define(`watch-extension:${name}`, task.series(cleanTask, () => { + const pipeline = createPipeline(false); + const nonts = gulp.src(src, srcOpts).pipe(filter(['**', '!**/*.ts'])); + const input = es.merge(nonts, pipeline.tsProjectSrc()); + const watchInput = watcher(src, { ...srcOpts, ...{ readDelay: 200 } }); + + return watchInput + .pipe(util.incremental(pipeline, input)) + .pipe(gulp.dest(out)); + })); + + // Tasks + gulp.task(transpileTask); + gulp.task(compileTask); + gulp.task(watchTask); + + return { transpileTask, compileTask, watchTask }; +}); + +const transpileExtensionsTask = task.define('transpile-extensions', task.parallel(...tasks.map(t => t.transpileTask))); +gulp.task(transpileExtensionsTask); + +export const compileExtensionsTask = task.define('compile-extensions', task.parallel(...tasks.map(t => t.compileTask))); +gulp.task(compileExtensionsTask); + +export const watchExtensionsTask = task.define('watch-extensions', task.parallel(...tasks.map(t => t.watchTask))); +gulp.task(watchExtensionsTask); + +//#region Extension media + +export const compileExtensionMediaTask = task.define('compile-extension-media', () => ext.buildExtensionMedia(false)); +gulp.task(compileExtensionMediaTask); + +export const watchExtensionMedia = task.define('watch-extension-media', () => ext.buildExtensionMedia(true)); +gulp.task(watchExtensionMedia); + +export const compileExtensionMediaBuildTask = task.define('compile-extension-media-build', () => ext.buildExtensionMedia(false, '.build/extensions')); +gulp.task(compileExtensionMediaBuildTask); + +//#endregion + +//#region Azure Pipelines + +/** + * Cleans the build directory for extensions + */ +export const cleanExtensionsBuildTask = task.define('clean-extensions-build', util.rimraf('.build/extensions')); + +/** + * brings in the marketplace extensions for the build + */ +const bundleMarketplaceExtensionsBuildTask = task.define('bundle-marketplace-extensions-build', () => ext.packageMarketplaceExtensionsStream(false).pipe(gulp.dest('.build'))); + +/** + * Compiles the non-native extensions for the build + * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. + */ +export const compileNonNativeExtensionsBuildTask = task.define('compile-non-native-extensions-build', task.series( + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-non-native-extensions-build', () => ext.packageNonNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))) +)); +gulp.task(compileNonNativeExtensionsBuildTask); + +/** + * Compiles the native extensions for the build + * @note this does not clean the directory ahead of it. See {@link cleanExtensionsBuildTask} for that. + */ +export const compileNativeExtensionsBuildTask = task.define('compile-native-extensions-build', () => ext.packageNativeLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))); +gulp.task(compileNativeExtensionsBuildTask); + +/** + * Compiles the extensions for the build. + * This is essentially a helper task that combines {@link cleanExtensionsBuildTask}, {@link compileNonNativeExtensionsBuildTask} and {@link compileNativeExtensionsBuildTask} + */ +export const compileAllExtensionsBuildTask = task.define('compile-extensions-build', task.series( + cleanExtensionsBuildTask, + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-extensions-build', () => ext.packageAllLocalExtensionsStream(false, false).pipe(gulp.dest('.build'))), +)); +gulp.task(compileAllExtensionsBuildTask); + +// This task is run in the compilation stage of the CI pipeline. We only compile the non-native extensions since those can be fully built regardless of platform. +// This defers the native extensions to the platform specific stage of the CI pipeline. +gulp.task(task.define('extensions-ci', task.series(compileNonNativeExtensionsBuildTask, compileExtensionMediaBuildTask))); + +const compileExtensionsBuildPullRequestTask = task.define('compile-extensions-build-pr', task.series( + cleanExtensionsBuildTask, + bundleMarketplaceExtensionsBuildTask, + task.define('bundle-extensions-build-pr', () => ext.packageAllLocalExtensionsStream(false, true).pipe(gulp.dest('.build'))), +)); +gulp.task(compileExtensionsBuildPullRequestTask); + +// This task is run in the compilation stage of the PR pipeline. We compile all extensions in it to verify compilation. +gulp.task(task.define('extensions-ci-pr', task.series(compileExtensionsBuildPullRequestTask, compileExtensionMediaBuildTask))); + +//#endregion + +export const compileWebExtensionsTask = task.define('compile-web', () => buildWebExtensions(false)); +gulp.task(compileWebExtensionsTask); + +export const watchWebExtensionsTask = task.define('watch-web', () => buildWebExtensions(true)); +gulp.task(watchWebExtensionsTask); + +async function buildWebExtensions(isWatch: boolean) { + const extensionsPath = path.join(root, 'extensions'); + const webpackConfigLocations = await nodeUtil.promisify(glob)( + path.join(extensionsPath, '**', 'extension-browser.webpack.config.js'), + { ignore: ['**/node_modules'] } + ); + return ext.webpackExtensions('packaging web extension', isWatch, webpackConfigLocations.map(configPath => ({ configPath }))); +} diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js deleted file mode 100644 index c76fab7abc6..00000000000 --- a/build/gulpfile.hygiene.js +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const gulp = require('gulp'); -const es = require('event-stream'); -const path = require('path'); -const task = require('./lib/task'); -const { hygiene } = require('./hygiene'); - -/** - * @param {string} actualPath - */ -function checkPackageJSON(actualPath) { - const actual = require(path.join(__dirname, '..', actualPath)); - const rootPackageJSON = require('../package.json'); - const checkIncluded = (set1, set2) => { - for (const depName in set1) { - const depVersion = set1[depName]; - const rootDepVersion = set2[depName]; - if (!rootDepVersion) { - // missing in root is allowed - continue; - } - if (depVersion !== rootDepVersion) { - this.emit( - 'error', - `The dependency ${depName} in '${actualPath}' (${depVersion}) is different than in the root package.json (${rootDepVersion})` - ); - } - } - }; - - checkIncluded(actual.dependencies, rootPackageJSON.dependencies); - checkIncluded(actual.devDependencies, rootPackageJSON.devDependencies); -} - -const checkPackageJSONTask = task.define('check-package-json', () => { - return gulp.src('package.json').pipe( - es.through(function () { - checkPackageJSON.call(this, 'remote/package.json'); - checkPackageJSON.call(this, 'remote/web/package.json'); - checkPackageJSON.call(this, 'build/package.json'); - }) - ); -}); -gulp.task(checkPackageJSONTask); - -const hygieneTask = task.define('hygiene', task.series(checkPackageJSONTask, () => hygiene(undefined, false))); -gulp.task(hygieneTask); diff --git a/build/gulpfile.hygiene.ts b/build/gulpfile.hygiene.ts new file mode 100644 index 00000000000..24595643c86 --- /dev/null +++ b/build/gulpfile.hygiene.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import gulp from 'gulp'; +import es from 'event-stream'; +import path from 'path'; +import fs from 'fs'; +import * as task from './lib/task.ts'; +import { hygiene } from './hygiene.ts'; + +const dirName = path.dirname(new URL(import.meta.url).pathname); + +function checkPackageJSON(this: NodeJS.ReadWriteStream, actualPath: string) { + const actual = JSON.parse(fs.readFileSync(path.join(dirName, '..', actualPath), 'utf8')); + const rootPackageJSON = JSON.parse(fs.readFileSync(path.join(dirName, '..', 'package.json'), 'utf8')); + const checkIncluded = (set1: Record, set2: Record) => { + for (const depName in set1) { + const depVersion = set1[depName]; + const rootDepVersion = set2[depName]; + if (!rootDepVersion) { + // missing in root is allowed + continue; + } + if (depVersion !== rootDepVersion) { + this.emit( + 'error', + `The dependency ${depName} in '${actualPath}' (${depVersion}) is different than in the root package.json (${rootDepVersion})` + ); + } + } + }; + + checkIncluded(actual.dependencies, rootPackageJSON.dependencies); + checkIncluded(actual.devDependencies, rootPackageJSON.devDependencies); +} + +const checkPackageJSONTask = task.define('check-package-json', () => { + return gulp.src('package.json').pipe( + es.through(function () { + checkPackageJSON.call(this, 'remote/package.json'); + checkPackageJSON.call(this, 'remote/web/package.json'); + checkPackageJSON.call(this, 'build/package.json'); + }) + ); +}); +gulp.task(checkPackageJSONTask); + +const hygieneTask = task.define('hygiene', task.series(checkPackageJSONTask, () => hygiene(undefined, false))); +gulp.task(hygieneTask); diff --git a/build/gulpfile.js b/build/gulpfile.js deleted file mode 100644 index 97971eec63e..00000000000 --- a/build/gulpfile.js +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -// Increase max listeners for event emitters -require('events').EventEmitter.defaultMaxListeners = 100; - -const gulp = require('gulp'); -const util = require('./lib/util'); -const task = require('./lib/task'); -const { transpileTask, compileTask, watchTask, compileApiProposalNamesTask, watchApiProposalNamesTask } = require('./lib/compilation'); -const { monacoTypecheckTask/* , monacoTypecheckWatchTask */ } = require('./gulpfile.editor'); -const { compileExtensionsTask, watchExtensionsTask, compileExtensionMediaTask } = require('./gulpfile.extensions'); - -// API proposal names -gulp.task(compileApiProposalNamesTask); -gulp.task(watchApiProposalNamesTask); - -// SWC Client Transpile -const transpileClientSWCTask = task.define('transpile-client-esbuild', task.series(util.rimraf('out'), transpileTask('src', 'out', true))); -gulp.task(transpileClientSWCTask); - -// Transpile only -const transpileClientTask = task.define('transpile-client', task.series(util.rimraf('out'), transpileTask('src', 'out'))); -gulp.task(transpileClientTask); - -// Fast compile for development time -const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compileApiProposalNamesTask, compileTask('src', 'out', false))); -gulp.task(compileClientTask); - -const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(watchTask('out', false), watchApiProposalNamesTask))); -gulp.task(watchClientTask); - -// All -const _compileTask = task.define('compile', task.parallel(monacoTypecheckTask, compileClientTask, compileExtensionsTask, compileExtensionMediaTask)); -gulp.task(_compileTask); - -gulp.task(task.define('watch', task.parallel(/* monacoTypecheckWatchTask, */ watchClientTask, watchExtensionsTask))); - -// Default -gulp.task('default', _compileTask); - -process.on('unhandledRejection', (reason, p) => { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - process.exit(1); -}); - -// Load all the gulpfiles only if running tasks other than the editor tasks -require('glob').sync('gulpfile.*.js', { cwd: __dirname }) - .forEach(f => require(`./${f}`)); diff --git a/build/gulpfile.reh.js b/build/gulpfile.reh.js deleted file mode 100644 index a9e9a4dc2c8..00000000000 --- a/build/gulpfile.reh.js +++ /dev/null @@ -1,491 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const gulp = require('gulp'); -const path = require('path'); -const es = require('event-stream'); -const util = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const task = require('./lib/task'); -const optimize = require('./lib/optimize'); -const { inlineMeta } = require('./lib/inlineMeta'); -const product = require('../product.json'); -const rename = require('gulp-rename'); -const replace = require('gulp-replace'); -const filter = require('gulp-filter'); -const { getProductionDependencies } = require('./lib/dependencies'); -const { readISODate } = require('./lib/date'); -const vfs = require('vinyl-fs'); -const packageJson = require('../package.json'); -const flatmap = require('gulp-flatmap'); -const gunzip = require('gulp-gunzip'); -const File = require('vinyl'); -const fs = require('fs'); -const glob = require('glob'); -const { compileBuildWithManglingTask } = require('./gulpfile.compile'); -const { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } = require('./gulpfile.extensions'); -const { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } = require('./gulpfile.vscode.web'); -const cp = require('child_process'); -const log = require('fancy-log'); -const buildfile = require('./buildfile'); - -const REPO_ROOT = path.dirname(__dirname); -const commit = getVersion(REPO_ROOT); -const BUILD_ROOT = path.dirname(REPO_ROOT); -const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); - -// Targets - -const BUILD_TARGETS = [ - { platform: 'win32', arch: 'x64' }, - { platform: 'win32', arch: 'arm64' }, - { platform: 'darwin', arch: 'x64' }, - { platform: 'darwin', arch: 'arm64' }, - { platform: 'linux', arch: 'x64' }, - { platform: 'linux', arch: 'armhf' }, - { platform: 'linux', arch: 'arm64' }, - { platform: 'linux', arch: 'ppc64le' }, - { platform: 'linux', arch: 'riscv64' }, - { platform: 'linux', arch: 'loong64' }, - { platform: 'alpine', arch: 'arm64' }, - // legacy: we use to ship only one alpine so it was put in the arch, but now we ship - // multiple alpine images and moved to a better model (alpine as the platform) - { platform: 'linux', arch: 'alpine' }, -]; - -const serverResourceIncludes = [ - - // NLS - 'out-build/nls.messages.json', - 'out-build/nls.keys.json', - - // Process monitor - 'out-build/vs/base/node/cpuUsage.sh', - 'out-build/vs/base/node/ps.sh', - - // External Terminal - 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', - - // Terminal shell integration - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1', - 'out-build/vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1', - 'out-build/vs/workbench/contrib/terminal/common/scripts/GitTabExpansion.psm1', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-env.zsh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', - -]; - -const serverResourceExcludes = [ - '!out-build/vs/**/{electron-browser,electron-main,electron-utility}/**', - '!out-build/vs/editor/standalone/**', - '!out-build/vs/workbench/**/*-tb.png', - '!**/test/**' -]; - -const serverResources = [ - ...serverResourceIncludes, - ...serverResourceExcludes -]; - -const serverWithWebResourceIncludes = [ - ...serverResourceIncludes, - 'out-build/vs/code/browser/workbench/*.html', - ...vscodeWebResourceIncludes -]; - -const serverWithWebResourceExcludes = [ - ...serverResourceExcludes, - '!out-build/vs/code/**/*-dev.html' -]; - -const serverWithWebResources = [ - ...serverWithWebResourceIncludes, - ...serverWithWebResourceExcludes -]; -const serverEntryPoints = buildfile.codeServer; - -const webEntryPoints = [ - buildfile.workerEditor, - buildfile.workerExtensionHost, - buildfile.workerNotebook, - buildfile.workerLanguageDetection, - buildfile.workerLocalFileSearch, - buildfile.workerOutputLinks, - buildfile.workerBackgroundTokenization, - buildfile.keyboardMaps, - buildfile.codeWeb -].flat(); - -const serverWithWebEntryPoints = [ - - // Include all of server - ...serverEntryPoints, - - // Include all of web - ...webEntryPoints, -].flat(); - -const bootstrapEntryPoints = [ - 'out-build/server-main.js', - 'out-build/server-cli.js', - 'out-build/bootstrap-fork.js' -]; - -function getNodeVersion() { - const npmrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.npmrc'), 'utf8'); - const nodeVersion = /^target="(.*)"$/m.exec(npmrc)[1]; - const internalNodeVersion = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; - return { nodeVersion, internalNodeVersion }; -} - -function getNodeChecksum(expectedName) { - const nodeJsChecksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'nodejs.txt'), 'utf8'); - for (const line of nodeJsChecksums.split('\n')) { - const [checksum, name] = line.split(/\s+/); - if (name === expectedName) { - return checksum; - } - } - return undefined; -} - -function extractAlpinefromDocker(nodeVersion, platform, arch) { - const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node'; - log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`); - // Increased buffer size to 500MB to handle larger Node.js binaries (v22+) - const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 500 * 1024 * 1024, encoding: 'buffer' }); - return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]); -} - -const { nodeVersion, internalNodeVersion } = getNodeVersion(); - -BUILD_TARGETS.forEach(({ platform, arch }) => { - gulp.task(task.define(`node-${platform}-${arch}`, () => { - const nodePath = path.join('.build', 'node', `v${nodeVersion}`, `${platform}-${arch}`); - - if (!fs.existsSync(nodePath)) { - util.rimraf(nodePath); - - return nodejs(platform, arch) - .pipe(vfs.dest(nodePath)); - } - - return Promise.resolve(null); - })); -}); - -const defaultNodeTask = gulp.task(`node-${process.platform}-${process.arch}`); - -if (defaultNodeTask) { - gulp.task(task.define('node', defaultNodeTask)); -} - -function nodejs(platform, arch) { - const { fetchUrls, fetchGithub } = require('./lib/fetch'); - const untar = require('gulp-untar'); - - if (arch === 'armhf') { - arch = 'armv7l'; - } else if (arch === 'alpine') { - platform = 'alpine'; - arch = 'x64'; - } - - log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); - - const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; - let expectedName; - switch (platform) { - case 'win32': - expectedName = product.nodejsRepository !== 'https://nodejs.org' ? - `win-${arch}-node.exe` : `win-${arch}/node.exe`; - break; - - case 'darwin': - expectedName = `node-v${nodeVersion}-${platform}-${arch}.tar.gz`; - break; - case 'linux': - expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; - break; - case 'alpine': - expectedName = `node-v${nodeVersion}-linux-${arch}-musl.tar.gz`; - break; - } - const checksumSha256 = getNodeChecksum(expectedName); - - if (checksumSha256) { - log(`Using SHA256 checksum for checking integrity: ${checksumSha256}`); - } else { - log.warn(`Unable to verify integrity of downloaded node.js binary because no SHA256 checksum was found!`); - } - - switch (platform) { - case 'win32': - return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : - fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) - .pipe(rename('node.exe')); - case 'darwin': - case 'linux': - return (product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) : - fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) - ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) - .pipe(filter('**/node')) - .pipe(util.setExecutableBit('**')) - .pipe(rename('node')); - case 'alpine': - return product.nodejsRepository !== 'https://nodejs.org' ? - fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName, checksumSha256 }) - .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) - .pipe(filter('**/node')) - .pipe(util.setExecutableBit('**')) - .pipe(rename('node')) - : extractAlpinefromDocker(nodeVersion, platform, arch); - } -} - -function packageTask(type, platform, arch, sourceFolderName, destinationFolderName) { - const destination = path.join(BUILD_ROOT, destinationFolderName); - - return () => { - const json = require('gulp-json-editor'); - - const src = gulp.src(sourceFolderName + '/**', { base: '.' }) - .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })) - .pipe(util.setExecutableBit(['**/*.sh'])) - .pipe(filter(['**', '!**/*.{js,css}.map'])); - - const workspaceExtensionPoints = ['debuggers', 'jsonValidation']; - const isUIExtension = (manifest) => { - switch (manifest.extensionKind) { - case 'ui': return true; - case 'workspace': return false; - default: { - if (manifest.main) { - return false; - } - if (manifest.contributes && Object.keys(manifest.contributes).some(key => workspaceExtensionPoints.indexOf(key) !== -1)) { - return false; - } - // Default is UI Extension - return true; - } - } - }; - const localWorkspaceExtensions = glob.sync('extensions/*/package.json') - .filter((extensionPath) => { - if (type === 'reh-web') { - return true; // web: ship all extensions for now - } - - // Skip shipping UI extensions because the client side will have them anyways - // and they'd just increase the download without being used - const manifest = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, extensionPath)).toString()); - return !isUIExtension(manifest); - }).map((extensionPath) => path.basename(path.dirname(extensionPath))) - .filter(name => name !== 'vscode-api-tests' && name !== 'vscode-test-resolver'); // Do not ship the test extensions - const marketplaceExtensions = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'product.json'), 'utf8')).builtInExtensions - .filter(entry => !entry.platforms || new Set(entry.platforms).has(platform)) - .filter(entry => !entry.clientOnly) - .map(entry => entry.name); - const extensionPaths = [...localWorkspaceExtensions, ...marketplaceExtensions] - .map(name => `.build/extensions/${name}/**`); - - const extensions = gulp.src(extensionPaths, { base: '.build', dot: true }); - const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true }); - const sources = es.merge(src, extensions, extensionsCommonDependencies) - .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); - - let version = packageJson.version; - const quality = product.quality; - - if (quality && quality !== 'stable') { - version += '-' + quality; - } - - const name = product.nameShort; - - let packageJsonContents; - const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) - .pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' })) - .pipe(es.through(function (file) { - packageJsonContents = file.contents.toString(); - this.emit('data', file); - })); - - let productJsonContents; - const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date: readISODate('out-build'), version })) - .pipe(es.through(function (file) { - productJsonContents = file.contents.toString(); - this.emit('data', file); - })); - - const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); - - const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); - - const productionDependencies = getProductionDependencies(REMOTE_FOLDER); - const dependenciesSrc = productionDependencies.map(d => path.relative(REPO_ROOT, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`]).flat(); - const deps = gulp.src(dependenciesSrc, { base: 'remote', dot: true }) - // filter out unnecessary files, no source maps in server build - .pipe(filter(['**', '!**/package-lock.json', '!**/*.{js,css}.map'])) - .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))) - .pipe(util.cleanNodeModules(path.join(__dirname, `.moduleignore.${process.platform}`))) - .pipe(jsFilter) - .pipe(util.stripSourceMappingURL()) - .pipe(jsFilter.restore); - - const nodePath = `.build/node/v${nodeVersion}/${platform}-${arch}`; - const node = gulp.src(`${nodePath}/**`, { base: nodePath, dot: true }); - - let web = []; - if (type === 'reh-web') { - web = [ - 'resources/server/favicon.ico', - 'resources/server/code-192.png', - 'resources/server/code-512.png', - 'resources/server/manifest.json' - ].map(resource => gulp.src(resource, { base: '.' }).pipe(rename(resource))); - } - - const all = es.merge( - packageJsonStream, - productJsonStream, - license, - sources, - deps, - node, - ...web - ); - - let result = all - .pipe(util.skipDirectories()) - .pipe(util.fixWin32DirectoryPermissions()); - - if (platform === 'win32') { - result = es.merge(result, - gulp.src('resources/server/bin/remote-cli/code.cmd', { base: '.' }) - .pipe(replace('@@VERSION@@', version)) - .pipe(replace('@@COMMIT@@', commit)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(`bin/remote-cli/${product.applicationName}.cmd`)), - gulp.src('resources/server/bin/helpers/browser.cmd', { base: '.' }) - .pipe(replace('@@VERSION@@', version)) - .pipe(replace('@@COMMIT@@', commit)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(`bin/helpers/browser.cmd`)), - gulp.src('resources/server/bin/code-server.cmd', { base: '.' }) - .pipe(rename(`bin/${product.serverApplicationName}.cmd`)), - ); - } else if (platform === 'linux' || platform === 'alpine' || platform === 'darwin') { - result = es.merge(result, - gulp.src(`resources/server/bin/remote-cli/${platform === 'darwin' ? 'code-darwin.sh' : 'code-linux.sh'}`, { base: '.' }) - .pipe(replace('@@VERSION@@', version)) - .pipe(replace('@@COMMIT@@', commit)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(`bin/remote-cli/${product.applicationName}`)) - .pipe(util.setExecutableBit()), - gulp.src(`resources/server/bin/helpers/${platform === 'darwin' ? 'browser-darwin.sh' : 'browser-linux.sh'}`, { base: '.' }) - .pipe(replace('@@VERSION@@', version)) - .pipe(replace('@@COMMIT@@', commit)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(`bin/helpers/browser.sh`)) - .pipe(util.setExecutableBit()), - gulp.src(`resources/server/bin/${platform === 'darwin' ? 'code-server-darwin.sh' : 'code-server-linux.sh'}`, { base: '.' }) - .pipe(rename(`bin/${product.serverApplicationName}`)) - .pipe(util.setExecutableBit()) - ); - } - - if (platform === 'linux' || platform === 'alpine') { - result = es.merge(result, - gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) - .pipe(rename(`bin/helpers/check-requirements.sh`)) - .pipe(util.setExecutableBit()) - ); - } - - result = inlineMeta(result, { - targetPaths: bootstrapEntryPoints, - packageJsonFn: () => packageJsonContents, - productJsonFn: () => productJsonContents - }); - - return result.pipe(vfs.dest(destination)); - }; -} - -/** - * @param {object} product The parsed product.json file contents - */ -function tweakProductForServerWeb(product) { - const result = { ...product }; - delete result.webEndpointUrlTemplate; - return result; -} - -['reh', 'reh-web'].forEach(type => { - const bundleTask = task.define(`bundle-vscode-${type}`, task.series( - util.rimraf(`out-vscode-${type}`), - optimize.bundleTask( - { - out: `out-vscode-${type}`, - esm: { - src: 'out-build', - entryPoints: [ - ...(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), - ...bootstrapEntryPoints - ], - resources: type === 'reh' ? serverResources : serverWithWebResources, - fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product) - } - } - ) - )); - - const minifyTask = task.define(`minify-vscode-${type}`, task.series( - bundleTask, - util.rimraf(`out-vscode-${type}-min`), - optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) - )); - gulp.task(minifyTask); - - BUILD_TARGETS.forEach(buildTarget => { - const dashed = (str) => (str ? `-${str}` : ``); - const platform = buildTarget.platform; - const arch = buildTarget.arch; - - ['', 'min'].forEach(minified => { - const sourceFolderName = `out-vscode-${type}${dashed(minified)}`; - const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; - - const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( - compileNativeExtensionsBuildTask, - gulp.task(`node-${platform}-${arch}`), - util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), - packageTask(type, platform, arch, sourceFolderName, destinationFolderName) - )); - gulp.task(serverTaskCI); - - const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( - compileBuildWithManglingTask, - cleanExtensionsBuildTask, - compileNonNativeExtensionsBuildTask, - compileExtensionMediaBuildTask, - minified ? minifyTask : bundleTask, - serverTaskCI - )); - gulp.task(serverTask); - }); - }); -}); diff --git a/build/gulpfile.reh.ts b/build/gulpfile.reh.ts new file mode 100644 index 00000000000..32aaf0ab652 --- /dev/null +++ b/build/gulpfile.reh.ts @@ -0,0 +1,506 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import * as path from 'path'; +import es from 'event-stream'; +import * as util from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import * as optimize from './lib/optimize.ts'; +import { inlineMeta } from './lib/inlineMeta.ts'; +import product from '../product.json' with { type: 'json' }; +import rename from 'gulp-rename'; +import replace from 'gulp-replace'; +import filter from 'gulp-filter'; +import { getProductionDependencies } from './lib/dependencies.ts'; +import { readISODate } from './lib/date.ts'; +import vfs from 'vinyl-fs'; +import packageJson from '../package.json' with { type: 'json' }; +import flatmap from 'gulp-flatmap'; +import gunzip from 'gulp-gunzip'; +import { untar } from './lib/util.ts'; +import File from 'vinyl'; +import * as fs from 'fs'; +import glob from 'glob'; +import { compileBuildWithManglingTask } from './gulpfile.compile.ts'; +import { cleanExtensionsBuildTask, compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileExtensionMediaBuildTask } from './gulpfile.extensions.ts'; +import { vscodeWebResourceIncludes, createVSCodeWebFileContentMapper } from './gulpfile.vscode.web.ts'; +import * as cp from 'child_process'; +import log from 'fancy-log'; +import buildfile from './buildfile.ts'; +import { fetchUrls, fetchGithub } from './lib/fetch.ts'; +import jsonEditor from 'gulp-json-editor'; + + +const REPO_ROOT = path.dirname(import.meta.dirname); +const commit = getVersion(REPO_ROOT); +const BUILD_ROOT = path.dirname(REPO_ROOT); +const REMOTE_FOLDER = path.join(REPO_ROOT, 'remote'); + +// Targets + +const BUILD_TARGETS = [ + { platform: 'win32', arch: 'x64' }, + { platform: 'win32', arch: 'arm64' }, + { platform: 'darwin', arch: 'x64' }, + { platform: 'darwin', arch: 'arm64' }, + { platform: 'linux', arch: 'x64' }, + { platform: 'linux', arch: 'armhf' }, + { platform: 'linux', arch: 'arm64' }, +<<<<<<< Updated upstream +======= + { platform: 'linux', arch: 'ppc64le' }, + { platform: 'linux', arch: 'riscv64' }, + { platform: 'linux', arch: 'loong64' }, +>>>>>>> Stashed changes + { platform: 'alpine', arch: 'arm64' }, + // legacy: we use to ship only one alpine so it was put in the arch, but now we ship + // multiple alpine images and moved to a better model (alpine as the platform) + { platform: 'linux', arch: 'alpine' }, +]; + +const serverResourceIncludes = [ + + // NLS + 'out-build/nls.messages.json', + 'out-build/nls.keys.json', + + // Process monitor + 'out-build/vs/base/node/cpuUsage.sh', + 'out-build/vs/base/node/ps.sh', + + // External Terminal + 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Terminal shell integration + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1', + 'out-build/vs/workbench/contrib/terminal/common/scripts/CodeTabExpansion.psm1', + 'out-build/vs/workbench/contrib/terminal/common/scripts/GitTabExpansion.psm1', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-env.zsh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-profile.zsh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration-login.zsh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish', + +]; + +const serverResourceExcludes = [ + '!out-build/vs/**/{electron-browser,electron-main,electron-utility}/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/workbench/**/*-tb.png', + '!**/test/**' +]; + +const serverResources = [ + ...serverResourceIncludes, + ...serverResourceExcludes +]; + +const serverWithWebResourceIncludes = [ + ...serverResourceIncludes, + 'out-build/vs/code/browser/workbench/*.html', + ...vscodeWebResourceIncludes +]; + +const serverWithWebResourceExcludes = [ + ...serverResourceExcludes, + '!out-build/vs/code/**/*-dev.html' +]; + +const serverWithWebResources = [ + ...serverWithWebResourceIncludes, + ...serverWithWebResourceExcludes +]; +const serverEntryPoints = buildfile.codeServer; + +const webEntryPoints = [ + buildfile.workerEditor, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.workerLocalFileSearch, + buildfile.workerOutputLinks, + buildfile.workerBackgroundTokenization, + buildfile.keyboardMaps, + buildfile.codeWeb +].flat(); + +const serverWithWebEntryPoints = [ + + // Include all of server + ...serverEntryPoints, + + // Include all of web + ...webEntryPoints, +].flat(); + +const bootstrapEntryPoints = [ + 'out-build/server-main.js', + 'out-build/server-cli.js', + 'out-build/bootstrap-fork.js' +]; + +function getNodeVersion() { + const npmrc = fs.readFileSync(path.join(REPO_ROOT, 'remote', '.npmrc'), 'utf8'); + const nodeVersion = /^target="(.*)"$/m.exec(npmrc)![1]; + const internalNodeVersion = /^ms_build_id="(.*)"$/m.exec(npmrc)![1]; + return { nodeVersion, internalNodeVersion }; +} + +function getNodeChecksum(expectedName: string): string | undefined { + const nodeJsChecksums = fs.readFileSync(path.join(REPO_ROOT, 'build', 'checksums', 'nodejs.txt'), 'utf8'); + for (const line of nodeJsChecksums.split('\n')) { + const [checksum, name] = line.split(/\s+/); + if (name === expectedName) { + return checksum; + } + } + return undefined; +} + +function extractAlpinefromDocker(nodeVersion: string, platform: string, arch: string) { + const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node'; + log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`); +<<<<<<< Updated upstream + const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); + // eslint-disable-next-line local/code-no-dangerous-type-assertions + return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } as fs.Stats })]); +======= +<<<<<<< HEAD:build/gulpfile.reh.js + // Increased buffer size to 500MB to handle larger Node.js binaries (v22+) + const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 500 * 1024 * 1024, encoding: 'buffer' }); + return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]); +======= + const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' }); + // eslint-disable-next-line local/code-no-dangerous-type-assertions + return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } as fs.Stats })]); +>>>>>>> vscode/main:build/gulpfile.reh.ts +>>>>>>> Stashed changes +} + +const { nodeVersion, internalNodeVersion } = getNodeVersion(); + +BUILD_TARGETS.forEach(({ platform, arch }) => { + gulp.task(task.define(`node-${platform}-${arch}`, () => { + const nodePath = path.join('.build', 'node', `v${nodeVersion}`, `${platform}-${arch}`); + + if (!fs.existsSync(nodePath)) { + util.rimraf(nodePath); + + return nodejs(platform, arch)! + .pipe(vfs.dest(nodePath)); + } + + return Promise.resolve(null); + })); +}); + +const defaultNodeTask = gulp.task(`node-${process.platform}-${process.arch}`); + +if (defaultNodeTask) { + // eslint-disable-next-line local/code-no-any-casts + gulp.task(task.define('node', defaultNodeTask as any)); +} + +function nodejs(platform: string, arch: string): NodeJS.ReadWriteStream | undefined { + + if (arch === 'armhf') { + arch = 'armv7l'; + } else if (arch === 'alpine') { + platform = 'alpine'; + arch = 'x64'; + } + + log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from ${product.nodejsRepository}...`); + + const glibcPrefix = process.env['VSCODE_NODE_GLIBC'] ?? ''; + let expectedName: string | undefined; + switch (platform) { + case 'win32': + expectedName = product.nodejsRepository !== 'https://nodejs.org' ? + `win-${arch}-node.exe` : `win-${arch}/node.exe`; + break; + + case 'darwin': + expectedName = `node-v${nodeVersion}-${platform}-${arch}.tar.gz`; + break; + case 'linux': + expectedName = `node-v${nodeVersion}${glibcPrefix}-${platform}-${arch}.tar.gz`; + break; + case 'alpine': + expectedName = `node-v${nodeVersion}-linux-${arch}-musl.tar.gz`; + break; + } + const checksumSha256 = expectedName ? getNodeChecksum(expectedName) : undefined; + + if (checksumSha256) { + log(`Using SHA256 checksum for checking integrity: ${checksumSha256}`); + } else { + log.warn(`Unable to verify integrity of downloaded node.js binary because no SHA256 checksum was found!`); + } + + switch (platform) { + case 'win32': + return (product.nodejsRepository !== 'https://nodejs.org' ? + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) : + fetchUrls(`/dist/v${nodeVersion}/win-${arch}/node.exe`, { base: 'https://nodejs.org', checksumSha256 })) + .pipe(rename('node.exe')); + case 'darwin': + case 'linux': + return (product.nodejsRepository !== 'https://nodejs.org' ? + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) : + fetchUrls(`/dist/v${nodeVersion}/node-v${nodeVersion}-${platform}-${arch}.tar.gz`, { base: 'https://nodejs.org', checksumSha256 }) + ).pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) + .pipe(filter('**/node')) + .pipe(util.setExecutableBit('**')) + .pipe(rename('node')); + case 'alpine': + return product.nodejsRepository !== 'https://nodejs.org' ? + fetchGithub(product.nodejsRepository, { version: `${nodeVersion}-${internalNodeVersion}`, name: expectedName!, checksumSha256 }) + .pipe(flatmap(stream => stream.pipe(gunzip()).pipe(untar()))) + .pipe(filter('**/node')) + .pipe(util.setExecutableBit('**')) + .pipe(rename('node')) + : extractAlpinefromDocker(nodeVersion, platform, arch); + } +} + +function packageTask(type: string, platform: string, arch: string, sourceFolderName: string, destinationFolderName: string) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname!.replace(new RegExp('^' + sourceFolderName), 'out'); })) + .pipe(util.setExecutableBit(['**/*.sh'])) + .pipe(filter(['**', '!**/*.{js,css}.map'])); + + const workspaceExtensionPoints = ['debuggers', 'jsonValidation']; + const isUIExtension = (manifest: { extensionKind?: string; main?: string; contributes?: Record }) => { + switch (manifest.extensionKind) { + case 'ui': return true; + case 'workspace': return false; + default: { + if (manifest.main) { + return false; + } + if (manifest.contributes && Object.keys(manifest.contributes).some(key => workspaceExtensionPoints.indexOf(key) !== -1)) { + return false; + } + // Default is UI Extension + return true; + } + } + }; + const localWorkspaceExtensions = glob.sync('extensions/*/package.json') + .filter((extensionPath) => { + if (type === 'reh-web') { + return true; // web: ship all extensions for now + } + + // Skip shipping UI extensions because the client side will have them anyways + // and they'd just increase the download without being used + const manifest = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, extensionPath)).toString()); + return !isUIExtension(manifest); + }).map((extensionPath) => path.basename(path.dirname(extensionPath))) + .filter(name => name !== 'vscode-api-tests' && name !== 'vscode-test-resolver'); // Do not ship the test extensions + const builtInExtensions: Array<{ name: string; platforms?: string[]; clientOnly?: boolean }> = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'product.json'), 'utf8')).builtInExtensions; + const marketplaceExtensions = builtInExtensions + .filter(entry => !entry.platforms || new Set(entry.platforms).has(platform)) + .filter(entry => !entry.clientOnly) + .map(entry => entry.name); + const extensionPaths = [...localWorkspaceExtensions, ...marketplaceExtensions] + .map(name => `.build/extensions/${name}/**`); + + const extensions = gulp.src(extensionPaths, { base: '.build', dot: true }); + const extensionsCommonDependencies = gulp.src('.build/extensions/node_modules/**', { base: '.build', dot: true }); + const sources = es.merge(src, extensions, extensionsCommonDependencies) + .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); + + let version = packageJson.version; + const quality = (product as typeof product & { quality?: string }).quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const name = product.nameShort; + + let packageJsonContents = ''; + const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' }) + .pipe(jsonEditor({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' })) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + + let productJsonContents = ''; + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(jsonEditor({ commit, date: readISODate('out-build'), version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); + + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); + + const productionDependencies = getProductionDependencies(REMOTE_FOLDER); + const dependenciesSrc = productionDependencies.map(d => path.relative(REPO_ROOT, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`]).flat(); + const deps = gulp.src(dependenciesSrc, { base: 'remote', dot: true }) + // filter out unnecessary files, no source maps in server build + .pipe(filter(['**', '!**/package-lock.json', '!**/*.{js,css}.map'])) + .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) + .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) + .pipe(jsFilter) + .pipe(util.stripSourceMappingURL()) + .pipe(jsFilter.restore); + + const nodePath = `.build/node/v${nodeVersion}/${platform}-${arch}`; + const node = gulp.src(`${nodePath}/**`, { base: nodePath, dot: true }); + + let web: NodeJS.ReadWriteStream[] = []; + if (type === 'reh-web') { + web = [ + 'resources/server/favicon.ico', + 'resources/server/code-192.png', + 'resources/server/code-512.png', + 'resources/server/manifest.json' + ].map(resource => gulp.src(resource, { base: '.' }).pipe(rename(resource))); + } + + const all = es.merge( + packageJsonStream, + productJsonStream, + license, + sources, + deps, + node, + ...web + ); + + let result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + if (platform === 'win32') { + result = es.merge(result, + gulp.src('resources/server/bin/remote-cli/code.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit || '')) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/remote-cli/${product.applicationName}.cmd`)), + gulp.src('resources/server/bin/helpers/browser.cmd', { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit || '')) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.cmd`)), + gulp.src('resources/server/bin/code-server.cmd', { base: '.' }) + .pipe(rename(`bin/${product.serverApplicationName}.cmd`)), + ); + } else if (platform === 'linux' || platform === 'alpine' || platform === 'darwin') { + result = es.merge(result, + gulp.src(`resources/server/bin/remote-cli/${platform === 'darwin' ? 'code-darwin.sh' : 'code-linux.sh'}`, { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit || '')) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/remote-cli/${product.applicationName}`)) + .pipe(util.setExecutableBit()), + gulp.src(`resources/server/bin/helpers/${platform === 'darwin' ? 'browser-darwin.sh' : 'browser-linux.sh'}`, { base: '.' }) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', commit || '')) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(`bin/helpers/browser.sh`)) + .pipe(util.setExecutableBit()), + gulp.src(`resources/server/bin/${platform === 'darwin' ? 'code-server-darwin.sh' : 'code-server-linux.sh'}`, { base: '.' }) + .pipe(rename(`bin/${product.serverApplicationName}`)) + .pipe(util.setExecutableBit()) + ); + } + + if (platform === 'linux' || platform === 'alpine') { + result = es.merge(result, + gulp.src(`resources/server/bin/helpers/check-requirements-linux.sh`, { base: '.' }) + .pipe(rename(`bin/helpers/check-requirements.sh`)) + .pipe(util.setExecutableBit()) + ); + } + + result = inlineMeta(result, { + targetPaths: bootstrapEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + + return result.pipe(vfs.dest(destination)); + }; +} + +/** + * @param product The parsed product.json file contents + */ +function tweakProductForServerWeb(product: typeof import('../product.json')) { + const result: typeof product & { webEndpointUrlTemplate?: string } = { ...product }; + delete result.webEndpointUrlTemplate; + return result; +} + +['reh', 'reh-web'].forEach(type => { + const bundleTask = task.define(`bundle-vscode-${type}`, task.series( + util.rimraf(`out-vscode-${type}`), + optimize.bundleTask( + { + out: `out-vscode-${type}`, + esm: { + src: 'out-build', + entryPoints: [ + ...(type === 'reh' ? serverEntryPoints : serverWithWebEntryPoints), + ...bootstrapEntryPoints + ], + resources: type === 'reh' ? serverResources : serverWithWebResources, + fileContentMapper: createVSCodeWebFileContentMapper('.build/extensions', type === 'reh-web' ? tweakProductForServerWeb(product) : product) + } + } + ) + )); + + const minifyTask = task.define(`minify-vscode-${type}`, task.series( + bundleTask, + util.rimraf(`out-vscode-${type}-min`), + optimize.minifyTask(`out-vscode-${type}`, `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) + )); + gulp.task(minifyTask); + + BUILD_TARGETS.forEach(buildTarget => { + const dashed = (str: string) => (str ? `-${str}` : ``); + const platform = buildTarget.platform; + const arch = buildTarget.arch; + + ['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-${type}${dashed(minified)}`; + const destinationFolderName = `vscode-${type}${dashed(platform)}${dashed(arch)}`; + + const serverTaskCI = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series( + compileNativeExtensionsBuildTask, + gulp.task(`node-${platform}-${arch}`) as task.Task, + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(type, platform, arch, sourceFolderName, destinationFolderName) + )); + gulp.task(serverTaskCI); + + const serverTask = task.define(`vscode-${type}${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + compileBuildWithManglingTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + minified ? minifyTask : bundleTask, + serverTaskCI + )); + gulp.task(serverTask); + }); + }); +}); diff --git a/build/gulpfile.scan.js b/build/gulpfile.scan.js deleted file mode 100644 index 8a8c1ebb1ac..00000000000 --- a/build/gulpfile.scan.js +++ /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. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const gulp = require('gulp'); -const path = require('path'); -const task = require('./lib/task'); -const util = require('./lib/util'); -const electron = require('@vscode/gulp-electron'); -const { config } = require('./lib/electron'); -const filter = require('gulp-filter'); -const deps = require('./lib/dependencies'); -const { existsSync, readdirSync } = require('fs'); - -const root = path.dirname(__dirname); - -const BUILD_TARGETS = [ - { platform: 'win32', arch: 'x64' }, - { platform: 'win32', arch: 'arm64' }, - { platform: 'darwin', arch: null, opts: { stats: true } }, - { platform: 'linux', arch: 'x64' }, - { platform: 'linux', arch: 'armhf' }, - { platform: 'linux', arch: 'arm64' }, - { platform: 'linux', arch: 'ppc64le' }, - { platform: 'linux', arch: 'riscv64' }, - { platform: 'linux', arch: 'loong64' }, -]; - -// The following files do not have PDBs downloaded for them during the download symbols process. -const excludedCheckList = ['d3dcompiler_47.dll']; - -BUILD_TARGETS.forEach(buildTarget => { - const dashed = (/** @type {string | null} */ str) => (str ? `-${str}` : ``); - const platform = buildTarget.platform; - const arch = buildTarget.arch; - - const destinationExe = path.join(path.dirname(root), 'scanbin', `VSCode${dashed(platform)}${dashed(arch)}`, 'bin'); - const destinationPdb = path.join(path.dirname(root), 'scanbin', `VSCode${dashed(platform)}${dashed(arch)}`, 'pdb'); - - const tasks = []; - - // removal tasks - tasks.push(util.rimraf(destinationExe), util.rimraf(destinationPdb)); - - // electron - tasks.push(() => electron.dest(destinationExe, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch })); - - // pdbs for windows - if (platform === 'win32') { - tasks.push( - () => electron.dest(destinationPdb, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, pdbs: true }), - () => confirmPdbsExist(destinationExe, destinationPdb) - ); - } - - if (platform === 'linux') { - tasks.push( - () => electron.dest(destinationPdb, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, symbols: true }) - ); - } - - // node modules - tasks.push( - nodeModules(destinationExe, destinationPdb, platform) - ); - - const setupSymbolsTask = task.define(`vscode-symbols${dashed(platform)}${dashed(arch)}`, - task.series(...tasks) - ); - - gulp.task(setupSymbolsTask); -}); - -function getProductionDependencySources() { - const productionDependencies = deps.getProductionDependencies(root); - return productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); -} - -function nodeModules(destinationExe, destinationPdb, platform) { - - const exe = () => { - return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) - .pipe(filter([ - '**/*.node', - // Exclude these paths. - // We don't build the prebuilt node files so we don't scan them - '!**/prebuilds/**/*.node' - ])) - .pipe(gulp.dest(destinationExe)); - }; - - if (platform === 'win32') { - const pdb = () => { - return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) - .pipe(filter(['**/*.pdb'])) - .pipe(gulp.dest(destinationPdb)); - }; - - return gulp.parallel(exe, pdb); - } - - if (platform === 'linux') { - const pdb = () => { - return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) - .pipe(filter(['**/*.sym'])) - .pipe(gulp.dest(destinationPdb)); - }; - - return gulp.parallel(exe, pdb); - } - - return exe; -} - -function confirmPdbsExist(destinationExe, destinationPdb) { - readdirSync(destinationExe).forEach(file => { - if (excludedCheckList.includes(file)) { - return; - } - - if (file.endsWith('.dll') || file.endsWith('.exe')) { - const pdb = `${file}.pdb`; - if (!existsSync(path.join(destinationPdb, pdb))) { - throw new Error(`Missing pdb file for ${file}. Tried searching for ${pdb} in ${destinationPdb}.`); - } - } - }); - return Promise.resolve(); -} diff --git a/build/gulpfile.scan.ts b/build/gulpfile.scan.ts new file mode 100644 index 00000000000..e7881dea3fb --- /dev/null +++ b/build/gulpfile.scan.ts @@ -0,0 +1,134 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import * as path from 'path'; +import * as task from './lib/task.ts'; +import * as util from './lib/util.ts'; +import electron from '@vscode/gulp-electron'; +import { config } from './lib/electron.ts'; +import filter from 'gulp-filter'; +import * as deps from './lib/dependencies.ts'; +import { existsSync, readdirSync } from 'fs'; + +const root = path.dirname(import.meta.dirname); + +const BUILD_TARGETS = [ + { platform: 'win32', arch: 'x64' }, + { platform: 'win32', arch: 'arm64' }, + { platform: 'darwin', arch: null, opts: { stats: true } }, + { platform: 'linux', arch: 'x64' }, + { platform: 'linux', arch: 'armhf' }, + { platform: 'linux', arch: 'arm64' }, + { platform: 'linux', arch: 'ppc64le' }, + { platform: 'linux', arch: 'riscv64' }, + { platform: 'linux', arch: 'loong64' }, +]; + +// The following files do not have PDBs downloaded for them during the download symbols process. +const excludedCheckList = [ + 'd3dcompiler_47.dll', + 'dxil.dll', + 'dxcompiler.dll', +]; + +BUILD_TARGETS.forEach(buildTarget => { + const dashed = (str: string | null) => (str ? `-${str}` : ``); + const platform = buildTarget.platform; + const arch = buildTarget.arch; + + const destinationExe = path.join(path.dirname(root), 'scanbin', `VSCode${dashed(platform)}${dashed(arch)}`, 'bin'); + const destinationPdb = path.join(path.dirname(root), 'scanbin', `VSCode${dashed(platform)}${dashed(arch)}`, 'pdb'); + + const tasks: task.Task[] = []; + + // removal tasks + tasks.push(util.rimraf(destinationExe), util.rimraf(destinationPdb)); + + // electron + tasks.push(() => electron.dest(destinationExe, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch })); + + // pdbs for windows + if (platform === 'win32') { + tasks.push( + () => electron.dest(destinationPdb, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, pdbs: true }), + () => confirmPdbsExist(destinationExe, destinationPdb) + ); + } + + if (platform === 'linux') { + tasks.push( + () => electron.dest(destinationPdb, { ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, symbols: true }) + ); + } + + // node modules + tasks.push( + nodeModules(destinationExe, destinationPdb, platform) + ); + + const setupSymbolsTask = task.define(`vscode-symbols${dashed(platform)}${dashed(arch)}`, + task.series(...tasks) + ); + + gulp.task(setupSymbolsTask); +}); + +function getProductionDependencySources() { + const productionDependencies = deps.getProductionDependencies(root); + return productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); +} + +function nodeModules(destinationExe: string, destinationPdb: string, platform: string): task.CallbackTask { + + const exe = () => { + return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) + .pipe(filter([ + '**/*.node', + // Exclude these paths. + // We don't build the prebuilt node files so we don't scan them + '!**/prebuilds/**/*.node' + ])) + .pipe(gulp.dest(destinationExe)); + }; + + if (platform === 'win32') { + const pdb = () => { + return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) + .pipe(filter(['**/*.pdb'])) + .pipe(gulp.dest(destinationPdb)); + }; + + return gulp.parallel(exe, pdb) as task.CallbackTask; + } + + if (platform === 'linux') { + const pdb = () => { + return gulp.src(getProductionDependencySources(), { base: '.', dot: true }) + .pipe(filter(['**/*.sym'])) + .pipe(gulp.dest(destinationPdb)); + }; + + return gulp.parallel(exe, pdb) as task.CallbackTask; + } + + return exe; +} + +function confirmPdbsExist(destinationExe: string, destinationPdb: string) { + readdirSync(destinationExe).forEach(file => { + if (excludedCheckList.includes(file)) { + return; + } + + if (file.endsWith('.dll') || file.endsWith('.exe')) { + const pdb = `${file}.pdb`; + if (!existsSync(path.join(destinationPdb, pdb))) { + throw new Error(`Missing pdb file for ${file}. Tried searching for ${pdb} in ${destinationPdb}.`); + } + } + }); + return Promise.resolve(); +} diff --git a/build/gulpfile.ts b/build/gulpfile.ts new file mode 100644 index 00000000000..a8e2917035e --- /dev/null +++ b/build/gulpfile.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { EventEmitter } from 'events'; +import glob from 'glob'; +import gulp from 'gulp'; +import { createRequire } from 'node:module'; +import { monacoTypecheckTask /* , monacoTypecheckWatchTask */ } from './gulpfile.editor.ts'; +import { compileExtensionMediaTask, compileExtensionsTask, watchExtensionsTask } from './gulpfile.extensions.ts'; +import * as compilation from './lib/compilation.ts'; +import * as task from './lib/task.ts'; +import * as util from './lib/util.ts'; + +EventEmitter.defaultMaxListeners = 100; + +const require = createRequire(import.meta.url); + +const { transpileTask, compileTask, watchTask, compileApiProposalNamesTask, watchApiProposalNamesTask } = compilation; + +// API proposal names +gulp.task(compileApiProposalNamesTask); +gulp.task(watchApiProposalNamesTask); + +// SWC Client Transpile +const transpileClientSWCTask = task.define('transpile-client-esbuild', task.series(util.rimraf('out'), transpileTask('src', 'out', true))); +gulp.task(transpileClientSWCTask); + +// Transpile only +const transpileClientTask = task.define('transpile-client', task.series(util.rimraf('out'), transpileTask('src', 'out'))); +gulp.task(transpileClientTask); + +// Fast compile for development time +const compileClientTask = task.define('compile-client', task.series(util.rimraf('out'), compilation.copyCodiconsTask, compileApiProposalNamesTask, compileTask('src', 'out', false))); +gulp.task(compileClientTask); + +const watchClientTask = task.define('watch-client', task.series(util.rimraf('out'), task.parallel(watchTask('out', false), watchApiProposalNamesTask, compilation.watchCodiconsTask))); +gulp.task(watchClientTask); + +// All +const _compileTask = task.define('compile', task.parallel(monacoTypecheckTask, compileClientTask, compileExtensionsTask, compileExtensionMediaTask)); +gulp.task(_compileTask); + +gulp.task(task.define('watch', task.parallel(/* monacoTypecheckWatchTask, */ watchClientTask, watchExtensionsTask))); + +// Default +gulp.task('default', _compileTask); + +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); +}); + +// Load all the gulpfiles only if running tasks other than the editor tasks +glob.sync('gulpfile.*.ts', { cwd: import.meta.dirname }) + .forEach(f => { + return require(`./${f}`); + }); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js deleted file mode 100644 index d87f090f647..00000000000 --- a/build/gulpfile.vscode.js +++ /dev/null @@ -1,594 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const gulp = require('gulp'); -const fs = require('fs'); -const path = require('path'); -const es = require('event-stream'); -const vfs = require('vinyl-fs'); -const rename = require('gulp-rename'); -const replace = require('gulp-replace'); -const filter = require('gulp-filter'); -const util = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const { readISODate } = require('./lib/date'); -const task = require('./lib/task'); -const buildfile = require('./buildfile'); -const optimize = require('./lib/optimize'); -const { inlineMeta } = require('./lib/inlineMeta'); -const root = path.dirname(__dirname); -const commit = getVersion(root); -const packageJson = require('../package.json'); -const product = require('../product.json'); -const crypto = require('crypto'); -const i18n = require('./lib/i18n'); -const { getProductionDependencies } = require('./lib/dependencies'); -const { config } = require('./lib/electron'); -const createAsar = require('./lib/asar').createAsar; -const minimist = require('minimist'); -const { compileBuildWithoutManglingTask, compileBuildWithManglingTask } = require('./gulpfile.compile'); -const { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } = require('./gulpfile.extensions'); -const { promisify } = require('util'); -const glob = promisify(require('glob')); -const rcedit = promisify(require('rcedit')); - -// Build -const vscodeEntryPoints = [ - buildfile.workerEditor, - buildfile.workerExtensionHost, - buildfile.workerNotebook, - buildfile.workerLanguageDetection, - buildfile.workerLocalFileSearch, - buildfile.workerProfileAnalysis, - buildfile.workerOutputLinks, - buildfile.workerBackgroundTokenization, - buildfile.workbenchDesktop, - buildfile.code -].flat(); - -const vscodeResourceIncludes = [ - - // NLS - 'out-build/nls.messages.json', - 'out-build/nls.keys.json', - - // Workbench - 'out-build/vs/code/electron-browser/workbench/workbench.html', - - // Electron Preload - 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', - 'out-build/vs/base/parts/sandbox/electron-browser/preload-aux.js', - - // Node Scripts - 'out-build/vs/base/node/{terminateProcess.sh,cpuUsage.sh,ps.sh}', - - // Touchbar - 'out-build/vs/workbench/browser/parts/editor/media/*.png', - 'out-build/vs/workbench/contrib/debug/browser/media/*.png', - - // External Terminal - 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', - - // Terminal shell integration - 'out-build/vs/workbench/contrib/terminal/common/scripts/*.fish', - 'out-build/vs/workbench/contrib/terminal/common/scripts/*.ps1', - 'out-build/vs/workbench/contrib/terminal/common/scripts/*.psm1', - 'out-build/vs/workbench/contrib/terminal/common/scripts/*.sh', - 'out-build/vs/workbench/contrib/terminal/common/scripts/*.zsh', - - // Accessibility Signals - 'out-build/vs/platform/accessibilitySignal/browser/media/*.mp3', - - // Welcome - 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', - - // Workbench Media (logo, icons) - 'out-build/vs/workbench/browser/media/**/*.{svg,png}', - - // Extensions - 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', - 'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}', - - // Webview - 'out-build/vs/workbench/contrib/webview/browser/pre/*.{js,html}', - - // Extension Host Worker - 'out-build/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', - - // Tree Sitter highlights - 'out-build/vs/editor/common/languages/highlights/*.scm', - - // Tree Sitter injection queries - 'out-build/vs/editor/common/languages/injections/*.scm' -]; - -const vscodeResources = [ - - // Includes - ...vscodeResourceIncludes, - - // Excludes - '!out-build/vs/code/browser/**', - '!out-build/vs/editor/standalone/**', - '!out-build/vs/code/**/*-dev.html', - '!out-build/vs/workbench/contrib/issue/**/*-dev.html', - '!**/test/**' -]; - -const bootstrapEntryPoints = [ - 'out-build/main.js', - 'out-build/cli.js', - 'out-build/bootstrap-fork.js' -]; - -const bundleVSCodeTask = task.define('bundle-vscode', task.series( - util.rimraf('out-vscode'), - // Optimize: bundles source files automatically based on - // import statements based on the passed in entry points. - // In addition, concat window related bootstrap files into - // a single file. - optimize.bundleTask( - { - out: 'out-vscode', - esm: { - src: 'out-build', - entryPoints: [ - ...vscodeEntryPoints, - ...bootstrapEntryPoints - ], - resources: vscodeResources, - skipTSBoilerplateRemoval: entryPoint => entryPoint === 'vs/code/electron-browser/workbench/workbench' - } - } - ) -)); -gulp.task(bundleVSCodeTask); - -const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; -const minifyVSCodeTask = task.define('minify-vscode', task.series( - bundleVSCodeTask, - util.rimraf('out-vscode-min'), - optimize.minifyTask('out-vscode', `${sourceMappingURLBase}/core`) -)); -gulp.task(minifyVSCodeTask); - -const coreCI = task.define('core-ci', task.series( - gulp.task('compile-build-with-mangling'), - task.parallel( - gulp.task('minify-vscode'), - gulp.task('minify-vscode-reh'), - gulp.task('minify-vscode-reh-web'), - ) -)); -gulp.task(coreCI); - -const coreCIPR = task.define('core-ci-pr', task.series( - gulp.task('compile-build-without-mangling'), - task.parallel( - gulp.task('minify-vscode'), - gulp.task('minify-vscode-reh'), - gulp.task('minify-vscode-reh-web'), - ) -)); -gulp.task(coreCIPR); - -/** - * Compute checksums for some files. - * - * @param {string} out The out folder to read the file from. - * @param {string[]} filenames The paths to compute a checksum for. - * @return {Object} A map of paths to checksums. - */ -function computeChecksums(out, filenames) { - const result = {}; - filenames.forEach(function (filename) { - const fullPath = path.join(process.cwd(), out, filename); - result[filename] = computeChecksum(fullPath); - }); - return result; -} - -/** - * Compute checksum for a file. - * - * @param {string} filename The absolute path to a filename. - * @return {string} The checksum for `filename`. - */ -function computeChecksum(filename) { - const contents = fs.readFileSync(filename); - - const hash = crypto - .createHash('sha256') - .update(contents) - .digest('base64') - .replace(/=+$/, ''); - - return hash; -} - -function packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) { - opts = opts || {}; - - const destination = path.join(path.dirname(root), destinationFolderName); - platform = platform || process.platform; - - const task = () => { - const electron = require('@vscode/gulp-electron'); - const json = require('gulp-json-editor'); - - const out = sourceFolderName; - - const checksums = computeChecksums(out, [ - 'vs/base/parts/sandbox/electron-browser/preload.js', - 'vs/workbench/workbench.desktop.main.js', - 'vs/workbench/workbench.desktop.main.css', - 'vs/workbench/api/node/extensionHostProcess.js', - 'vs/code/electron-browser/workbench/workbench.html', - 'vs/code/electron-browser/workbench/workbench.js' - ]); - - const src = gulp.src(out + '/**', { base: '.' }) - .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + out), 'out'); })) - .pipe(util.setExecutableBit(['**/*.sh'])); - - const platformSpecificBuiltInExtensionsExclusions = product.builtInExtensions.filter(ext => { - if (!ext.platforms) { - return false; - } - - const set = new Set(ext.platforms); - return !set.has(platform); - }).map(ext => `!.build/extensions/${ext.name}/**`); - - const extensions = gulp.src(['.build/extensions/**', ...platformSpecificBuiltInExtensionsExclusions], { base: '.build', dot: true }); - - const sources = es.merge(src, extensions) - .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); - - let version = packageJson.version; - const quality = product.quality; - - if (quality && quality !== 'stable') { - version += '-' + quality; - } - - const name = product.nameShort; - const packageJsonUpdates = { name, version }; - - if (platform === 'linux') { - packageJsonUpdates.desktopName = `${product.applicationName}.desktop`; - } - - let packageJsonContents; - const packageJsonStream = gulp.src(['package.json'], { base: '.' }) - .pipe(json(packageJsonUpdates)) - .pipe(es.through(function (file) { - packageJsonContents = file.contents.toString(); - this.emit('data', file); - })); - - let productJsonContents; - const productJsonStream = gulp.src(['product.json'], { base: '.' }) - .pipe(json({ commit, date: readISODate('out-build'), checksums, version })) - .pipe(es.through(function (file) { - productJsonContents = file.contents.toString(); - this.emit('data', file); - })); - - const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); - - // TODO the API should be copied to `out` during compile, not here - const api = gulp.src('src/vscode-dts/vscode.d.ts').pipe(rename('out/vscode-dts/vscode.d.ts')); - - const telemetry = gulp.src('.build/telemetry/**', { base: '.build/telemetry', dot: true }); - - const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); - const root = path.resolve(path.join(__dirname, '..')); - const productionDependencies = getProductionDependencies(root); - const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); - - const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) - .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.{js,css}.map'])) - .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))) - .pipe(util.cleanNodeModules(path.join(__dirname, `.moduleignore.${process.platform}`))) - .pipe(jsFilter) - .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) - .pipe(jsFilter.restore) - .pipe(createAsar(path.join(process.cwd(), 'node_modules'), [ - '**/*.node', - '**/@vscode/ripgrep/bin/*', - '**/node-pty/build/Release/*', - '**/node-pty/build/Release/conpty/*', - '**/node-pty/lib/worker/conoutSocketWorker.js', - '**/node-pty/lib/shared/conout.js', - '**/*.wasm', - '**/@vscode/vsce-sign/bin/*', - ], [ - '**/*.mk', - '!node_modules/vsda/**' // stay compatible with extensions that depend on us shipping `vsda` into ASAR - ], [ - 'node_modules/vsda/**' // retain copy of `vsda` in node_modules for internal use - ], 'node_modules.asar')); - - let all = es.merge( - packageJsonStream, - productJsonStream, - license, - api, - telemetry, - sources, - deps - ); - - if (platform === 'win32') { - all = es.merge(all, gulp.src([ - 'resources/win32/bower.ico', - 'resources/win32/c.ico', - 'resources/win32/config.ico', - 'resources/win32/cpp.ico', - 'resources/win32/csharp.ico', - 'resources/win32/css.ico', - 'resources/win32/default.ico', - 'resources/win32/go.ico', - 'resources/win32/html.ico', - 'resources/win32/jade.ico', - 'resources/win32/java.ico', - 'resources/win32/javascript.ico', - 'resources/win32/json.ico', - 'resources/win32/less.ico', - 'resources/win32/markdown.ico', - 'resources/win32/php.ico', - 'resources/win32/powershell.ico', - 'resources/win32/python.ico', - 'resources/win32/react.ico', - 'resources/win32/ruby.ico', - 'resources/win32/sass.ico', - 'resources/win32/shell.ico', - 'resources/win32/sql.ico', - 'resources/win32/typescript.ico', - 'resources/win32/vue.ico', - 'resources/win32/xml.ico', - 'resources/win32/yaml.ico', - 'resources/win32/code_70x70.png', - 'resources/win32/code_150x150.png' - ], { base: '.' })); - } else if (platform === 'linux') { - const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) - .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); - all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }), policyDest); - } else if (platform === 'darwin') { - const shortcut = gulp.src('resources/darwin/bin/code.sh') - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('bin/code')); - const policyDest = gulp.src('.build/policies/darwin/**', { base: '.build/policies/darwin' }) - .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); - all = es.merge(all, shortcut, policyDest); - } - - // CORTEXIDE/VSCODIUM: Support custom Electron repositories for alternative architectures - // This allows using VSCODE_ELECTRON_REPOSITORY and VSCODE_ELECTRON_TAG env vars - const electronOverride = {}; - if (process.env.VSCODE_ELECTRON_REPOSITORY) { - electronOverride.repo = process.env.VSCODE_ELECTRON_REPOSITORY; - } - if (process.env.VSCODE_ELECTRON_TAG) { - electronOverride.tag = process.env.VSCODE_ELECTRON_TAG; - } - const hasElectronOverride = electronOverride.repo || electronOverride.tag; - - let result = all - .pipe(util.skipDirectories()) - .pipe(util.fixWin32DirectoryPermissions()) - .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 - .pipe(electron({ ...config, ...(hasElectronOverride ? electronOverride : {}), platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: false })) - .pipe(filter(['**', '!LICENSE', '!version'], { dot: true })); - - if (platform === 'linux') { - result = es.merge(result, gulp.src('resources/completions/bash/code', { base: '.' }) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(function (f) { f.basename = product.applicationName; }))); - - result = es.merge(result, gulp.src('resources/completions/zsh/_code', { base: '.' }) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename(function (f) { f.basename = '_' + product.applicationName; }))); - } - - if (platform === 'win32') { - result = es.merge(result, gulp.src('resources/win32/bin/code.js', { base: 'resources/win32', allowEmpty: true })); - - result = es.merge(result, gulp.src('resources/win32/bin/code.cmd', { base: 'resources/win32' }) - .pipe(replace('@@NAME@@', product.nameShort)) - .pipe(rename(function (f) { f.basename = product.applicationName; }))); - - result = es.merge(result, gulp.src('resources/win32/bin/code.sh', { base: 'resources/win32' }) - .pipe(replace('@@NAME@@', product.nameShort)) - .pipe(replace('@@PRODNAME@@', product.nameLong)) - .pipe(replace('@@VERSION@@', version)) - .pipe(replace('@@COMMIT@@', commit)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(replace('@@SERVERDATAFOLDER@@', product.serverDataFolderName || '.vscode-remote')) - .pipe(replace('@@QUALITY@@', quality)) - .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); - - result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) - .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); - - result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) - .pipe(rename(f => f.dirname = `policies/${f.dirname}`))); - - if (quality === 'stable' || quality === 'insider') { - result = es.merge(result, gulp.src('.build/win32/appx/**', { base: '.build/win32' })); - const rawVersion = version.replace(/-\w+$/, '').split('.'); - const appxVersion = `${rawVersion[0]}.0.${rawVersion[1]}.${rawVersion[2]}`; - result = es.merge(result, gulp.src('resources/win32/appx/AppxManifest.xml', { base: '.' }) - .pipe(replace('@@AppxPackageName@@', product.win32AppUserModelId)) - .pipe(replace('@@AppxPackageVersion@@', appxVersion)) - .pipe(replace('@@AppxPackageDisplayName@@', product.nameLong)) - .pipe(replace('@@AppxPackageDescription@@', product.win32NameVersion)) - .pipe(replace('@@ApplicationIdShort@@', product.win32RegValueName)) - .pipe(replace('@@ApplicationExe@@', product.nameShort + '.exe')) - .pipe(replace('@@FileExplorerContextMenuID@@', quality === 'stable' ? 'OpenWithCode' : 'OpenWithCodeInsiders')) - .pipe(replace('@@FileExplorerContextMenuCLSID@@', product.win32ContextMenu[arch].clsid)) - .pipe(replace('@@FileExplorerContextMenuDLL@@', `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`)) - .pipe(rename(f => f.dirname = `appx/manifest`))); - } - } else if (platform === 'linux') { - result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) - .pipe(replace('@@PRODNAME@@', product.nameLong)) - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('bin/' + product.applicationName))); - } - - result = inlineMeta(result, { - targetPaths: bootstrapEntryPoints, - packageJsonFn: () => packageJsonContents, - productJsonFn: () => productJsonContents - }); - - return result.pipe(vfs.dest(destination)); - }; - task.taskName = `package-${platform}-${arch}`; - return task; -} - -function patchWin32DependenciesTask(destinationFolderName) { - const cwd = path.join(path.dirname(root), destinationFolderName); - - return async () => { - const deps = await glob('**/*.node', { cwd, ignore: 'extensions/node_modules/@parcel/watcher/**' }); - const packageJson = JSON.parse(await fs.promises.readFile(path.join(cwd, 'resources', 'app', 'package.json'), 'utf8')); - const product = JSON.parse(await fs.promises.readFile(path.join(cwd, 'resources', 'app', 'product.json'), 'utf8')); - const baseVersion = packageJson.version.replace(/-.*$/, ''); - - await Promise.all(deps.map(async dep => { - const basename = path.basename(dep); - - await rcedit(path.join(cwd, dep), { - 'file-version': baseVersion, - 'version-string': { - 'CompanyName': 'Microsoft Corporation', - 'FileDescription': product.nameLong, - 'FileVersion': packageJson.version, - 'InternalName': basename, - 'LegalCopyright': 'Copyright (C) 2022 Microsoft. All rights reserved', - 'OriginalFilename': basename, - 'ProductName': product.nameLong, - 'ProductVersion': packageJson.version, - } - }); - })); - }; -} - -const buildRoot = path.dirname(root); - -const BUILD_TARGETS = [ - { platform: 'win32', arch: 'x64' }, - { platform: 'win32', arch: 'arm64' }, - { platform: 'darwin', arch: 'x64', opts: { stats: true } }, - { platform: 'darwin', arch: 'arm64', opts: { stats: true } }, - { platform: 'linux', arch: 'x64' }, - { platform: 'linux', arch: 'armhf' }, - { platform: 'linux', arch: 'arm64' }, - { platform: 'linux', arch: 'ppc64le' }, - { platform: 'linux', arch: 'riscv64' }, - { platform: 'linux', arch: 'loong64' }, -]; -BUILD_TARGETS.forEach(buildTarget => { - const dashed = (str) => (str ? `-${str}` : ``); - const platform = buildTarget.platform; - const arch = buildTarget.arch; - const opts = buildTarget.opts; - - const [vscode, vscodeMin] = ['', 'min'].map(minified => { - const sourceFolderName = `out-vscode${dashed(minified)}`; - const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; - - const tasks = [ - compileNativeExtensionsBuildTask, - util.rimraf(path.join(buildRoot, destinationFolderName)), - packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) - ]; - - if (platform === 'win32') { - tasks.push(patchWin32DependenciesTask(destinationFolderName)); - } - - const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...tasks)); - gulp.task(vscodeTaskCI); - - const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( - minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask, - cleanExtensionsBuildTask, - compileNonNativeExtensionsBuildTask, - compileExtensionMediaBuildTask, - minified ? minifyVSCodeTask : bundleVSCodeTask, - vscodeTaskCI - )); - gulp.task(vscodeTask); - - return vscodeTask; - }); - - if (process.platform === platform && process.arch === arch) { - gulp.task(task.define('vscode', task.series(vscode))); - gulp.task(task.define('vscode-min', task.series(vscodeMin))); - } -}); - -// #region nls - -const innoSetupConfig = { - 'zh-cn': { codePage: 'CP936', defaultInfo: { name: 'Simplified Chinese', id: '$0804', } }, - 'zh-tw': { codePage: 'CP950', defaultInfo: { name: 'Traditional Chinese', id: '$0404' } }, - 'ko': { codePage: 'CP949', defaultInfo: { name: 'Korean', id: '$0412' } }, - 'ja': { codePage: 'CP932' }, - 'de': { codePage: 'CP1252' }, - 'fr': { codePage: 'CP1252' }, - 'es': { codePage: 'CP1252' }, - 'ru': { codePage: 'CP1251' }, - 'it': { codePage: 'CP1252' }, - 'pt-br': { codePage: 'CP1252' }, - 'hu': { codePage: 'CP1250' }, - 'tr': { codePage: 'CP1254' } -}; - -gulp.task(task.define( - 'vscode-translations-export', - task.series( - coreCI, - compileAllExtensionsBuildTask, - function () { - const pathToMetadata = './out-build/nls.metadata.json'; - const pathToExtensions = '.build/extensions/*'; - const pathToSetup = 'build/win32/i18n/messages.en.isl'; - - return es.merge( - gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), - gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), - gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) - ).pipe(vfs.dest('../vscode-translations-export')); - } - ) -)); - -gulp.task('vscode-translations-import', function () { - const options = minimist(process.argv.slice(2), { - string: 'location', - default: { - location: '../vscode-translations-import' - } - }); - return es.merge([...i18n.defaultLanguages, ...i18n.extraLanguages].map(language => { - const id = language.id; - return gulp.src(`${options.location}/${id}/vscode-setup/messages.xlf`) - .pipe(i18n.prepareIslFiles(language, innoSetupConfig[language.id])) - .pipe(vfs.dest(`./build/win32/i18n`)); - })); -}); - -// #endregion diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js deleted file mode 100644 index c239457e224..00000000000 --- a/build/gulpfile.vscode.linux.js +++ /dev/null @@ -1,328 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const gulp = require('gulp'); -const replace = require('gulp-replace'); -const rename = require('gulp-rename'); -const es = require('event-stream'); -const vfs = require('vinyl-fs'); -const { rimraf } = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const task = require('./lib/task'); -const packageJson = require('../package.json'); -const product = require('../product.json'); -const dependenciesGenerator = require('./linux/dependencies-generator'); -const debianRecommendedDependencies = require('./linux/debian/dep-lists').recommendedDeps; -const path = require('path'); -const cp = require('child_process'); -const util = require('util'); - -const exec = util.promisify(cp.exec); -const root = path.dirname(__dirname); -const commit = getVersion(root); - -const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); - -/** - * @param {string} arch - */ -function getDebPackageArch(arch) { - return { x64: 'amd64', armhf: 'armhf', arm64: 'arm64', ppc64le: 'ppc64el', riscv64: 'riscv64', loong64: 'loong64' }[arch]; -} - -function prepareDebPackage(arch) { - const binaryDir = '../VSCode-linux-' + arch; - const debArch = getDebPackageArch(arch); - const destination = '.build/linux/deb/' + debArch + '/' + product.applicationName + '-' + debArch; - - return async function () { - const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch); - - const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) - .pipe(rename('usr/share/applications/' + product.applicationName + '.desktop')); - - const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) - .pipe(rename('usr/share/applications/' + product.applicationName + '-url-handler.desktop')); - - const desktops = es.merge(desktop, desktopUrlHandler) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME_SHORT@@', product.nameShort)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@EXEC@@', `/usr/share/${product.applicationName}/${product.applicationName}`)) - .pipe(replace('@@ICON@@', product.linuxIconName)) - .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); - - const appdata = gulp.src('resources/linux/code.appdata.xml', { base: '.' }) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@LICENSE@@', product.licenseName)) - .pipe(rename('usr/share/appdata/' + product.applicationName + '.appdata.xml')); - - const workspaceMime = gulp.src('resources/linux/code-workspace.xml', { base: '.' }) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(rename('usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); - - const icon = gulp.src('resources/linux/code.png', { base: '.' }) - .pipe(rename('usr/share/pixmaps/' + product.linuxIconName + '.png')); - - const bash_completion = gulp.src('resources/completions/bash/code') - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('usr/share/bash-completion/completions/' + product.applicationName)); - - const zsh_completion = gulp.src('resources/completions/zsh/_code') - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('usr/share/zsh/vendor-completions/_' + product.applicationName)); - - const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) - .pipe(rename(function (p) { p.dirname = 'usr/share/' + product.applicationName + '/' + p.dirname; })); - - let size = 0; - const control = code.pipe(es.through( - function (f) { size += f.isDirectory() ? 4096 : f.contents.length; }, - function () { - const that = this; - gulp.src('resources/linux/debian/control.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision)) - .pipe(replace('@@ARCHITECTURE@@', debArch)) - .pipe(replace('@@DEPENDS@@', dependencies.join(', '))) - .pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', '))) - .pipe(replace('@@INSTALLEDSIZE@@', Math.ceil(size / 1024))) - .pipe(rename('DEBIAN/control')) - .pipe(es.through(function (f) { that.emit('data', f); }, function () { that.emit('end'); })); - })); - - const prerm = gulp.src('resources/linux/debian/prerm.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(rename('DEBIAN/prerm')); - - const postrm = gulp.src('resources/linux/debian/postrm.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(rename('DEBIAN/postrm')); - - const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@ARCHITECTURE@@', debArch)) - .pipe(rename('DEBIAN/postinst')); - - const templates = gulp.src('resources/linux/debian/templates.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(rename('DEBIAN/templates')); - - const all = es.merge(control, templates, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); - - return all.pipe(vfs.dest(destination)); - }; -} - -/** - * @param {string} arch - */ -function buildDebPackage(arch) { - const debArch = getDebPackageArch(arch); - const cwd = `.build/linux/deb/${debArch}`; - - return async () => { - await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); - await exec('mkdir -p deb', { cwd }); - await exec(`fakeroot dpkg-deb -Zxz -b ${product.applicationName}-${debArch} deb`, { cwd }); - }; -} - -/** - * @param {string} rpmArch - */ -function getRpmBuildPath(rpmArch) { - return '.build/linux/rpm/' + rpmArch + '/rpmbuild'; -} - -/** - * @param {string} arch - */ -function getRpmPackageArch(arch) { - return { x64: 'x86_64', armhf: 'armv7hl', arm64: 'aarch64', ppc64le: 'ppc64le', riscv64: 'riscv64', loong64: 'loong64' }[arch]; -} - -/** - * @param {string} arch - */ -function prepareRpmPackage(arch) { - const binaryDir = '../VSCode-linux-' + arch; - const rpmArch = getRpmPackageArch(arch); - const stripBinary = process.env['STRIP'] ?? '/usr/bin/strip'; - - return async function () { - const dependencies = await dependenciesGenerator.getDependencies('rpm', binaryDir, product.applicationName, rpmArch); - - const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) - .pipe(rename('BUILD/usr/share/applications/' + product.applicationName + '.desktop')); - - const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) - .pipe(rename('BUILD/usr/share/applications/' + product.applicationName + '-url-handler.desktop')); - - const desktops = es.merge(desktop, desktopUrlHandler) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME_SHORT@@', product.nameShort)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@EXEC@@', `/usr/share/${product.applicationName}/${product.applicationName}`)) - .pipe(replace('@@ICON@@', product.linuxIconName)) - .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); - - const appdata = gulp.src('resources/linux/code.appdata.xml', { base: '.' }) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@LICENSE@@', product.licenseName)) - .pipe(rename('BUILD/usr/share/appdata/' + product.applicationName + '.appdata.xml')); - - const workspaceMime = gulp.src('resources/linux/code-workspace.xml', { base: '.' }) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(rename('BUILD/usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); - - const icon = gulp.src('resources/linux/code.png', { base: '.' }) - .pipe(rename('BUILD/usr/share/pixmaps/' + product.linuxIconName + '.png')); - - const bash_completion = gulp.src('resources/completions/bash/code') - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('BUILD/usr/share/bash-completion/completions/' + product.applicationName)); - - const zsh_completion = gulp.src('resources/completions/zsh/_code') - .pipe(replace('@@APPNAME@@', product.applicationName)) - .pipe(rename('BUILD/usr/share/zsh/site-functions/_' + product.applicationName)); - - const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) - .pipe(rename(function (p) { p.dirname = 'BUILD/usr/share/' + product.applicationName + '/' + p.dirname; })); - - const spec = gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@ICON@@', product.linuxIconName)) - .pipe(replace('@@VERSION@@', packageJson.version)) - .pipe(replace('@@RELEASE@@', linuxPackageRevision)) - .pipe(replace('@@ARCHITECTURE@@', rpmArch)) - .pipe(replace('@@LICENSE@@', product.licenseName)) - .pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@')) - .pipe(replace('@@UPDATEURL@@', product.updateUrl || '@@UPDATEURL@@')) - .pipe(replace('@@DEPENDENCIES@@', dependencies.join(', '))) - .pipe(replace('@@STRIP@@', stripBinary)) - .pipe(rename('SPECS/' + product.applicationName + '.spec')); - - const specIcon = gulp.src('resources/linux/rpm/code.xpm', { base: '.' }) - .pipe(rename('SOURCES/' + product.applicationName + '.xpm')); - - const all = es.merge(code, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, spec, specIcon); - - return all.pipe(vfs.dest(getRpmBuildPath(rpmArch))); - }; -} - -/** - * @param {string} arch - */ -function buildRpmPackage(arch) { - const rpmArch = getRpmPackageArch(arch); - const rpmBuildPath = getRpmBuildPath(rpmArch); - const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; - const destination = `.build/linux/rpm/${rpmArch}`; - - return async () => { - await exec(`mkdir -p ${destination}`); - await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); - await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); - }; -} - -/** - * @param {string} arch - */ -function getSnapBuildPath(arch) { - return `.build/linux/snap/${arch}/${product.applicationName}-${arch}`; -} - -/** - * @param {string} arch - */ -function prepareSnapPackage(arch) { - const binaryDir = '../VSCode-linux-' + arch; - const destination = getSnapBuildPath(arch); - - return function () { - // A desktop file that is placed in snap/gui will be placed into meta/gui verbatim. - const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) - .pipe(rename(`snap/gui/${product.applicationName}.desktop`)); - - // A desktop file that is placed in snap/gui will be placed into meta/gui verbatim. - const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) - .pipe(rename(`snap/gui/${product.applicationName}-url-handler.desktop`)); - - const desktops = es.merge(desktop, desktopUrlHandler) - .pipe(replace('@@NAME_LONG@@', product.nameLong)) - .pipe(replace('@@NAME_SHORT@@', product.nameShort)) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@EXEC@@', `${product.applicationName} --force-user-env`)) - .pipe(replace('@@ICON@@', `\${SNAP}/meta/gui/${product.linuxIconName}.png`)) - .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); - - // An icon that is placed in snap/gui will be placed into meta/gui verbatim. - const icon = gulp.src('resources/linux/code.png', { base: '.' }) - .pipe(rename(`snap/gui/${product.linuxIconName}.png`)); - - const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) - .pipe(rename(function (p) { p.dirname = `usr/share/${product.applicationName}/${p.dirname}`; })); - - const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' }) - .pipe(replace('@@NAME@@', product.applicationName)) - .pipe(replace('@@VERSION@@', commit.substr(0, 8))) - // Possible run-on values https://snapcraft.io/docs/architectures - .pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch)) - .pipe(rename('snap/snapcraft.yaml')); - - const electronLaunch = gulp.src('resources/linux/snap/electron-launch', { base: '.' }) - .pipe(rename('electron-launch')); - - const all = es.merge(desktops, icon, code, snapcraft, electronLaunch); - - return all.pipe(vfs.dest(destination)); - }; -} - -/** - * @param {string} arch - */ -function buildSnapPackage(arch) { - const cwd = getSnapBuildPath(arch); - return () => exec('snapcraft', { cwd }); -} - -const BUILD_TARGETS = [ - { arch: 'x64' }, - { arch: 'armhf' }, - { arch: 'arm64' }, - { arch: 'ppc64le' }, - { arch: 'riscv64' }, - { arch: 'loong64' }, -]; - -BUILD_TARGETS.forEach(({ arch }) => { - const debArch = getDebPackageArch(arch); - const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); - gulp.task(prepareDebTask); - const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); - gulp.task(buildDebTask); - - const rpmArch = getRpmPackageArch(arch); - const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); - gulp.task(prepareRpmTask); - const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); - gulp.task(buildRpmTask); - - const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); - gulp.task(prepareSnapTask); - const buildSnapTask = task.define(`vscode-linux-${arch}-build-snap`, task.series(prepareSnapTask, buildSnapPackage(arch))); - gulp.task(buildSnapTask); -}); diff --git a/build/gulpfile.vscode.linux.ts b/build/gulpfile.vscode.linux.ts new file mode 100644 index 00000000000..e6bc200f151 --- /dev/null +++ b/build/gulpfile.vscode.linux.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import replace from 'gulp-replace'; +import rename from 'gulp-rename'; +import es from 'event-stream'; +import vfs from 'vinyl-fs'; +import { rimraf } from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import packageJson from '../package.json' with { type: 'json' }; +import product from '../product.json' with { type: 'json' }; +import { getDependencies } from './linux/dependencies-generator.ts'; +import { recommendedDeps as debianRecommendedDependencies } from './linux/debian/dep-lists.ts'; +import * as path from 'path'; +import * as cp from 'child_process'; +import { promisify } from 'util'; + +const exec = promisify(cp.exec); +const root = path.dirname(import.meta.dirname); +const commit = getVersion(root); + +const linuxPackageRevision = Math.floor(new Date().getTime() / 1000); + +<<<<<<< Updated upstream +======= +<<<<<<< HEAD:build/gulpfile.vscode.linux.js +/** + * @param {string} arch + */ +function getDebPackageArch(arch) { + return { x64: 'amd64', armhf: 'armhf', arm64: 'arm64', ppc64le: 'ppc64el', riscv64: 'riscv64', loong64: 'loong64' }[arch]; +======= +>>>>>>> Stashed changes +function getDebPackageArch(arch: string): string { + switch (arch) { + case 'x64': return 'amd64'; + case 'armhf': return 'armhf'; + case 'arm64': return 'arm64'; + default: throw new Error(`Unknown arch: ${arch}`); + } +<<<<<<< Updated upstream +======= +>>>>>>> vscode/main:build/gulpfile.vscode.linux.ts +>>>>>>> Stashed changes +} + +function prepareDebPackage(arch: string) { + const binaryDir = '../VSCode-linux-' + arch; + const debArch = getDebPackageArch(arch); + const destination = '.build/linux/deb/' + debArch + '/' + product.applicationName + '-' + debArch; + + return async function () { + const dependencies = await getDependencies('deb', binaryDir, product.applicationName, debArch); + + const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) + .pipe(rename('usr/share/applications/' + product.applicationName + '.desktop')); + + const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) + .pipe(rename('usr/share/applications/' + product.applicationName + '-url-handler.desktop')); + + const desktops = es.merge(desktop, desktopUrlHandler) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME_SHORT@@', product.nameShort)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@EXEC@@', `/usr/share/${product.applicationName}/${product.applicationName}`)) + .pipe(replace('@@ICON@@', product.linuxIconName)) + .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); + + const appdata = gulp.src('resources/linux/code.appdata.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@LICENSE@@', product.licenseName)) + .pipe(rename('usr/share/appdata/' + product.applicationName + '.appdata.xml')); + + const workspaceMime = gulp.src('resources/linux/code-workspace.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); + + const icon = gulp.src('resources/linux/code.png', { base: '.' }) + .pipe(rename('usr/share/pixmaps/' + product.linuxIconName + '.png')); + + const bash_completion = gulp.src('resources/completions/bash/code') + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('usr/share/bash-completion/completions/' + product.applicationName)); + + const zsh_completion = gulp.src('resources/completions/zsh/_code') + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('usr/share/zsh/vendor-completions/_' + product.applicationName)); + + const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) + .pipe(rename(function (p) { p.dirname = 'usr/share/' + product.applicationName + '/' + p.dirname; })); + + let size = 0; + const control = code.pipe(es.through( + function (f) { size += f.isDirectory() ? 4096 : f.contents.length; }, + function () { + const that = this; + gulp.src('resources/linux/debian/control.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision)) + .pipe(replace('@@ARCHITECTURE@@', debArch)) + .pipe(replace('@@DEPENDS@@', dependencies.join(', '))) + .pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', '))) + .pipe(replace('@@INSTALLEDSIZE@@', Math.ceil(size / 1024).toString())) + .pipe(rename('DEBIAN/control')) + .pipe(es.through(function (f) { that.emit('data', f); }, function () { that.emit('end'); })); + })); + + const prerm = gulp.src('resources/linux/debian/prerm.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('DEBIAN/prerm')); + + const postrm = gulp.src('resources/linux/debian/postrm.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('DEBIAN/postrm')); + + const postinst = gulp.src('resources/linux/debian/postinst.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@ARCHITECTURE@@', debArch)) + .pipe(rename('DEBIAN/postinst')); + + const templates = gulp.src('resources/linux/debian/templates.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('DEBIAN/templates')); + + const all = es.merge(control, templates, postinst, postrm, prerm, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, code); + + return all.pipe(vfs.dest(destination)); + }; +} + +function buildDebPackage(arch: string) { + const debArch = getDebPackageArch(arch); + const cwd = `.build/linux/deb/${debArch}`; + + return async () => { + await exec(`chmod 755 ${product.applicationName}-${debArch}/DEBIAN/postinst ${product.applicationName}-${debArch}/DEBIAN/prerm ${product.applicationName}-${debArch}/DEBIAN/postrm`, { cwd }); + await exec('mkdir -p deb', { cwd }); + await exec(`fakeroot dpkg-deb -Zxz -b ${product.applicationName}-${debArch} deb`, { cwd }); + }; +} + +function getRpmBuildPath(rpmArch: string): string { + return '.build/linux/rpm/' + rpmArch + '/rpmbuild'; +} + +<<<<<<< Updated upstream +======= +<<<<<<< HEAD:build/gulpfile.vscode.linux.js +/** + * @param {string} arch + */ +function getRpmPackageArch(arch) { + return { x64: 'x86_64', armhf: 'armv7hl', arm64: 'aarch64', ppc64le: 'ppc64le', riscv64: 'riscv64', loong64: 'loong64' }[arch]; +======= +>>>>>>> Stashed changes +function getRpmPackageArch(arch: string): string { + switch (arch) { + case 'x64': return 'x86_64'; + case 'armhf': return 'armv7hl'; + case 'arm64': return 'aarch64'; + default: throw new Error(`Unknown arch: ${arch}`); + } +<<<<<<< Updated upstream +======= +>>>>>>> vscode/main:build/gulpfile.vscode.linux.ts +>>>>>>> Stashed changes +} + +function prepareRpmPackage(arch: string) { + const binaryDir = '../VSCode-linux-' + arch; + const rpmArch = getRpmPackageArch(arch); + const stripBinary = process.env['STRIP'] ?? '/usr/bin/strip'; + + return async function () { + const dependencies = await getDependencies('rpm', binaryDir, product.applicationName, rpmArch); + + const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) + .pipe(rename('BUILD/usr/share/applications/' + product.applicationName + '.desktop')); + + const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) + .pipe(rename('BUILD/usr/share/applications/' + product.applicationName + '-url-handler.desktop')); + + const desktops = es.merge(desktop, desktopUrlHandler) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME_SHORT@@', product.nameShort)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@EXEC@@', `/usr/share/${product.applicationName}/${product.applicationName}`)) + .pipe(replace('@@ICON@@', product.linuxIconName)) + .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); + + const appdata = gulp.src('resources/linux/code.appdata.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@LICENSE@@', product.licenseName)) + .pipe(rename('BUILD/usr/share/appdata/' + product.applicationName + '.appdata.xml')); + + const workspaceMime = gulp.src('resources/linux/code-workspace.xml', { base: '.' }) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(rename('BUILD/usr/share/mime/packages/' + product.applicationName + '-workspace.xml')); + + const icon = gulp.src('resources/linux/code.png', { base: '.' }) + .pipe(rename('BUILD/usr/share/pixmaps/' + product.linuxIconName + '.png')); + + const bash_completion = gulp.src('resources/completions/bash/code') + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('BUILD/usr/share/bash-completion/completions/' + product.applicationName)); + + const zsh_completion = gulp.src('resources/completions/zsh/_code') + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('BUILD/usr/share/zsh/site-functions/_' + product.applicationName)); + + const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) + .pipe(rename(function (p) { p.dirname = 'BUILD/usr/share/' + product.applicationName + '/' + p.dirname; })); + + const spec = gulp.src('resources/linux/rpm/code.spec.template', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@ICON@@', product.linuxIconName)) + .pipe(replace('@@VERSION@@', packageJson.version)) + .pipe(replace('@@RELEASE@@', linuxPackageRevision.toString())) + .pipe(replace('@@ARCHITECTURE@@', rpmArch)) + .pipe(replace('@@LICENSE@@', product.licenseName)) + .pipe(replace('@@QUALITY@@', (product as typeof product & { quality?: string }).quality || '@@QUALITY@@')) + .pipe(replace('@@UPDATEURL@@', (product as typeof product & { updateUrl?: string }).updateUrl || '@@UPDATEURL@@')) + .pipe(replace('@@DEPENDENCIES@@', dependencies.join(', '))) + .pipe(replace('@@STRIP@@', stripBinary)) + .pipe(rename('SPECS/' + product.applicationName + '.spec')); + + const specIcon = gulp.src('resources/linux/rpm/code.xpm', { base: '.' }) + .pipe(rename('SOURCES/' + product.applicationName + '.xpm')); + + const all = es.merge(code, desktops, appdata, workspaceMime, icon, bash_completion, zsh_completion, spec, specIcon); + + return all.pipe(vfs.dest(getRpmBuildPath(rpmArch))); + }; +} + +function buildRpmPackage(arch: string) { + const rpmArch = getRpmPackageArch(arch); + const rpmBuildPath = getRpmBuildPath(rpmArch); + const rpmOut = `${rpmBuildPath}/RPMS/${rpmArch}`; + const destination = `.build/linux/rpm/${rpmArch}`; + + return async () => { + await exec(`mkdir -p ${destination}`); + await exec(`HOME="$(pwd)/${destination}" rpmbuild -bb ${rpmBuildPath}/SPECS/${product.applicationName}.spec --target=${rpmArch}`); + await exec(`cp "${rpmOut}/$(ls ${rpmOut})" ${destination}/`); + }; +} + +function getSnapBuildPath(arch: string): string { + return `.build/linux/snap/${arch}/${product.applicationName}-${arch}`; +} + +function prepareSnapPackage(arch: string) { + const binaryDir = '../VSCode-linux-' + arch; + const destination = getSnapBuildPath(arch); + + return function () { + // A desktop file that is placed in snap/gui will be placed into meta/gui verbatim. + const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) + .pipe(rename(`snap/gui/${product.applicationName}.desktop`)); + + // A desktop file that is placed in snap/gui will be placed into meta/gui verbatim. + const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) + .pipe(rename(`snap/gui/${product.applicationName}-url-handler.desktop`)); + + const desktops = es.merge(desktop, desktopUrlHandler) + .pipe(replace('@@NAME_LONG@@', product.nameLong)) + .pipe(replace('@@NAME_SHORT@@', product.nameShort)) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@EXEC@@', `${product.applicationName} --force-user-env`)) + .pipe(replace('@@ICON@@', `\${SNAP}/meta/gui/${product.linuxIconName}.png`)) + .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); + + // An icon that is placed in snap/gui will be placed into meta/gui verbatim. + const icon = gulp.src('resources/linux/code.png', { base: '.' }) + .pipe(rename(`snap/gui/${product.linuxIconName}.png`)); + + const code = gulp.src(binaryDir + '/**/*', { base: binaryDir }) + .pipe(rename(function (p) { p.dirname = `usr/share/${product.applicationName}/${p.dirname}`; })); + + const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' }) + .pipe(replace('@@NAME@@', product.applicationName)) + .pipe(replace('@@VERSION@@', commit!.substr(0, 8))) + // Possible run-on values https://snapcraft.io/docs/architectures + .pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch)) + .pipe(rename('snap/snapcraft.yaml')); + + const electronLaunch = gulp.src('resources/linux/snap/electron-launch', { base: '.' }) + .pipe(rename('electron-launch')); + + const all = es.merge(desktops, icon, code, snapcraft, electronLaunch); + + return all.pipe(vfs.dest(destination)); + }; +} + +function buildSnapPackage(arch: string) { + const cwd = getSnapBuildPath(arch); + return () => exec('snapcraft', { cwd }); +} + +const BUILD_TARGETS = [ + { arch: 'x64' }, + { arch: 'armhf' }, + { arch: 'arm64' }, +<<<<<<< Updated upstream +======= + { arch: 'ppc64le' }, + { arch: 'riscv64' }, + { arch: 'loong64' }, +>>>>>>> Stashed changes +]; + +BUILD_TARGETS.forEach(({ arch }) => { + const debArch = getDebPackageArch(arch); + const prepareDebTask = task.define(`vscode-linux-${arch}-prepare-deb`, task.series(rimraf(`.build/linux/deb/${debArch}`), prepareDebPackage(arch))); + gulp.task(prepareDebTask); + const buildDebTask = task.define(`vscode-linux-${arch}-build-deb`, buildDebPackage(arch)); + gulp.task(buildDebTask); + + const rpmArch = getRpmPackageArch(arch); + const prepareRpmTask = task.define(`vscode-linux-${arch}-prepare-rpm`, task.series(rimraf(`.build/linux/rpm/${rpmArch}`), prepareRpmPackage(arch))); + gulp.task(prepareRpmTask); + const buildRpmTask = task.define(`vscode-linux-${arch}-build-rpm`, buildRpmPackage(arch)); + gulp.task(buildRpmTask); + + const prepareSnapTask = task.define(`vscode-linux-${arch}-prepare-snap`, task.series(rimraf(`.build/linux/snap/${arch}`), prepareSnapPackage(arch))); + gulp.task(prepareSnapTask); + const buildSnapTask = task.define(`vscode-linux-${arch}-build-snap`, task.series(prepareSnapTask, buildSnapPackage(arch))); + gulp.task(buildSnapTask); +}); diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts new file mode 100644 index 00000000000..ea1e3502237 --- /dev/null +++ b/build/gulpfile.vscode.ts @@ -0,0 +1,634 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import * as fs from 'fs'; +import * as path from 'path'; +import es from 'event-stream'; +import vfs from 'vinyl-fs'; +import rename from 'gulp-rename'; +import replace from 'gulp-replace'; +import filter from 'gulp-filter'; +import electron from '@vscode/gulp-electron'; +import jsonEditor from 'gulp-json-editor'; +import * as util from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import { readISODate } from './lib/date.ts'; +import * as task from './lib/task.ts'; +import buildfile from './buildfile.ts'; +import * as optimize from './lib/optimize.ts'; +import { inlineMeta } from './lib/inlineMeta.ts'; +import packageJson from '../package.json' with { type: 'json' }; +import product from '../product.json' with { type: 'json' }; +import * as crypto from 'crypto'; +import * as i18n from './lib/i18n.ts'; +import { getProductionDependencies } from './lib/dependencies.ts'; +import { config } from './lib/electron.ts'; +import { createAsar } from './lib/asar.ts'; +import minimist from 'minimist'; +import { compileBuildWithoutManglingTask, compileBuildWithManglingTask } from './gulpfile.compile.ts'; +import { compileNonNativeExtensionsBuildTask, compileNativeExtensionsBuildTask, compileAllExtensionsBuildTask, compileExtensionMediaBuildTask, cleanExtensionsBuildTask } from './gulpfile.extensions.ts'; +import { promisify } from 'util'; +import globCallback from 'glob'; +import rceditCallback from 'rcedit'; + + +const glob = promisify(globCallback); +const rcedit = promisify(rceditCallback); +const root = path.dirname(import.meta.dirname); +const commit = getVersion(root); +const useVersionedUpdate = process.platform === 'win32' && (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; +const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; + +// Build +const vscodeEntryPoints = [ + buildfile.workerEditor, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.workerLocalFileSearch, + buildfile.workerProfileAnalysis, + buildfile.workerOutputLinks, + buildfile.workerBackgroundTokenization, + buildfile.workbenchDesktop, + buildfile.code +].flat(); + +const vscodeResourceIncludes = [ + + // NLS + 'out-build/nls.messages.json', + 'out-build/nls.keys.json', + + // Workbench + 'out-build/vs/code/electron-browser/workbench/workbench.html', + + // Electron Preload + 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', + 'out-build/vs/base/parts/sandbox/electron-browser/preload-aux.js', + + // Node Scripts + 'out-build/vs/base/node/{terminateProcess.sh,cpuUsage.sh,ps.sh}', + + // Touchbar + 'out-build/vs/workbench/browser/parts/editor/media/*.png', + 'out-build/vs/workbench/contrib/debug/browser/media/*.png', + + // External Terminal + 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', + + // Terminal shell integration + 'out-build/vs/workbench/contrib/terminal/common/scripts/*.fish', + 'out-build/vs/workbench/contrib/terminal/common/scripts/*.ps1', + 'out-build/vs/workbench/contrib/terminal/common/scripts/*.psm1', + 'out-build/vs/workbench/contrib/terminal/common/scripts/*.sh', + 'out-build/vs/workbench/contrib/terminal/common/scripts/*.zsh', + + // Accessibility Signals + 'out-build/vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Welcome + 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', + +<<<<<<< Updated upstream +======= + // Workbench Media (logo, icons) + 'out-build/vs/workbench/browser/media/**/*.{svg,png}', + +>>>>>>> Stashed changes + // Extensions + 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', + 'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}', + + // Webview + 'out-build/vs/workbench/contrib/webview/browser/pre/*.{js,html}', + + // Extension Host Worker + 'out-build/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html', + + // Tree Sitter highlights + 'out-build/vs/editor/common/languages/highlights/*.scm', + + // Tree Sitter injection queries + 'out-build/vs/editor/common/languages/injections/*.scm' +]; + +const vscodeResources = [ + + // Includes + ...vscodeResourceIncludes, + + // Excludes + '!out-build/vs/code/browser/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/code/**/*-dev.html', + '!out-build/vs/workbench/contrib/issue/**/*-dev.html', + '!**/test/**' +]; + +const bootstrapEntryPoints = [ + 'out-build/main.js', + 'out-build/cli.js', + 'out-build/bootstrap-fork.js' +]; + +const bundleVSCodeTask = task.define('bundle-vscode', task.series( + util.rimraf('out-vscode'), + // Optimize: bundles source files automatically based on + // import statements based on the passed in entry points. + // In addition, concat window related bootstrap files into + // a single file. + optimize.bundleTask( + { + out: 'out-vscode', + esm: { + src: 'out-build', + entryPoints: [ + ...vscodeEntryPoints, + ...bootstrapEntryPoints + ], + resources: vscodeResources, + skipTSBoilerplateRemoval: entryPoint => entryPoint === 'vs/code/electron-browser/workbench/workbench' + } + } + ) +)); +gulp.task(bundleVSCodeTask); + +const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; +const minifyVSCodeTask = task.define('minify-vscode', task.series( + bundleVSCodeTask, + util.rimraf('out-vscode-min'), + optimize.minifyTask('out-vscode', `${sourceMappingURLBase}/core`) +)); +gulp.task(minifyVSCodeTask); + +const coreCI = task.define('core-ci', task.series( + gulp.task('compile-build-with-mangling') as task.Task, + task.parallel( + gulp.task('minify-vscode') as task.Task, + gulp.task('minify-vscode-reh') as task.Task, + gulp.task('minify-vscode-reh-web') as task.Task, + ) +)); +gulp.task(coreCI); + +const coreCIPR = task.define('core-ci-pr', task.series( + gulp.task('compile-build-without-mangling') as task.Task, + task.parallel( + gulp.task('minify-vscode') as task.Task, + gulp.task('minify-vscode-reh') as task.Task, + gulp.task('minify-vscode-reh-web') as task.Task, + ) +)); +gulp.task(coreCIPR); + +/** + * Compute checksums for some files. + * + * @param out The out folder to read the file from. + * @param filenames The paths to compute a checksum for. + * @return A map of paths to checksums. + */ +function computeChecksums(out: string, filenames: string[]): Record { + const result: Record = {}; + filenames.forEach(function (filename) { + const fullPath = path.join(process.cwd(), out, filename); + result[filename] = computeChecksum(fullPath); + }); + return result; +} + +/** + * Compute checksums for a file. + * + * @param filename The absolute path to a filename. + * @return The checksum for `filename`. + */ +function computeChecksum(filename: string): string { + const contents = fs.readFileSync(filename); + + const hash = crypto + .createHash('sha256') + .update(contents) + .digest('base64') + .replace(/=+$/, ''); + + return hash; +} + +function packageTask(platform: string, arch: string, sourceFolderName: string, destinationFolderName: string, _opts?: { stats?: boolean }) { + const destination = path.join(path.dirname(root), destinationFolderName); + platform = platform || process.platform; + + const task = () => { + const out = sourceFolderName; + + const checksums = computeChecksums(out, [ + 'vs/base/parts/sandbox/electron-browser/preload.js', + 'vs/workbench/workbench.desktop.main.js', + 'vs/workbench/workbench.desktop.main.css', + 'vs/workbench/api/node/extensionHostProcess.js', + 'vs/code/electron-browser/workbench/workbench.html', + 'vs/code/electron-browser/workbench/workbench.js' + ]); + + const src = gulp.src(out + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname!.replace(new RegExp('^' + out), 'out'); })) + .pipe(util.setExecutableBit(['**/*.sh'])); + + const platformSpecificBuiltInExtensionsExclusions = product.builtInExtensions.filter(ext => { + if (!(ext as { platforms?: string[] }).platforms) { + return false; + } + + const set = new Set((ext as { platforms?: string[] }).platforms); + return !set.has(platform); + }).map(ext => `!.build/extensions/${ext.name}/**`); + + const extensions = gulp.src(['.build/extensions/**', ...platformSpecificBuiltInExtensionsExclusions], { base: '.build', dot: true }); + + const sources = es.merge(src, extensions) + .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); + + let version = packageJson.version; + const quality = (product as { quality?: string }).quality; + + if (quality && quality !== 'stable') { + version += '-' + quality; + } + + const name = product.nameShort; + const packageJsonUpdates: Record = { name, version }; + + if (platform === 'linux') { + packageJsonUpdates.desktopName = `${product.applicationName}.desktop`; + } + + let packageJsonContents: string; + const packageJsonStream = gulp.src(['package.json'], { base: '.' }) + .pipe(jsonEditor(packageJsonUpdates)) + .pipe(es.through(function (file) { + packageJsonContents = file.contents.toString(); + this.emit('data', file); + })); + + let productJsonContents: string; + const productJsonStream = gulp.src(['product.json'], { base: '.' }) + .pipe(jsonEditor({ commit, date: readISODate('out-build'), checksums, version })) + .pipe(es.through(function (file) { + productJsonContents = file.contents.toString(); + this.emit('data', file); + })); + + const license = gulp.src([product.licenseFileName, 'ThirdPartyNotices.txt', 'licenses/**'], { base: '.', allowEmpty: true }); + + // TODO the API should be copied to `out` during compile, not here + const api = gulp.src('src/vscode-dts/vscode.d.ts').pipe(rename('out/vscode-dts/vscode.d.ts')); + + const telemetry = gulp.src('.build/telemetry/**', { base: '.build/telemetry', dot: true }); + + const workbenchModes = gulp.src('resources/workbenchModes/**', { base: '.', dot: true }); + + const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); + const root = path.resolve(path.join(import.meta.dirname, '..')); + const productionDependencies = getProductionDependencies(root); + const dependenciesSrc = productionDependencies.map(d => path.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat().concat('!**/*.mk'); + + const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) + .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.{js,css}.map'])) + .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.moduleignore'))) + .pipe(util.cleanNodeModules(path.join(import.meta.dirname, `.moduleignore.${process.platform}`))) + .pipe(jsFilter) + .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) + .pipe(jsFilter.restore) + .pipe(createAsar(path.join(process.cwd(), 'node_modules'), [ + '**/*.node', + '**/@vscode/ripgrep/bin/*', + '**/node-pty/build/Release/*', + '**/node-pty/build/Release/conpty/*', + '**/node-pty/lib/worker/conoutSocketWorker.js', + '**/node-pty/lib/shared/conout.js', + '**/*.wasm', + '**/@vscode/vsce-sign/bin/*', + ], [ + '**/*.mk', + '!node_modules/vsda/**' // stay compatible with extensions that depend on us shipping `vsda` into ASAR + ], [ + 'node_modules/vsda/**' // retain copy of `vsda` in node_modules for internal use + ], 'node_modules.asar')); + + let all = es.merge( + packageJsonStream, + productJsonStream, + license, + api, + telemetry, + workbenchModes, + sources, + deps + ); + + if (platform === 'win32') { + all = es.merge(all, gulp.src([ + 'resources/win32/bower.ico', + 'resources/win32/c.ico', + 'resources/win32/config.ico', + 'resources/win32/cpp.ico', + 'resources/win32/csharp.ico', + 'resources/win32/css.ico', + 'resources/win32/default.ico', + 'resources/win32/go.ico', + 'resources/win32/html.ico', + 'resources/win32/jade.ico', + 'resources/win32/java.ico', + 'resources/win32/javascript.ico', + 'resources/win32/json.ico', + 'resources/win32/less.ico', + 'resources/win32/markdown.ico', + 'resources/win32/php.ico', + 'resources/win32/powershell.ico', + 'resources/win32/python.ico', + 'resources/win32/react.ico', + 'resources/win32/ruby.ico', + 'resources/win32/sass.ico', + 'resources/win32/shell.ico', + 'resources/win32/sql.ico', + 'resources/win32/typescript.ico', + 'resources/win32/vue.ico', + 'resources/win32/xml.ico', + 'resources/win32/yaml.ico', + 'resources/win32/code_70x70.png', + 'resources/win32/code_150x150.png' + ], { base: '.' })); + } else if (platform === 'linux') { + const policyDest = gulp.src('.build/policies/linux/**', { base: '.build/policies/linux' }) + .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); + all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }), policyDest); + } else if (platform === 'darwin') { + const shortcut = gulp.src('resources/darwin/bin/code.sh') + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('bin/code')); + const policyDest = gulp.src('.build/policies/darwin/**', { base: '.build/policies/darwin' }) + .pipe(rename(f => f.dirname = `policies/${f.dirname}`)); + all = es.merge(all, shortcut, policyDest); + } + +<<<<<<< Updated upstream + let result: NodeJS.ReadWriteStream = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()) + .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 + .pipe(electron({ ...config, platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: false })) +======= +<<<<<<< HEAD:build/gulpfile.vscode.js + // CORTEXIDE/VSCODIUM: Support custom Electron repositories for alternative architectures + // This allows using VSCODE_ELECTRON_REPOSITORY and VSCODE_ELECTRON_TAG env vars + const electronOverride = {}; + if (process.env.VSCODE_ELECTRON_REPOSITORY) { + electronOverride.repo = process.env.VSCODE_ELECTRON_REPOSITORY; + } + if (process.env.VSCODE_ELECTRON_TAG) { + electronOverride.tag = process.env.VSCODE_ELECTRON_TAG; + } + const hasElectronOverride = electronOverride.repo || electronOverride.tag; + + let result = all +======= + let result: NodeJS.ReadWriteStream = all +>>>>>>> vscode/main:build/gulpfile.vscode.ts + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()) + .pipe(filter(['**', '!**/.github/**'], { dot: true })) // https://github.com/microsoft/vscode/issues/116523 + .pipe(electron({ ...config, ...(hasElectronOverride ? electronOverride : {}), platform, arch: arch === 'armhf' ? 'arm' : arch, ffmpegChromium: false })) +>>>>>>> Stashed changes + .pipe(filter(['**', '!LICENSE', '!version'], { dot: true })); + + if (platform === 'linux') { + result = es.merge(result, gulp.src('resources/completions/bash/code', { base: '.' }) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(function (f) { f.basename = product.applicationName; }))); + + result = es.merge(result, gulp.src('resources/completions/zsh/_code', { base: '.' }) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename(function (f) { f.basename = '_' + product.applicationName; }))); + } + + if (platform === 'win32') { + result = es.merge(result, gulp.src('resources/win32/bin/code.js', { base: 'resources/win32', allowEmpty: true })); + + if (useVersionedUpdate) { + result = es.merge(result, gulp.src('resources/win32/versioned/bin/code.cmd', { base: 'resources/win32/versioned' }) + .pipe(replace('@@NAME@@', product.nameShort)) + .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder)) + .pipe(rename(function (f) { f.basename = product.applicationName; }))); + + result = es.merge(result, gulp.src('resources/win32/versioned/bin/code.sh', { base: 'resources/win32/versioned' }) + .pipe(replace('@@NAME@@', product.nameShort)) + .pipe(replace('@@PRODNAME@@', product.nameLong)) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', String(commit))) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(replace('@@VERSIONFOLDER@@', versionedResourcesFolder)) + .pipe(replace('@@SERVERDATAFOLDER@@', product.serverDataFolderName || '.vscode-remote')) + .pipe(replace('@@QUALITY@@', quality!)) + .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); + } else { + result = es.merge(result, gulp.src('resources/win32/bin/code.cmd', { base: 'resources/win32' }) + .pipe(replace('@@NAME@@', product.nameShort)) + .pipe(rename(function (f) { f.basename = product.applicationName; }))); + + result = es.merge(result, gulp.src('resources/win32/bin/code.sh', { base: 'resources/win32' }) + .pipe(replace('@@NAME@@', product.nameShort)) + .pipe(replace('@@PRODNAME@@', product.nameLong)) + .pipe(replace('@@VERSION@@', version)) + .pipe(replace('@@COMMIT@@', String(commit))) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(replace('@@SERVERDATAFOLDER@@', product.serverDataFolderName || '.vscode-remote')) + .pipe(replace('@@QUALITY@@', String(quality))) + .pipe(rename(function (f) { f.basename = product.applicationName; f.extname = ''; }))); + } + + result = es.merge(result, gulp.src('resources/win32/VisualElementsManifest.xml', { base: 'resources/win32' }) + .pipe(rename(product.nameShort + '.VisualElementsManifest.xml'))); + + result = es.merge(result, gulp.src('.build/policies/win32/**', { base: '.build/policies/win32' }) + .pipe(rename(f => f.dirname = `policies/${f.dirname}`))); + + if (quality === 'stable' || quality === 'insider') { + result = es.merge(result, gulp.src('.build/win32/appx/**', { base: '.build/win32' })); + const rawVersion = version.replace(/-\w+$/, '').split('.'); + const appxVersion = `${rawVersion[0]}.0.${rawVersion[1]}.${rawVersion[2]}`; + result = es.merge(result, gulp.src('resources/win32/appx/AppxManifest.xml', { base: '.' }) + .pipe(replace('@@AppxPackageName@@', product.win32AppUserModelId)) + .pipe(replace('@@AppxPackageVersion@@', appxVersion)) + .pipe(replace('@@AppxPackageDisplayName@@', product.nameLong)) + .pipe(replace('@@AppxPackageDescription@@', product.win32NameVersion)) + .pipe(replace('@@ApplicationIdShort@@', product.win32RegValueName)) + .pipe(replace('@@ApplicationExe@@', product.nameShort + '.exe')) + .pipe(replace('@@FileExplorerContextMenuID@@', quality === 'stable' ? 'OpenWithCode' : 'OpenWithCodeInsiders')) + .pipe(replace('@@FileExplorerContextMenuCLSID@@', (product as { win32ContextMenu?: Record }).win32ContextMenu![arch].clsid)) + .pipe(replace('@@FileExplorerContextMenuDLL@@', `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`)) + .pipe(rename(f => f.dirname = `appx/manifest`))); + } + } else if (platform === 'linux') { + result = es.merge(result, gulp.src('resources/linux/bin/code.sh', { base: '.' }) + .pipe(replace('@@PRODNAME@@', product.nameLong)) + .pipe(replace('@@APPNAME@@', product.applicationName)) + .pipe(rename('bin/' + product.applicationName))); + } + + result = inlineMeta(result, { + targetPaths: bootstrapEntryPoints, + packageJsonFn: () => packageJsonContents, + productJsonFn: () => productJsonContents + }); + + return result.pipe(vfs.dest(destination)); + }; + task.taskName = `package-${platform}-${arch}`; + return task; +} + +function patchWin32DependenciesTask(destinationFolderName: string) { + const cwd = path.join(path.dirname(root), destinationFolderName); + + return async () => { + const deps = await glob('**/*.node', { cwd, ignore: 'extensions/node_modules/@parcel/watcher/**' }); + const packageJson = JSON.parse(await fs.promises.readFile(path.join(cwd, versionedResourcesFolder, 'resources', 'app', 'package.json'), 'utf8')); + const product = JSON.parse(await fs.promises.readFile(path.join(cwd, versionedResourcesFolder, 'resources', 'app', 'product.json'), 'utf8')); + const baseVersion = packageJson.version.replace(/-.*$/, ''); + + await Promise.all(deps.map(async dep => { + const basename = path.basename(dep); + + await rcedit(path.join(cwd, dep), { + 'file-version': baseVersion, + 'version-string': { + 'CompanyName': 'Microsoft Corporation', + 'FileDescription': product.nameLong, + 'FileVersion': packageJson.version, + 'InternalName': basename, + 'LegalCopyright': 'Copyright (C) 2022 Microsoft. All rights reserved', + 'OriginalFilename': basename, + 'ProductName': product.nameLong, + 'ProductVersion': packageJson.version, + } + }); + })); + }; +} + +const buildRoot = path.dirname(root); + +const BUILD_TARGETS = [ + { platform: 'win32', arch: 'x64' }, + { platform: 'win32', arch: 'arm64' }, + { platform: 'darwin', arch: 'x64', opts: { stats: true } }, + { platform: 'darwin', arch: 'arm64', opts: { stats: true } }, + { platform: 'linux', arch: 'x64' }, + { platform: 'linux', arch: 'armhf' }, + { platform: 'linux', arch: 'arm64' }, +<<<<<<< Updated upstream +======= + { platform: 'linux', arch: 'ppc64le' }, + { platform: 'linux', arch: 'riscv64' }, + { platform: 'linux', arch: 'loong64' }, +>>>>>>> Stashed changes +]; +BUILD_TARGETS.forEach(buildTarget => { + const dashed = (str: string) => (str ? `-${str}` : ``); + const platform = buildTarget.platform; + const arch = buildTarget.arch; + const opts = buildTarget.opts; + + const [vscode, vscodeMin] = ['', 'min'].map(minified => { + const sourceFolderName = `out-vscode${dashed(minified)}`; + const destinationFolderName = `VSCode${dashed(platform)}${dashed(arch)}`; + + const tasks = [ + compileNativeExtensionsBuildTask, + util.rimraf(path.join(buildRoot, destinationFolderName)), + packageTask(platform, arch, sourceFolderName, destinationFolderName, opts) + ]; + + if (platform === 'win32') { + tasks.push(patchWin32DependenciesTask(destinationFolderName)); + } + + const vscodeTaskCI = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}-ci`, task.series(...tasks)); + gulp.task(vscodeTaskCI); + + const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series( + minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask, + cleanExtensionsBuildTask, + compileNonNativeExtensionsBuildTask, + compileExtensionMediaBuildTask, + minified ? minifyVSCodeTask : bundleVSCodeTask, + vscodeTaskCI + )); + gulp.task(vscodeTask); + + return vscodeTask; + }); + + if (process.platform === platform && process.arch === arch) { + gulp.task(task.define('vscode', task.series(vscode))); + gulp.task(task.define('vscode-min', task.series(vscodeMin))); + } +}); + +// #region nls + +const innoSetupConfig: Record = { + 'zh-cn': { codePage: 'CP936', defaultInfo: { name: 'Simplified Chinese', id: '$0804', } }, + 'zh-tw': { codePage: 'CP950', defaultInfo: { name: 'Traditional Chinese', id: '$0404' } }, + 'ko': { codePage: 'CP949', defaultInfo: { name: 'Korean', id: '$0412' } }, + 'ja': { codePage: 'CP932' }, + 'de': { codePage: 'CP1252' }, + 'fr': { codePage: 'CP1252' }, + 'es': { codePage: 'CP1252' }, + 'ru': { codePage: 'CP1251' }, + 'it': { codePage: 'CP1252' }, + 'pt-br': { codePage: 'CP1252' }, + 'hu': { codePage: 'CP1250' }, + 'tr': { codePage: 'CP1254' } +}; + +gulp.task(task.define( + 'vscode-translations-export', + task.series( + coreCI, + compileAllExtensionsBuildTask, + function () { + const pathToMetadata = './out-build/nls.metadata.json'; + const pathToExtensions = '.build/extensions/*'; + const pathToSetup = 'build/win32/i18n/messages.en.isl'; + + return es.merge( + gulp.src(pathToMetadata).pipe(i18n.createXlfFilesForCoreBundle()), + gulp.src(pathToSetup).pipe(i18n.createXlfFilesForIsl()), + gulp.src(pathToExtensions).pipe(i18n.createXlfFilesForExtensions()) + ).pipe(vfs.dest('../vscode-translations-export')); + } + ) +)); + +gulp.task('vscode-translations-import', function () { + const options = minimist(process.argv.slice(2), { + string: 'location', + default: { + location: '../vscode-translations-import' + } + }); + return es.merge([...i18n.defaultLanguages, ...i18n.extraLanguages].map(language => { + const id = language.id; + return gulp.src(`${options.location}/${id}/vscode-setup/messages.xlf`) + .pipe(i18n.prepareIslFiles(language, innoSetupConfig[language.id])) + .pipe(vfs.dest(`./build/win32/i18n`)); + })); +}); + +// #endregion diff --git a/build/gulpfile.vscode.web.js b/build/gulpfile.vscode.web.js deleted file mode 100644 index 35844f3e1e7..00000000000 --- a/build/gulpfile.vscode.web.js +++ /dev/null @@ -1,244 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -const gulp = require('gulp'); -const path = require('path'); -const es = require('event-stream'); -const util = require('./lib/util'); -const { getVersion } = require('./lib/getVersion'); -const task = require('./lib/task'); -const optimize = require('./lib/optimize'); -const { readISODate } = require('./lib/date'); -const product = require('../product.json'); -const rename = require('gulp-rename'); -const filter = require('gulp-filter'); -const { getProductionDependencies } = require('./lib/dependencies'); -const vfs = require('vinyl-fs'); -const packageJson = require('../package.json'); -const { compileBuildWithManglingTask } = require('./gulpfile.compile'); -const extensions = require('./lib/extensions'); -const VinylFile = require('vinyl'); - -const REPO_ROOT = path.dirname(__dirname); -const BUILD_ROOT = path.dirname(REPO_ROOT); -const WEB_FOLDER = path.join(REPO_ROOT, 'remote', 'web'); - -const commit = getVersion(REPO_ROOT); -const quality = product.quality; -const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; - -const vscodeWebResourceIncludes = [ - - // NLS - 'out-build/nls.messages.js', - - // Accessibility Signals - 'out-build/vs/platform/accessibilitySignal/browser/media/*.mp3', - - // Welcome - 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', - - // Workbench Media (logo, icons) - 'out-build/vs/workbench/browser/media/**/*.{svg,png}', - - // Extensions - 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', - 'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}', - - // Webview - 'out-build/vs/workbench/contrib/webview/browser/pre/*.{js,html}', - - // Tree Sitter highlights - 'out-build/vs/editor/common/languages/highlights/*.scm', - - // Tree Sitter injections - 'out-build/vs/editor/common/languages/injections/*.scm', - - // Extension Host Worker - 'out-build/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html' -]; -exports.vscodeWebResourceIncludes = vscodeWebResourceIncludes; - -const vscodeWebResources = [ - - // Includes - ...vscodeWebResourceIncludes, - - // Excludes - '!out-build/vs/**/{node,electron-browser,electron-main,electron-utility}/**', - '!out-build/vs/editor/standalone/**', - '!out-build/vs/workbench/**/*-tb.png', - '!out-build/vs/code/**/*-dev.html', - '!**/test/**' -]; - -const buildfile = require('./buildfile'); - -const vscodeWebEntryPoints = [ - buildfile.workerEditor, - buildfile.workerExtensionHost, - buildfile.workerNotebook, - buildfile.workerLanguageDetection, - buildfile.workerLocalFileSearch, - buildfile.workerOutputLinks, - buildfile.workerBackgroundTokenization, - buildfile.keyboardMaps, - buildfile.workbenchWeb, - buildfile.entrypoint('vs/workbench/workbench.web.main.internal') // TODO@esm remove line when we stop supporting web-amd-esm-bridge -].flat(); - -/** - * @param extensionsRoot {string} The location where extension will be read from - * @param {object} product The parsed product.json file contents - */ -const createVSCodeWebFileContentMapper = (extensionsRoot, product) => { - /** - * @param {string} path - * @returns {((content: string) => string) | undefined} - */ - return path => { - if (path.endsWith('vs/platform/product/common/product.js')) { - return content => { - const productConfiguration = JSON.stringify({ - ...product, - version, - commit, - date: readISODate('out-build') - }); - return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); - }; - } else if (path.endsWith('vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.js')) { - return content => { - const builtinExtensions = JSON.stringify(extensions.scanBuiltinExtensions(extensionsRoot)); - return content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensions.substr(1, builtinExtensions.length - 2) /* without [ and ]*/); - }; - } - - return undefined; - }; -}; -exports.createVSCodeWebFileContentMapper = createVSCodeWebFileContentMapper; - -const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( - util.rimraf('out-vscode-web'), - optimize.bundleTask( - { - out: 'out-vscode-web', - esm: { - src: 'out-build', - entryPoints: vscodeWebEntryPoints, - resources: vscodeWebResources, - fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions', product) - } - } - ) -)); - -const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( - bundleVSCodeWebTask, - util.rimraf('out-vscode-web-min'), - optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) -)); -gulp.task(minifyVSCodeWebTask); - -/** - * @param {string} sourceFolderName - * @param {string} destinationFolderName - */ -function packageTask(sourceFolderName, destinationFolderName) { - const destination = path.join(BUILD_ROOT, destinationFolderName); - - return () => { - const json = require('gulp-json-editor'); - - const src = gulp.src(sourceFolderName + '/**', { base: '.' }) - .pipe(rename(function (path) { path.dirname = path.dirname.replace(new RegExp('^' + sourceFolderName), 'out'); })); - - const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); - - const loader = gulp.src('build/loader.min', { base: 'build', dot: true }).pipe(rename('out/vs/loader.js')); // TODO@esm remove line when we stop supporting web-amd-esm-bridge - - const sources = es.merge(src, extensions, loader) - .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })) - // TODO@esm remove me once we stop supporting our web-esm-bridge - .pipe(es.through(function (file) { - if (file.relative === 'out/vs/workbench/workbench.web.main.internal.css') { - this.emit('data', new VinylFile({ - contents: file.contents, - path: file.path.replace('workbench.web.main.internal.css', 'workbench.web.main.css'), - base: file.base - })); - } - this.emit('data', file); - })); - - const name = product.nameShort; - const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) - .pipe(json({ name, version, type: 'module' })); - - const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); - - const productionDependencies = getProductionDependencies(WEB_FOLDER); - const dependenciesSrc = productionDependencies.map(d => path.relative(REPO_ROOT, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`]).flat(); - - const deps = gulp.src(dependenciesSrc, { base: 'remote/web', dot: true }) - .pipe(filter(['**', '!**/package-lock.json'])) - .pipe(util.cleanNodeModules(path.join(__dirname, '.webignore'))); - - const favicon = gulp.src('resources/server/favicon.ico', { base: 'resources/server' }); - const manifest = gulp.src('resources/server/manifest.json', { base: 'resources/server' }); - const pwaicons = es.merge( - gulp.src('resources/server/code-192.png', { base: 'resources/server' }), - gulp.src('resources/server/code-512.png', { base: 'resources/server' }) - ); - - const all = es.merge( - packageJsonStream, - license, - sources, - deps, - favicon, - manifest, - pwaicons - ); - - const result = all - .pipe(util.skipDirectories()) - .pipe(util.fixWin32DirectoryPermissions()); - - return result.pipe(vfs.dest(destination)); - }; -} - -const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build', task.series( - task.define('clean-web-extensions-build', util.rimraf('.build/web/extensions')), - task.define('bundle-web-extensions-build', () => extensions.packageAllLocalExtensionsStream(true, false).pipe(gulp.dest('.build/web'))), - task.define('bundle-marketplace-web-extensions-build', () => extensions.packageMarketplaceExtensionsStream(true).pipe(gulp.dest('.build/web'))), - task.define('bundle-web-extension-media-build', () => extensions.buildExtensionMedia(false, '.build/web/extensions')), -)); -gulp.task(compileWebExtensionsBuildTask); - -const dashed = (/** @type {string} */ str) => (str ? `-${str}` : ``); - -['', 'min'].forEach(minified => { - const sourceFolderName = `out-vscode-web${dashed(minified)}`; - const destinationFolderName = `vscode-web`; - - const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( - compileWebExtensionsBuildTask, - minified ? minifyVSCodeWebTask : bundleVSCodeWebTask, - util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), - packageTask(sourceFolderName, destinationFolderName) - )); - gulp.task(vscodeWebTaskCI); - - const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series( - compileBuildWithManglingTask, - vscodeWebTaskCI - )); - gulp.task(vscodeWebTask); -}); diff --git a/build/gulpfile.vscode.web.ts b/build/gulpfile.vscode.web.ts new file mode 100644 index 00000000000..f70c4fde200 --- /dev/null +++ b/build/gulpfile.vscode.web.ts @@ -0,0 +1,215 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import gulp from 'gulp'; +import * as path from 'path'; +import es from 'event-stream'; +import * as util from './lib/util.ts'; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import * as optimize from './lib/optimize.ts'; +import { readISODate } from './lib/date.ts'; +import product from '../product.json' with { type: 'json' }; +import rename from 'gulp-rename'; +import filter from 'gulp-filter'; +import { getProductionDependencies } from './lib/dependencies.ts'; +import vfs from 'vinyl-fs'; +import packageJson from '../package.json' with { type: 'json' }; +import { compileBuildWithManglingTask } from './gulpfile.compile.ts'; +import * as extensions from './lib/extensions.ts'; +import jsonEditor from 'gulp-json-editor'; +import buildfile from './buildfile.ts'; + +const REPO_ROOT = path.dirname(import.meta.dirname); +const BUILD_ROOT = path.dirname(REPO_ROOT); +const WEB_FOLDER = path.join(REPO_ROOT, 'remote', 'web'); + +const commit = getVersion(REPO_ROOT); +const quality = (product as { quality?: string }).quality; +const version = (quality && quality !== 'stable') ? `${packageJson.version}-${quality}` : packageJson.version; + +export const vscodeWebResourceIncludes = [ + + // NLS + 'out-build/nls.messages.js', + + // Accessibility Signals + 'out-build/vs/platform/accessibilitySignal/browser/media/*.mp3', + + // Welcome + 'out-build/vs/workbench/contrib/welcomeGettingStarted/common/media/**/*.{svg,png}', + + // Workbench Media (logo, icons) + 'out-build/vs/workbench/browser/media/**/*.{svg,png}', + + // Extensions + 'out-build/vs/workbench/contrib/extensions/browser/media/{theme-icon.png,language-icon.svg}', + 'out-build/vs/workbench/services/extensionManagement/common/media/*.{svg,png}', + + // Webview + 'out-build/vs/workbench/contrib/webview/browser/pre/*.{js,html}', + + // Tree Sitter highlights + 'out-build/vs/editor/common/languages/highlights/*.scm', + + // Tree Sitter injections + 'out-build/vs/editor/common/languages/injections/*.scm', + + // Extension Host Worker + 'out-build/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.html' +]; + +const vscodeWebResources = [ + + // Includes + ...vscodeWebResourceIncludes, + + // Excludes + '!out-build/vs/**/{node,electron-browser,electron-main,electron-utility}/**', + '!out-build/vs/editor/standalone/**', + '!out-build/vs/workbench/**/*-tb.png', + '!out-build/vs/code/**/*-dev.html', + '!**/test/**' +]; + +const vscodeWebEntryPoints = [ + buildfile.workerEditor, + buildfile.workerExtensionHost, + buildfile.workerNotebook, + buildfile.workerLanguageDetection, + buildfile.workerLocalFileSearch, + buildfile.workerOutputLinks, + buildfile.workerBackgroundTokenization, + buildfile.keyboardMaps, + buildfile.workbenchWeb, +].flat(); + +/** + * @param extensionsRoot The location where extension will be read from + * @param product The parsed product.json file contents + */ +export const createVSCodeWebFileContentMapper = (extensionsRoot: string, product: typeof import('../product.json')) => { + return (path: string): ((content: string) => string) | undefined => { + if (path.endsWith('vs/platform/product/common/product.js')) { + return content => { + const productConfiguration = JSON.stringify({ + ...product, + version, + commit, + date: readISODate('out-build') + }); + return content.replace('/*BUILD->INSERT_PRODUCT_CONFIGURATION*/', () => productConfiguration.substr(1, productConfiguration.length - 2) /* without { and }*/); + }; + } else if (path.endsWith('vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.js')) { + return content => { + const builtinExtensions = JSON.stringify(extensions.scanBuiltinExtensions(extensionsRoot)); + return content.replace('/*BUILD->INSERT_BUILTIN_EXTENSIONS*/', () => builtinExtensions.substr(1, builtinExtensions.length - 2) /* without [ and ]*/); + }; + } + + return undefined; + }; +}; + +const bundleVSCodeWebTask = task.define('bundle-vscode-web', task.series( + util.rimraf('out-vscode-web'), + optimize.bundleTask( + { + out: 'out-vscode-web', + esm: { + src: 'out-build', + entryPoints: vscodeWebEntryPoints, + resources: vscodeWebResources, + fileContentMapper: createVSCodeWebFileContentMapper('.build/web/extensions', product) + } + } + ) +)); + +const minifyVSCodeWebTask = task.define('minify-vscode-web', task.series( + bundleVSCodeWebTask, + util.rimraf('out-vscode-web-min'), + optimize.minifyTask('out-vscode-web', `https://main.vscode-cdn.net/sourcemaps/${commit}/core`) +)); +gulp.task(minifyVSCodeWebTask); + +function packageTask(sourceFolderName: string, destinationFolderName: string) { + const destination = path.join(BUILD_ROOT, destinationFolderName); + + return () => { + const src = gulp.src(sourceFolderName + '/**', { base: '.' }) + .pipe(rename(function (path) { path.dirname = path.dirname!.replace(new RegExp('^' + sourceFolderName), 'out'); })); + + const extensions = gulp.src('.build/web/extensions/**', { base: '.build/web', dot: true }); + + const sources = es.merge(src, extensions) + .pipe(filter(['**', '!**/*.{js,css}.map'], { dot: true })); + + const name = product.nameShort; + const packageJsonStream = gulp.src(['remote/web/package.json'], { base: 'remote/web' }) + .pipe(jsonEditor({ name, version, type: 'module' })); + + const license = gulp.src(['remote/LICENSE'], { base: 'remote', allowEmpty: true }); + + const productionDependencies = getProductionDependencies(WEB_FOLDER); + const dependenciesSrc = productionDependencies.map(d => path.relative(REPO_ROOT, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`, `!${d}/.bin/**`]).flat(); + + const deps = gulp.src(dependenciesSrc, { base: 'remote/web', dot: true }) + .pipe(filter(['**', '!**/package-lock.json'])) + .pipe(util.cleanNodeModules(path.join(import.meta.dirname, '.webignore'))); + + const favicon = gulp.src('resources/server/favicon.ico', { base: 'resources/server' }); + const manifest = gulp.src('resources/server/manifest.json', { base: 'resources/server' }); + const pwaicons = es.merge( + gulp.src('resources/server/code-192.png', { base: 'resources/server' }), + gulp.src('resources/server/code-512.png', { base: 'resources/server' }) + ); + + const all = es.merge( + packageJsonStream, + license, + sources, + deps, + favicon, + manifest, + pwaicons + ); + + const result = all + .pipe(util.skipDirectories()) + .pipe(util.fixWin32DirectoryPermissions()); + + return result.pipe(vfs.dest(destination)); + }; +} + +const compileWebExtensionsBuildTask = task.define('compile-web-extensions-build', task.series( + task.define('clean-web-extensions-build', util.rimraf('.build/web/extensions')), + task.define('bundle-web-extensions-build', () => extensions.packageAllLocalExtensionsStream(true, false).pipe(gulp.dest('.build/web'))), + task.define('bundle-marketplace-web-extensions-build', () => extensions.packageMarketplaceExtensionsStream(true).pipe(gulp.dest('.build/web'))), + task.define('bundle-web-extension-media-build', () => extensions.buildExtensionMedia(false, '.build/web/extensions')), +)); +gulp.task(compileWebExtensionsBuildTask); + +const dashed = (str: string) => (str ? `-${str}` : ``); + +['', 'min'].forEach(minified => { + const sourceFolderName = `out-vscode-web${dashed(minified)}`; + const destinationFolderName = `vscode-web`; + + const vscodeWebTaskCI = task.define(`vscode-web${dashed(minified)}-ci`, task.series( + compileWebExtensionsBuildTask, + minified ? minifyVSCodeWebTask : bundleVSCodeWebTask, + util.rimraf(path.join(BUILD_ROOT, destinationFolderName)), + packageTask(sourceFolderName, destinationFolderName) + )); + gulp.task(vscodeWebTaskCI); + + const vscodeWebTask = task.define(`vscode-web${dashed(minified)}`, task.series( + compileBuildWithManglingTask, + vscodeWebTaskCI + )); + gulp.task(vscodeWebTask); +}); diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js deleted file mode 100644 index 8319fc879cd..00000000000 --- a/build/gulpfile.vscode.win32.js +++ /dev/null @@ -1,161 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -const gulp = require('gulp'); -const path = require('path'); -const fs = require('fs'); -const assert = require('assert'); -const cp = require('child_process'); -const util = require('./lib/util'); -const task = require('./lib/task'); -const pkg = require('../package.json'); -const product = require('../product.json'); -const vfs = require('vinyl-fs'); -const rcedit = require('rcedit'); - -const repoPath = path.dirname(__dirname); -const buildPath = (/** @type {string} */ arch) => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); -const setupDir = (/** @type {string} */ arch, /** @type {string} */ target) => path.join(repoPath, '.build', `win32-${arch}`, `${target}-setup`); -const issPath = path.join(__dirname, 'win32', 'code.iss'); -const innoSetupPath = path.join(path.dirname(path.dirname(require.resolve('innosetup'))), 'bin', 'ISCC.exe'); -const signWin32Path = path.join(repoPath, 'build', 'azure-pipelines', 'common', 'sign-win32'); - -function packageInnoSetup(iss, options, cb) { - options = options || {}; - - const definitions = options.definitions || {}; - - if (process.argv.some(arg => arg === '--debug-inno')) { - definitions['Debug'] = 'true'; - } - - if (process.argv.some(arg => arg === '--sign')) { - definitions['Sign'] = 'true'; - } - - const keys = Object.keys(definitions); - - keys.forEach(key => assert(typeof definitions[key] === 'string', `Missing value for '${key}' in Inno Setup package step`)); - - const defs = keys.map(key => `/d${key}=${definitions[key]}`); - const args = [ - iss, - ...defs, - `/sesrp=node ${signWin32Path} $f` - ]; - - cp.spawn(innoSetupPath, args, { stdio: ['ignore', 'inherit', 'inherit'] }) - .on('error', cb) - .on('exit', code => { - if (code === 0) { - cb(null); - } else { - cb(new Error(`InnoSetup returned exit code: ${code}`)); - } - }); -} - -/** - * @param {string} arch - * @param {string} target - */ -function buildWin32Setup(arch, target) { - if (target !== 'system' && target !== 'user') { - throw new Error('Invalid setup target'); - } - - return cb => { - const x64AppId = target === 'system' ? product.win32x64AppId : product.win32x64UserAppId; - const arm64AppId = target === 'system' ? product.win32arm64AppId : product.win32arm64UserAppId; - - const sourcePath = buildPath(arch); - const outputPath = setupDir(arch, target); - fs.mkdirSync(outputPath, { recursive: true }); - - const originalProductJsonPath = path.join(sourcePath, 'resources/app/product.json'); - const productJsonPath = path.join(outputPath, 'product.json'); - const productJson = JSON.parse(fs.readFileSync(originalProductJsonPath, 'utf8')); - productJson['target'] = target; - fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); - - const quality = product.quality || 'dev'; - const definitions = { - NameLong: product.nameLong, - NameShort: product.nameShort, - DirName: product.win32DirName, - Version: pkg.version, - RawVersion: pkg.version.replace(/-\w+$/, ''), - NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''), - ExeBasename: product.nameShort, - RegValueName: product.win32RegValueName, - ShellNameShort: product.win32ShellNameShort, - AppMutex: product.win32MutexName, - TunnelMutex: product.win32TunnelMutex, - TunnelServiceMutex: product.win32TunnelServiceMutex, - TunnelApplicationName: product.tunnelApplicationName, - ApplicationName: product.applicationName, - Arch: arch, - AppId: { 'x64': x64AppId, 'arm64': arm64AppId }[arch], - IncompatibleTargetAppId: { 'x64': product.win32x64AppId, 'arm64': product.win32arm64AppId }[arch], - AppUserId: product.win32AppUserModelId, - ArchitecturesAllowed: { 'x64': 'x64', 'arm64': 'arm64' }[arch], - ArchitecturesInstallIn64BitMode: { 'x64': 'x64', 'arm64': 'arm64' }[arch], - SourceDir: sourcePath, - RepoDir: repoPath, - OutputDir: outputPath, - InstallTarget: target, - ProductJsonPath: productJsonPath, - Quality: quality - }; - - // CortexIDE: Disable APPX packaging (Windows Store packages) - // APPX packages are not needed for CortexIDE distribution - // if (quality !== 'exploration') { - // definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; - // definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; - // definitions['AppxPackageName'] = `${product.win32AppUserModelId}`; - // } - - packageInnoSetup(issPath, { definitions }, cb); - }; -} - -/** - * @param {string} arch - * @param {string} target - */ -function defineWin32SetupTasks(arch, target) { - const cleanTask = util.rimraf(setupDir(arch, target)); - gulp.task(task.define(`vscode-win32-${arch}-${target}-setup`, task.series(cleanTask, buildWin32Setup(arch, target)))); -} - -defineWin32SetupTasks('x64', 'system'); -defineWin32SetupTasks('arm64', 'system'); -defineWin32SetupTasks('x64', 'user'); -defineWin32SetupTasks('arm64', 'user'); - -/** - * @param {string} arch - */ -function copyInnoUpdater(arch) { - return () => { - return gulp.src('build/win32/{inno_updater.exe,vcruntime140.dll}', { base: 'build/win32' }) - .pipe(vfs.dest(path.join(buildPath(arch), 'tools'))); - }; -} - -/** - * @param {string} executablePath - */ -function updateIcon(executablePath) { - return cb => { - const icon = path.join(repoPath, 'resources', 'win32', 'code.ico'); - rcedit(executablePath, { icon }, cb); - }; -} - -gulp.task(task.define('vscode-win32-x64-inno-updater', task.series(copyInnoUpdater('x64'), updateIcon(path.join(buildPath('x64'), 'tools', 'inno_updater.exe'))))); -gulp.task(task.define('vscode-win32-arm64-inno-updater', task.series(copyInnoUpdater('arm64'), updateIcon(path.join(buildPath('arm64'), 'tools', 'inno_updater.exe'))))); diff --git a/build/gulpfile.vscode.win32.ts b/build/gulpfile.vscode.win32.ts new file mode 100644 index 00000000000..bfe1d12a04d --- /dev/null +++ b/build/gulpfile.vscode.win32.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as cp from 'child_process'; +import * as fs from 'fs'; +import gulp from 'gulp'; +import * as path from 'path'; +import rcedit from 'rcedit'; +import vfs from 'vinyl-fs'; +import pkg from '../package.json' with { type: 'json' }; +import product from '../product.json' with { type: 'json' }; +import { getVersion } from './lib/getVersion.ts'; +import * as task from './lib/task.ts'; +import * as util from './lib/util.ts'; + +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +const repoPath = path.dirname(import.meta.dirname); +const commit = getVersion(repoPath); +const buildPath = (arch: string) => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); +const setupDir = (arch: string, target: string) => path.join(repoPath, '.build', `win32-${arch}`, `${target}-setup`); +const innoSetupPath = path.join(path.dirname(path.dirname(require.resolve('innosetup'))), 'bin', 'ISCC.exe'); +const signWin32Path = path.join(repoPath, 'build', 'azure-pipelines', 'common', 'sign-win32.ts'); + +function packageInnoSetup(iss: string, options: { definitions?: Record }, cb: (err?: Error | null) => void) { + const definitions = options.definitions || {}; + + if (process.argv.some(arg => arg === '--debug-inno')) { + definitions['Debug'] = 'true'; + } + + if (process.argv.some(arg => arg === '--sign')) { + definitions['Sign'] = 'true'; + } + + const keys = Object.keys(definitions); + + keys.forEach(key => assert(typeof definitions[key] === 'string', `Missing value for '${key}' in Inno Setup package step`)); + + const defs = keys.map(key => `/d${key}=${definitions[key]}`); + const args = [ + iss, + ...defs, + `/sesrp=node ${signWin32Path} $f` + ]; + + cp.spawn(innoSetupPath, args, { stdio: ['ignore', 'inherit', 'inherit'] }) + .on('error', cb) + .on('exit', code => { + if (code === 0) { + cb(null); + } else { + cb(new Error(`InnoSetup returned exit code: ${code}`)); + } + }); +} + +function buildWin32Setup(arch: string, target: string): task.CallbackTask { + if (target !== 'system' && target !== 'user') { + throw new Error('Invalid setup target'); + } + + return (cb) => { + const x64AppId = target === 'system' ? product.win32x64AppId : product.win32x64UserAppId; + const arm64AppId = target === 'system' ? product.win32arm64AppId : product.win32arm64UserAppId; + + const sourcePath = buildPath(arch); + const outputPath = setupDir(arch, target); + fs.mkdirSync(outputPath, { recursive: true }); + + const quality = (product as typeof product & { quality?: string }).quality || 'dev'; + const useVersionedUpdate = (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; + const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; + const issPath = path.join(import.meta.dirname, 'win32', 'code.iss'); + const originalProductJsonPath = path.join(sourcePath, versionedResourcesFolder, 'resources/app/product.json'); + const productJsonPath = path.join(outputPath, 'product.json'); + const productJson = JSON.parse(fs.readFileSync(originalProductJsonPath, 'utf8')); + productJson['target'] = target; + fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); + + const definitions: Record = { + NameLong: product.nameLong, + NameShort: product.nameShort, + DirName: product.win32DirName, + Version: pkg.version, + RawVersion: pkg.version.replace(/-\w+$/, ''), + Commit: commit, + NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''), + ExeBasename: product.nameShort, + RegValueName: product.win32RegValueName, + ShellNameShort: product.win32ShellNameShort, + AppMutex: product.win32MutexName, + TunnelMutex: product.win32TunnelMutex, + TunnelServiceMutex: product.win32TunnelServiceMutex, + TunnelApplicationName: product.tunnelApplicationName, + ApplicationName: product.applicationName, + Arch: arch, + AppId: { 'x64': x64AppId, 'arm64': arm64AppId }[arch], + IncompatibleTargetAppId: { 'x64': product.win32x64AppId, 'arm64': product.win32arm64AppId }[arch], + AppUserId: product.win32AppUserModelId, + ArchitecturesAllowed: { 'x64': 'x64', 'arm64': 'arm64' }[arch], + ArchitecturesInstallIn64BitMode: { 'x64': 'x64', 'arm64': 'arm64' }[arch], + SourceDir: sourcePath, + RepoDir: repoPath, + OutputDir: outputPath, + InstallTarget: target, + ProductJsonPath: productJsonPath, + VersionedResourcesFolder: versionedResourcesFolder, + Quality: quality + }; + +<<<<<<< Updated upstream +======= +<<<<<<< HEAD:build/gulpfile.vscode.win32.js + // CortexIDE: Disable APPX packaging (Windows Store packages) + // APPX packages are not needed for CortexIDE distribution + // if (quality !== 'exploration') { + // definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; + // definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; + // definitions['AppxPackageName'] = `${product.win32AppUserModelId}`; + // } +======= +>>>>>>> Stashed changes + if (quality === 'stable' || quality === 'insider') { + definitions['AppxPackage'] = `${quality === 'stable' ? 'code' : 'code_insider'}_${arch}.appx`; + definitions['AppxPackageDll'] = `${quality === 'stable' ? 'code' : 'code_insider'}_explorer_command_${arch}.dll`; + definitions['AppxPackageName'] = `${product.win32AppUserModelId}`; + } +<<<<<<< Updated upstream +======= +>>>>>>> vscode/main:build/gulpfile.vscode.win32.ts +>>>>>>> Stashed changes + + packageInnoSetup(issPath, { definitions }, cb as (err?: Error | null) => void); + }; +} + +function defineWin32SetupTasks(arch: string, target: string) { + const cleanTask = util.rimraf(setupDir(arch, target)); + gulp.task(task.define(`vscode-win32-${arch}-${target}-setup`, task.series(cleanTask, buildWin32Setup(arch, target)))); +} + +defineWin32SetupTasks('x64', 'system'); +defineWin32SetupTasks('arm64', 'system'); +defineWin32SetupTasks('x64', 'user'); +defineWin32SetupTasks('arm64', 'user'); + +function copyInnoUpdater(arch: string) { + return () => { + return gulp.src('build/win32/{inno_updater.exe,vcruntime140.dll}', { base: 'build/win32' }) + .pipe(vfs.dest(path.join(buildPath(arch), 'tools'))); + }; +} + +function updateIcon(executablePath: string): task.CallbackTask { + return cb => { + const icon = path.join(repoPath, 'resources', 'win32', 'code.ico'); + rcedit(executablePath, { icon }, cb); + }; +} + +gulp.task(task.define('vscode-win32-x64-inno-updater', task.series(copyInnoUpdater('x64'), updateIcon(path.join(buildPath('x64'), 'tools', 'inno_updater.exe'))))); +gulp.task(task.define('vscode-win32-arm64-inno-updater', task.series(copyInnoUpdater('arm64'), updateIcon(path.join(buildPath('arm64'), 'tools', 'inno_updater.exe'))))); diff --git a/build/hygiene.js b/build/hygiene.js deleted file mode 100644 index 0a1cc94278c..00000000000 --- a/build/hygiene.js +++ /dev/null @@ -1,326 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -const filter = require('gulp-filter'); -const es = require('event-stream'); -const VinylFile = require('vinyl'); -const vfs = require('vinyl-fs'); -const path = require('path'); -const fs = require('fs'); -const pall = require('p-all'); - -const { all, copyrightFilter, unicodeFilter, indentationFilter, tsFormattingFilter, eslintFilter, stylelintFilter } = require('./filters'); - -const copyrightHeaderLines = [ - '/*---------------------------------------------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' * Licensed under the MIT License. See License.txt in the project root for license information.', - ' *--------------------------------------------------------------------------------------------*/', -]; - -/** - * @param {string[] | NodeJS.ReadWriteStream} some - * @param {boolean} runEslint - */ -function hygiene(some, runEslint = true) { - const eslint = require('./gulp-eslint'); - const gulpstylelint = require('./stylelint'); - const formatter = require('./lib/formatter'); - - let errorCount = 0; - - const productJson = es.through(function (file) { - const product = JSON.parse(file.contents.toString('utf8')); - - if (product.extensionsGallery) { - console.error(`product.json: Contains 'extensionsGallery'`); - errorCount++; - } - - this.emit('data', file); - }); - - const unicode = es.through(function (file) { - /** @type {string[]} */ - const lines = file.contents.toString('utf8').split(/\r\n|\r|\n/); - file.__lines = lines; - const allowInComments = lines.some(line => /allow-any-unicode-comment-file/.test(line)); - let skipNext = false; - lines.forEach((line, i) => { - if (/allow-any-unicode-next-line/.test(line)) { - skipNext = true; - return; - } - if (skipNext) { - skipNext = false; - return; - } - // If unicode is allowed in comments, trim the comment from the line - if (allowInComments) { - if (line.match(/\s+(\*)/)) { // Naive multi-line comment check - line = ''; - } else { - const index = line.indexOf('\/\/'); - line = index === -1 ? line : line.substring(0, index); - } - } - // Please do not add symbols that resemble ASCII letters! - // eslint-disable-next-line no-misleading-character-class - const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); - if (m) { - console.error( - file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` - ); - errorCount++; - } - }); - - this.emit('data', file); - }); - - const indentation = es.through(function (file) { - /** @type {string[]} */ - const lines = file.__lines || file.contents.toString('utf8').split(/\r\n|\r|\n/); - file.__lines = lines; - - lines.forEach((line, i) => { - if (/^\s*$/.test(line)) { - // empty or whitespace lines are OK - } else if (/^[\t]*[^\s]/.test(line)) { - // good indent - } else if (/^[\t]* \*/.test(line)) { - // block comment using an extra space - } else { - console.error( - file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation' - ); - errorCount++; - } - }); - - this.emit('data', file); - }); - - const copyrights = es.through(function (file) { - const lines = file.__lines; - - for (let i = 0; i < copyrightHeaderLines.length; i++) { - if (lines[i] !== copyrightHeaderLines[i]) { - console.error(file.relative + ': Missing or bad copyright statement'); - errorCount++; - break; - } - } - - this.emit('data', file); - }); - - const formatting = es.map(function (file, cb) { - try { - const rawInput = file.contents.toString('utf8'); - const rawOutput = formatter.format(file.path, rawInput); - - const original = rawInput.replace(/\r\n/gm, '\n'); - const formatted = rawOutput.replace(/\r\n/gm, '\n'); - if (original !== formatted) { - console.error( - `File not formatted. Run the 'Format Document' command to fix it:`, - file.relative - ); - errorCount++; - } - cb(undefined, file); - } catch (err) { - cb(err); - } - }); - - let input; - if (Array.isArray(some) || typeof some === 'string' || !some) { - const options = { base: '.', follow: true, allowEmpty: true }; - if (some) { - input = vfs.src(some, options).pipe(filter(all)); // split this up to not unnecessarily filter all a second time - } else { - input = vfs.src(all, options); - } - } else { - input = some; - } - - const productJsonFilter = filter('product.json', { restore: true }); - const snapshotFilter = filter(['**', '!**/*.snap', '!**/*.snap.actual']); - const yarnLockFilter = filter(['**', '!**/yarn.lock']); - const unicodeFilterStream = filter(unicodeFilter, { restore: true }); - - const result = input - .pipe(filter((f) => Boolean(f.stat && !f.stat.isDirectory()))) - .pipe(snapshotFilter) - .pipe(yarnLockFilter) - .pipe(productJsonFilter) - .pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson) - .pipe(productJsonFilter.restore) - .pipe(unicodeFilterStream) - .pipe(unicode) - .pipe(unicodeFilterStream.restore) - .pipe(filter(indentationFilter)) - .pipe(indentation) - .pipe(filter(copyrightFilter)) - .pipe(copyrights); - - /** @type {import('stream').Stream[]} */ - const streams = [ - result.pipe(filter(tsFormattingFilter)).pipe(formatting) - ]; - - if (runEslint) { - streams.push( - result - .pipe(filter(eslintFilter)) - .pipe( - eslint((results) => { - errorCount += results.warningCount; - errorCount += results.errorCount; - }) - ) - ); - } - - streams.push( - result.pipe(filter(stylelintFilter)).pipe(gulpstylelint(((message, isError) => { - if (isError) { - console.error(message); - errorCount++; - } else { - console.warn(message); - } - }))) - ); - - let count = 0; - return es.merge(...streams).pipe( - es.through( - function (data) { - count++; - if (process.env['TRAVIS'] && count % 10 === 0) { - process.stdout.write('.'); - } - this.emit('data', data); - }, - function () { - process.stdout.write('\n'); - if (errorCount > 0) { - this.emit( - 'error', - 'Hygiene failed with ' + - errorCount + - ` errors. Check 'build / gulpfile.hygiene.js'.` - ); - } else { - this.emit('end'); - } - } - ) - ); -} - -module.exports.hygiene = hygiene; - -/** - * @param {string[]} paths - */ -function createGitIndexVinyls(paths) { - const cp = require('child_process'); - const repositoryPath = process.cwd(); - - const fns = paths.map((relativePath) => () => - new Promise((c, e) => { - const fullPath = path.join(repositoryPath, relativePath); - - fs.stat(fullPath, (err, stat) => { - if (err && err.code === 'ENOENT') { - // ignore deletions - return c(null); - } else if (err) { - return e(err); - } - - cp.exec( - process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`, - { maxBuffer: stat.size, encoding: 'buffer' }, - (err, out) => { - if (err) { - return e(err); - } - - c( - new VinylFile({ - path: fullPath, - base: repositoryPath, - contents: out, - stat, - }) - ); - } - ); - }); - }) - ); - - return pall(fns, { concurrency: 4 }).then((r) => r.filter((p) => !!p)); -} - -// this allows us to run hygiene as a git pre-commit hook -if (require.main === module) { - const cp = require('child_process'); - - process.on('unhandledRejection', (reason, p) => { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - process.exit(1); - }); - - if (process.argv.length > 2) { - hygiene(process.argv.slice(2)).on('error', (err) => { - console.error(); - console.error(err); - process.exit(1); - }); - } else { - cp.exec( - 'git diff --cached --name-only', - { maxBuffer: 2000 * 1024 }, - (err, out) => { - if (err) { - console.error(); - console.error(err); - process.exit(1); - } - - const some = out.split(/\r?\n/).filter((l) => !!l); - - if (some.length > 0) { - console.log('Reading git index versions...'); - - createGitIndexVinyls(some) - .then( - (vinyls) => { - /** @type {Promise} */ - return (new Promise((c, e) => - hygiene(es.readArray(vinyls).pipe(filter(all))) - .on('end', () => c()) - .on('error', e) - )) - } - ) - .catch((err) => { - console.error(); - console.error(err); - process.exit(1); - }); - } - } - ); - } -} diff --git a/build/hygiene.ts b/build/hygiene.ts new file mode 100644 index 00000000000..8778907f13f --- /dev/null +++ b/build/hygiene.ts @@ -0,0 +1,314 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import cp from 'child_process'; +import es from 'event-stream'; +import fs from 'fs'; +import filter from 'gulp-filter'; +import pall from 'p-all'; +import path from 'path'; +import VinylFile from 'vinyl'; +import vfs from 'vinyl-fs'; +import { all, copyrightFilter, eslintFilter, indentationFilter, stylelintFilter, tsFormattingFilter, unicodeFilter } from './filters.ts'; +import eslint from './gulp-eslint.ts'; +import * as formatter from './lib/formatter.ts'; +import gulpstylelint from './stylelint.ts'; + +const copyrightHeaderLines = [ + '/*---------------------------------------------------------------------------------------------', + ' * Copyright (c) Microsoft Corporation. All rights reserved.', + ' * Licensed under the MIT License. See License.txt in the project root for license information.', + ' *--------------------------------------------------------------------------------------------*/', +]; + +interface VinylFileWithLines extends VinylFile { + __lines: string[]; +} + +/** + * Main hygiene function that runs checks on files + */ +export function hygiene(some: NodeJS.ReadWriteStream | string[] | undefined, runEslint = true): NodeJS.ReadWriteStream { + console.log('Starting hygiene...'); + let errorCount = 0; + + const productJson = es.through(function (file: VinylFile) { + const product = JSON.parse(file.contents!.toString('utf8')); + + if (product.extensionsGallery) { + console.error(`product.json: Contains 'extensionsGallery'`); + errorCount++; + } + + this.emit('data', file); + }); + + const unicode = es.through(function (file: VinylFileWithLines) { + const lines = file.contents!.toString('utf8').split(/\r\n|\r|\n/); + file.__lines = lines; + const allowInComments = lines.some(line => /allow-any-unicode-comment-file/.test(line)); + let skipNext = false; + lines.forEach((line, i) => { + if (/allow-any-unicode-next-line/.test(line)) { + skipNext = true; + return; + } + if (skipNext) { + skipNext = false; + return; + } + // If unicode is allowed in comments, trim the comment from the line + if (allowInComments) { + if (line.match(/\s+(\*)/)) { // Naive multi-line comment check + line = ''; + } else { + const index = line.indexOf('//'); + line = index === -1 ? line : line.substring(0, index); + } + } + // Please do not add symbols that resemble ASCII letters! + // eslint-disable-next-line no-misleading-character-class + const m = /([^\t\n\r\x20-\x7E⊃⊇✔︎✓🎯🧪✍️⚠️🛑🔴🚗🚙🚕🎉✨❗⇧⌥⌘×÷¦⋯…↑↓→→←↔⟷·•●◆▼⟪⟫┌└├⏎↩√φ]+)/g.exec(line); + if (m) { + console.error( + file.relative + `(${i + 1},${m.index + 1}): Unexpected unicode character: "${m[0]}" (charCode: ${m[0].charCodeAt(0)}). To suppress, use // allow-any-unicode-next-line` + ); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const indentation = es.through(function (file: VinylFileWithLines) { + const lines = file.__lines || file.contents!.toString('utf8').split(/\r\n|\r|\n/); + file.__lines = lines; + + lines.forEach((line, i) => { + if (/^\s*$/.test(line)) { + // empty or whitespace lines are OK + } else if (/^[\t]*[^\s]/.test(line)) { + // good indent + } else if (/^[\t]* \*/.test(line)) { + // block comment using an extra space + } else { + console.error( + file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation' + ); + errorCount++; + } + }); + + this.emit('data', file); + }); + + const copyrights = es.through(function (file: VinylFileWithLines) { + const lines = file.__lines; + + for (let i = 0; i < copyrightHeaderLines.length; i++) { + if (lines[i] !== copyrightHeaderLines[i]) { + console.error(file.relative + ': Missing or bad copyright statement'); + errorCount++; + break; + } + } + + this.emit('data', file); + }); + + const formatting = es.map(function (file: any, cb) { + try { + const rawInput = file.contents!.toString('utf8'); + const rawOutput = formatter.format(file.path, rawInput); + + const original = rawInput.replace(/\r\n/gm, '\n'); + const formatted = rawOutput.replace(/\r\n/gm, '\n'); + if (original !== formatted) { + console.error( + `File not formatted. Run the 'Format Document' command to fix it:`, + file.relative + ); + errorCount++; + } + cb(undefined, file); + } catch (err) { + cb(err); + } + }); + + let input: NodeJS.ReadWriteStream; + if (Array.isArray(some) || typeof some === 'string' || !some) { + const options = { base: '.', follow: true, allowEmpty: true }; + if (some) { + input = vfs.src(some, options).pipe(filter(Array.from(all))); // split this up to not unnecessarily filter all a second time + } else { + input = vfs.src(Array.from(all), options); + } + } else { + input = some; + } + + const productJsonFilter = filter('product.json', { restore: true }); + const snapshotFilter = filter(['**', '!**/*.snap', '!**/*.snap.actual']); + const yarnLockFilter = filter(['**', '!**/yarn.lock']); + const unicodeFilterStream = filter(Array.from(unicodeFilter), { restore: true }); + + const result = input + .pipe(filter((f) => Boolean(f.stat && !f.stat.isDirectory()))) + .pipe(snapshotFilter) + .pipe(yarnLockFilter) + .pipe(productJsonFilter) + .pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson) + .pipe(productJsonFilter.restore) + .pipe(unicodeFilterStream) + .pipe(unicode) + .pipe(unicodeFilterStream.restore) + .pipe(filter(Array.from(indentationFilter))) + .pipe(indentation) + .pipe(filter(Array.from(copyrightFilter))) + .pipe(copyrights); + + const streams: NodeJS.ReadWriteStream[] = [ + result.pipe(filter(Array.from(tsFormattingFilter))).pipe(formatting) + ]; + + if (runEslint) { + streams.push( + result + .pipe(filter(Array.from(eslintFilter))) + .pipe( + eslint((results) => { + errorCount += results.warningCount; + errorCount += results.errorCount; + }) + ) + ); + } + + streams.push( + result.pipe(filter(Array.from(stylelintFilter))).pipe(gulpstylelint(((message: string, isError: boolean) => { + if (isError) { + console.error(message); + errorCount++; + } else { + console.warn(message); + } + }))) + ); + + let count = 0; + return es.merge(...streams).pipe( + es.through( + function (data: unknown) { + count++; + if (process.env['TRAVIS'] && count % 10 === 0) { + process.stdout.write('.'); + } + this.emit('data', data); + }, + function () { + process.stdout.write('\n'); + if (errorCount > 0) { + this.emit( + 'error', + 'Hygiene failed with ' + + errorCount + + ` errors. Check 'build / gulpfile.hygiene.js'.` + ); + } else { + this.emit('end'); + } + } + ) + ); +} + +function createGitIndexVinyls(paths: string[]): Promise { + const repositoryPath = process.cwd(); + + const fns = paths.map((relativePath) => () => + new Promise((c, e) => { + const fullPath = path.join(repositoryPath, relativePath); + + fs.stat(fullPath, (err, stat) => { + if (err && err.code === 'ENOENT') { + // ignore deletions + return c(null); + } else if (err) { + return e(err); + } + + cp.exec( + process.platform === 'win32' ? `git show :${relativePath}` : `git show ':${relativePath}'`, + { maxBuffer: stat.size, encoding: 'buffer' }, + (err, out) => { + if (err) { + return e(err); + } + + c(new VinylFile({ + path: fullPath, + base: repositoryPath, + contents: out, + stat: stat, + })); + } + ); + }); + }) + ); + + return pall(fns, { concurrency: 4 }).then((r) => r.filter((p): p is VinylFile => !!p)); +} + +// this allows us to run hygiene as a git pre-commit hook +if (import.meta.main) { + process.on('unhandledRejection', (reason: unknown, p: Promise) => { + console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); + process.exit(1); + }); + + if (process.argv.length > 2) { + hygiene(process.argv.slice(2)).on('error', (err: Error) => { + console.error(); + console.error(err); + process.exit(1); + }); + } else { + cp.exec( + 'git diff --cached --name-only', + { maxBuffer: 2000 * 1024 }, + (err, out) => { + if (err) { + console.error(); + console.error(err); + process.exit(1); + } + + const some = out.split(/\r?\n/).filter((l) => !!l); + + if (some.length > 0) { + console.log('Reading git index versions...'); + + createGitIndexVinyls(some) + .then( + (vinyls) => { + return new Promise((c, e) => + hygiene(es.readArray(vinyls).pipe(filter(Array.from(all)))) + .on('end', () => c()) + .on('error', e) + ); + } + ) + .catch((err: Error) => { + console.error(); + console.error(err); + process.exit(1); + }); + } + } + ); + } +} diff --git a/build/lib/asar.js b/build/lib/asar.js deleted file mode 100644 index 20c982a6621..00000000000 --- a/build/lib/asar.js +++ /dev/null @@ -1,156 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAsar = createAsar; -const path_1 = __importDefault(require("path")); -const event_stream_1 = __importDefault(require("event-stream")); -const pickle = require('chromium-pickle-js'); -const Filesystem = require('asar/lib/filesystem'); -const vinyl_1 = __importDefault(require("vinyl")); -const minimatch_1 = __importDefault(require("minimatch")); -function createAsar(folderPath, unpackGlobs, skipGlobs, duplicateGlobs, destFilename) { - const shouldUnpackFile = (file) => { - for (let i = 0; i < unpackGlobs.length; i++) { - if ((0, minimatch_1.default)(file.relative, unpackGlobs[i])) { - return true; - } - } - return false; - }; - const shouldSkipFile = (file) => { - for (const skipGlob of skipGlobs) { - if ((0, minimatch_1.default)(file.relative, skipGlob)) { - return true; - } - } - return false; - }; - // Files that should be duplicated between - // node_modules.asar and node_modules - const shouldDuplicateFile = (file) => { - for (const duplicateGlob of duplicateGlobs) { - if ((0, minimatch_1.default)(file.relative, duplicateGlob)) { - return true; - } - } - return false; - }; - const filesystem = new Filesystem(folderPath); - const out = []; - // Keep track of pending inserts - let pendingInserts = 0; - let onFileInserted = () => { pendingInserts--; }; - // Do not insert twice the same directory - const seenDir = {}; - const insertDirectoryRecursive = (dir) => { - if (seenDir[dir]) { - return; - } - let lastSlash = dir.lastIndexOf('/'); - if (lastSlash === -1) { - lastSlash = dir.lastIndexOf('\\'); - } - if (lastSlash !== -1) { - insertDirectoryRecursive(dir.substring(0, lastSlash)); - } - seenDir[dir] = true; - filesystem.insertDirectory(dir); - }; - const insertDirectoryForFile = (file) => { - let lastSlash = file.lastIndexOf('/'); - if (lastSlash === -1) { - lastSlash = file.lastIndexOf('\\'); - } - if (lastSlash !== -1) { - insertDirectoryRecursive(file.substring(0, lastSlash)); - } - }; - const insertFile = (relativePath, stat, shouldUnpack) => { - insertDirectoryForFile(relativePath); - pendingInserts++; - // Do not pass `onFileInserted` directly because it gets overwritten below. - // Create a closure capturing `onFileInserted`. - filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}).then(() => onFileInserted(), () => onFileInserted()); - }; - return event_stream_1.default.through(function (file) { - if (file.stat.isDirectory()) { - return; - } - if (!file.stat.isFile()) { - throw new Error(`unknown item in stream!`); - } - if (shouldSkipFile(file)) { - this.queue(new vinyl_1.default({ - base: '.', - path: file.path, - stat: file.stat, - contents: file.contents - })); - return; - } - if (shouldDuplicateFile(file)) { - this.queue(new vinyl_1.default({ - base: '.', - path: file.path, - stat: file.stat, - contents: file.contents - })); - } - const shouldUnpack = shouldUnpackFile(file); - insertFile(file.relative, { size: file.contents.length, mode: file.stat.mode }, shouldUnpack); - if (shouldUnpack) { - // The file goes outside of xx.asar, in a folder xx.asar.unpacked - const relative = path_1.default.relative(folderPath, file.path); - this.queue(new vinyl_1.default({ - base: '.', - path: path_1.default.join(destFilename + '.unpacked', relative), - stat: file.stat, - contents: file.contents - })); - } - else { - // The file goes inside of xx.asar - out.push(file.contents); - } - }, function () { - const finish = () => { - { - const headerPickle = pickle.createEmpty(); - headerPickle.writeString(JSON.stringify(filesystem.header)); - const headerBuf = headerPickle.toBuffer(); - const sizePickle = pickle.createEmpty(); - sizePickle.writeUInt32(headerBuf.length); - const sizeBuf = sizePickle.toBuffer(); - out.unshift(headerBuf); - out.unshift(sizeBuf); - } - const contents = Buffer.concat(out); - out.length = 0; - this.queue(new vinyl_1.default({ - base: '.', - path: destFilename, - contents: contents - })); - this.queue(null); - }; - // Call finish() only when all file inserts have finished... - if (pendingInserts === 0) { - finish(); - } - else { - onFileInserted = () => { - pendingInserts--; - if (pendingInserts === 0) { - finish(); - } - }; - } - }); -} -//# sourceMappingURL=asar.js.map \ No newline at end of file diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 5f2df925bde..873b3f946fd 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -5,18 +5,11 @@ import path from 'path'; import es from 'event-stream'; -const pickle = require('chromium-pickle-js'); -const Filesystem = require('asar/lib/filesystem'); +import pickle from 'chromium-pickle-js'; +import Filesystem from 'asar/lib/filesystem.js'; import VinylFile from 'vinyl'; import minimatch from 'minimatch'; -declare class AsarFilesystem { - readonly header: unknown; - constructor(src: string); - insertDirectory(path: string, shouldUnpack?: boolean): unknown; - insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number } }, options: {}): Promise; -} - export function createAsar(folderPath: string, unpackGlobs: string[], skipGlobs: string[], duplicateGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { const shouldUnpackFile = (file: VinylFile): boolean => { diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js deleted file mode 100644 index 249777c4458..00000000000 --- a/build/lib/builtInExtensions.js +++ /dev/null @@ -1,179 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getExtensionStream = getExtensionStream; -exports.getBuiltInExtensions = getBuiltInExtensions; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const os_1 = __importDefault(require("os")); -const rimraf_1 = __importDefault(require("rimraf")); -const event_stream_1 = __importDefault(require("event-stream")); -const gulp_rename_1 = __importDefault(require("gulp-rename")); -const vinyl_fs_1 = __importDefault(require("vinyl-fs")); -const ext = __importStar(require("./extensions")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions || []; -const webBuiltInExtensions = productjson.webBuiltInExtensions || []; -const controlFilePath = path_1.default.join(os_1.default.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); -const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; -function log(...messages) { - if (ENABLE_LOGGING) { - (0, fancy_log_1.default)(...messages); - } -} -function getExtensionPath(extension) { - return path_1.default.join(root, '.build', 'builtInExtensions', extension.name); -} -function isUpToDate(extension) { - const packagePath = path_1.default.join(getExtensionPath(extension), 'package.json'); - if (!fs_1.default.existsSync(packagePath)) { - return false; - } - const packageContents = fs_1.default.readFileSync(packagePath, { encoding: 'utf8' }); - try { - const diskVersion = JSON.parse(packageContents).version; - return (diskVersion === extension.version); - } - catch (err) { - return false; - } -} -function getExtensionDownloadStream(extension) { - let input; - if (extension.vsix) { - input = ext.fromVsix(path_1.default.join(root, extension.vsix), extension); - } - else if (productjson.extensionsGallery?.serviceUrl) { - input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension); - } - else { - input = ext.fromGithub(extension); - } - return input.pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); -} -function getExtensionStream(extension) { - // if the extension exists on disk, use those files instead of downloading anew - if (isUpToDate(extension)) { - log('[extensions]', `${extension.name}@${extension.version} up to date`, ansi_colors_1.default.green('✔︎')); - return vinyl_fs_1.default.src(['**'], { cwd: getExtensionPath(extension), dot: true }) - .pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`)); - } - return getExtensionDownloadStream(extension); -} -function syncMarketplaceExtension(extension) { - const galleryServiceUrl = productjson.extensionsGallery?.serviceUrl; - const source = ansi_colors_1.default.blue(galleryServiceUrl ? '[marketplace]' : '[github]'); - if (isUpToDate(extension)) { - log(source, `${extension.name}@${extension.version}`, ansi_colors_1.default.green('✔︎')); - return event_stream_1.default.readArray([]); - } - rimraf_1.default.sync(getExtensionPath(extension)); - return getExtensionDownloadStream(extension) - .pipe(vinyl_fs_1.default.dest('.build/builtInExtensions')) - .on('end', () => log(source, extension.name, ansi_colors_1.default.green('✔︎'))); -} -function syncExtension(extension, controlState) { - if (extension.platforms) { - const platforms = new Set(extension.platforms); - if (!platforms.has(process.platform)) { - log(ansi_colors_1.default.gray('[skip]'), `${extension.name}@${extension.version}: Platform '${process.platform}' not supported: [${extension.platforms}]`, ansi_colors_1.default.green('✔︎')); - return event_stream_1.default.readArray([]); - } - } - switch (controlState) { - case 'disabled': - log(ansi_colors_1.default.blue('[disabled]'), ansi_colors_1.default.gray(extension.name)); - return event_stream_1.default.readArray([]); - case 'marketplace': - return syncMarketplaceExtension(extension); - default: - if (!fs_1.default.existsSync(controlState)) { - log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but that path does not exist.`)); - return event_stream_1.default.readArray([]); - } - else if (!fs_1.default.existsSync(path_1.default.join(controlState, 'package.json'))) { - log(ansi_colors_1.default.red(`Error: Built-in extension '${extension.name}' is configured to run from '${controlState}' but there is no 'package.json' file in that directory.`)); - return event_stream_1.default.readArray([]); - } - log(ansi_colors_1.default.blue('[local]'), `${extension.name}: ${ansi_colors_1.default.cyan(controlState)}`, ansi_colors_1.default.green('✔︎')); - return event_stream_1.default.readArray([]); - } -} -function readControlFile() { - try { - return JSON.parse(fs_1.default.readFileSync(controlFilePath, 'utf8')); - } - catch (err) { - return {}; - } -} -function writeControlFile(control) { - fs_1.default.mkdirSync(path_1.default.dirname(controlFilePath), { recursive: true }); - fs_1.default.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); -} -function getBuiltInExtensions() { - log('Synchronizing built-in extensions...'); - log(`You can manage built-in extensions with the ${ansi_colors_1.default.cyan('--builtin')} flag`); - const control = readControlFile(); - const streams = []; - for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { - const controlState = control[extension.name] || 'marketplace'; - control[extension.name] = controlState; - streams.push(syncExtension(extension, controlState)); - } - writeControlFile(control); - return new Promise((resolve, reject) => { - event_stream_1.default.merge(streams) - .on('error', reject) - .on('end', resolve); - }); -} -if (require.main === module) { - getBuiltInExtensions().then(() => process.exit(0)).catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=builtInExtensions.js.map \ No newline at end of file diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index e9a1180ce35..d52567b17d1 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -10,7 +10,7 @@ import rimraf from 'rimraf'; import es from 'event-stream'; import rename from 'gulp-rename'; import vfs from 'vinyl-fs'; -import * as ext from './extensions'; +import * as ext from './extensions.ts'; import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import { Stream } from 'stream'; @@ -34,10 +34,10 @@ export interface IExtensionDefinition { }; } -const root = path.dirname(path.dirname(__dirname)); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions || []; -const webBuiltInExtensions = productjson.webBuiltInExtensions || []; +const root = path.dirname(path.dirname(import.meta.dirname)); +const productjson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../product.json'), 'utf8')); +const builtInExtensions = productjson.builtInExtensions as IExtensionDefinition[] || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions as IExtensionDefinition[] || []; const controlFilePath = path.join(os.homedir(), '.vscode-oss-dev', 'extensions', 'control.json'); const ENABLE_LOGGING = !process.env['VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE']; @@ -181,7 +181,7 @@ export function getBuiltInExtensions(): Promise { }); } -if (require.main === module) { +if (import.meta.main) { getBuiltInExtensions().then(() => process.exit(0)).catch(err => { console.error(err); process.exit(1); diff --git a/build/lib/builtInExtensionsCG.js b/build/lib/builtInExtensionsCG.js deleted file mode 100644 index 3dc0ae27f0a..00000000000 --- a/build/lib/builtInExtensionsCG.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const url_1 = __importDefault(require("url")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const rootCG = path_1.default.join(root, 'extensionsCG'); -const productjson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions || []; -const webBuiltInExtensions = productjson.webBuiltInExtensions || []; -const token = process.env['GITHUB_TOKEN']; -const contentBasePath = 'raw.githubusercontent.com'; -const contentFileNames = ['package.json', 'package-lock.json']; -async function downloadExtensionDetails(extension) { - const extensionLabel = `${extension.name}@${extension.version}`; - const repository = url_1.default.parse(extension.repo).path.substr(1); - const repositoryContentBaseUrl = `https://${token ? `${token}@` : ''}${contentBasePath}/${repository}/v${extension.version}`; - async function getContent(fileName) { - try { - const response = await fetch(`${repositoryContentBaseUrl}/${fileName}`); - if (response.ok) { - return { fileName, body: Buffer.from(await response.arrayBuffer()) }; - } - else if (response.status === 404) { - return { fileName, body: undefined }; - } - else { - return { fileName, body: null }; - } - } - catch (e) { - return { fileName, body: null }; - } - } - const promises = contentFileNames.map(getContent); - console.log(extensionLabel); - const results = await Promise.all(promises); - for (const result of results) { - if (result.body) { - const extensionFolder = path_1.default.join(rootCG, extension.name); - fs_1.default.mkdirSync(extensionFolder, { recursive: true }); - fs_1.default.writeFileSync(path_1.default.join(extensionFolder, result.fileName), result.body); - console.log(` - ${result.fileName} ${ansi_colors_1.default.green('✔︎')}`); - } - else if (result.body === undefined) { - console.log(` - ${result.fileName} ${ansi_colors_1.default.yellow('⚠️')}`); - } - else { - console.log(` - ${result.fileName} ${ansi_colors_1.default.red('🛑')}`); - } - } - // Validation - if (!results.find(r => r.fileName === 'package.json')?.body) { - // throw new Error(`The "package.json" file could not be found for the built-in extension - ${extensionLabel}`); - } - if (!results.find(r => r.fileName === 'package-lock.json')?.body) { - // throw new Error(`The "package-lock.json" could not be found for the built-in extension - ${extensionLabel}`); - } -} -async function main() { - for (const extension of [...builtInExtensions, ...webBuiltInExtensions]) { - await downloadExtensionDetails(extension); - } -} -main().then(() => { - console.log(`Built-in extensions component data downloaded ${ansi_colors_1.default.green('✔︎')}`); - process.exit(0); -}, err => { - console.log(`Built-in extensions component data could not be downloaded ${ansi_colors_1.default.red('🛑')}`); - console.error(err); - process.exit(1); -}); -//# sourceMappingURL=builtInExtensionsCG.js.map \ No newline at end of file diff --git a/build/lib/builtInExtensionsCG.ts b/build/lib/builtInExtensionsCG.ts index 4628b365a2e..1c4ce609c3d 100644 --- a/build/lib/builtInExtensionsCG.ts +++ b/build/lib/builtInExtensionsCG.ts @@ -7,13 +7,13 @@ import fs from 'fs'; import path from 'path'; import url from 'url'; import ansiColors from 'ansi-colors'; -import { IExtensionDefinition } from './builtInExtensions'; +import type { IExtensionDefinition } from './builtInExtensions.ts'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); const rootCG = path.join(root, 'extensionsCG'); -const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productjson.builtInExtensions || []; -const webBuiltInExtensions = productjson.webBuiltInExtensions || []; +const productjson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../product.json'), 'utf8')); +const builtInExtensions = productjson.builtInExtensions as IExtensionDefinition[] || []; +const webBuiltInExtensions = productjson.webBuiltInExtensions as IExtensionDefinition[] || []; const token = process.env['GITHUB_TOKEN']; const contentBasePath = 'raw.githubusercontent.com'; diff --git a/build/lib/bundle.js b/build/lib/bundle.js deleted file mode 100644 index 382b648defb..00000000000 --- a/build/lib/bundle.js +++ /dev/null @@ -1,62 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.removeAllTSBoilerplate = removeAllTSBoilerplate; -function removeAllTSBoilerplate(source) { - const seen = new Array(BOILERPLATE.length).fill(true, 0, BOILERPLATE.length); - return removeDuplicateTSBoilerplate(source, seen); -} -// Taken from typescript compiler => emitFiles -const BOILERPLATE = [ - { start: /^var __extends/, end: /^}\)\(\);$/ }, - { start: /^var __assign/, end: /^};$/ }, - { start: /^var __decorate/, end: /^};$/ }, - { start: /^var __metadata/, end: /^};$/ }, - { start: /^var __param/, end: /^};$/ }, - { start: /^var __awaiter/, end: /^};$/ }, - { start: /^var __generator/, end: /^};$/ }, - { start: /^var __createBinding/, end: /^}\)\);$/ }, - { start: /^var __setModuleDefault/, end: /^}\);$/ }, - { start: /^var __importStar/, end: /^};$/ }, - { start: /^var __addDisposableResource/, end: /^};$/ }, - { start: /^var __disposeResources/, end: /^}\);$/ }, -]; -function removeDuplicateTSBoilerplate(source, SEEN_BOILERPLATE = []) { - const lines = source.split(/\r\n|\n|\r/); - const newLines = []; - let IS_REMOVING_BOILERPLATE = false, END_BOILERPLATE; - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - if (END_BOILERPLATE.test(line)) { - IS_REMOVING_BOILERPLATE = false; - } - } - else { - for (let j = 0; j < BOILERPLATE.length; j++) { - const boilerplate = BOILERPLATE[j]; - if (boilerplate.start.test(line)) { - if (SEEN_BOILERPLATE[j]) { - IS_REMOVING_BOILERPLATE = true; - END_BOILERPLATE = boilerplate.end; - } - else { - SEEN_BOILERPLATE[j] = true; - } - } - } - if (IS_REMOVING_BOILERPLATE) { - newLines.push(''); - } - else { - newLines.push(line); - } - } - } - return newLines.join('\n'); -} -//# sourceMappingURL=bundle.js.map \ No newline at end of file diff --git a/build/lib/compilation.js b/build/lib/compilation.js deleted file mode 100644 index ac6eae352b0..00000000000 --- a/build/lib/compilation.js +++ /dev/null @@ -1,340 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.watchApiProposalNamesTask = exports.compileApiProposalNamesTask = void 0; -exports.createCompile = createCompile; -exports.transpileTask = transpileTask; -exports.compileTask = compileTask; -exports.watchTask = watchTask; -const event_stream_1 = __importDefault(require("event-stream")); -const fs_1 = __importDefault(require("fs")); -const gulp_1 = __importDefault(require("gulp")); -const path_1 = __importDefault(require("path")); -const monacodts = __importStar(require("./monaco-api")); -const nls = __importStar(require("./nls")); -const reporter_1 = require("./reporter"); -const util = __importStar(require("./util")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const os_1 = __importDefault(require("os")); -const vinyl_1 = __importDefault(require("vinyl")); -const task = __importStar(require("./task")); -const index_1 = require("./mangle/index"); -const ts = require("typescript"); -const watch = require('./watch'); -// --- gulp-tsb: compile and transpile -------------------------------- -const reporter = (0, reporter_1.createReporter)(); -function getTypeScriptCompilerOptions(src) { - const rootDir = path_1.default.join(__dirname, `../../${src}`); - const options = {}; - options.verbose = false; - options.sourceMap = true; - if (process.env['VSCODE_NO_SOURCEMAP']) { // To be used by developers in a hurry - options.sourceMap = false; - } - options.rootDir = rootDir; - options.baseUrl = rootDir; - options.sourceRoot = util.toFileUri(rootDir); - options.newLine = /\r\n/.test(fs_1.default.readFileSync(__filename, 'utf8')) ? 0 : 1; - return options; -} -function createCompile(src, { build, emitError, transpileOnly, preserveEnglish }) { - const tsb = require('./tsb'); - const sourcemaps = require('gulp-sourcemaps'); - const projectPath = path_1.default.join(__dirname, '../../', src, 'tsconfig.json'); - const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; - if (!build) { - overrideOptions.inlineSourceMap = true; - } - const compilation = tsb.create(projectPath, overrideOptions, { - verbose: false, - transpileOnly: Boolean(transpileOnly), - transpileWithEsbuild: typeof transpileOnly !== 'boolean' && transpileOnly.esbuild - }, err => reporter(err)); - function pipeline(token) { - const bom = require('gulp-bom'); - const tsFilter = util.filter(data => /\.ts$/.test(data.path)); - const isUtf8Test = (f) => /(\/|\\)test(\/|\\).*utf8/.test(f.path); - const isRuntimeJs = (f) => f.path.endsWith('.js') && !f.path.includes('fixtures'); - const noDeclarationsFilter = util.filter(data => !(/\.d\.ts$/.test(data.path))); - const input = event_stream_1.default.through(); - const output = input - .pipe(util.$if(isUtf8Test, bom())) // this is required to preserve BOM in test files that loose it otherwise - .pipe(util.$if(!build && isRuntimeJs, util.appendOwnPathSourceURL())) - .pipe(tsFilter) - .pipe(util.loadSourcemaps()) - .pipe(compilation(token)) - .pipe(noDeclarationsFilter) - .pipe(util.$if(build, nls.nls({ preserveEnglish }))) - .pipe(noDeclarationsFilter.restore) - .pipe(util.$if(!transpileOnly, sourcemaps.write('.', { - addComment: false, - includeContent: !!build, - sourceRoot: overrideOptions.sourceRoot - }))) - .pipe(tsFilter.restore) - .pipe(reporter.end(!!emitError)); - return event_stream_1.default.duplex(input, output); - } - pipeline.tsProjectSrc = () => { - return compilation.src({ base: src }); - }; - pipeline.projectPath = projectPath; - return pipeline; -} -function transpileTask(src, out, esbuild) { - const task = () => { - const transpile = createCompile(src, { build: false, emitError: true, transpileOnly: { esbuild: !!esbuild }, preserveEnglish: false }); - const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); - return srcPipe - .pipe(transpile()) - .pipe(gulp_1.default.dest(out)); - }; - task.taskName = `transpile-${path_1.default.basename(src)}`; - return task; -} -function compileTask(src, out, build, options = {}) { - const task = () => { - if (os_1.default.totalmem() < 4_000_000_000) { - throw new Error('compilation requires 4GB of RAM'); - } - const compile = createCompile(src, { build, emitError: true, transpileOnly: false, preserveEnglish: !!options.preserveEnglish }); - const srcPipe = gulp_1.default.src(`${src}/**`, { base: `${src}` }); - const generator = new MonacoGenerator(false); - if (src === 'src') { - generator.execute(); - } - // mangle: TypeScript to TypeScript - let mangleStream = event_stream_1.default.through(); - if (build && !options.disableMangle) { - let ts2tsMangler = new index_1.Mangler(compile.projectPath, (...data) => (0, fancy_log_1.default)(ansi_colors_1.default.blue('[mangler]'), ...data), { mangleExports: true, manglePrivateFields: true }); - const newContentsByFileName = ts2tsMangler.computeNewFileContents(new Set(['saveState'])); - mangleStream = event_stream_1.default.through(async function write(data) { - const tsNormalPath = ts.normalizePath(data.path); - const newContents = (await newContentsByFileName).get(tsNormalPath); - if (newContents !== undefined) { - data.contents = Buffer.from(newContents.out); - data.sourceMap = newContents.sourceMap && JSON.parse(newContents.sourceMap); - } - this.push(data); - }, async function end() { - // free resources - (await newContentsByFileName).clear(); - this.push(null); - ts2tsMangler = undefined; - }); - } - return srcPipe - .pipe(mangleStream) - .pipe(generator.stream) - .pipe(compile()) - .pipe(gulp_1.default.dest(out)); - }; - task.taskName = `compile-${path_1.default.basename(src)}`; - return task; -} -function watchTask(out, build, srcPath = 'src') { - const task = () => { - const compile = createCompile(srcPath, { build, emitError: false, transpileOnly: false, preserveEnglish: false }); - const src = gulp_1.default.src(`${srcPath}/**`, { base: srcPath }); - const watchSrc = watch(`${srcPath}/**`, { base: srcPath, readDelay: 200 }); - const generator = new MonacoGenerator(true); - generator.execute(); - return watchSrc - .pipe(generator.stream) - .pipe(util.incremental(compile, src, true)) - .pipe(gulp_1.default.dest(out)); - }; - task.taskName = `watch-${path_1.default.basename(out)}`; - return task; -} -const REPO_SRC_FOLDER = path_1.default.join(__dirname, '../../src'); -class MonacoGenerator { - _isWatch; - stream; - _watchedFiles; - _fsProvider; - _declarationResolver; - constructor(isWatch) { - this._isWatch = isWatch; - this.stream = event_stream_1.default.through(); - this._watchedFiles = {}; - const onWillReadFile = (moduleId, filePath) => { - if (!this._isWatch) { - return; - } - if (this._watchedFiles[filePath]) { - return; - } - this._watchedFiles[filePath] = true; - fs_1.default.watchFile(filePath, () => { - this._declarationResolver.invalidateCache(moduleId); - this._executeSoon(); - }); - }; - this._fsProvider = new class extends monacodts.FSProvider { - readFileSync(moduleId, filePath) { - onWillReadFile(moduleId, filePath); - return super.readFileSync(moduleId, filePath); - } - }; - this._declarationResolver = new monacodts.DeclarationResolver(this._fsProvider); - if (this._isWatch) { - fs_1.default.watchFile(monacodts.RECIPE_PATH, () => { - this._executeSoon(); - }); - } - } - _executeSoonTimer = null; - _executeSoon() { - if (this._executeSoonTimer !== null) { - clearTimeout(this._executeSoonTimer); - this._executeSoonTimer = null; - } - this._executeSoonTimer = setTimeout(() => { - this._executeSoonTimer = null; - this.execute(); - }, 20); - } - _run() { - const r = monacodts.run3(this._declarationResolver); - if (!r && !this._isWatch) { - // The build must always be able to generate the monaco.d.ts - throw new Error(`monaco.d.ts generation error - Cannot continue`); - } - return r; - } - _log(message, ...rest) { - (0, fancy_log_1.default)(ansi_colors_1.default.cyan('[monaco.d.ts]'), message, ...rest); - } - execute() { - const startTime = Date.now(); - const result = this._run(); - if (!result) { - // nothing really changed - return; - } - if (result.isTheSame) { - return; - } - fs_1.default.writeFileSync(result.filePath, result.content); - fs_1.default.writeFileSync(path_1.default.join(REPO_SRC_FOLDER, 'vs/editor/common/standalone/standaloneEnums.ts'), result.enums); - this._log(`monaco.d.ts is changed - total time took ${Date.now() - startTime} ms`); - if (!this._isWatch) { - this.stream.emit('error', 'monaco.d.ts is no longer up to date. Please run gulp watch and commit the new file.'); - } - } -} -function generateApiProposalNames() { - let eol; - try { - const src = fs_1.default.readFileSync('src/vs/platform/extensions/common/extensionsApiProposals.ts', 'utf-8'); - const match = /\r?\n/m.exec(src); - eol = match ? match[0] : os_1.default.EOL; - } - catch { - eol = os_1.default.EOL; - } - const pattern = /vscode\.proposed\.([a-zA-Z\d]+)\.d\.ts$/; - const versionPattern = /^\s*\/\/\s*version\s*:\s*(\d+)\s*$/mi; - const proposals = new Map(); - const input = event_stream_1.default.through(); - const output = input - .pipe(util.filter((f) => pattern.test(f.path))) - .pipe(event_stream_1.default.through((f) => { - const name = path_1.default.basename(f.path); - const match = pattern.exec(name); - if (!match) { - return; - } - const proposalName = match[1]; - const contents = f.contents.toString('utf8'); - const versionMatch = versionPattern.exec(contents); - const version = versionMatch ? versionMatch[1] : undefined; - proposals.set(proposalName, { - proposal: `https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.${proposalName}.d.ts`, - version: version ? parseInt(version) : undefined - }); - }, function () { - const names = [...proposals.keys()].sort(); - const contents = [ - '/*---------------------------------------------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' * Licensed under the MIT License. See License.txt in the project root for license information.', - ' *--------------------------------------------------------------------------------------------*/', - '', - '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', - '', - 'const _allApiProposals = {', - `${names.map(proposalName => { - const proposal = proposals.get(proposalName); - return `\t${proposalName}: {${eol}\t\tproposal: '${proposal.proposal}',${eol}${proposal.version ? `\t\tversion: ${proposal.version}${eol}` : ''}\t}`; - }).join(`,${eol}`)}`, - '};', - 'export const allApiProposals = Object.freeze<{ [proposalName: string]: Readonly<{ proposal: string; version?: number }> }>(_allApiProposals);', - 'export type ApiProposalName = keyof typeof _allApiProposals;', - '', - ].join(eol); - this.emit('data', new vinyl_1.default({ - path: 'vs/platform/extensions/common/extensionsApiProposals.ts', - contents: Buffer.from(contents) - })); - this.emit('end'); - })); - return event_stream_1.default.duplex(input, output); -} -const apiProposalNamesReporter = (0, reporter_1.createReporter)('api-proposal-names'); -exports.compileApiProposalNamesTask = task.define('compile-api-proposal-names', () => { - return gulp_1.default.src('src/vscode-dts/**') - .pipe(generateApiProposalNames()) - .pipe(gulp_1.default.dest('src')) - .pipe(apiProposalNamesReporter.end(true)); -}); -exports.watchApiProposalNamesTask = task.define('watch-api-proposal-names', () => { - const task = () => gulp_1.default.src('src/vscode-dts/**') - .pipe(generateApiProposalNames()) - .pipe(apiProposalNamesReporter.end(true)); - return watch('src/vscode-dts/**', { readDelay: 200 }) - .pipe(util.debounce(task)) - .pipe(gulp_1.default.dest('src')); -}); -//# sourceMappingURL=compilation.js.map \ No newline at end of file diff --git a/build/lib/compilation.ts b/build/lib/compilation.ts index a8b72914925..f440dc28dd0 100644 --- a/build/lib/compilation.ts +++ b/build/lib/compilation.ts @@ -7,19 +7,22 @@ import es from 'event-stream'; import fs from 'fs'; import gulp from 'gulp'; import path from 'path'; -import * as monacodts from './monaco-api'; -import * as nls from './nls'; -import { createReporter } from './reporter'; -import * as util from './util'; +import * as monacodts from './monaco-api.ts'; +import * as nls from './nls.ts'; +import { createReporter } from './reporter.ts'; +import * as util from './util.ts'; import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import os from 'os'; import File from 'vinyl'; -import * as task from './task'; -import { Mangler } from './mangle/index'; -import { RawSourceMap } from 'source-map'; -import ts = require('typescript'); -const watch = require('./watch'); +import * as task from './task.ts'; +import { Mangler } from './mangle/index.ts'; +import type { RawSourceMap } from 'source-map'; +import ts from 'typescript'; +import watch from './watch/index.ts'; +import bom from 'gulp-bom'; +import * as tsb from './tsb/index.ts'; +import sourcemaps from 'gulp-sourcemaps'; // --- gulp-tsb: compile and transpile -------------------------------- @@ -27,7 +30,7 @@ const watch = require('./watch'); const reporter = createReporter(); function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions { - const rootDir = path.join(__dirname, `../../${src}`); + const rootDir = path.join(import.meta.dirname, `../../${src}`); const options: ts.CompilerOptions = {}; options.verbose = false; options.sourceMap = true; @@ -37,7 +40,7 @@ function getTypeScriptCompilerOptions(src: string): ts.CompilerOptions { options.rootDir = rootDir; options.baseUrl = rootDir; options.sourceRoot = util.toFileUri(rootDir); - options.newLine = /\r\n/.test(fs.readFileSync(__filename, 'utf8')) ? 0 : 1; + options.newLine = /\r\n/.test(fs.readFileSync(import.meta.filename, 'utf8')) ? 0 : 1; return options; } @@ -49,11 +52,7 @@ interface ICompileTaskOptions { } export function createCompile(src: string, { build, emitError, transpileOnly, preserveEnglish }: ICompileTaskOptions) { - const tsb = require('./tsb') as typeof import('./tsb'); - const sourcemaps = require('gulp-sourcemaps') as typeof import('gulp-sourcemaps'); - - - const projectPath = path.join(__dirname, '../../', src, 'tsconfig.json'); + const projectPath = path.join(import.meta.dirname, '../../', src, 'tsconfig.json'); const overrideOptions = { ...getTypeScriptCompilerOptions(src), inlineSources: Boolean(build) }; if (!build) { overrideOptions.inlineSourceMap = true; @@ -66,7 +65,6 @@ export function createCompile(src: string, { build, emitError, transpileOnly, pr }, err => reporter(err)); function pipeline(token?: util.ICancellationToken) { - const bom = require('gulp-bom') as typeof import('gulp-bom'); const tsFilter = util.filter(data => /\.ts$/.test(data.path)); const isUtf8Test = (f: File) => /(\/|\\)test(\/|\\).*utf8/.test(f.path); @@ -138,7 +136,7 @@ export function compileTask(src: string, out: string, build: boolean, options: { const newContentsByFileName = ts2tsMangler.computeNewFileContents(new Set(['saveState'])); mangleStream = es.through(async function write(data: File & { sourceMap?: RawSourceMap }) { type TypeScriptExt = typeof ts & { normalizePath(path: string): string }; - const tsNormalPath = (ts).normalizePath(data.path); + const tsNormalPath = (ts as TypeScriptExt).normalizePath(data.path); const newContents = (await newContentsByFileName).get(tsNormalPath); if (newContents !== undefined) { data.contents = Buffer.from(newContents.out); @@ -185,7 +183,7 @@ export function watchTask(out: string, build: boolean, srcPath: string = 'src'): return task; } -const REPO_SRC_FOLDER = path.join(__dirname, '../../src'); +const REPO_SRC_FOLDER = path.join(import.meta.dirname, '../../src'); class MonacoGenerator { private readonly _isWatch: boolean; @@ -249,7 +247,7 @@ class MonacoGenerator { return r; } - private _log(message: any, ...rest: unknown[]): void { + private _log(message: string, ...rest: unknown[]): void { fancyLog(ansiColors.cyan('[monaco.d.ts]'), message, ...rest); } @@ -358,3 +356,34 @@ export const watchApiProposalNamesTask = task.define('watch-api-proposal-names', .pipe(util.debounce(task)) .pipe(gulp.dest('src')); }); + +// Codicons +const root = path.dirname(path.dirname(import.meta.dirname)); +const codiconSource = path.join(root, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.ttf'); +const codiconDest = path.join(root, 'src', 'vs', 'base', 'browser', 'ui', 'codicons', 'codicon', 'codicon.ttf'); + +function copyCodiconsImpl() { + try { + if (fs.existsSync(codiconSource)) { + fs.mkdirSync(path.dirname(codiconDest), { recursive: true }); + fs.copyFileSync(codiconSource, codiconDest); + } else { + fancyLog(ansiColors.red('[codicons]'), `codicon.ttf not found in node_modules. Please run 'npm install' to install dependencies.`); + } + } catch (e) { + fancyLog(ansiColors.red('[codicons]'), `Error copying codicon.ttf: ${e}`); + } +} + +export const copyCodiconsTask = task.define('copy-codicons', () => { + copyCodiconsImpl(); + return Promise.resolve(); +}); +gulp.task(copyCodiconsTask); + +export const watchCodiconsTask = task.define('watch-codicons', () => { + copyCodiconsImpl(); + return watch('node_modules/@vscode/codicons/dist/**', { readDelay: 200 }) + .on('data', () => copyCodiconsImpl()); +}); +gulp.task(watchCodiconsTask); diff --git a/build/lib/date.js b/build/lib/date.js deleted file mode 100644 index 1ed884fb7ee..00000000000 --- a/build/lib/date.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.writeISODate = writeISODate; -exports.readISODate = readISODate; -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const root = path_1.default.join(__dirname, '..', '..'); -/** - * Writes a `outDir/date` file with the contents of the build - * so that other tasks during the build process can use it and - * all use the same date. - */ -function writeISODate(outDir) { - const result = () => new Promise((resolve, _) => { - const outDirectory = path_1.default.join(root, outDir); - fs_1.default.mkdirSync(outDirectory, { recursive: true }); - const date = new Date().toISOString(); - fs_1.default.writeFileSync(path_1.default.join(outDirectory, 'date'), date, 'utf8'); - resolve(); - }); - result.taskName = 'build-date-file'; - return result; -} -function readISODate(outDir) { - const outDirectory = path_1.default.join(root, outDir); - return fs_1.default.readFileSync(path_1.default.join(outDirectory, 'date'), 'utf8'); -} -//# sourceMappingURL=date.js.map \ No newline at end of file diff --git a/build/lib/date.ts b/build/lib/date.ts index 8a933178952..9c20c9eeb22 100644 --- a/build/lib/date.ts +++ b/build/lib/date.ts @@ -6,7 +6,7 @@ import path from 'path'; import fs from 'fs'; -const root = path.join(__dirname, '..', '..'); +const root = path.join(import.meta.dirname, '..', '..'); /** * Writes a `outDir/date` file with the contents of the build diff --git a/build/lib/dependencies.js b/build/lib/dependencies.js deleted file mode 100644 index 04a09f98708..00000000000 --- a/build/lib/dependencies.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getProductionDependencies = getProductionDependencies; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const child_process_1 = __importDefault(require("child_process")); -const root = fs_1.default.realpathSync(path_1.default.dirname(path_1.default.dirname(__dirname))); -function getNpmProductionDependencies(folder) { - let raw; - try { - raw = child_process_1.default.execSync('npm ls --all --omit=dev --parseable', { cwd: folder, encoding: 'utf8', env: { ...process.env, NODE_ENV: 'production' }, stdio: [null, null, null] }); - } - catch (err) { - const regex = /^npm ERR! .*$/gm; - let match; - while (match = regex.exec(err.message)) { - if (/ELSPROBLEMS/.test(match[0])) { - continue; - } - else if (/invalid: xterm/.test(match[0])) { - continue; - } - else if (/A complete log of this run/.test(match[0])) { - continue; - } - else { - throw err; - } - } - raw = err.stdout; - } - return raw.split(/\r?\n/).filter(line => { - return !!line.trim() && path_1.default.relative(root, line) !== path_1.default.relative(root, folder); - }); -} -function getProductionDependencies(folderPath) { - const result = getNpmProductionDependencies(folderPath); - // Account for distro npm dependencies - const realFolderPath = fs_1.default.realpathSync(folderPath); - const relativeFolderPath = path_1.default.relative(root, realFolderPath); - const distroFolderPath = `${root}/.build/distro/npm/${relativeFolderPath}`; - if (fs_1.default.existsSync(distroFolderPath)) { - result.push(...getNpmProductionDependencies(distroFolderPath)); - } - return [...new Set(result)]; -} -if (require.main === module) { - console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); -} -//# sourceMappingURL=dependencies.js.map \ No newline at end of file diff --git a/build/lib/dependencies.ts b/build/lib/dependencies.ts index a5bc70088a7..ed7cbfbef02 100644 --- a/build/lib/dependencies.ts +++ b/build/lib/dependencies.ts @@ -6,7 +6,7 @@ import fs from 'fs'; import path from 'path'; import cp from 'child_process'; -const root = fs.realpathSync(path.dirname(path.dirname(__dirname))); +const root = fs.realpathSync(path.dirname(path.dirname(import.meta.dirname))); function getNpmProductionDependencies(folder: string): string[] { let raw: string; @@ -51,6 +51,6 @@ export function getProductionDependencies(folderPath: string): string[] { return [...new Set(result)]; } -if (require.main === module) { +if (import.meta.main) { console.log(JSON.stringify(getProductionDependencies(root), null, ' ')); } diff --git a/build/lib/electron.js b/build/lib/electron.js deleted file mode 100644 index 79f6d515636..00000000000 --- a/build/lib/electron.js +++ /dev/null @@ -1,258 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.config = void 0; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const vinyl_fs_1 = __importDefault(require("vinyl-fs")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const util = __importStar(require("./util")); -const getVersion_1 = require("./getVersion"); -function isDocumentSuffix(str) { - return str === 'document' || str === 'script' || str === 'file' || str === 'source code'; -} -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const product = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'product.json'), 'utf8')); -const commit = (0, getVersion_1.getVersion)(root); -function createTemplate(input) { - return (params) => { - return input.replace(/<%=\s*([^\s]+)\s*%>/g, (match, key) => { - return params[key] || match; - }); - }; -} -const darwinCreditsTemplate = product.darwinCredits && createTemplate(fs_1.default.readFileSync(path_1.default.join(root, product.darwinCredits), 'utf8')); -/** - * Generate a `DarwinDocumentType` given a list of file extensions, an icon name, and an optional suffix or file type name. - * @param extensions A list of file extensions, such as `['bat', 'cmd']` - * @param icon A sentence-cased file type name that matches the lowercase name of a darwin icon resource. - * For example, `'HTML'` instead of `'html'`, or `'Java'` instead of `'java'`. - * This parameter is lowercased before it is used to reference an icon file. - * @param nameOrSuffix An optional suffix or a string to use as the file type. If a suffix is provided, - * it is used with the icon parameter to generate a file type string. If nothing is provided, - * `'document'` is used with the icon parameter to generate file type string. - * - * For example, if you call `darwinBundleDocumentType(..., 'HTML')`, the resulting file type is `"HTML document"`, - * and the `'html'` darwin icon is used. - * - * If you call `darwinBundleDocumentType(..., 'Javascript', 'file')`, the resulting file type is `"Javascript file"`. - * and the `'javascript'` darwin icon is used. - * - * If you call `darwinBundleDocumentType(..., 'bat', 'Windows command script')`, the file type is `"Windows command script"`, - * and the `'bat'` darwin icon is used. - */ -function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) { - // If given a suffix, generate a name from it. If not given anything, default to 'document' - if (isDocumentSuffix(nameOrSuffix) || !nameOrSuffix) { - nameOrSuffix = icon.charAt(0).toUpperCase() + icon.slice(1) + ' ' + (nameOrSuffix ?? 'document'); - } - return { - name: nameOrSuffix, - role: 'Editor', - ostypes: ['TEXT', 'utxt', 'TUTX', '****'], - extensions, - iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', - utis - }; -} -/** - * Generate several `DarwinDocumentType`s with unique names and a shared icon. - * @param types A map of file type names to their associated file extensions. - * @param icon A darwin icon resource to use. For example, `'HTML'` would refer to `resources/darwin/html.icns` - * - * Examples: - * ``` - * darwinBundleDocumentTypes({ 'C header file': 'h', 'C source code': 'c' },'c') - * darwinBundleDocumentTypes({ 'React source code': ['jsx', 'tsx'] }, 'react') - * ``` - */ -function darwinBundleDocumentTypes(types, icon) { - return Object.keys(types).map((name) => { - const extensions = types[name]; - return { - name, - role: 'Editor', - ostypes: ['TEXT', 'utxt', 'TUTX', '****'], - extensions: Array.isArray(extensions) ? extensions : [extensions], - iconFile: 'resources/darwin/' + icon + '.icns' - }; - }); -} -const { electronVersion, msBuildId } = util.getElectronVersion(); -exports.config = { - version: electronVersion, - tag: product.electronRepository ? `v${electronVersion}-${msBuildId}` : undefined, - productAppName: product.nameLong, - companyName: 'Microsoft Corporation', - copyright: 'Copyright (C) 2025 Microsoft. All rights reserved', - darwinIcon: 'resources/darwin/code.icns', - darwinBundleIdentifier: product.darwinBundleIdentifier, - darwinApplicationCategoryType: 'public.app-category.developer-tools', - darwinHelpBookFolder: 'VS Code HelpBook', - darwinHelpBookName: 'VS Code HelpBook', - darwinBundleDocumentTypes: [ - ...darwinBundleDocumentTypes({ 'C header file': 'h', 'C source code': 'c' }, 'c'), - ...darwinBundleDocumentTypes({ 'Git configuration file': ['gitattributes', 'gitconfig', 'gitignore'] }, 'config'), - ...darwinBundleDocumentTypes({ 'HTML template document': ['asp', 'aspx', 'cshtml', 'jshtm', 'jsp', 'phtml', 'shtml'] }, 'html'), - darwinBundleDocumentType(['bat', 'cmd'], 'bat', 'Windows command script'), - darwinBundleDocumentType(['bowerrc'], 'Bower'), - darwinBundleDocumentType(['config', 'editorconfig', 'ini', 'cfg'], 'config', 'Configuration file'), - darwinBundleDocumentType(['hh', 'hpp', 'hxx', 'h++'], 'cpp', 'C++ header file'), - darwinBundleDocumentType(['cc', 'cpp', 'cxx', 'c++'], 'cpp', 'C++ source code'), - darwinBundleDocumentType(['m'], 'default', 'Objective-C source code'), - darwinBundleDocumentType(['mm'], 'cpp', 'Objective-C++ source code'), - darwinBundleDocumentType(['cs', 'csx'], 'csharp', 'C# source code'), - darwinBundleDocumentType(['css'], 'css', 'CSS'), - darwinBundleDocumentType(['go'], 'go', 'Go source code'), - darwinBundleDocumentType(['htm', 'html', 'xhtml'], 'HTML'), - darwinBundleDocumentType(['jade'], 'Jade'), - darwinBundleDocumentType(['jav', 'java'], 'Java'), - darwinBundleDocumentType(['js', 'jscsrc', 'jshintrc', 'mjs', 'cjs'], 'Javascript', 'file'), - darwinBundleDocumentType(['json'], 'JSON'), - darwinBundleDocumentType(['less'], 'Less'), - darwinBundleDocumentType(['markdown', 'md', 'mdoc', 'mdown', 'mdtext', 'mdtxt', 'mdwn', 'mkd', 'mkdn'], 'Markdown'), - darwinBundleDocumentType(['php'], 'PHP', 'source code'), - darwinBundleDocumentType(['ps1', 'psd1', 'psm1'], 'Powershell', 'script'), - darwinBundleDocumentType(['py', 'pyi'], 'Python', 'script'), - darwinBundleDocumentType(['gemspec', 'rb', 'erb'], 'Ruby', 'source code'), - darwinBundleDocumentType(['scss', 'sass'], 'SASS', 'file'), - darwinBundleDocumentType(['sql'], 'SQL', 'script'), - darwinBundleDocumentType(['ts'], 'TypeScript', 'file'), - darwinBundleDocumentType(['tsx', 'jsx'], 'React', 'source code'), - darwinBundleDocumentType(['vue'], 'Vue', 'source code'), - darwinBundleDocumentType(['ascx', 'csproj', 'dtd', 'plist', 'wxi', 'wxl', 'wxs', 'xml', 'xaml'], 'XML'), - darwinBundleDocumentType(['eyaml', 'eyml', 'yaml', 'yml'], 'YAML'), - darwinBundleDocumentType([ - 'bash', 'bash_login', 'bash_logout', 'bash_profile', 'bashrc', - 'profile', 'rhistory', 'rprofile', 'sh', 'zlogin', 'zlogout', - 'zprofile', 'zsh', 'zshenv', 'zshrc' - ], 'Shell', 'script'), - // Default icon with specified names - ...darwinBundleDocumentTypes({ - 'Clojure source code': ['clj', 'cljs', 'cljx', 'clojure'], - 'VS Code workspace file': 'code-workspace', - 'CoffeeScript source code': 'coffee', - 'Comma Separated Values': 'csv', - 'CMake script': 'cmake', - 'Dart script': 'dart', - 'Diff file': 'diff', - 'Dockerfile': 'dockerfile', - 'Gradle file': 'gradle', - 'Groovy script': 'groovy', - 'Makefile': ['makefile', 'mk'], - 'Lua script': 'lua', - 'Pug document': 'pug', - 'Jupyter': 'ipynb', - 'Lockfile': 'lock', - 'Log file': 'log', - 'Plain Text File': 'txt', - 'Xcode project file': 'xcodeproj', - 'Xcode workspace file': 'xcworkspace', - 'Visual Basic script': 'vb', - 'R source code': 'r', - 'Rust source code': 'rs', - 'Restructured Text document': 'rst', - 'LaTeX document': ['tex', 'cls'], - 'F# source code': 'fs', - 'F# signature file': 'fsi', - 'F# script': ['fsx', 'fsscript'], - 'SVG document': ['svg'], - 'TOML document': 'toml', - 'Swift source code': 'swift', - }, 'default'), - // Default icon with default name - darwinBundleDocumentType([ - 'containerfile', 'ctp', 'dot', 'edn', 'handlebars', 'hbs', 'ml', 'mli', - 'pl', 'pl6', 'pm', 'pm6', 'pod', 'pp', 'properties', 'psgi', 'rt', 't' - ], 'default', product.nameLong + ' document'), - // Folder support () - darwinBundleDocumentType([], 'default', 'Folder', ['public.folder']) - ], - darwinBundleURLTypes: [{ - role: 'Viewer', - name: product.nameLong, - urlSchemes: [product.urlProtocol] - }], - darwinForceDarkModeSupport: true, - darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined, - linuxExecutableName: product.applicationName, - winIcon: 'resources/win32/code.ico', - token: process.env['GITHUB_TOKEN'], - repo: product.electronRepository || undefined, - validateChecksum: true, - checksumFile: path_1.default.join(root, 'build', 'checksums', 'electron.txt'), -}; -function getElectron(arch) { - return () => { - const electron = require('@vscode/gulp-electron'); - const json = require('gulp-json-editor'); - const electronOpts = { - ...exports.config, - platform: process.platform, - arch: arch === 'armhf' ? 'arm' : arch, - ffmpegChromium: false, - keepDefaultApp: true - }; - return vinyl_fs_1.default.src('package.json') - .pipe(json({ name: product.nameShort })) - .pipe(electron(electronOpts)) - .pipe((0, gulp_filter_1.default)(['**', '!**/app/package.json'])) - .pipe(vinyl_fs_1.default.dest('.build/electron')); - }; -} -async function main(arch = process.arch) { - const version = electronVersion; - const electronPath = path_1.default.join(root, '.build', 'electron'); - const versionFile = path_1.default.join(electronPath, 'version'); - const isUpToDate = fs_1.default.existsSync(versionFile) && fs_1.default.readFileSync(versionFile, 'utf8') === `${version}`; - if (!isUpToDate) { - await util.rimraf(electronPath)(); - await util.streamToPromise(getElectron(arch)()); - } -} -if (require.main === module) { - main(process.argv[2]).catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=electron.js.map \ No newline at end of file diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 08ba68e1b89..aadc9b5fbe7 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -7,8 +7,10 @@ import fs from 'fs'; import path from 'path'; import vfs from 'vinyl-fs'; import filter from 'gulp-filter'; -import * as util from './util'; -import { getVersion } from './getVersion'; +import * as util from './util.ts'; +import { getVersion } from './getVersion.ts'; +import electron from '@vscode/gulp-electron'; +import json from 'gulp-json-editor'; type DarwinDocumentSuffix = 'document' | 'script' | 'file' | 'source code'; type DarwinDocumentType = { @@ -24,9 +26,11 @@ function isDocumentSuffix(str?: string): str is DarwinDocumentSuffix { return str === 'document' || str === 'script' || str === 'file' || str === 'source code'; } -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = getVersion(root); +const useVersionedUpdate = process.platform === 'win32' && (product as typeof product & { win32VersionedUpdate?: boolean })?.win32VersionedUpdate; +const versionedResourcesFolder = useVersionedUpdate ? commit!.substring(0, 10) : ''; function createTemplate(input: string): (params: Record) => string { return (params: Record) => { @@ -104,7 +108,7 @@ export const config = { tag: product.electronRepository ? `v${electronVersion}-${msBuildId}` : undefined, productAppName: product.nameLong, companyName: 'Microsoft Corporation', - copyright: 'Copyright (C) 2025 Microsoft. All rights reserved', + copyright: 'Copyright (C) 2026 Microsoft. All rights reserved', darwinIcon: 'resources/darwin/code.icns', darwinBundleIdentifier: product.darwinBundleIdentifier, darwinApplicationCategoryType: 'public.app-category.developer-tools', @@ -201,13 +205,12 @@ export const config = { repo: product.electronRepository || undefined, validateChecksum: true, checksumFile: path.join(root, 'build', 'checksums', 'electron.txt'), + createVersionedResources: useVersionedUpdate, + productVersionString: versionedResourcesFolder, }; function getElectron(arch: string): () => NodeJS.ReadWriteStream { return () => { - const electron = require('@vscode/gulp-electron'); - const json = require('gulp-json-editor') as typeof import('gulp-json-editor'); - const electronOpts = { ...config, platform: process.platform, @@ -227,7 +230,7 @@ function getElectron(arch: string): () => NodeJS.ReadWriteStream { async function main(arch: string = process.arch): Promise { const version = electronVersion; const electronPath = path.join(root, '.build', 'electron'); - const versionFile = path.join(electronPath, 'version'); + const versionFile = path.join(electronPath, versionedResourcesFolder, 'version'); const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; if (!isUpToDate) { @@ -236,7 +239,7 @@ async function main(arch: string = process.arch): Promise { } } -if (require.main === module) { +if (import.meta.main) { main(process.argv[2]).catch(err => { console.error(err); process.exit(1); diff --git a/build/lib/extensions.js b/build/lib/extensions.js deleted file mode 100644 index f9e7d7f5f80..00000000000 --- a/build/lib/extensions.js +++ /dev/null @@ -1,626 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.fromMarketplace = fromMarketplace; -exports.fromVsix = fromVsix; -exports.fromGithub = fromGithub; -exports.packageNonNativeLocalExtensionsStream = packageNonNativeLocalExtensionsStream; -exports.packageNativeLocalExtensionsStream = packageNativeLocalExtensionsStream; -exports.packageAllLocalExtensionsStream = packageAllLocalExtensionsStream; -exports.packageMarketplaceExtensionsStream = packageMarketplaceExtensionsStream; -exports.scanBuiltinExtensions = scanBuiltinExtensions; -exports.translatePackageJSON = translatePackageJSON; -exports.webpackExtensions = webpackExtensions; -exports.buildExtensionMedia = buildExtensionMedia; -const event_stream_1 = __importDefault(require("event-stream")); -const fs_1 = __importDefault(require("fs")); -const child_process_1 = __importDefault(require("child_process")); -const glob_1 = __importDefault(require("glob")); -const gulp_1 = __importDefault(require("gulp")); -const path_1 = __importDefault(require("path")); -const crypto_1 = __importDefault(require("crypto")); -const vinyl_1 = __importDefault(require("vinyl")); -const stats_1 = require("./stats"); -const util2 = __importStar(require("./util")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const gulp_rename_1 = __importDefault(require("gulp-rename")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const gulp_buffer_1 = __importDefault(require("gulp-buffer")); -const jsoncParser = __importStar(require("jsonc-parser")); -const dependencies_1 = require("./dependencies"); -const builtInExtensions_1 = require("./builtInExtensions"); -const getVersion_1 = require("./getVersion"); -const fetch_1 = require("./fetch"); -const vzip = require('gulp-vinyl-zip'); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const commit = (0, getVersion_1.getVersion)(root); -const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; -function minifyExtensionResources(input) { - const jsonFilter = (0, gulp_filter_1.default)(['**/*.json', '**/*.code-snippets'], { restore: true }); - return input - .pipe(jsonFilter) - .pipe((0, gulp_buffer_1.default)()) - .pipe(event_stream_1.default.mapSync((f) => { - const errors = []; - const value = jsoncParser.parse(f.contents.toString('utf8'), errors, { allowTrailingComma: true }); - if (errors.length === 0) { - // file parsed OK => just stringify to drop whitespace and comments - f.contents = Buffer.from(JSON.stringify(value)); - } - return f; - })) - .pipe(jsonFilter.restore); -} -function updateExtensionPackageJSON(input, update) { - const packageJsonFilter = (0, gulp_filter_1.default)('extensions/*/package.json', { restore: true }); - return input - .pipe(packageJsonFilter) - .pipe((0, gulp_buffer_1.default)()) - .pipe(event_stream_1.default.mapSync((f) => { - const data = JSON.parse(f.contents.toString('utf8')); - f.contents = Buffer.from(JSON.stringify(update(data))); - return f; - })) - .pipe(packageJsonFilter.restore); -} -function fromLocal(extensionPath, forWeb, disableMangle) { - const webpackConfigFileName = forWeb - ? `extension-browser.webpack.config.js` - : `extension.webpack.config.js`; - const isWebPacked = fs_1.default.existsSync(path_1.default.join(extensionPath, webpackConfigFileName)); - let input = isWebPacked - ? fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) - : fromLocalNormal(extensionPath); - if (isWebPacked) { - input = updateExtensionPackageJSON(input, (data) => { - delete data.scripts; - delete data.dependencies; - delete data.devDependencies; - if (data.main) { - data.main = data.main.replace('/out/', '/dist/'); - } - return data; - }); - } - return input; -} -function fromLocalWebpack(extensionPath, webpackConfigFileName, disableMangle) { - const vsce = require('@vscode/vsce'); - const webpack = require('webpack'); - const webpackGulp = require('webpack-stream'); - const result = event_stream_1.default.through(); - const packagedDependencies = []; - const packageJsonConfig = require(path_1.default.join(extensionPath, 'package.json')); - if (packageJsonConfig.dependencies) { - const webpackRootConfig = require(path_1.default.join(extensionPath, webpackConfigFileName)).default; - for (const key in webpackRootConfig.externals) { - if (key in packageJsonConfig.dependencies) { - packagedDependencies.push(key); - } - } - } - // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar - // to vsce.PackageManager.Yarn. - // A static analysis showed there are no webpack externals that are dependencies of the current - // local extensions so we can use the vsce.PackageManager.None config to ignore dependencies list - // as a temporary workaround. - vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.None, packagedDependencies }).then(fileNames => { - const files = fileNames - .map(fileName => path_1.default.join(extensionPath, fileName)) - .map(filePath => new vinyl_1.default({ - path: filePath, - stat: fs_1.default.statSync(filePath), - base: extensionPath, - contents: fs_1.default.createReadStream(filePath) - })); - // check for a webpack configuration files, then invoke webpack - // and merge its output with the files stream. - const webpackConfigLocations = glob_1.default.sync(path_1.default.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); - const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { - const webpackDone = (err, stats) => { - (0, fancy_log_1.default)(`Bundled extension: ${ansi_colors_1.default.yellow(path_1.default.join(path_1.default.basename(extensionPath), path_1.default.relative(extensionPath, webpackConfigPath)))}...`); - if (err) { - result.emit('error', err); - } - const { compilation } = stats; - if (compilation.errors.length > 0) { - result.emit('error', compilation.errors.join('\n')); - } - if (compilation.warnings.length > 0) { - result.emit('error', compilation.warnings.join('\n')); - } - }; - const exportedConfig = require(webpackConfigPath).default; - return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { - const webpackConfig = { - ...config, - ...{ mode: 'production' } - }; - if (disableMangle) { - if (Array.isArray(config.module.rules)) { - for (const rule of config.module.rules) { - if (Array.isArray(rule.use)) { - for (const use of rule.use) { - if (String(use.loader).endsWith('mangle-loader.js')) { - use.options.disabled = true; - } - } - } - } - } - } - const relativeOutputPath = path_1.default.relative(extensionPath, webpackConfig.output.path); - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(event_stream_1.default.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(event_stream_1.default.through(function (data) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - if (path_1.default.extname(data.basename) === '.js') { - const contents = data.contents.toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path_1.default.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - } - this.emit('data', data); - })); - }); - }); - event_stream_1.default.merge(...webpackStreams, event_stream_1.default.readArray(files)) - // .pipe(es.through(function (data) { - // // debug - // console.log('out', data.path, data.contents.length); - // this.emit('data', data); - // })) - .pipe(result); - }).catch(err => { - console.error(extensionPath); - console.error(packagedDependencies); - result.emit('error', err); - }); - return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); -} -function fromLocalNormal(extensionPath) { - const vsce = require('@vscode/vsce'); - const result = event_stream_1.default.through(); - vsce.listFiles({ cwd: extensionPath, packageManager: vsce.PackageManager.Npm }) - .then(fileNames => { - const files = fileNames - .map(fileName => path_1.default.join(extensionPath, fileName)) - .map(filePath => new vinyl_1.default({ - path: filePath, - stat: fs_1.default.statSync(filePath), - base: extensionPath, - contents: fs_1.default.createReadStream(filePath) - })); - event_stream_1.default.readArray(files).pipe(result); - }) - .catch(err => result.emit('error', err)); - return result.pipe((0, stats_1.createStatsStream)(path_1.default.basename(extensionPath))); -} -const userAgent = 'VSCode Build'; -const baseHeaders = { - 'X-Market-Client-Id': 'VSCode Build', - 'User-Agent': userAgent, - 'X-Market-User-Id': '291C1CD0-051A-4123-9B4B-30D60EF52EE2', -}; -function fromMarketplace(serviceUrl, { name: extensionName, version, sha256, metadata }) { - const json = require('gulp-json-editor'); - const [publisher, name] = extensionName.split('.'); - const url = `${serviceUrl}/publishers/${publisher}/vsextensions/${name}/${version}/vspackage`; - (0, fancy_log_1.default)('Downloading extension:', ansi_colors_1.default.yellow(`${extensionName}@${version}`), '...'); - const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); - return (0, fetch_1.fetchUrls)('', { - base: url, - nodeFetchOptions: { - headers: baseHeaders - }, - checksumSha256: sha256 - }) - .pipe(vzip.src()) - .pipe((0, gulp_filter_1.default)('extension/**')) - .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) - .pipe(packageJsonFilter) - .pipe((0, gulp_buffer_1.default)()) - .pipe(json({ __metadata: metadata })) - .pipe(packageJsonFilter.restore); -} -function fromVsix(vsixPath, { name: extensionName, version, sha256, metadata }) { - const json = require('gulp-json-editor'); - (0, fancy_log_1.default)('Using local VSIX for extension:', ansi_colors_1.default.yellow(`${extensionName}@${version}`), '...'); - const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); - return gulp_1.default.src(vsixPath) - .pipe((0, gulp_buffer_1.default)()) - .pipe(event_stream_1.default.mapSync((f) => { - const hash = crypto_1.default.createHash('sha256'); - hash.update(f.contents); - const checksum = hash.digest('hex'); - if (checksum !== sha256) { - throw new Error(`Checksum mismatch for ${vsixPath} (expected ${sha256}, actual ${checksum}))`); - } - return f; - })) - .pipe(vzip.src()) - .pipe((0, gulp_filter_1.default)('extension/**')) - .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) - .pipe(packageJsonFilter) - .pipe((0, gulp_buffer_1.default)()) - .pipe(json({ __metadata: metadata })) - .pipe(packageJsonFilter.restore); -} -function fromGithub({ name, version, repo, sha256, metadata }) { - const json = require('gulp-json-editor'); - (0, fancy_log_1.default)('Downloading extension from GH:', ansi_colors_1.default.yellow(`${name}@${version}`), '...'); - const packageJsonFilter = (0, gulp_filter_1.default)('package.json', { restore: true }); - return (0, fetch_1.fetchGithub)(new URL(repo).pathname, { - version, - name: name => name.endsWith('.vsix'), - checksumSha256: sha256 - }) - .pipe((0, gulp_buffer_1.default)()) - .pipe(vzip.src()) - .pipe((0, gulp_filter_1.default)('extension/**')) - .pipe((0, gulp_rename_1.default)(p => p.dirname = p.dirname.replace(/^extension\/?/, ''))) - .pipe(packageJsonFilter) - .pipe((0, gulp_buffer_1.default)()) - .pipe(json({ __metadata: metadata })) - .pipe(packageJsonFilter.restore); -} -/** - * All extensions that are known to have some native component and thus must be built on the - * platform that is being built. - */ -const nativeExtensions = [ - 'microsoft-authentication', -]; -const excludedExtensions = [ - 'vscode-api-tests', - 'vscode-colorize-tests', - 'vscode-colorize-perf-tests', - 'vscode-test-resolver', - 'ms-vscode.node-debug', - 'ms-vscode.node-debug2', -]; -const marketplaceWebExtensionsExclude = new Set([ - 'ms-vscode.node-debug', - 'ms-vscode.node-debug2', - 'ms-vscode.js-debug-companion', - 'ms-vscode.js-debug', - 'ms-vscode.vscode-js-profile-table' -]); -const productJson = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '../../product.json'), 'utf8')); -const builtInExtensions = productJson.builtInExtensions || []; -const webBuiltInExtensions = productJson.webBuiltInExtensions || []; -/** - * Loosely based on `getExtensionKind` from `src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts` - */ -function isWebExtension(manifest) { - if (Boolean(manifest.browser)) { - return true; - } - if (Boolean(manifest.main)) { - return false; - } - // neither browser nor main - if (typeof manifest.extensionKind !== 'undefined') { - const extensionKind = Array.isArray(manifest.extensionKind) ? manifest.extensionKind : [manifest.extensionKind]; - if (extensionKind.indexOf('web') >= 0) { - return true; - } - } - if (typeof manifest.contributes !== 'undefined') { - for (const id of ['debuggers', 'terminal', 'typescriptServerPlugins']) { - if (manifest.contributes.hasOwnProperty(id)) { - return false; - } - } - } - return true; -} -/** - * Package local extensions that are known to not have native dependencies. Mutually exclusive to {@link packageNativeLocalExtensionsStream}. - * @param forWeb build the extensions that have web targets - * @param disableMangle disable the mangler - * @returns a stream - */ -function packageNonNativeLocalExtensionsStream(forWeb, disableMangle) { - return doPackageLocalExtensionsStream(forWeb, disableMangle, false); -} -/** - * Package local extensions that are known to have native dependencies. Mutually exclusive to {@link packageNonNativeLocalExtensionsStream}. - * @note it's possible that the extension does not have native dependencies for the current platform, especially if building for the web, - * but we simplify the logic here by having a flat list of extensions (See {@link nativeExtensions}) that are known to have native - * dependencies on some platform and thus should be packaged on the platform that they are building for. - * @param forWeb build the extensions that have web targets - * @param disableMangle disable the mangler - * @returns a stream - */ -function packageNativeLocalExtensionsStream(forWeb, disableMangle) { - return doPackageLocalExtensionsStream(forWeb, disableMangle, true); -} -/** - * Package all the local extensions... both those that are known to have native dependencies and those that are not. - * @param forWeb build the extensions that have web targets - * @param disableMangle disable the mangler - * @returns a stream - */ -function packageAllLocalExtensionsStream(forWeb, disableMangle) { - return event_stream_1.default.merge([ - packageNonNativeLocalExtensionsStream(forWeb, disableMangle), - packageNativeLocalExtensionsStream(forWeb, disableMangle) - ]); -} -/** - * @param forWeb build the extensions that have web targets - * @param disableMangle disable the mangler - * @param native build the extensions that are marked as having native dependencies - */ -function doPackageLocalExtensionsStream(forWeb, disableMangle, native) { - const nativeExtensionsSet = new Set(nativeExtensions); - const localExtensionsDescriptions = (glob_1.default.sync('extensions/*/package.json') - .map(manifestPath => { - const absoluteManifestPath = path_1.default.join(root, manifestPath); - const extensionPath = path_1.default.dirname(path_1.default.join(root, manifestPath)); - const extensionName = path_1.default.basename(extensionPath); - return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; - }) - .filter(({ name }) => native ? nativeExtensionsSet.has(name) : !nativeExtensionsSet.has(name)) - .filter(({ name }) => excludedExtensions.indexOf(name) === -1) - .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) - .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); - const localExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...localExtensionsDescriptions.map(extension => { - return fromLocal(extension.path, forWeb, disableMangle) - .pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${extension.name}/${p.dirname}`)); - }))); - let result; - if (forWeb) { - result = localExtensionsStream; - } - else { - // also include shared production node modules - const productionDependencies = (0, dependencies_1.getProductionDependencies)('extensions/'); - const dependenciesSrc = productionDependencies.map(d => path_1.default.relative(root, d)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`]).flat(); - if (dependenciesSrc.length > 0) { - result = event_stream_1.default.merge(localExtensionsStream, gulp_1.default.src(dependenciesSrc, { base: '.' }) - .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', '.moduleignore'))) - .pipe(util2.cleanNodeModules(path_1.default.join(root, 'build', `.moduleignore.${process.platform}`)))); - } - else { - result = localExtensionsStream; - } - } - return (result - .pipe(util2.setExecutableBit(['**/*.sh']))); -} -function packageMarketplaceExtensionsStream(forWeb) { - const marketplaceExtensionsDescriptions = [ - ...builtInExtensions.filter(({ name }) => (forWeb ? !marketplaceWebExtensionsExclude.has(name) : true)), - ...(forWeb ? webBuiltInExtensions : []) - ]; - const marketplaceExtensionsStream = minifyExtensionResources(event_stream_1.default.merge(...marketplaceExtensionsDescriptions - .map(extension => { - const src = (0, builtInExtensions_1.getExtensionStream)(extension).pipe((0, gulp_rename_1.default)(p => p.dirname = `extensions/${p.dirname}`)); - return updateExtensionPackageJSON(src, (data) => { - delete data.scripts; - delete data.dependencies; - delete data.devDependencies; - return data; - }); - }))); - return (marketplaceExtensionsStream - .pipe(util2.setExecutableBit(['**/*.sh']))); -} -function scanBuiltinExtensions(extensionsRoot, exclude = []) { - const scannedExtensions = []; - try { - const extensionsFolders = fs_1.default.readdirSync(extensionsRoot); - for (const extensionFolder of extensionsFolders) { - if (exclude.indexOf(extensionFolder) >= 0) { - continue; - } - const packageJSONPath = path_1.default.join(extensionsRoot, extensionFolder, 'package.json'); - if (!fs_1.default.existsSync(packageJSONPath)) { - continue; - } - const packageJSON = JSON.parse(fs_1.default.readFileSync(packageJSONPath).toString('utf8')); - if (!isWebExtension(packageJSON)) { - continue; - } - const children = fs_1.default.readdirSync(path_1.default.join(extensionsRoot, extensionFolder)); - const packageNLSPath = children.filter(child => child === 'package.nls.json')[0]; - const packageNLS = packageNLSPath ? JSON.parse(fs_1.default.readFileSync(path_1.default.join(extensionsRoot, extensionFolder, packageNLSPath)).toString()) : undefined; - const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - scannedExtensions.push({ - extensionPath: extensionFolder, - packageJSON, - packageNLS, - readmePath: readme ? path_1.default.join(extensionFolder, readme) : undefined, - changelogPath: changelog ? path_1.default.join(extensionFolder, changelog) : undefined, - }); - } - return scannedExtensions; - } - catch (ex) { - return scannedExtensions; - } -} -function translatePackageJSON(packageJSON, packageNLSPath) { - const CharCode_PC = '%'.charCodeAt(0); - const packageNls = JSON.parse(fs_1.default.readFileSync(packageNLSPath).toString()); - const translate = (obj) => { - for (const key in obj) { - const val = obj[key]; - if (Array.isArray(val)) { - val.forEach(translate); - } - else if (val && typeof val === 'object') { - translate(val); - } - else if (typeof val === 'string' && val.charCodeAt(0) === CharCode_PC && val.charCodeAt(val.length - 1) === CharCode_PC) { - const translated = packageNls[val.substr(1, val.length - 2)]; - if (translated) { - obj[key] = typeof translated === 'string' ? translated : (typeof translated.message === 'string' ? translated.message : val); - } - } - } - }; - translate(packageJSON); - return packageJSON; -} -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', - 'simple-browser/esbuild-preview.mjs', -]; -async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { - const webpack = require('webpack'); - const webpackConfigs = []; - for (const { configPath, outputRoot } of webpackConfigLocations) { - const configOrFnOrArray = require(configPath).default; - function addConfig(configOrFnOrArray) { - for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { - const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; - if (outputRoot) { - config.output.path = path_1.default.join(outputRoot, path_1.default.relative(path_1.default.dirname(configPath), config.output.path)); - } - webpackConfigs.push(config); - } - } - addConfig(configOrFnOrArray); - } - function reporter(fullStats) { - if (Array.isArray(fullStats.children)) { - for (const stats of fullStats.children) { - const outputPath = stats.outputPath; - if (outputPath) { - const relativePath = path_1.default.relative(extensionsPath, outputPath).replace(/\\/g, '/'); - const match = relativePath.match(/[^\/]+(\/server|\/client)?/); - (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${ansi_colors_1.default.cyan(match[0])} with ${stats.errors.length} errors.`); - } - if (Array.isArray(stats.errors)) { - stats.errors.forEach((error) => { - fancy_log_1.default.error(error); - }); - } - if (Array.isArray(stats.warnings)) { - stats.warnings.forEach((warning) => { - fancy_log_1.default.warn(warning); - }); - } - } - } - } - return new Promise((resolve, reject) => { - if (isWatch) { - webpack(webpackConfigs).watch({}, (err, stats) => { - if (err) { - reject(); - } - else { - reporter(stats?.toJson()); - } - }); - } - else { - webpack(webpackConfigs).run((err, stats) => { - if (err) { - fancy_log_1.default.error(err); - reject(); - } - else { - reporter(stats?.toJson()); - resolve(); - } - }); - } - }); -} -async function esbuildExtensions(taskName, isWatch, scripts) { - function reporter(stdError, script) { - const matches = (stdError || '').match(/\> (.+): error: (.+)?/g); - (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green(taskName)} ${script} with ${matches ? matches.length : 0} errors.`); - for (const match of matches || []) { - fancy_log_1.default.error(match); - } - } - const tasks = scripts.map(({ script, outputRoot }) => { - return new Promise((resolve, reject) => { - const args = [script]; - if (isWatch) { - args.push('--watch'); - } - if (outputRoot) { - args.push('--outputRoot', outputRoot); - } - const proc = child_process_1.default.execFile(process.argv[0], args, {}, (error, _stdout, stderr) => { - if (error) { - return reject(error); - } - reporter(stderr, script); - return resolve(); - }); - proc.stdout.on('data', (data) => { - (0, fancy_log_1.default)(`${ansi_colors_1.default.green(taskName)}: ${data.toString('utf8')}`); - }); - }); - }); - return Promise.all(tasks); -} -async function buildExtensionMedia(isWatch, outputRoot) { - return esbuildExtensions('esbuilding extension media', isWatch, esbuildMediaScripts.map(p => ({ - script: path_1.default.join(extensionsPath, p), - outputRoot: outputRoot ? path_1.default.join(root, outputRoot, path_1.default.dirname(p)) : undefined - }))); -} -//# sourceMappingURL=extensions.js.map \ No newline at end of file diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 6d49f68546c..a74ae53465a 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -12,8 +12,8 @@ import path from 'path'; import crypto from 'crypto'; import { Stream } from 'stream'; import File from 'vinyl'; -import { createStatsStream } from './stats'; -import * as util2 from './util'; +import { createStatsStream } from './stats.ts'; +import * as util2 from './util.ts'; import filter from 'gulp-filter'; import rename from 'gulp-rename'; import fancyLog from 'fancy-log'; @@ -21,13 +21,16 @@ import ansiColors from 'ansi-colors'; import buffer from 'gulp-buffer'; import * as jsoncParser from 'jsonc-parser'; import webpack from 'webpack'; -import { getProductionDependencies } from './dependencies'; -import { IExtensionDefinition, getExtensionStream } from './builtInExtensions'; -import { getVersion } from './getVersion'; -import { fetchUrls, fetchGithub } from './fetch'; -const vzip = require('gulp-vinyl-zip'); +import { getProductionDependencies } from './dependencies.ts'; +import { type IExtensionDefinition, getExtensionStream } from './builtInExtensions.ts'; +import { getVersion } from './getVersion.ts'; +import { fetchUrls, fetchGithub } from './fetch.ts'; +import vzip from 'gulp-vinyl-zip'; -const root = path.dirname(path.dirname(__dirname)); +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); + +const root = path.dirname(path.dirname(import.meta.dirname)); const commit = getVersion(root); const sourceMappingURLBase = `https://main.vscode-cdn.net/sourcemaps/${commit}`; @@ -95,14 +98,22 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, const result = es.through(); const packagedDependencies: string[] = []; + const stripOutSourceMaps: string[] = []; const packageJsonConfig = require(path.join(extensionPath, 'package.json')); if (packageJsonConfig.dependencies) { - const webpackRootConfig = require(path.join(extensionPath, webpackConfigFileName)).default; + const webpackConfig = require(path.join(extensionPath, webpackConfigFileName)); + const webpackRootConfig = webpackConfig.default; for (const key in webpackRootConfig.externals) { if (key in packageJsonConfig.dependencies) { packagedDependencies.push(key); } } + + if (webpackConfig.StripOutSourceMaps) { + for (const filePath of webpackConfig.StripOutSourceMaps) { + stripOutSourceMaps.push(filePath); + } + } } // TODO: add prune support based on packagedDependencies to vsce.PackageManager.Npm similar @@ -122,14 +133,13 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. - const webpackConfigLocations = (glob.sync( + const webpackConfigLocations = (glob.sync( path.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] } - )); - + ) as string[]); const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { - const webpackDone = (err: any, stats: any) => { + const webpackDone = (err: Error | undefined, stats: any) => { fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); if (err) { result.emit('error', err); @@ -175,10 +185,15 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string, // * rewrite sourceMappingURL // * save to disk so that upload-task picks this up if (path.extname(data.basename) === '.js') { - const contents = (data.contents).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); + if (stripOutSourceMaps.indexOf(data.relative) >= 0) { // remove source map + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); + } else { + const contents = (data.contents as Buffer).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + } } this.emit('data', data); @@ -333,7 +348,7 @@ const marketplaceWebExtensionsExclude = new Set([ 'ms-vscode.vscode-js-profile-table' ]); -const productJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); +const productJson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../product.json'), 'utf8')); const builtInExtensions: IExtensionDefinition[] = productJson.builtInExtensions || []; const webBuiltInExtensions: IExtensionDefinition[] = productJson.webBuiltInExtensions || []; @@ -417,7 +432,7 @@ export function packageAllLocalExtensionsStream(forWeb: boolean, disableMangle: function doPackageLocalExtensionsStream(forWeb: boolean, disableMangle: boolean, native: boolean): Stream { const nativeExtensionsSet = new Set(nativeExtensions); const localExtensionsDescriptions = ( - (glob.sync('extensions/*/package.json')) + (glob.sync('extensions/*/package.json') as string[]) .map(manifestPath => { const absoluteManifestPath = path.join(root, manifestPath); const extensionPath = path.dirname(path.join(root, manifestPath)); diff --git a/build/lib/fetch.js b/build/lib/fetch.js deleted file mode 100644 index b0876cda75a..00000000000 --- a/build/lib/fetch.js +++ /dev/null @@ -1,141 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.fetchUrls = fetchUrls; -exports.fetchUrl = fetchUrl; -exports.fetchGithub = fetchGithub; -const event_stream_1 = __importDefault(require("event-stream")); -const vinyl_1 = __importDefault(require("vinyl")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const crypto_1 = __importDefault(require("crypto")); -const through2_1 = __importDefault(require("through2")); -function fetchUrls(urls, options) { - if (options === undefined) { - options = {}; - } - if (typeof options.base !== 'string' && options.base !== null) { - options.base = '/'; - } - if (!Array.isArray(urls)) { - urls = [urls]; - } - return event_stream_1.default.readArray(urls).pipe(event_stream_1.default.map((data, cb) => { - const url = [options.base, data].join(''); - fetchUrl(url, options).then(file => { - cb(undefined, file); - }, error => { - cb(error); - }); - })); -} -async function fetchUrl(url, options, retries = 10, retryDelay = 1000) { - const verbose = !!options.verbose || !!process.env['CI'] || !!process.env['BUILD_ARTIFACTSTAGINGDIRECTORY'] || !!process.env['GITHUB_WORKSPACE']; - try { - let startTime = 0; - if (verbose) { - (0, fancy_log_1.default)(`Start fetching ${ansi_colors_1.default.magenta(url)}${retries !== 10 ? ` (${10 - retries} retry)` : ''}`); - startTime = new Date().getTime(); - } - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 30 * 1000); - try { - const response = await fetch(url, { - ...options.nodeFetchOptions, - signal: controller.signal - }); - if (verbose) { - (0, fancy_log_1.default)(`Fetch completed: Status ${response.status}. Took ${ansi_colors_1.default.magenta(`${new Date().getTime() - startTime} ms`)}`); - } - if (response.ok && (response.status >= 200 && response.status < 300)) { - const contents = Buffer.from(await response.arrayBuffer()); - if (options.checksumSha256) { - const actualSHA256Checksum = crypto_1.default.createHash('sha256').update(contents).digest('hex'); - if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${ansi_colors_1.default.cyan(url)} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); - } - else if (verbose) { - (0, fancy_log_1.default)(`Verified SHA256 checksums match for ${ansi_colors_1.default.cyan(url)}`); - } - } - else if (verbose) { - (0, fancy_log_1.default)(`Skipping checksum verification for ${ansi_colors_1.default.cyan(url)} because no expected checksum was provided`); - } - if (verbose) { - (0, fancy_log_1.default)(`Fetched response body buffer: ${ansi_colors_1.default.magenta(`${contents.byteLength} bytes`)}`); - } - return new vinyl_1.default({ - cwd: '/', - base: options.base, - path: url, - contents - }); - } - let err = `Request ${ansi_colors_1.default.magenta(url)} failed with status code: ${response.status}`; - if (response.status === 403) { - err += ' (you may be rate limited)'; - } - throw new Error(err); - } - finally { - clearTimeout(timeout); - } - } - catch (e) { - if (verbose) { - (0, fancy_log_1.default)(`Fetching ${ansi_colors_1.default.cyan(url)} failed: ${e}`); - } - if (retries > 0) { - await new Promise(resolve => setTimeout(resolve, retryDelay)); - return fetchUrl(url, options, retries - 1, retryDelay); - } - throw e; - } -} -const ghApiHeaders = { - Accept: 'application/vnd.github.v3+json', - 'User-Agent': 'VSCode Build', -}; -if (process.env.GITHUB_TOKEN) { - ghApiHeaders.Authorization = 'Basic ' + Buffer.from(process.env.GITHUB_TOKEN).toString('base64'); -} -const ghDownloadHeaders = { - ...ghApiHeaders, - Accept: 'application/octet-stream', -}; -/** - * @param repo for example `Microsoft/vscode` - * @param version for example `16.17.1` - must be a valid releases tag - * @param assetName for example (name) => name === `win-x64-node.exe` - must be an asset that exists - * @returns a stream with the asset as file - */ -function fetchGithub(repo, options) { - return fetchUrls(`/repos/${repo.replace(/^\/|\/$/g, '')}/releases/tags/v${options.version}`, { - base: 'https://api.github.com', - verbose: options.verbose, - nodeFetchOptions: { headers: ghApiHeaders } - }).pipe(through2_1.default.obj(async function (file, _enc, callback) { - const assetFilter = typeof options.name === 'string' ? (name) => name === options.name : options.name; - const asset = JSON.parse(file.contents.toString()).assets.find((a) => assetFilter(a.name)); - if (!asset) { - return callback(new Error(`Could not find asset in release of ${repo} @ ${options.version}`)); - } - try { - callback(null, await fetchUrl(asset.url, { - nodeFetchOptions: { headers: ghDownloadHeaders }, - verbose: options.verbose, - checksumSha256: options.checksumSha256 - })); - } - catch (error) { - callback(error); - } - })); -} -//# sourceMappingURL=fetch.js.map \ No newline at end of file diff --git a/build/lib/fetch.ts b/build/lib/fetch.ts index 970887b3e55..0d2c47a7fd8 100644 --- a/build/lib/fetch.ts +++ b/build/lib/fetch.ts @@ -50,7 +50,7 @@ export async function fetchUrl(url: string, options: IFetchOptions, retries = 10 startTime = new Date().getTime(); } const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 30 * 1000); + let timeout = setTimeout(() => controller.abort(), 30 * 1000); try { const response = await fetch(url, { ...options.nodeFetchOptions, @@ -60,6 +60,9 @@ export async function fetchUrl(url: string, options: IFetchOptions, retries = 10 log(`Fetch completed: Status ${response.status}. Took ${ansiColors.magenta(`${new Date().getTime() - startTime} ms`)}`); } if (response.ok && (response.status >= 200 && response.status < 300)) { + // Reset timeout for body download - large files need more time + clearTimeout(timeout); + timeout = setTimeout(() => controller.abort(), 5 * 60 * 1000); const contents = Buffer.from(await response.arrayBuffer()); if (options.checksumSha256) { const actualSHA256Checksum = crypto.createHash('sha256').update(contents).digest('hex'); diff --git a/build/lib/formatter.js b/build/lib/formatter.js deleted file mode 100644 index 1085ea8f488..00000000000 --- a/build/lib/formatter.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.format = format; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const typescript_1 = __importDefault(require("typescript")); -class LanguageServiceHost { - files = {}; - addFile(fileName, text) { - this.files[fileName] = typescript_1.default.ScriptSnapshot.fromString(text); - } - fileExists(path) { - return !!this.files[path]; - } - readFile(path) { - return this.files[path]?.getText(0, this.files[path].getLength()); - } - // for ts.LanguageServiceHost - getCompilationSettings = () => typescript_1.default.getDefaultCompilerOptions(); - getScriptFileNames = () => Object.keys(this.files); - getScriptVersion = (_fileName) => '0'; - getScriptSnapshot = (fileName) => this.files[fileName]; - getCurrentDirectory = () => process.cwd(); - getDefaultLibFileName = (options) => typescript_1.default.getDefaultLibFilePath(options); -} -const defaults = { - baseIndentSize: 0, - indentSize: 4, - tabSize: 4, - indentStyle: typescript_1.default.IndentStyle.Smart, - newLineCharacter: '\r\n', - convertTabsToSpaces: false, - insertSpaceAfterCommaDelimiter: true, - insertSpaceAfterSemicolonInForStatements: true, - insertSpaceBeforeAndAfterBinaryOperators: true, - insertSpaceAfterConstructor: false, - insertSpaceAfterKeywordsInControlFlowStatements: true, - insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, - insertSpaceAfterTypeAssertion: false, - insertSpaceBeforeFunctionParenthesis: false, - placeOpenBraceOnNewLineForFunctions: false, - placeOpenBraceOnNewLineForControlBlocks: false, - insertSpaceBeforeTypeAnnotation: false, -}; -const getOverrides = (() => { - let value; - return () => { - value ??= JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); - return value; - }; -})(); -function format(fileName, text) { - const host = new LanguageServiceHost(); - host.addFile(fileName, text); - const languageService = typescript_1.default.createLanguageService(host); - const edits = languageService.getFormattingEditsForDocument(fileName, { ...defaults, ...getOverrides() }); - edits - .sort((a, b) => a.span.start - b.span.start) - .reverse() - .forEach(edit => { - const head = text.slice(0, edit.span.start); - const tail = text.slice(edit.span.start + edit.span.length); - text = `${head}${edit.newText}${tail}`; - }); - return text; -} -//# sourceMappingURL=formatter.js.map \ No newline at end of file diff --git a/build/lib/formatter.ts b/build/lib/formatter.ts index 993722e5f92..09c1de929ba 100644 --- a/build/lib/formatter.ts +++ b/build/lib/formatter.ts @@ -59,7 +59,7 @@ const defaults: ts.FormatCodeSettings = { const getOverrides = (() => { let value: ts.FormatCodeSettings | undefined; return () => { - value ??= JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'tsfmt.json'), 'utf8')); + value ??= JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '..', '..', 'tsfmt.json'), 'utf8')); return value; }; })(); diff --git a/build/lib/getVersion.js b/build/lib/getVersion.js deleted file mode 100644 index 7606c17ab14..00000000000 --- a/build/lib/getVersion.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = getVersion; -const git = __importStar(require("./git")); -function getVersion(root) { - let version = process.env['BUILD_SOURCEVERSION']; - if (!version || !/^[0-9a-f]{40}$/i.test(version.trim())) { - version = git.getVersion(root); - } - return version; -} -//# sourceMappingURL=getVersion.js.map \ No newline at end of file diff --git a/build/lib/getVersion.ts b/build/lib/getVersion.ts index 2fddb309f83..1dc4600dadf 100644 --- a/build/lib/getVersion.ts +++ b/build/lib/getVersion.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as git from './git'; +import * as git from './git.ts'; export function getVersion(root: string): string | undefined { let version = process.env['BUILD_SOURCEVERSION']; diff --git a/build/lib/git.js b/build/lib/git.js deleted file mode 100644 index 30de97ed6e3..00000000000 --- a/build/lib/git.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVersion = getVersion; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -/** - * Returns the sha1 commit version of a repository or undefined in case of failure. - */ -function getVersion(repo) { - const git = path_1.default.join(repo, '.git'); - const headPath = path_1.default.join(git, 'HEAD'); - let head; - try { - head = fs_1.default.readFileSync(headPath, 'utf8').trim(); - } - catch (e) { - return undefined; - } - if (/^[0-9a-f]{40}$/i.test(head)) { - return head; - } - const refMatch = /^ref: (.*)$/.exec(head); - if (!refMatch) { - return undefined; - } - const ref = refMatch[1]; - const refPath = path_1.default.join(git, ref); - try { - return fs_1.default.readFileSync(refPath, 'utf8').trim(); - } - catch (e) { - // noop - } - const packedRefsPath = path_1.default.join(git, 'packed-refs'); - let refsRaw; - try { - refsRaw = fs_1.default.readFileSync(packedRefsPath, 'utf8').trim(); - } - catch (e) { - return undefined; - } - const refsRegex = /^([0-9a-f]{40})\s+(.+)$/gm; - let refsMatch; - const refs = {}; - while (refsMatch = refsRegex.exec(refsRaw)) { - refs[refsMatch[2]] = refsMatch[1]; - } - return refs[ref]; -} -//# sourceMappingURL=git.js.map \ No newline at end of file diff --git a/build/lib/i18n.js b/build/lib/i18n.js deleted file mode 100644 index 0b371c8b812..00000000000 --- a/build/lib/i18n.js +++ /dev/null @@ -1,785 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.EXTERNAL_EXTENSIONS = exports.XLF = exports.Line = exports.extraLanguages = exports.defaultLanguages = void 0; -exports.processNlsFiles = processNlsFiles; -exports.getResource = getResource; -exports.createXlfFilesForCoreBundle = createXlfFilesForCoreBundle; -exports.createXlfFilesForExtensions = createXlfFilesForExtensions; -exports.createXlfFilesForIsl = createXlfFilesForIsl; -exports.prepareI18nPackFiles = prepareI18nPackFiles; -exports.prepareIslFiles = prepareIslFiles; -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const event_stream_1 = require("event-stream"); -const gulp_merge_json_1 = __importDefault(require("gulp-merge-json")); -const vinyl_1 = __importDefault(require("vinyl")); -const xml2js_1 = __importDefault(require("xml2js")); -const gulp_1 = __importDefault(require("gulp")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const iconv_lite_umd_1 = __importDefault(require("@vscode/iconv-lite-umd")); -const l10n_dev_1 = require("@vscode/l10n-dev"); -const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); -function log(message, ...rest) { - (0, fancy_log_1.default)(ansi_colors_1.default.green('[i18n]'), message, ...rest); -} -exports.defaultLanguages = [ - { id: 'zh-tw', folderName: 'cht', translationId: 'zh-hant' }, - { id: 'zh-cn', folderName: 'chs', translationId: 'zh-hans' }, - { id: 'ja', folderName: 'jpn' }, - { id: 'ko', folderName: 'kor' }, - { id: 'de', folderName: 'deu' }, - { id: 'fr', folderName: 'fra' }, - { id: 'es', folderName: 'esn' }, - { id: 'ru', folderName: 'rus' }, - { id: 'it', folderName: 'ita' } -]; -// languages requested by the community -exports.extraLanguages = [ - { id: 'pt-br', folderName: 'ptb' }, - { id: 'tr', folderName: 'trk' }, - { id: 'cs' }, - { id: 'pl' } -]; -var LocalizeInfo; -(function (LocalizeInfo) { - function is(value) { - const candidate = value; - return candidate && typeof candidate.key === 'string' && (candidate.comment === undefined || (Array.isArray(candidate.comment) && candidate.comment.every(element => typeof element === 'string'))); - } - LocalizeInfo.is = is; -})(LocalizeInfo || (LocalizeInfo = {})); -var BundledFormat; -(function (BundledFormat) { - function is(value) { - if (value === undefined) { - return false; - } - const candidate = value; - const length = Object.keys(value).length; - return length === 3 && !!candidate.keys && !!candidate.messages && !!candidate.bundles; - } - BundledFormat.is = is; -})(BundledFormat || (BundledFormat = {})); -var NLSKeysFormat; -(function (NLSKeysFormat) { - function is(value) { - if (value === undefined) { - return false; - } - const candidate = value; - return Array.isArray(candidate) && Array.isArray(candidate[1]); - } - NLSKeysFormat.is = is; -})(NLSKeysFormat || (NLSKeysFormat = {})); -class Line { - buffer = []; - constructor(indent = 0) { - if (indent > 0) { - this.buffer.push(new Array(indent + 1).join(' ')); - } - } - append(value) { - this.buffer.push(value); - return this; - } - toString() { - return this.buffer.join(''); - } -} -exports.Line = Line; -class TextModel { - _lines; - constructor(contents) { - this._lines = contents.split(/\r\n|\r|\n/); - } - get lines() { - return this._lines; - } -} -class XLF { - project; - buffer; - files; - numberOfMessages; - constructor(project) { - this.project = project; - this.buffer = []; - this.files = Object.create(null); - this.numberOfMessages = 0; - } - toString() { - this.appendHeader(); - const files = Object.keys(this.files).sort(); - for (const file of files) { - this.appendNewLine(``, 2); - const items = this.files[file].sort((a, b) => { - return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; - }); - for (const item of items) { - this.addStringItem(file, item); - } - this.appendNewLine(''); - } - this.appendFooter(); - return this.buffer.join('\r\n'); - } - addFile(original, keys, messages) { - if (keys.length === 0) { - console.log('No keys in ' + original); - return; - } - if (keys.length !== messages.length) { - throw new Error(`Unmatching keys(${keys.length}) and messages(${messages.length}).`); - } - this.numberOfMessages += keys.length; - this.files[original] = []; - const existingKeys = new Set(); - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - let realKey; - let comment; - if (typeof key === 'string') { - realKey = key; - comment = undefined; - } - else if (LocalizeInfo.is(key)) { - realKey = key.key; - if (key.comment && key.comment.length > 0) { - comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); - } - } - if (!realKey || existingKeys.has(realKey)) { - continue; - } - existingKeys.add(realKey); - const message = encodeEntities(messages[i]); - this.files[original].push({ id: realKey, message: message, comment: comment }); - } - } - addStringItem(file, item) { - if (!item.id || item.message === undefined || item.message === null) { - throw new Error(`No item ID or value specified: ${JSON.stringify(item)}. File: ${file}`); - } - if (item.message.length === 0) { - log(`Item with id ${item.id} in file ${file} has an empty message.`); - } - this.appendNewLine(``, 4); - this.appendNewLine(`${item.message}`, 6); - if (item.comment) { - this.appendNewLine(`${item.comment}`, 6); - } - this.appendNewLine('', 4); - } - appendHeader() { - this.appendNewLine('', 0); - this.appendNewLine('', 0); - } - appendFooter() { - this.appendNewLine('', 0); - } - appendNewLine(content, indent) { - const line = new Line(indent); - line.append(content); - this.buffer.push(line.toString()); - } - static parse = function (xlfString) { - return new Promise((resolve, reject) => { - const parser = new xml2js_1.default.Parser(); - const files = []; - parser.parseString(xlfString, function (err, result) { - if (err) { - reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); - } - const fileNodes = result['xliff']['file']; - if (!fileNodes) { - reject(new Error(`XLF parsing error: XLIFF file does not contain "xliff" or "file" node(s) required for parsing.`)); - } - fileNodes.forEach((file) => { - const name = file.$.original; - if (!name) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain original attribute to determine the original location of the resource file.`)); - } - const language = file.$['target-language']; - if (!language) { - reject(new Error(`XLF parsing error: XLIFF file node does not contain target-language attribute to determine translated language.`)); - } - const messages = {}; - const transUnits = file.body[0]['trans-unit']; - if (transUnits) { - transUnits.forEach((unit) => { - const key = unit.$.id; - if (!unit.target) { - return; // No translation available - } - let val = unit.target[0]; - if (typeof val !== 'string') { - // We allow empty source values so support them for translations as well. - val = val._ ? val._ : ''; - } - if (!key) { - reject(new Error(`XLF parsing error: trans-unit ${JSON.stringify(unit, undefined, 0)} defined in file ${name} is missing the ID attribute.`)); - return; - } - messages[key] = decodeEntities(val); - }); - files.push({ messages, name, language: language.toLowerCase() }); - } - }); - resolve(files); - }); - }); - }; -} -exports.XLF = XLF; -function sortLanguages(languages) { - return languages.sort((a, b) => { - return a.id < b.id ? -1 : (a.id > b.id ? 1 : 0); - }); -} -function stripComments(content) { - // Copied from stripComments.js - // - // First group matches a double quoted string - // Second group matches a single quoted string - // Third group matches a multi line comment - // Forth group matches a single line comment - // Fifth group matches a trailing comma - const regexp = /("[^"\\]*(?:\\.[^"\\]*)*")|('[^'\\]*(?:\\.[^'\\]*)*')|(\/\*[^\/\*]*(?:(?:\*|\/)[^\/\*]*)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))|(,\s*[}\]])/g; - const result = content.replace(regexp, (match, _m1, _m2, m3, m4, m5) => { - // Only one of m1, m2, m3, m4, m5 matches - if (m3) { - // A block comment. Replace with nothing - return ''; - } - else if (m4) { - // Since m4 is a single line comment is is at least of length 2 (e.g. //) - // If it ends in \r?\n then keep it. - const length = m4.length; - if (m4[length - 1] === '\n') { - return m4[length - 2] === '\r' ? '\r\n' : '\n'; - } - else { - return ''; - } - } - else if (m5) { - // Remove the trailing comma - return match.substring(1); - } - else { - // We match a string - return match; - } - }); - return result; -} -function processCoreBundleFormat(base, fileHeader, languages, json, emitter) { - const languageDirectory = path_1.default.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); - if (!fs_1.default.existsSync(languageDirectory)) { - log(`No VS Code localization repository found. Looking at ${languageDirectory}`); - log(`To bundle translations please check out the vscode-loc repository as a sibling of the vscode repository.`); - } - const sortedLanguages = sortLanguages(languages); - sortedLanguages.forEach((language) => { - if (process.env['VSCODE_BUILD_VERBOSE']) { - log(`Generating nls bundles for: ${language.id}`); - } - const languageFolderName = language.translationId || language.id; - const i18nFile = path_1.default.join(languageDirectory, `vscode-language-pack-${languageFolderName}`, 'translations', 'main.i18n.json'); - let allMessages; - if (fs_1.default.existsSync(i18nFile)) { - const content = stripComments(fs_1.default.readFileSync(i18nFile, 'utf8')); - allMessages = JSON.parse(content); - } - let nlsIndex = 0; - const nlsResult = []; - for (const [moduleId, nlsKeys] of json) { - const moduleTranslations = allMessages?.contents[moduleId]; - for (const nlsKey of nlsKeys) { - nlsResult.push(moduleTranslations?.[nlsKey]); // pushing `undefined` is fine, as we keep english strings as fallback for monaco editor in the build - nlsIndex++; - } - } - emitter.queue(new vinyl_1.default({ - contents: Buffer.from(`${fileHeader} -globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(nlsResult)}; -globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), - base, - path: `${base}/nls.messages.${language.id}.js` - })); - }); -} -function processNlsFiles(opts) { - return (0, event_stream_1.through)(function (file) { - const fileName = path_1.default.basename(file.path); - if (fileName === 'nls.keys.json') { - try { - const contents = file.contents.toString('utf8'); - const json = JSON.parse(contents); - if (NLSKeysFormat.is(json)) { - processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); - } - } - catch (error) { - this.emit('error', `Failed to read component file: ${error}`); - } - } - this.queue(file); - }); -} -const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench', extensionsProject = 'vscode-extensions', setupProject = 'vscode-setup', serverProject = 'vscode-server'; -function getResource(sourceFile) { - let resource; - if (/^vs\/platform/.test(sourceFile)) { - return { name: 'vs/platform', project: editorProject }; - } - else if (/^vs\/editor\/contrib/.test(sourceFile)) { - return { name: 'vs/editor/contrib', project: editorProject }; - } - else if (/^vs\/editor/.test(sourceFile)) { - return { name: 'vs/editor', project: editorProject }; - } - else if (/^vs\/base/.test(sourceFile)) { - return { name: 'vs/base', project: editorProject }; - } - else if (/^vs\/code/.test(sourceFile)) { - return { name: 'vs/code', project: workbenchProject }; - } - else if (/^vs\/server/.test(sourceFile)) { - return { name: 'vs/server', project: serverProject }; - } - else if (/^vs\/workbench\/contrib/.test(sourceFile)) { - resource = sourceFile.split('/', 4).join('/'); - return { name: resource, project: workbenchProject }; - } - else if (/^vs\/workbench\/services/.test(sourceFile)) { - resource = sourceFile.split('/', 4).join('/'); - return { name: resource, project: workbenchProject }; - } - else if (/^vs\/workbench/.test(sourceFile)) { - return { name: 'vs/workbench', project: workbenchProject }; - } - throw new Error(`Could not identify the XLF bundle for ${sourceFile}`); -} -function createXlfFilesForCoreBundle() { - return (0, event_stream_1.through)(function (file) { - const basename = path_1.default.basename(file.path); - if (basename === 'nls.metadata.json') { - if (file.isBuffer()) { - const xlfs = Object.create(null); - const json = JSON.parse(file.contents.toString('utf8')); - for (const coreModule in json.keys) { - const projectResource = getResource(coreModule); - const resource = projectResource.name; - const project = projectResource.project; - const keys = json.keys[coreModule]; - const messages = json.messages[coreModule]; - if (keys.length !== messages.length) { - this.emit('error', `There is a mismatch between keys and messages in ${file.relative} for module ${coreModule}`); - return; - } - else { - let xlf = xlfs[resource]; - if (!xlf) { - xlf = new XLF(project); - xlfs[resource] = xlf; - } - xlf.addFile(`src/${coreModule}`, keys, messages); - } - } - for (const resource in xlfs) { - const xlf = xlfs[resource]; - const filePath = `${xlf.project}/${resource.replace(/\//g, '_')}.xlf`; - const xlfFile = new vinyl_1.default({ - path: filePath, - contents: Buffer.from(xlf.toString(), 'utf8') - }); - this.queue(xlfFile); - } - } - else { - this.emit('error', new Error(`File ${file.relative} is not using a buffer content`)); - return; - } - } - else { - this.emit('error', new Error(`File ${file.relative} is not a core meta data file.`)); - return; - } - }); -} -function createL10nBundleForExtension(extensionFolderName, prefixWithBuildFolder) { - const prefix = prefixWithBuildFolder ? '.build/' : ''; - return gulp_1.default - .src([ - // For source code of extensions - `${prefix}extensions/${extensionFolderName}/{src,client,server}/**/*.{ts,tsx}`, - // // For any dependencies pulled in (think vscode-css-languageservice or @vscode/emmet-helper) - `${prefix}extensions/${extensionFolderName}/**/node_modules/{@vscode,vscode-*}/**/*.{js,jsx}`, - // // For any dependencies pulled in that bundle @vscode/l10n. They needed to export the bundle - `${prefix}extensions/${extensionFolderName}/**/bundle.l10n.json`, - ]) - .pipe((0, event_stream_1.map)(function (data, callback) { - const file = data; - if (!file.isBuffer()) { - // Not a buffer so we drop it - callback(); - return; - } - const extension = path_1.default.extname(file.relative); - if (extension !== '.json') { - const contents = file.contents.toString('utf8'); - (0, l10n_dev_1.getL10nJson)([{ contents, extension }]) - .then((json) => { - callback(undefined, new vinyl_1.default({ - path: `extensions/${extensionFolderName}/bundle.l10n.json`, - contents: Buffer.from(JSON.stringify(json), 'utf8') - })); - }) - .catch((err) => { - callback(new Error(`File ${file.relative} threw an error when parsing: ${err}`)); - }); - // signal pause? - return false; - } - // for bundle.l10n.jsons - let bundleJson; - try { - bundleJson = JSON.parse(file.contents.toString('utf8')); - } - catch (err) { - callback(new Error(`File ${file.relative} threw an error when parsing: ${err}`)); - return; - } - // some validation of the bundle.l10n.json format - for (const key in bundleJson) { - if (typeof bundleJson[key] !== 'string' && - (typeof bundleJson[key].message !== 'string' || !Array.isArray(bundleJson[key].comment))) { - callback(new Error(`Invalid bundle.l10n.json file. The value for key ${key} is not in the expected format.`)); - return; - } - } - callback(undefined, file); - })) - .pipe((0, gulp_merge_json_1.default)({ - fileName: `extensions/${extensionFolderName}/bundle.l10n.json`, - jsonSpace: '', - concatArrays: true - })); -} -exports.EXTERNAL_EXTENSIONS = [ - 'ms-vscode.js-debug', - 'ms-vscode.js-debug-companion', - 'ms-vscode.vscode-js-profile-table', -]; -function createXlfFilesForExtensions() { - let counter = 0; - let folderStreamEnded = false; - let folderStreamEndEmitted = false; - return (0, event_stream_1.through)(function (extensionFolder) { - const folderStream = this; - const stat = fs_1.default.statSync(extensionFolder.path); - if (!stat.isDirectory()) { - return; - } - const extensionFolderName = path_1.default.basename(extensionFolder.path); - if (extensionFolderName === 'node_modules') { - return; - } - // Get extension id and use that as the id - const manifest = fs_1.default.readFileSync(path_1.default.join(extensionFolder.path, 'package.json'), 'utf-8'); - const manifestJson = JSON.parse(manifest); - const extensionId = manifestJson.publisher + '.' + manifestJson.name; - counter++; - let _l10nMap; - function getL10nMap() { - if (!_l10nMap) { - _l10nMap = new Map(); - } - return _l10nMap; - } - (0, event_stream_1.merge)(gulp_1.default.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, exports.EXTERNAL_EXTENSIONS.includes(extensionId))).pipe((0, event_stream_1.through)(function (file) { - if (file.isBuffer()) { - const buffer = file.contents; - const basename = path_1.default.basename(file.path); - if (basename === 'package.nls.json') { - const json = JSON.parse(buffer.toString('utf8')); - getL10nMap().set(`extensions/${extensionId}/package`, json); - } - else if (basename === 'nls.metadata.json') { - const json = JSON.parse(buffer.toString('utf8')); - const relPath = path_1.default.relative(`.build/extensions/${extensionFolderName}`, path_1.default.dirname(file.path)); - for (const file in json) { - const fileContent = json[file]; - const info = Object.create(null); - for (let i = 0; i < fileContent.messages.length; i++) { - const message = fileContent.messages[i]; - const { key, comment } = LocalizeInfo.is(fileContent.keys[i]) - ? fileContent.keys[i] - : { key: fileContent.keys[i], comment: undefined }; - info[key] = comment ? { message, comment } : message; - } - getL10nMap().set(`extensions/${extensionId}/${relPath}/${file}`, info); - } - } - else if (basename === 'bundle.l10n.json') { - const json = JSON.parse(buffer.toString('utf8')); - getL10nMap().set(`extensions/${extensionId}/bundle`, json); - } - else { - this.emit('error', new Error(`${file.path} is not a valid extension nls file`)); - return; - } - } - }, function () { - if (_l10nMap?.size > 0) { - const xlfFile = new vinyl_1.default({ - path: path_1.default.join(extensionsProject, extensionId + '.xlf'), - contents: Buffer.from((0, l10n_dev_1.getL10nXlf)(_l10nMap), 'utf8') - }); - folderStream.queue(xlfFile); - } - this.queue(null); - counter--; - if (counter === 0 && folderStreamEnded && !folderStreamEndEmitted) { - folderStreamEndEmitted = true; - folderStream.queue(null); - } - })); - }, function () { - folderStreamEnded = true; - if (counter === 0) { - folderStreamEndEmitted = true; - this.queue(null); - } - }); -} -function createXlfFilesForIsl() { - return (0, event_stream_1.through)(function (file) { - let projectName, resourceFile; - if (path_1.default.basename(file.path) === 'messages.en.isl') { - projectName = setupProject; - resourceFile = 'messages.xlf'; - } - else { - throw new Error(`Unknown input file ${file.path}`); - } - const xlf = new XLF(projectName), keys = [], messages = []; - const model = new TextModel(file.contents.toString()); - let inMessageSection = false; - model.lines.forEach(line => { - if (line.length === 0) { - return; - } - const firstChar = line.charAt(0); - switch (firstChar) { - case ';': - // Comment line; - return; - case '[': - inMessageSection = '[Messages]' === line || '[CustomMessages]' === line; - return; - } - if (!inMessageSection) { - return; - } - const sections = line.split('='); - if (sections.length !== 2) { - throw new Error(`Badly formatted message found: ${line}`); - } - else { - const key = sections[0]; - const value = sections[1]; - if (key.length > 0 && value.length > 0) { - keys.push(key); - messages.push(value); - } - } - }); - const originalPath = file.path.substring(file.cwd.length + 1, file.path.split('.')[0].length).replace(/\\/g, '/'); - xlf.addFile(originalPath, keys, messages); - // Emit only upon all ISL files combined into single XLF instance - const newFilePath = path_1.default.join(projectName, resourceFile); - const xlfFile = new vinyl_1.default({ path: newFilePath, contents: Buffer.from(xlf.toString(), 'utf-8') }); - this.queue(xlfFile); - }); -} -function createI18nFile(name, messages) { - const result = Object.create(null); - result[''] = [ - '--------------------------------------------------------------------------------------------', - 'Copyright (c) Microsoft Corporation. All rights reserved.', - 'Licensed under the MIT License. See License.txt in the project root for license information.', - '--------------------------------------------------------------------------------------------', - 'Do not edit this file. It is machine generated.' - ]; - for (const key of Object.keys(messages)) { - result[key] = messages[key]; - } - let content = JSON.stringify(result, null, '\t'); - if (process.platform === 'win32') { - content = content.replace(/\n/g, '\r\n'); - } - return new vinyl_1.default({ - path: path_1.default.join(name + '.i18n.json'), - contents: Buffer.from(content, 'utf8') - }); -} -const i18nPackVersion = '1.0.0'; -function getRecordFromL10nJsonFormat(l10nJsonFormat) { - const record = {}; - for (const key of Object.keys(l10nJsonFormat).sort()) { - const value = l10nJsonFormat[key]; - record[key] = typeof value === 'string' ? value : value.message; - } - return record; -} -function prepareI18nPackFiles(resultingTranslationPaths) { - const parsePromises = []; - const mainPack = { version: i18nPackVersion, contents: {} }; - const extensionsPacks = {}; - const errors = []; - return (0, event_stream_1.through)(function (xlf) { - let project = path_1.default.basename(path_1.default.dirname(path_1.default.dirname(xlf.relative))); - // strip `-new` since vscode-extensions-loc uses the `-new` suffix to indicate that it's from the new loc pipeline - const resource = path_1.default.basename(path_1.default.basename(xlf.relative, '.xlf'), '-new'); - if (exports.EXTERNAL_EXTENSIONS.find(e => e === resource)) { - project = extensionsProject; - } - const contents = xlf.contents.toString(); - log(`Found ${project}: ${resource}`); - const parsePromise = (0, l10n_dev_1.getL10nFilesFromXlf)(contents); - parsePromises.push(parsePromise); - parsePromise.then(resolvedFiles => { - resolvedFiles.forEach(file => { - const path = file.name; - const firstSlash = path.indexOf('/'); - if (project === extensionsProject) { - // resource will be the extension id - let extPack = extensionsPacks[resource]; - if (!extPack) { - extPack = extensionsPacks[resource] = { version: i18nPackVersion, contents: {} }; - } - // remove 'extensions/extensionId/' segment - const secondSlash = path.indexOf('/', firstSlash + 1); - extPack.contents[path.substring(secondSlash + 1)] = getRecordFromL10nJsonFormat(file.messages); - } - else { - mainPack.contents[path.substring(firstSlash + 1)] = getRecordFromL10nJsonFormat(file.messages); - } - }); - }).catch(reason => { - errors.push(reason); - }); - }, function () { - Promise.all(parsePromises) - .then(() => { - if (errors.length > 0) { - throw errors; - } - const translatedMainFile = createI18nFile('./main', mainPack); - resultingTranslationPaths.push({ id: 'vscode', resourceName: 'main.i18n.json' }); - this.queue(translatedMainFile); - for (const extensionId in extensionsPacks) { - const translatedExtFile = createI18nFile(`extensions/${extensionId}`, extensionsPacks[extensionId]); - this.queue(translatedExtFile); - resultingTranslationPaths.push({ id: extensionId, resourceName: `extensions/${extensionId}.i18n.json` }); - } - this.queue(null); - }) - .catch((reason) => { - this.emit('error', reason); - }); - }); -} -function prepareIslFiles(language, innoSetupConfig) { - const parsePromises = []; - return (0, event_stream_1.through)(function (xlf) { - const stream = this; - const parsePromise = XLF.parse(xlf.contents.toString()); - parsePromises.push(parsePromise); - parsePromise.then(resolvedFiles => { - resolvedFiles.forEach(file => { - const translatedFile = createIslFile(file.name, file.messages, language, innoSetupConfig); - stream.queue(translatedFile); - }); - }).catch(reason => { - this.emit('error', reason); - }); - }, function () { - Promise.all(parsePromises) - .then(() => { this.queue(null); }) - .catch(reason => { - this.emit('error', reason); - }); - }); -} -function createIslFile(name, messages, language, innoSetup) { - const content = []; - let originalContent; - if (path_1.default.basename(name) === 'Default') { - originalContent = new TextModel(fs_1.default.readFileSync(name + '.isl', 'utf8')); - } - else { - originalContent = new TextModel(fs_1.default.readFileSync(name + '.en.isl', 'utf8')); - } - originalContent.lines.forEach(line => { - if (line.length > 0) { - const firstChar = line.charAt(0); - if (firstChar === '[' || firstChar === ';') { - content.push(line); - } - else { - const sections = line.split('='); - const key = sections[0]; - let translated = line; - if (key) { - const translatedMessage = messages[key]; - if (translatedMessage) { - translated = `${key}=${translatedMessage}`; - } - } - content.push(translated); - } - } - }); - const basename = path_1.default.basename(name); - const filePath = `${basename}.${language.id}.isl`; - const encoded = iconv_lite_umd_1.default.encode(Buffer.from(content.join('\r\n'), 'utf8').toString(), innoSetup.codePage); - return new vinyl_1.default({ - path: filePath, - contents: Buffer.from(encoded), - }); -} -function encodeEntities(value) { - const result = []; - for (let i = 0; i < value.length; i++) { - const ch = value[i]; - switch (ch) { - case '<': - result.push('<'); - break; - case '>': - result.push('>'); - break; - case '&': - result.push('&'); - break; - default: - result.push(ch); - } - } - return result.join(''); -} -function decodeEntities(value) { - return value.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); -} -//# sourceMappingURL=i18n.js.map \ No newline at end of file diff --git a/build/lib/i18n.ts b/build/lib/i18n.ts index 4506b2e3cd0..8ebcb1f177b 100644 --- a/build/lib/i18n.ts +++ b/build/lib/i18n.ts @@ -5,8 +5,7 @@ import path from 'path'; import fs from 'fs'; - -import { map, merge, through, ThroughStream } from 'event-stream'; +import eventStream from 'event-stream'; import jsonMerge from 'gulp-merge-json'; import File from 'vinyl'; import xml2js from 'xml2js'; @@ -14,9 +13,9 @@ import gulp from 'gulp'; import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; import iconv from '@vscode/iconv-lite-umd'; -import { l10nJsonFormat, getL10nXlf, l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; +import { type l10nJsonFormat, getL10nXlf, type l10nJsonDetails, getL10nFilesFromXlf, getL10nJson } from '@vscode/l10n-dev'; -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const REPO_ROOT_PATH = path.join(import.meta.dirname, '../..'); function log(message: any, ...rest: unknown[]): void { fancyLog(ansiColors.green('[i18n]'), message, ...rest); @@ -68,11 +67,9 @@ interface LocalizeInfo { comment: string[]; } -module LocalizeInfo { - export function is(value: unknown): value is LocalizeInfo { - const candidate = value as LocalizeInfo; - return candidate && typeof candidate.key === 'string' && (candidate.comment === undefined || (Array.isArray(candidate.comment) && candidate.comment.every(element => typeof element === 'string'))); - } +function isLocalizeInfo(value: unknown): value is LocalizeInfo { + const candidate = value as LocalizeInfo; + return candidate && typeof candidate.key === 'string' && (candidate.comment === undefined || (Array.isArray(candidate.comment) && candidate.comment.every(element => typeof element === 'string'))); } interface BundledFormat { @@ -81,30 +78,15 @@ interface BundledFormat { bundles: Record; } -module BundledFormat { - export function is(value: any): value is BundledFormat { - if (value === undefined) { - return false; - } - - const candidate = value as BundledFormat; - const length = Object.keys(value).length; - - return length === 3 && !!candidate.keys && !!candidate.messages && !!candidate.bundles; - } -} - type NLSKeysFormat = [string /* module ID */, string[] /* keys */]; -module NLSKeysFormat { - export function is(value: any): value is NLSKeysFormat { - if (value === undefined) { - return false; - } - - const candidate = value as NLSKeysFormat; - return Array.isArray(candidate) && Array.isArray(candidate[1]); +function isNLSKeysFormat(value: unknown): value is NLSKeysFormat { + if (value === undefined) { + return false; } + + const candidate = value as NLSKeysFormat; + return Array.isArray(candidate) && Array.isArray(candidate[1]); } interface BundledExtensionFormat { @@ -158,8 +140,10 @@ export class XLF { private buffer: string[]; private files: Record; public numberOfMessages: number; + public project: string; - constructor(public project: string) { + constructor(project: string) { + this.project = project; this.buffer = []; this.files = Object.create(null); this.numberOfMessages = 0; @@ -201,7 +185,7 @@ export class XLF { if (typeof key === 'string') { realKey = key; comment = undefined; - } else if (LocalizeInfo.is(key)) { + } else if (isLocalizeInfo(key)) { realKey = key.key; if (key.comment && key.comment.length > 0) { comment = key.comment.map(comment => encodeEntities(comment)).join('\r\n'); @@ -255,7 +239,7 @@ export class XLF { const files: { messages: Record; name: string; language: string }[] = []; - parser.parseString(xlfString, function (err: any, result: any) { + parser.parseString(xlfString, function (err: Error | undefined, result: any) { if (err) { reject(new Error(`XLF parsing error: Failed to parse XLIFF string. ${err}`)); } @@ -345,7 +329,7 @@ function stripComments(content: string): string { return result; } -function processCoreBundleFormat(base: string, fileHeader: string, languages: Language[], json: NLSKeysFormat, emitter: ThroughStream) { +function processCoreBundleFormat(base: string, fileHeader: string, languages: Language[], json: NLSKeysFormat, emitter: eventStream.ThroughStream) { const languageDirectory = path.join(REPO_ROOT_PATH, '..', 'vscode-loc', 'i18n'); if (!fs.existsSync(languageDirectory)) { log(`No VS Code localization repository found. Looking at ${languageDirectory}`); @@ -385,14 +369,14 @@ globalThis._VSCODE_NLS_LANGUAGE=${JSON.stringify(language.id)};`), }); } -export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): ThroughStream { - return through(function (this: ThroughStream, file: File) { +export function processNlsFiles(opts: { out: string; fileHeader: string; languages: Language[] }): eventStream.ThroughStream { + return eventStream.through(function (this: eventStream.ThroughStream, file: File) { const fileName = path.basename(file.path); if (fileName === 'nls.keys.json') { try { const contents = file.contents!.toString('utf8'); const json = JSON.parse(contents); - if (NLSKeysFormat.is(json)) { + if (isNLSKeysFormat(json)) { processCoreBundleFormat(file.base, opts.fileHeader, opts.languages, json, this); } } catch (error) { @@ -438,8 +422,8 @@ export function getResource(sourceFile: string): Resource { } -export function createXlfFilesForCoreBundle(): ThroughStream { - return through(function (this: ThroughStream, file: File) { +export function createXlfFilesForCoreBundle(): eventStream.ThroughStream { + return eventStream.through(function (this: eventStream.ThroughStream, file: File) { const basename = path.basename(file.path); if (basename === 'nls.metadata.json') { if (file.isBuffer()) { @@ -495,7 +479,7 @@ function createL10nBundleForExtension(extensionFolderName: string, prefixWithBui // // For any dependencies pulled in that bundle @vscode/l10n. They needed to export the bundle `${prefix}extensions/${extensionFolderName}/**/bundle.l10n.json`, ]) - .pipe(map(function (data, callback) { + .pipe(eventStream.map(function (data, callback) { const file = data as File; if (!file.isBuffer()) { // Not a buffer so we drop it @@ -554,11 +538,11 @@ export const EXTERNAL_EXTENSIONS = [ 'ms-vscode.vscode-js-profile-table', ]; -export function createXlfFilesForExtensions(): ThroughStream { +export function createXlfFilesForExtensions(): eventStream.ThroughStream { let counter: number = 0; let folderStreamEnded: boolean = false; let folderStreamEndEmitted: boolean = false; - return through(function (this: ThroughStream, extensionFolder: File) { + return eventStream.through(function (this: eventStream.ThroughStream, extensionFolder: File) { const folderStream = this; const stat = fs.statSync(extensionFolder.path); if (!stat.isDirectory()) { @@ -581,10 +565,10 @@ export function createXlfFilesForExtensions(): ThroughStream { } return _l10nMap; } - merge( + eventStream.merge( gulp.src([`.build/extensions/${extensionFolderName}/package.nls.json`, `.build/extensions/${extensionFolderName}/**/nls.metadata.json`], { allowEmpty: true }), createL10nBundleForExtension(extensionFolderName, EXTERNAL_EXTENSIONS.includes(extensionId)) - ).pipe(through(function (file: File) { + ).pipe(eventStream.through(function (file: File) { if (file.isBuffer()) { const buffer: Buffer = file.contents as Buffer; const basename = path.basename(file.path); @@ -599,7 +583,7 @@ export function createXlfFilesForExtensions(): ThroughStream { const info: l10nJsonFormat = Object.create(null); for (let i = 0; i < fileContent.messages.length; i++) { const message = fileContent.messages[i]; - const { key, comment } = LocalizeInfo.is(fileContent.keys[i]) + const { key, comment } = isLocalizeInfo(fileContent.keys[i]) ? fileContent.keys[i] as LocalizeInfo : { key: fileContent.keys[i] as string, comment: undefined }; @@ -639,8 +623,8 @@ export function createXlfFilesForExtensions(): ThroughStream { }); } -export function createXlfFilesForIsl(): ThroughStream { - return through(function (this: ThroughStream, file: File) { +export function createXlfFilesForIsl(): eventStream.ThroughStream { + return eventStream.through(function (this: eventStream.ThroughStream, file: File) { let projectName: string, resourceFile: string; if (path.basename(file.path) === 'messages.en.isl') { @@ -746,7 +730,7 @@ export function prepareI18nPackFiles(resultingTranslationPaths: TranslationPath[ const mainPack: I18nPack = { version: i18nPackVersion, contents: {} }; const extensionsPacks: Record = {}; const errors: unknown[] = []; - return through(function (this: ThroughStream, xlf: File) { + return eventStream.through(function (this: eventStream.ThroughStream, xlf: File) { let project = path.basename(path.dirname(path.dirname(xlf.relative))); // strip `-new` since vscode-extensions-loc uses the `-new` suffix to indicate that it's from the new loc pipeline const resource = path.basename(path.basename(xlf.relative, '.xlf'), '-new'); @@ -804,10 +788,10 @@ export function prepareI18nPackFiles(resultingTranslationPaths: TranslationPath[ }); } -export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): ThroughStream { +export function prepareIslFiles(language: Language, innoSetupConfig: InnoSetup): eventStream.ThroughStream { const parsePromises: Promise[] = []; - return through(function (this: ThroughStream, xlf: File) { + return eventStream.through(function (this: eventStream.ThroughStream, xlf: File) { const stream = this; const parsePromise = XLF.parse(xlf.contents!.toString()); parsePromises.push(parsePromise); diff --git a/build/lib/inlineMeta.js b/build/lib/inlineMeta.js deleted file mode 100644 index 6e66251e602..00000000000 --- a/build/lib/inlineMeta.js +++ /dev/null @@ -1,81 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.inlineMeta = inlineMeta; -const es = __importStar(require("event-stream")); -const path = __importStar(require("path")); -const packageJsonMarkerId = 'BUILD_INSERT_PACKAGE_CONFIGURATION'; -// TODO in order to inline `product.json`, more work is -// needed to ensure that we cover all cases where modifications -// are done to the product configuration during build. There are -// at least 2 more changes that kick in very late: -// - a `darwinUniversalAssetId` is added in`create-universal-app.ts` -// - a `target` is added in `gulpfile.vscode.win32.js` -// const productJsonMarkerId = 'BUILD_INSERT_PRODUCT_CONFIGURATION'; -function inlineMeta(result, ctx) { - return result.pipe(es.through(function (file) { - if (matchesFile(file, ctx)) { - let content = file.contents.toString(); - let markerFound = false; - const packageMarker = `${packageJsonMarkerId}:"${packageJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) - if (content.includes(packageMarker)) { - content = content.replace(packageMarker, JSON.stringify(JSON.parse(ctx.packageJsonFn())).slice(1, -1) /* trim braces */); - markerFound = true; - } - // const productMarker = `${productJsonMarkerId}:"${productJsonMarkerId}"`; // this needs to be the format after esbuild has processed the file (e.g. double quotes) - // if (content.includes(productMarker)) { - // content = content.replace(productMarker, JSON.stringify(JSON.parse(ctx.productJsonFn())).slice(1, -1) /* trim braces */); - // markerFound = true; - // } - if (markerFound) { - file.contents = Buffer.from(content); - } - } - this.emit('data', file); - })); -} -function matchesFile(file, ctx) { - for (const targetPath of ctx.targetPaths) { - if (file.basename === path.basename(targetPath)) { // TODO would be nicer to figure out root relative path to not match on false positives - return true; - } - } - return false; -} -//# sourceMappingURL=inlineMeta.js.map \ No newline at end of file diff --git a/build/lib/mangle/index.js b/build/lib/mangle/index.js deleted file mode 100644 index fa729052f7c..00000000000 --- a/build/lib/mangle/index.js +++ /dev/null @@ -1,661 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Mangler = void 0; -const node_v8_1 = __importDefault(require("node:v8")); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const process_1 = require("process"); -const source_map_1 = require("source-map"); -const typescript_1 = __importDefault(require("typescript")); -const url_1 = require("url"); -const workerpool_1 = __importDefault(require("workerpool")); -const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); -const buildfile = require('../../buildfile'); -class ShortIdent { - prefix; - static _keywords = new Set(['await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', - 'default', 'delete', 'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', - 'import', 'in', 'instanceof', 'let', 'new', 'null', 'return', 'static', 'super', 'switch', 'this', 'throw', - 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield']); - static _alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$_'.split(''); - _value = 0; - constructor(prefix) { - this.prefix = prefix; - } - next(isNameTaken) { - const candidate = this.prefix + ShortIdent.convert(this._value); - this._value++; - if (ShortIdent._keywords.has(candidate) || /^[_0-9]/.test(candidate) || isNameTaken?.(candidate)) { - // try again - return this.next(isNameTaken); - } - return candidate; - } - static convert(n) { - const base = this._alphabet.length; - let result = ''; - do { - const rest = n % base; - result += this._alphabet[rest]; - n = (n / base) | 0; - } while (n > 0); - return result; - } -} -var FieldType; -(function (FieldType) { - FieldType[FieldType["Public"] = 0] = "Public"; - FieldType[FieldType["Protected"] = 1] = "Protected"; - FieldType[FieldType["Private"] = 2] = "Private"; -})(FieldType || (FieldType = {})); -class ClassData { - fileName; - node; - fields = new Map(); - replacements; - parent; - children; - constructor(fileName, node) { - // analyse all fields (properties and methods). Find usages of all protected and - // private ones and keep track of all public ones (to prevent naming collisions) - this.fileName = fileName; - this.node = node; - const candidates = []; - for (const member of node.members) { - if (typescript_1.default.isMethodDeclaration(member)) { - // method `foo() {}` - candidates.push(member); - } - else if (typescript_1.default.isPropertyDeclaration(member)) { - // property `foo = 234` - candidates.push(member); - } - else if (typescript_1.default.isGetAccessor(member)) { - // getter: `get foo() { ... }` - candidates.push(member); - } - else if (typescript_1.default.isSetAccessor(member)) { - // setter: `set foo() { ... }` - candidates.push(member); - } - else if (typescript_1.default.isConstructorDeclaration(member)) { - // constructor-prop:`constructor(private foo) {}` - for (const param of member.parameters) { - if (hasModifier(param, typescript_1.default.SyntaxKind.PrivateKeyword) - || hasModifier(param, typescript_1.default.SyntaxKind.ProtectedKeyword) - || hasModifier(param, typescript_1.default.SyntaxKind.PublicKeyword) - || hasModifier(param, typescript_1.default.SyntaxKind.ReadonlyKeyword)) { - candidates.push(param); - } - } - } - } - for (const member of candidates) { - const ident = ClassData._getMemberName(member); - if (!ident) { - continue; - } - const type = ClassData._getFieldType(member); - this.fields.set(ident, { type, pos: member.name.getStart() }); - } - } - static _getMemberName(node) { - if (!node.name) { - return undefined; - } - const { name } = node; - let ident = name.getText(); - if (name.kind === typescript_1.default.SyntaxKind.ComputedPropertyName) { - if (name.expression.kind !== typescript_1.default.SyntaxKind.StringLiteral) { - // unsupported: [Symbol.foo] or [abc + 'field'] - return; - } - // ['foo'] - ident = name.expression.getText().slice(1, -1); - } - return ident; - } - static _getFieldType(node) { - if (hasModifier(node, typescript_1.default.SyntaxKind.PrivateKeyword)) { - return 2 /* FieldType.Private */; - } - else if (hasModifier(node, typescript_1.default.SyntaxKind.ProtectedKeyword)) { - return 1 /* FieldType.Protected */; - } - else { - return 0 /* FieldType.Public */; - } - } - static _shouldMangle(type) { - return type === 2 /* FieldType.Private */ - || type === 1 /* FieldType.Protected */; - } - static makeImplicitPublicActuallyPublic(data, reportViolation) { - // TS-HACK - // A subtype can make an inherited protected field public. To prevent accidential - // mangling of public fields we mark the original (protected) fields as public... - for (const [name, info] of data.fields) { - if (info.type !== 0 /* FieldType.Public */) { - continue; - } - let parent = data.parent; - while (parent) { - if (parent.fields.get(name)?.type === 1 /* FieldType.Protected */) { - const parentPos = parent.node.getSourceFile().getLineAndCharacterOfPosition(parent.fields.get(name).pos); - const infoPos = data.node.getSourceFile().getLineAndCharacterOfPosition(info.pos); - reportViolation(name, `'${name}' from ${parent.fileName}:${parentPos.line + 1}`, `${data.fileName}:${infoPos.line + 1}`); - parent.fields.get(name).type = 0 /* FieldType.Public */; - } - parent = parent.parent; - } - } - } - static fillInReplacement(data) { - if (data.replacements) { - // already done - return; - } - // fill in parents first - if (data.parent) { - ClassData.fillInReplacement(data.parent); - } - data.replacements = new Map(); - const isNameTaken = (name) => { - // locally taken - if (data._isNameTaken(name)) { - return true; - } - // parents - let parent = data.parent; - while (parent) { - if (parent._isNameTaken(name)) { - return true; - } - parent = parent.parent; - } - // children - if (data.children) { - const stack = [...data.children]; - while (stack.length) { - const node = stack.pop(); - if (node._isNameTaken(name)) { - return true; - } - if (node.children) { - stack.push(...node.children); - } - } - } - return false; - }; - const identPool = new ShortIdent(''); - for (const [name, info] of data.fields) { - if (ClassData._shouldMangle(info.type)) { - const shortName = identPool.next(isNameTaken); - data.replacements.set(name, shortName); - } - } - } - // a name is taken when a field that doesn't get mangled exists or - // when the name is already in use for replacement - _isNameTaken(name) { - if (this.fields.has(name) && !ClassData._shouldMangle(this.fields.get(name).type)) { - // public field - return true; - } - if (this.replacements) { - for (const shortName of this.replacements.values()) { - if (shortName === name) { - // replaced already (happens wih super types) - return true; - } - } - } - if (isNameTakenInFile(this.node, name)) { - return true; - } - return false; - } - lookupShortName(name) { - let value = this.replacements.get(name); - let parent = this.parent; - while (parent) { - if (parent.replacements.has(name) && parent.fields.get(name)?.type === 1 /* FieldType.Protected */) { - value = parent.replacements.get(name) ?? value; - } - parent = parent.parent; - } - return value; - } - // --- parent chaining - addChild(child) { - this.children ??= []; - this.children.push(child); - child.parent = this; - } -} -function isNameTakenInFile(node, name) { - const identifiers = node.getSourceFile().identifiers; - if (identifiers instanceof Map) { - if (identifiers.has(name)) { - return true; - } - } - return false; -} -const skippedExportMangledFiles = [ - // Monaco - 'editorCommon', - 'editorOptions', - 'editorZoom', - 'standaloneEditor', - 'standaloneEnums', - 'standaloneLanguages', - // Generated - 'extensionsApiProposals', - // Module passed around as type - 'pfs', - // entry points - ...[ - buildfile.workerEditor, - buildfile.workerExtensionHost, - buildfile.workerNotebook, - buildfile.workerLanguageDetection, - buildfile.workerLocalFileSearch, - buildfile.workerProfileAnalysis, - buildfile.workerOutputLinks, - buildfile.workerBackgroundTokenization, - buildfile.workbenchDesktop, - buildfile.workbenchWeb, - buildfile.code, - buildfile.codeWeb - ].flat().map(x => x.name), -]; -const skippedExportMangledProjects = [ - // Test projects - 'vscode-api-tests', - // These projects use webpack to dynamically rewrite imports, which messes up our mangling - 'configuration-editing', - 'microsoft-authentication', - 'github-authentication', - 'html-language-features/server', -]; -const skippedExportMangledSymbols = [ - // Don't mangle extension entry points - 'activate', - 'deactivate', -]; -class DeclarationData { - fileName; - node; - replacementName; - constructor(fileName, node, fileIdents) { - this.fileName = fileName; - this.node = node; - // Todo: generate replacement names based on usage count, with more used names getting shorter identifiers - this.replacementName = fileIdents.next(); - } - getLocations(service) { - if (typescript_1.default.isVariableDeclaration(this.node)) { - // If the const aliases any types, we need to rename those too - const definitionResult = service.getDefinitionAndBoundSpan(this.fileName, this.node.name.getStart()); - if (definitionResult?.definitions && definitionResult.definitions.length > 1) { - return definitionResult.definitions.map(x => ({ fileName: x.fileName, offset: x.textSpan.start })); - } - } - return [{ - fileName: this.fileName, - offset: this.node.name.getStart() - }]; - } - shouldMangle(newName) { - const currentName = this.node.name.getText(); - if (currentName.startsWith('$') || skippedExportMangledSymbols.includes(currentName)) { - return false; - } - // New name is longer the existing one :'( - if (newName.length >= currentName.length) { - return false; - } - // Don't mangle functions we've explicitly opted out - if (this.node.getFullText().includes('@skipMangle')) { - return false; - } - return true; - } -} -/** - * TypeScript2TypeScript transformer that mangles all private and protected fields - * - * 1. Collect all class fields (properties, methods) - * 2. Collect all sub and super-type relations between classes - * 3. Compute replacement names for each field - * 4. Lookup rename locations for these fields - * 5. Prepare and apply edits - */ -class Mangler { - projectPath; - log; - config; - allClassDataByKey = new Map(); - allExportedSymbols = new Set(); - renameWorkerPool; - constructor(projectPath, log = () => { }, config) { - this.projectPath = projectPath; - this.log = log; - this.config = config; - this.renameWorkerPool = workerpool_1.default.pool(path_1.default.join(__dirname, 'renameWorker.js'), { - maxWorkers: 4, - minWorkers: 'max' - }); - } - async computeNewFileContents(strictImplicitPublicHandling) { - const service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(this.projectPath)); - // STEP: - // - Find all classes and their field info. - // - Find exported symbols. - const fileIdents = new ShortIdent('$'); - const visit = (node) => { - if (this.config.manglePrivateFields) { - if (typescript_1.default.isClassDeclaration(node) || typescript_1.default.isClassExpression(node)) { - const anchor = node.name ?? node; - const key = `${node.getSourceFile().fileName}|${anchor.getStart()}`; - if (this.allClassDataByKey.has(key)) { - throw new Error('DUPE?'); - } - this.allClassDataByKey.set(key, new ClassData(node.getSourceFile().fileName, node)); - } - } - if (this.config.mangleExports) { - // Find exported classes, functions, and vars - if (( - // Exported class - typescript_1.default.isClassDeclaration(node) - && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) - && node.name) || ( - // Exported function - typescript_1.default.isFunctionDeclaration(node) - && typescript_1.default.isSourceFile(node.parent) - && hasModifier(node, typescript_1.default.SyntaxKind.ExportKeyword) - && node.name && node.body // On named function and not on the overload - ) || ( - // Exported variable - typescript_1.default.isVariableDeclaration(node) - && hasModifier(node.parent.parent, typescript_1.default.SyntaxKind.ExportKeyword) // Variable statement is exported - && typescript_1.default.isSourceFile(node.parent.parent.parent)) - // Disabled for now because we need to figure out how to handle - // enums that are used in monaco or extHost interfaces. - /* || ( - // Exported enum - ts.isEnumDeclaration(node) - && ts.isSourceFile(node.parent) - && hasModifier(node, ts.SyntaxKind.ExportKeyword) - && !hasModifier(node, ts.SyntaxKind.ConstKeyword) // Don't bother mangling const enums because these are inlined - && node.name - */ - ) { - if (isInAmbientContext(node)) { - return; - } - this.allExportedSymbols.add(new DeclarationData(node.getSourceFile().fileName, node, fileIdents)); - } - } - typescript_1.default.forEachChild(node, visit); - }; - for (const file of service.getProgram().getSourceFiles()) { - if (!file.isDeclarationFile) { - typescript_1.default.forEachChild(file, visit); - } - } - this.log(`Done collecting. Classes: ${this.allClassDataByKey.size}. Exported symbols: ${this.allExportedSymbols.size}`); - // STEP: connect sub and super-types - const setupParents = (data) => { - const extendsClause = data.node.heritageClauses?.find(h => h.token === typescript_1.default.SyntaxKind.ExtendsKeyword); - if (!extendsClause) { - // no EXTENDS-clause - return; - } - const info = service.getDefinitionAtPosition(data.fileName, extendsClause.types[0].expression.getEnd()); - if (!info || info.length === 0) { - // throw new Error('SUPER type not found'); - return; - } - if (info.length !== 1) { - // inherits from declared/library type - return; - } - const [definition] = info; - const key = `${definition.fileName}|${definition.textSpan.start}`; - const parent = this.allClassDataByKey.get(key); - if (!parent) { - // throw new Error(`SUPER type not found: ${key}`); - return; - } - parent.addChild(data); - }; - for (const data of this.allClassDataByKey.values()) { - setupParents(data); - } - // STEP: make implicit public (actually protected) field really public - const violations = new Map(); - let violationsCauseFailure = false; - for (const data of this.allClassDataByKey.values()) { - ClassData.makeImplicitPublicActuallyPublic(data, (name, what, why) => { - const arr = violations.get(what); - if (arr) { - arr.push(why); - } - else { - violations.set(what, [why]); - } - if (strictImplicitPublicHandling && !strictImplicitPublicHandling.has(name)) { - violationsCauseFailure = true; - } - }); - } - for (const [why, whys] of violations) { - this.log(`WARN: ${why} became PUBLIC because of: ${whys.join(' , ')}`); - } - if (violationsCauseFailure) { - const message = 'Protected fields have been made PUBLIC. This hurts minification and is therefore not allowed. Review the WARN messages further above'; - this.log(`ERROR: ${message}`); - throw new Error(message); - } - // STEP: compute replacement names for each class - for (const data of this.allClassDataByKey.values()) { - ClassData.fillInReplacement(data); - } - this.log(`Done creating class replacements`); - // STEP: prepare rename edits - this.log(`Starting prepare rename edits`); - const editsByFile = new Map(); - const appendEdit = (fileName, edit) => { - const edits = editsByFile.get(fileName); - if (!edits) { - editsByFile.set(fileName, [edit]); - } - else { - edits.push(edit); - } - }; - const appendRename = (newText, loc) => { - appendEdit(loc.fileName, { - newText: (loc.prefixText || '') + newText + (loc.suffixText || ''), - offset: loc.textSpan.start, - length: loc.textSpan.length - }); - }; - const renameResults = []; - const queueRename = (fileName, pos, newName) => { - renameResults.push(Promise.resolve(this.renameWorkerPool.exec('findRenameLocations', [this.projectPath, fileName, pos])) - .then((locations) => ({ newName, locations }))); - }; - for (const data of this.allClassDataByKey.values()) { - if (hasModifier(data.node, typescript_1.default.SyntaxKind.DeclareKeyword)) { - continue; - } - fields: for (const [name, info] of data.fields) { - if (!ClassData._shouldMangle(info.type)) { - continue fields; - } - // TS-HACK: protected became public via 'some' child - // and because of that we might need to ignore this now - let parent = data.parent; - while (parent) { - if (parent.fields.get(name)?.type === 0 /* FieldType.Public */) { - continue fields; - } - parent = parent.parent; - } - const newName = data.lookupShortName(name); - queueRename(data.fileName, info.pos, newName); - } - } - for (const data of this.allExportedSymbols.values()) { - if (data.fileName.endsWith('.d.ts') - || skippedExportMangledProjects.some(proj => data.fileName.includes(proj)) - || skippedExportMangledFiles.some(file => data.fileName.endsWith(file + '.ts'))) { - continue; - } - if (!data.shouldMangle(data.replacementName)) { - continue; - } - const newText = data.replacementName; - for (const { fileName, offset } of data.getLocations(service)) { - queueRename(fileName, offset, newText); - } - } - await Promise.all(renameResults).then((result) => { - for (const { newName, locations } of result) { - for (const loc of locations) { - appendRename(newName, loc); - } - } - }); - await this.renameWorkerPool.terminate(); - this.log(`Done preparing edits: ${editsByFile.size} files`); - // STEP: apply all rename edits (per file) - const result = new Map(); - let savedBytes = 0; - for (const item of service.getProgram().getSourceFiles()) { - const { mapRoot, sourceRoot } = service.getProgram().getCompilerOptions(); - const projectDir = path_1.default.dirname(this.projectPath); - const sourceMapRoot = mapRoot ?? (0, url_1.pathToFileURL)(sourceRoot ?? projectDir).toString(); - // source maps - let generator; - let newFullText; - const edits = editsByFile.get(item.fileName); - if (!edits) { - // just copy - newFullText = item.getFullText(); - } - else { - // source map generator - const relativeFileName = normalize(path_1.default.relative(projectDir, item.fileName)); - const mappingsByLine = new Map(); - // apply renames - edits.sort((a, b) => b.offset - a.offset); - const characters = item.getFullText().split(''); - let lastEdit; - for (const edit of edits) { - if (lastEdit && lastEdit.offset === edit.offset) { - // - if (lastEdit.length !== edit.length || lastEdit.newText !== edit.newText) { - this.log('ERROR: Overlapping edit', item.fileName, edit.offset, edits); - throw new Error('OVERLAPPING edit'); - } - else { - continue; - } - } - lastEdit = edit; - const mangledName = characters.splice(edit.offset, edit.length, edit.newText).join(''); - savedBytes += mangledName.length - edit.newText.length; - // source maps - const pos = item.getLineAndCharacterOfPosition(edit.offset); - let mappings = mappingsByLine.get(pos.line); - if (!mappings) { - mappings = []; - mappingsByLine.set(pos.line, mappings); - } - mappings.unshift({ - source: relativeFileName, - original: { line: pos.line + 1, column: pos.character }, - generated: { line: pos.line + 1, column: pos.character }, - name: mangledName - }, { - source: relativeFileName, - original: { line: pos.line + 1, column: pos.character + edit.length }, - generated: { line: pos.line + 1, column: pos.character + edit.newText.length }, - }); - } - // source map generation, make sure to get mappings per line correct - generator = new source_map_1.SourceMapGenerator({ file: path_1.default.basename(item.fileName), sourceRoot: sourceMapRoot }); - generator.setSourceContent(relativeFileName, item.getFullText()); - for (const [, mappings] of mappingsByLine) { - let lineDelta = 0; - for (const mapping of mappings) { - generator.addMapping({ - ...mapping, - generated: { line: mapping.generated.line, column: mapping.generated.column - lineDelta } - }); - lineDelta += mapping.original.column - mapping.generated.column; - } - } - newFullText = characters.join(''); - } - result.set(item.fileName, { out: newFullText, sourceMap: generator?.toString() }); - } - service.dispose(); - this.renameWorkerPool.terminate(); - this.log(`Done: ${savedBytes / 1000}kb saved, memory-usage: ${JSON.stringify(node_v8_1.default.getHeapStatistics())}`); - return result; - } -} -exports.Mangler = Mangler; -// --- ast utils -function hasModifier(node, kind) { - const modifiers = typescript_1.default.canHaveModifiers(node) ? typescript_1.default.getModifiers(node) : undefined; - return Boolean(modifiers?.find(mode => mode.kind === kind)); -} -function isInAmbientContext(node) { - for (let p = node.parent; p; p = p.parent) { - if (typescript_1.default.isModuleDeclaration(p)) { - return true; - } - } - return false; -} -function normalize(path) { - return path.replace(/\\/g, '/'); -} -async function _run() { - const root = path_1.default.join(__dirname, '..', '..', '..'); - const projectBase = path_1.default.join(root, 'src'); - const projectPath = path_1.default.join(projectBase, 'tsconfig.json'); - const newProjectBase = path_1.default.join(path_1.default.dirname(projectBase), path_1.default.basename(projectBase) + '2'); - fs_1.default.cpSync(projectBase, newProjectBase, { recursive: true }); - const mangler = new Mangler(projectPath, console.log, { - mangleExports: true, - manglePrivateFields: true, - }); - for (const [fileName, contents] of await mangler.computeNewFileContents(new Set(['saveState']))) { - const newFilePath = path_1.default.join(newProjectBase, path_1.default.relative(projectBase, fileName)); - await fs_1.default.promises.mkdir(path_1.default.dirname(newFilePath), { recursive: true }); - await fs_1.default.promises.writeFile(newFilePath, contents.out); - if (contents.sourceMap) { - await fs_1.default.promises.writeFile(newFilePath + '.map', contents.sourceMap); - } - } -} -if (__filename === process_1.argv[1]) { - _run(); -} -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/build/lib/mangle/index.ts b/build/lib/mangle/index.ts index 02050d2e6a2..e53c58d32eb 100644 --- a/build/lib/mangle/index.ts +++ b/build/lib/mangle/index.ts @@ -6,13 +6,12 @@ import v8 from 'node:v8'; import fs from 'fs'; import path from 'path'; -import { argv } from 'process'; -import { Mapping, SourceMapGenerator } from 'source-map'; +import { type Mapping, SourceMapGenerator } from 'source-map'; import ts from 'typescript'; import { pathToFileURL } from 'url'; import workerpool from 'workerpool'; -import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; -const buildfile = require('../../buildfile'); +import { StaticLanguageServiceHost } from './staticLanguageServiceHost.ts'; +import * as buildfile from '../../buildfile.ts'; class ShortIdent { @@ -24,10 +23,13 @@ class ShortIdent { private static _alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890$_'.split(''); private _value = 0; + private readonly prefix: string; constructor( - private readonly prefix: string - ) { } + prefix: string + ) { + this.prefix = prefix; + } next(isNameTaken?: (name: string) => boolean): string { const candidate = this.prefix + ShortIdent.convert(this._value); @@ -51,11 +53,12 @@ class ShortIdent { } } -const enum FieldType { - Public, - Protected, - Private -} +const FieldType = Object.freeze({ + Public: 0, + Protected: 1, + Private: 2 +}); +type FieldType = typeof FieldType[keyof typeof FieldType]; class ClassData { @@ -66,10 +69,15 @@ class ClassData { parent: ClassData | undefined; children: ClassData[] | undefined; + readonly fileName: string; + readonly node: ts.ClassDeclaration | ts.ClassExpression; + constructor( - readonly fileName: string, - readonly node: ts.ClassDeclaration | ts.ClassExpression, + fileName: string, + node: ts.ClassDeclaration | ts.ClassExpression, ) { + this.fileName = fileName; + this.node = node; // analyse all fields (properties and methods). Find usages of all protected and // private ones and keep track of all public ones (to prevent naming collisions) @@ -338,12 +346,16 @@ const skippedExportMangledSymbols = [ class DeclarationData { readonly replacementName: string; + readonly fileName: string; + readonly node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.VariableDeclaration; constructor( - readonly fileName: string, - readonly node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.VariableDeclaration, + fileName: string, + node: ts.FunctionDeclaration | ts.ClassDeclaration | ts.EnumDeclaration | ts.VariableDeclaration, fileIdents: ShortIdent, ) { + this.fileName = fileName; + this.node = node; // Todo: generate replacement names based on usage count, with more used names getting shorter identifiers this.replacementName = fileIdents.next(); } @@ -404,13 +416,20 @@ export class Mangler { private readonly renameWorkerPool: workerpool.WorkerPool; + private readonly projectPath: string; + private readonly log: typeof console.log; + private readonly config: { readonly manglePrivateFields: boolean; readonly mangleExports: boolean }; + constructor( - private readonly projectPath: string, - private readonly log: typeof console.log = () => { }, - private readonly config: { readonly manglePrivateFields: boolean; readonly mangleExports: boolean }, + projectPath: string, + log: typeof console.log = () => { }, + config: { readonly manglePrivateFields: boolean; readonly mangleExports: boolean }, ) { + this.projectPath = projectPath; + this.log = log; + this.config = config; - this.renameWorkerPool = workerpool.pool(path.join(__dirname, 'renameWorker.js'), { + this.renameWorkerPool = workerpool.pool(path.join(import.meta.dirname, 'renameWorker.ts'), { maxWorkers: 4, minWorkers: 'max' }); @@ -753,7 +772,7 @@ function normalize(path: string): string { } async function _run() { - const root = path.join(__dirname, '..', '..', '..'); + const root = path.join(import.meta.dirname, '..', '..', '..'); const projectBase = path.join(root, 'src'); const projectPath = path.join(projectBase, 'tsconfig.json'); const newProjectBase = path.join(path.dirname(projectBase), path.basename(projectBase) + '2'); @@ -774,6 +793,6 @@ async function _run() { } } -if (__filename === argv[1]) { +if (import.meta.main) { _run(); } diff --git a/build/lib/mangle/renameWorker.js b/build/lib/mangle/renameWorker.js deleted file mode 100644 index 8bd59a4e2d5..00000000000 --- a/build/lib/mangle/renameWorker.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const typescript_1 = __importDefault(require("typescript")); -const workerpool_1 = __importDefault(require("workerpool")); -const staticLanguageServiceHost_1 = require("./staticLanguageServiceHost"); -let service; -function findRenameLocations(projectPath, fileName, position) { - if (!service) { - service = typescript_1.default.createLanguageService(new staticLanguageServiceHost_1.StaticLanguageServiceHost(projectPath)); - } - return service.findRenameLocations(fileName, position, false, false, { - providePrefixAndSuffixTextForRename: true, - }) ?? []; -} -workerpool_1.default.worker({ - findRenameLocations -}); -//# sourceMappingURL=renameWorker.js.map \ No newline at end of file diff --git a/build/lib/mangle/renameWorker.ts b/build/lib/mangle/renameWorker.ts index 0cce5677593..b7bfb539398 100644 --- a/build/lib/mangle/renameWorker.ts +++ b/build/lib/mangle/renameWorker.ts @@ -5,7 +5,7 @@ import ts from 'typescript'; import workerpool from 'workerpool'; -import { StaticLanguageServiceHost } from './staticLanguageServiceHost'; +import { StaticLanguageServiceHost } from './staticLanguageServiceHost.ts'; let service: ts.LanguageService | undefined; diff --git a/build/lib/mangle/staticLanguageServiceHost.js b/build/lib/mangle/staticLanguageServiceHost.js deleted file mode 100644 index 7777888dd06..00000000000 --- a/build/lib/mangle/staticLanguageServiceHost.js +++ /dev/null @@ -1,68 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StaticLanguageServiceHost = void 0; -const typescript_1 = __importDefault(require("typescript")); -const path_1 = __importDefault(require("path")); -class StaticLanguageServiceHost { - projectPath; - _cmdLine; - _scriptSnapshots = new Map(); - constructor(projectPath) { - this.projectPath = projectPath; - const existingOptions = {}; - const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); - if (parsed.error) { - throw parsed.error; - } - this._cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, path_1.default.dirname(projectPath), existingOptions); - if (this._cmdLine.errors.length > 0) { - throw parsed.error; - } - } - getCompilationSettings() { - return this._cmdLine.options; - } - getScriptFileNames() { - return this._cmdLine.fileNames; - } - getScriptVersion(_fileName) { - return '1'; - } - getProjectVersion() { - return '1'; - } - getScriptSnapshot(fileName) { - let result = this._scriptSnapshots.get(fileName); - if (result === undefined) { - const content = typescript_1.default.sys.readFile(fileName); - if (content === undefined) { - return undefined; - } - result = typescript_1.default.ScriptSnapshot.fromString(content); - this._scriptSnapshots.set(fileName, result); - } - return result; - } - getCurrentDirectory() { - return path_1.default.dirname(this.projectPath); - } - getDefaultLibFileName(options) { - return typescript_1.default.getDefaultLibFilePath(options); - } - directoryExists = typescript_1.default.sys.directoryExists; - getDirectories = typescript_1.default.sys.getDirectories; - fileExists = typescript_1.default.sys.fileExists; - readFile = typescript_1.default.sys.readFile; - readDirectory = typescript_1.default.sys.readDirectory; - // this is necessary to make source references work. - realpath = typescript_1.default.sys.realpath; -} -exports.StaticLanguageServiceHost = StaticLanguageServiceHost; -//# sourceMappingURL=staticLanguageServiceHost.js.map \ No newline at end of file diff --git a/build/lib/mangle/staticLanguageServiceHost.ts b/build/lib/mangle/staticLanguageServiceHost.ts index b41b4e52133..4fcf107f716 100644 --- a/build/lib/mangle/staticLanguageServiceHost.ts +++ b/build/lib/mangle/staticLanguageServiceHost.ts @@ -10,8 +10,10 @@ export class StaticLanguageServiceHost implements ts.LanguageServiceHost { private readonly _cmdLine: ts.ParsedCommandLine; private readonly _scriptSnapshots: Map = new Map(); + readonly projectPath: string; - constructor(readonly projectPath: string) { + constructor(projectPath: string) { + this.projectPath = projectPath; const existingOptions: Partial = {}; const parsed = ts.readConfigFile(projectPath, ts.sys.readFile); if (parsed.error) { diff --git a/build/lib/monaco-api.js b/build/lib/monaco-api.js deleted file mode 100644 index 1112b47370d..00000000000 --- a/build/lib/monaco-api.js +++ /dev/null @@ -1,578 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DeclarationResolver = exports.FSProvider = exports.RECIPE_PATH = void 0; -exports.run3 = run3; -exports.execute = execute; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const typeScriptLanguageServiceHost_1 = require("./typeScriptLanguageServiceHost"); -const dtsv = '3'; -const tsfmt = require('../../tsfmt.json'); -const SRC = path_1.default.join(__dirname, '../../src'); -exports.RECIPE_PATH = path_1.default.join(__dirname, '../monaco/monaco.d.ts.recipe'); -const DECLARATION_PATH = path_1.default.join(__dirname, '../../src/vs/monaco.d.ts'); -function logErr(message, ...rest) { - (0, fancy_log_1.default)(ansi_colors_1.default.yellow(`[monaco.d.ts]`), message, ...rest); -} -function isDeclaration(ts, a) { - return (a.kind === ts.SyntaxKind.InterfaceDeclaration - || a.kind === ts.SyntaxKind.EnumDeclaration - || a.kind === ts.SyntaxKind.ClassDeclaration - || a.kind === ts.SyntaxKind.TypeAliasDeclaration - || a.kind === ts.SyntaxKind.FunctionDeclaration - || a.kind === ts.SyntaxKind.ModuleDeclaration); -} -function visitTopLevelDeclarations(ts, sourceFile, visitor) { - let stop = false; - const visit = (node) => { - if (stop) { - return; - } - switch (node.kind) { - case ts.SyntaxKind.InterfaceDeclaration: - case ts.SyntaxKind.EnumDeclaration: - case ts.SyntaxKind.ClassDeclaration: - case ts.SyntaxKind.VariableStatement: - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.ModuleDeclaration: - stop = visitor(node); - } - if (stop) { - return; - } - ts.forEachChild(node, visit); - }; - visit(sourceFile); -} -function getAllTopLevelDeclarations(ts, sourceFile) { - const all = []; - visitTopLevelDeclarations(ts, sourceFile, (node) => { - if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { - const interfaceDeclaration = node; - const triviaStart = interfaceDeclaration.pos; - const triviaEnd = interfaceDeclaration.name.pos; - const triviaText = getNodeText(sourceFile, { pos: triviaStart, end: triviaEnd }); - if (triviaText.indexOf('@internal') === -1) { - all.push(node); - } - } - else { - const nodeText = getNodeText(sourceFile, node); - if (nodeText.indexOf('@internal') === -1) { - all.push(node); - } - } - return false /*continue*/; - }); - return all; -} -function getTopLevelDeclaration(ts, sourceFile, typeName) { - let result = null; - visitTopLevelDeclarations(ts, sourceFile, (node) => { - if (isDeclaration(ts, node) && node.name) { - if (node.name.text === typeName) { - result = node; - return true /*stop*/; - } - return false /*continue*/; - } - // node is ts.VariableStatement - if (getNodeText(sourceFile, node).indexOf(typeName) >= 0) { - result = node; - return true /*stop*/; - } - return false /*continue*/; - }); - return result; -} -function getNodeText(sourceFile, node) { - return sourceFile.getFullText().substring(node.pos, node.end); -} -function hasModifier(modifiers, kind) { - if (modifiers) { - for (let i = 0; i < modifiers.length; i++) { - const mod = modifiers[i]; - if (mod.kind === kind) { - return true; - } - } - } - return false; -} -function isStatic(ts, member) { - if (ts.canHaveModifiers(member)) { - return hasModifier(ts.getModifiers(member), ts.SyntaxKind.StaticKeyword); - } - return false; -} -function isDefaultExport(ts, declaration) { - return (hasModifier(declaration.modifiers, ts.SyntaxKind.DefaultKeyword) - && hasModifier(declaration.modifiers, ts.SyntaxKind.ExportKeyword)); -} -function getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums) { - let result = getNodeText(sourceFile, declaration); - if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { - const interfaceDeclaration = declaration; - const staticTypeName = (isDefaultExport(ts, interfaceDeclaration) - ? `${importName}.default` - : `${importName}.${declaration.name.text}`); - let instanceTypeName = staticTypeName; - const typeParametersCnt = (interfaceDeclaration.typeParameters ? interfaceDeclaration.typeParameters.length : 0); - if (typeParametersCnt > 0) { - const arr = []; - for (let i = 0; i < typeParametersCnt; i++) { - arr.push('any'); - } - instanceTypeName = `${instanceTypeName}<${arr.join(',')}>`; - } - const members = interfaceDeclaration.members; - members.forEach((member) => { - try { - const memberText = getNodeText(sourceFile, member); - if (memberText.indexOf('@internal') >= 0 || memberText.indexOf('private') >= 0) { - result = result.replace(memberText, ''); - } - else { - const memberName = member.name.text; - const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); - if (isStatic(ts, member)) { - usage.push(`a = ${staticTypeName}${memberAccess};`); - } - else { - usage.push(`a = (<${instanceTypeName}>b)${memberAccess};`); - } - } - } - catch (err) { - // life.. - } - }); - } - result = result.replace(/export default /g, 'export '); - result = result.replace(/export declare /g, 'export '); - result = result.replace(/declare /g, ''); - const lines = result.split(/\r\n|\r|\n/); - for (let i = 0; i < lines.length; i++) { - if (/\s*\*/.test(lines[i])) { - // very likely a comment - continue; - } - lines[i] = lines[i].replace(/"/g, '\''); - } - result = lines.join('\n'); - if (declaration.kind === ts.SyntaxKind.EnumDeclaration) { - result = result.replace(/const enum/, 'enum'); - enums.push({ - enumName: declaration.name.getText(sourceFile), - text: result - }); - } - return result; -} -function format(ts, text, endl) { - const REALLY_FORMAT = false; - text = preformat(text, endl); - if (!REALLY_FORMAT) { - return text; - } - // Parse the source text - const sourceFile = ts.createSourceFile('file.ts', text, ts.ScriptTarget.Latest, /*setParentPointers*/ true); - // Get the formatting edits on the input sources - const edits = ts.formatting.formatDocument(sourceFile, getRuleProvider(tsfmt), tsfmt); - // Apply the edits on the input code - return applyEdits(text, edits); - function countParensCurly(text) { - let cnt = 0; - for (let i = 0; i < text.length; i++) { - if (text.charAt(i) === '(' || text.charAt(i) === '{') { - cnt++; - } - if (text.charAt(i) === ')' || text.charAt(i) === '}') { - cnt--; - } - } - return cnt; - } - function repeatStr(s, cnt) { - let r = ''; - for (let i = 0; i < cnt; i++) { - r += s; - } - return r; - } - function preformat(text, endl) { - const lines = text.split(endl); - let inComment = false; - let inCommentDeltaIndent = 0; - let indent = 0; - for (let i = 0; i < lines.length; i++) { - let line = lines[i].replace(/\s$/, ''); - let repeat = false; - let lineIndent = 0; - do { - repeat = false; - if (line.substring(0, 4) === ' ') { - line = line.substring(4); - lineIndent++; - repeat = true; - } - if (line.charAt(0) === '\t') { - line = line.substring(1); - lineIndent++; - repeat = true; - } - } while (repeat); - if (line.length === 0) { - continue; - } - if (inComment) { - if (/\*\//.test(line)) { - inComment = false; - } - lines[i] = repeatStr('\t', lineIndent + inCommentDeltaIndent) + line; - continue; - } - if (/\/\*/.test(line)) { - inComment = true; - inCommentDeltaIndent = indent - lineIndent; - lines[i] = repeatStr('\t', indent) + line; - continue; - } - const cnt = countParensCurly(line); - let shouldUnindentAfter = false; - let shouldUnindentBefore = false; - if (cnt < 0) { - if (/[({]/.test(line)) { - shouldUnindentAfter = true; - } - else { - shouldUnindentBefore = true; - } - } - else if (cnt === 0) { - shouldUnindentBefore = /^\}/.test(line); - } - let shouldIndentAfter = false; - if (cnt > 0) { - shouldIndentAfter = true; - } - else if (cnt === 0) { - shouldIndentAfter = /{$/.test(line); - } - if (shouldUnindentBefore) { - indent--; - } - lines[i] = repeatStr('\t', indent) + line; - if (shouldUnindentAfter) { - indent--; - } - if (shouldIndentAfter) { - indent++; - } - } - return lines.join(endl); - } - function getRuleProvider(options) { - // Share this between multiple formatters using the same options. - // This represents the bulk of the space the formatter uses. - return ts.formatting.getFormatContext(options); - } - function applyEdits(text, edits) { - // Apply edits in reverse on the existing text - let result = text; - for (let i = edits.length - 1; i >= 0; i--) { - const change = edits[i]; - const head = result.slice(0, change.span.start); - const tail = result.slice(change.span.start + change.span.length); - result = head + change.newText + tail; - } - return result; - } -} -function createReplacerFromDirectives(directives) { - return (str) => { - for (let i = 0; i < directives.length; i++) { - str = str.replace(directives[i][0], directives[i][1]); - } - return str; - }; -} -function createReplacer(data) { - data = data || ''; - const rawDirectives = data.split(';'); - const directives = []; - rawDirectives.forEach((rawDirective) => { - if (rawDirective.length === 0) { - return; - } - const pieces = rawDirective.split('=>'); - let findStr = pieces[0]; - const replaceStr = pieces[1]; - findStr = findStr.replace(/[\-\\\{\}\*\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&'); - findStr = '\\b' + findStr + '\\b'; - directives.push([new RegExp(findStr, 'g'), replaceStr]); - }); - return createReplacerFromDirectives(directives); -} -function generateDeclarationFile(ts, recipe, sourceFileGetter) { - const endl = /\r\n/.test(recipe) ? '\r\n' : '\n'; - const lines = recipe.split(endl); - const result = []; - let usageCounter = 0; - const usageImports = []; - const usage = []; - let failed = false; - usage.push(`var a: any;`); - usage.push(`var b: any;`); - const generateUsageImport = (moduleId) => { - const importName = 'm' + (++usageCounter); - usageImports.push(`import * as ${importName} from './${moduleId}';`); - return importName; - }; - const enums = []; - let version = null; - lines.forEach(line => { - if (failed) { - return; - } - const m0 = line.match(/^\/\/dtsv=(\d+)$/); - if (m0) { - version = m0[1]; - } - const m1 = line.match(/^\s*#include\(([^;)]*)(;[^)]*)?\)\:(.*)$/); - if (m1) { - const moduleId = m1[1]; - const sourceFile = sourceFileGetter(moduleId); - if (!sourceFile) { - logErr(`While handling ${line}`); - logErr(`Cannot find ${moduleId}`); - failed = true; - return; - } - const importName = generateUsageImport(moduleId); - const replacer = createReplacer(m1[2]); - const typeNames = m1[3].split(/,/); - typeNames.forEach((typeName) => { - typeName = typeName.trim(); - if (typeName.length === 0) { - return; - } - const declaration = getTopLevelDeclaration(ts, sourceFile, typeName); - if (!declaration) { - logErr(`While handling ${line}`); - logErr(`Cannot find ${typeName}`); - failed = true; - return; - } - result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); - }); - return; - } - const m2 = line.match(/^\s*#includeAll\(([^;)]*)(;[^)]*)?\)\:(.*)$/); - if (m2) { - const moduleId = m2[1]; - const sourceFile = sourceFileGetter(moduleId); - if (!sourceFile) { - logErr(`While handling ${line}`); - logErr(`Cannot find ${moduleId}`); - failed = true; - return; - } - const importName = generateUsageImport(moduleId); - const replacer = createReplacer(m2[2]); - const typeNames = m2[3].split(/,/); - const typesToExcludeMap = {}; - const typesToExcludeArr = []; - typeNames.forEach((typeName) => { - typeName = typeName.trim(); - if (typeName.length === 0) { - return; - } - typesToExcludeMap[typeName] = true; - typesToExcludeArr.push(typeName); - }); - getAllTopLevelDeclarations(ts, sourceFile).forEach((declaration) => { - if (isDeclaration(ts, declaration) && declaration.name) { - if (typesToExcludeMap[declaration.name.text]) { - return; - } - } - else { - // node is ts.VariableStatement - const nodeText = getNodeText(sourceFile, declaration); - for (let i = 0; i < typesToExcludeArr.length; i++) { - if (nodeText.indexOf(typesToExcludeArr[i]) >= 0) { - return; - } - } - } - result.push(replacer(getMassagedTopLevelDeclarationText(ts, sourceFile, declaration, importName, usage, enums))); - }); - return; - } - result.push(line); - }); - if (failed) { - return null; - } - if (version !== dtsv) { - if (!version) { - logErr(`gulp watch restart required. 'monaco.d.ts.recipe' is written before versioning was introduced.`); - } - else { - logErr(`gulp watch restart required. 'monaco.d.ts.recipe' v${version} does not match runtime v${dtsv}.`); - } - return null; - } - let resultTxt = result.join(endl); - resultTxt = resultTxt.replace(/\bURI\b/g, 'Uri'); - resultTxt = resultTxt.replace(/\bEvent { - if (e1.enumName < e2.enumName) { - return -1; - } - if (e1.enumName > e2.enumName) { - return 1; - } - return 0; - }); - let resultEnums = [ - '/*---------------------------------------------------------------------------------------------', - ' * Copyright (c) Microsoft Corporation. All rights reserved.', - ' * Licensed under the MIT License. See License.txt in the project root for license information.', - ' *--------------------------------------------------------------------------------------------*/', - '', - '// THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY.', - '' - ].concat(enums.map(e => e.text)).join(endl); - resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); - resultEnums = format(ts, resultEnums, endl); - resultEnums = resultEnums.split(/\r\n|\n|\r/).join(endl); - return { - result: resultTxt, - usageContent: `${usageImports.join('\n')}\n\n${usage.join('\n')}`, - enums: resultEnums - }; -} -function _run(ts, sourceFileGetter) { - const recipe = fs_1.default.readFileSync(exports.RECIPE_PATH).toString(); - const t = generateDeclarationFile(ts, recipe, sourceFileGetter); - if (!t) { - return null; - } - const result = t.result; - const usageContent = t.usageContent; - const enums = t.enums; - const currentContent = fs_1.default.readFileSync(DECLARATION_PATH).toString(); - const one = currentContent.replace(/\r\n/gm, '\n'); - const other = result.replace(/\r\n/gm, '\n'); - const isTheSame = (one === other); - return { - content: result, - usageContent: usageContent, - enums: enums, - filePath: DECLARATION_PATH, - isTheSame - }; -} -class FSProvider { - existsSync(filePath) { - return fs_1.default.existsSync(filePath); - } - statSync(filePath) { - return fs_1.default.statSync(filePath); - } - readFileSync(_moduleId, filePath) { - return fs_1.default.readFileSync(filePath); - } -} -exports.FSProvider = FSProvider; -class CacheEntry { - sourceFile; - mtime; - constructor(sourceFile, mtime) { - this.sourceFile = sourceFile; - this.mtime = mtime; - } -} -class DeclarationResolver { - _fsProvider; - ts; - _sourceFileCache; - constructor(_fsProvider) { - this._fsProvider = _fsProvider; - this.ts = require('typescript'); - this._sourceFileCache = Object.create(null); - } - invalidateCache(moduleId) { - this._sourceFileCache[moduleId] = null; - } - getDeclarationSourceFile(moduleId) { - if (this._sourceFileCache[moduleId]) { - // Since we cannot trust file watching to invalidate the cache, check also the mtime - const fileName = this._getFileName(moduleId); - const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); - if (this._sourceFileCache[moduleId].mtime !== mtime) { - this._sourceFileCache[moduleId] = null; - } - } - if (!this._sourceFileCache[moduleId]) { - this._sourceFileCache[moduleId] = this._getDeclarationSourceFile(moduleId); - } - return this._sourceFileCache[moduleId] ? this._sourceFileCache[moduleId].sourceFile : null; - } - _getFileName(moduleId) { - if (/\.d\.ts$/.test(moduleId)) { - return path_1.default.join(SRC, moduleId); - } - if (/\.js$/.test(moduleId)) { - return path_1.default.join(SRC, moduleId.replace(/\.js$/, '.ts')); - } - return path_1.default.join(SRC, `${moduleId}.ts`); - } - _getDeclarationSourceFile(moduleId) { - const fileName = this._getFileName(moduleId); - if (!this._fsProvider.existsSync(fileName)) { - return null; - } - const mtime = this._fsProvider.statSync(fileName).mtime.getTime(); - if (/\.d\.ts$/.test(moduleId)) { - // const mtime = this._fsProvider.statFileSync() - const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); - return new CacheEntry(this.ts.createSourceFile(fileName, fileContents, this.ts.ScriptTarget.ES5), mtime); - } - const fileContents = this._fsProvider.readFileSync(moduleId, fileName).toString(); - const fileMap = new Map([ - ['file.ts', fileContents] - ]); - const service = this.ts.createLanguageService(new typeScriptLanguageServiceHost_1.TypeScriptLanguageServiceHost(this.ts, fileMap, {})); - const text = service.getEmitOutput('file.ts', true, true).outputFiles[0].text; - return new CacheEntry(this.ts.createSourceFile(fileName, text, this.ts.ScriptTarget.ES5), mtime); - } -} -exports.DeclarationResolver = DeclarationResolver; -function run3(resolver) { - const sourceFileGetter = (moduleId) => resolver.getDeclarationSourceFile(moduleId); - return _run(resolver.ts, sourceFileGetter); -} -function execute() { - const r = run3(new DeclarationResolver(new FSProvider())); - if (!r) { - throw new Error(`monaco.d.ts generation error - Cannot continue`); - } - return r; -} -//# sourceMappingURL=monaco-api.js.map \ No newline at end of file diff --git a/build/lib/monaco-api.ts b/build/lib/monaco-api.ts index e0622bcd336..fa6c2a28c91 100644 --- a/build/lib/monaco-api.ts +++ b/build/lib/monaco-api.ts @@ -4,19 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import fs from 'fs'; -import type * as ts from 'typescript'; import path from 'path'; import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; -import { IFileMap, TypeScriptLanguageServiceHost } from './typeScriptLanguageServiceHost'; +import { type IFileMap, TypeScriptLanguageServiceHost } from './typeScriptLanguageServiceHost.ts'; +import ts from 'typescript'; -const dtsv = '3'; +import tsfmt from '../../tsfmt.json' with { type: 'json' }; -const tsfmt = require('../../tsfmt.json'); +const dtsv = '3'; -const SRC = path.join(__dirname, '../../src'); -export const RECIPE_PATH = path.join(__dirname, '../monaco/monaco.d.ts.recipe'); -const DECLARATION_PATH = path.join(__dirname, '../../src/vs/monaco.d.ts'); +const SRC = path.join(import.meta.dirname, '../../src'); +export const RECIPE_PATH = path.join(import.meta.dirname, '../monaco/monaco.d.ts.recipe'); +const DECLARATION_PATH = path.join(import.meta.dirname, '../../src/vs/monaco.d.ts'); function logErr(message: any, ...rest: unknown[]): void { fancyLog(ansiColors.yellow(`[monaco.d.ts]`), message, ...rest); @@ -54,7 +54,7 @@ function visitTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: case ts.SyntaxKind.TypeAliasDeclaration: case ts.SyntaxKind.FunctionDeclaration: case ts.SyntaxKind.ModuleDeclaration: - stop = visitor(node); + stop = visitor(node as TSTopLevelDeclare); } if (stop) { @@ -71,7 +71,7 @@ function getAllTopLevelDeclarations(ts: typeof import('typescript'), sourceFile: const all: TSTopLevelDeclare[] = []; visitTopLevelDeclarations(ts, sourceFile, (node) => { if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.ModuleDeclaration) { - const interfaceDeclaration = node; + const interfaceDeclaration = node as ts.InterfaceDeclaration; const triviaStart = interfaceDeclaration.pos; const triviaEnd = interfaceDeclaration.name.pos; const triviaText = getNodeText(sourceFile, { pos: triviaStart, end: triviaEnd }); @@ -145,7 +145,7 @@ function isDefaultExport(ts: typeof import('typescript'), declaration: ts.Interf function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), sourceFile: ts.SourceFile, declaration: TSTopLevelDeclare, importName: string, usage: string[], enums: IEnumEntry[]): string { let result = getNodeText(sourceFile, declaration); if (declaration.kind === ts.SyntaxKind.InterfaceDeclaration || declaration.kind === ts.SyntaxKind.ClassDeclaration) { - const interfaceDeclaration = declaration; + const interfaceDeclaration = declaration as ts.InterfaceDeclaration | ts.ClassDeclaration; const staticTypeName = ( isDefaultExport(ts, interfaceDeclaration) @@ -170,7 +170,7 @@ function getMassagedTopLevelDeclarationText(ts: typeof import('typescript'), sou if (memberText.indexOf('@internal') >= 0 || memberText.indexOf('private') >= 0) { result = result.replace(memberText, ''); } else { - const memberName = (member.name).text; + const memberName = (member.name as ts.Identifier | ts.StringLiteral).text; const memberAccess = (memberName.indexOf('.') >= 0 ? `['${memberName}']` : `.${memberName}`); if (isStatic(ts, member)) { usage.push(`a = ${staticTypeName}${memberAccess};`); @@ -602,19 +602,27 @@ export class FSProvider { } class CacheEntry { + public readonly sourceFile: ts.SourceFile; + public readonly mtime: number; + constructor( - public readonly sourceFile: ts.SourceFile, - public readonly mtime: number - ) { } + sourceFile: ts.SourceFile, + mtime: number + ) { + this.sourceFile = sourceFile; + this.mtime = mtime; + } } export class DeclarationResolver { public readonly ts: typeof import('typescript'); private _sourceFileCache: { [moduleId: string]: CacheEntry | null }; + private readonly _fsProvider: FSProvider; - constructor(private readonly _fsProvider: FSProvider) { - this.ts = require('typescript') as typeof import('typescript'); + constructor(fsProvider: FSProvider) { + this._fsProvider = fsProvider; + this.ts = ts; this._sourceFileCache = Object.create(null); } diff --git a/build/lib/nls.js b/build/lib/nls.js deleted file mode 100644 index 55984151ddb..00000000000 --- a/build/lib/nls.js +++ /dev/null @@ -1,411 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.nls = nls; -const lazy_js_1 = __importDefault(require("lazy.js")); -const event_stream_1 = require("event-stream"); -const vinyl_1 = __importDefault(require("vinyl")); -const source_map_1 = __importDefault(require("source-map")); -const path_1 = __importDefault(require("path")); -const gulp_sort_1 = __importDefault(require("gulp-sort")); -var CollectStepResult; -(function (CollectStepResult) { - CollectStepResult[CollectStepResult["Yes"] = 0] = "Yes"; - CollectStepResult[CollectStepResult["YesAndRecurse"] = 1] = "YesAndRecurse"; - CollectStepResult[CollectStepResult["No"] = 2] = "No"; - CollectStepResult[CollectStepResult["NoAndRecurse"] = 3] = "NoAndRecurse"; -})(CollectStepResult || (CollectStepResult = {})); -function collect(ts, node, fn) { - const result = []; - function loop(node) { - const stepResult = fn(node); - if (stepResult === CollectStepResult.Yes || stepResult === CollectStepResult.YesAndRecurse) { - result.push(node); - } - if (stepResult === CollectStepResult.YesAndRecurse || stepResult === CollectStepResult.NoAndRecurse) { - ts.forEachChild(node, loop); - } - } - loop(node); - return result; -} -function clone(object) { - const result = {}; - for (const id in object) { - result[id] = object[id]; - } - return result; -} -/** - * Returns a stream containing the patched JavaScript and source maps. - */ -function nls(options) { - let base; - const input = (0, event_stream_1.through)(); - const output = input - .pipe((0, gulp_sort_1.default)()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files - .pipe((0, event_stream_1.through)(function (f) { - if (!f.sourceMap) { - return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); - } - let source = f.sourceMap.sources[0]; - if (!source) { - return this.emit('error', new Error(`File ${f.relative} does not have a source in the source map.`)); - } - const root = f.sourceMap.sourceRoot; - if (root) { - source = path_1.default.join(root, source); - } - const typescript = f.sourceMap.sourcesContent[0]; - if (!typescript) { - return this.emit('error', new Error(`File ${f.relative} does not have the original content in the source map.`)); - } - base = f.base; - this.emit('data', _nls.patchFile(f, typescript, options)); - }, function () { - for (const file of [ - new vinyl_1.default({ - contents: Buffer.from(JSON.stringify({ - keys: _nls.moduleToNLSKeys, - messages: _nls.moduleToNLSMessages, - }, null, '\t')), - base, - path: `${base}/nls.metadata.json` - }), - new vinyl_1.default({ - contents: Buffer.from(JSON.stringify(_nls.allNLSMessages)), - base, - path: `${base}/nls.messages.json` - }), - new vinyl_1.default({ - contents: Buffer.from(JSON.stringify(_nls.allNLSModulesAndKeys)), - base, - path: `${base}/nls.keys.json` - }), - new vinyl_1.default({ - contents: Buffer.from(`/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ -globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), - base, - path: `${base}/nls.messages.js` - }) - ]) { - this.emit('data', file); - } - this.emit('end'); - })); - return (0, event_stream_1.duplex)(input, output); -} -function isImportNode(ts, node) { - return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; -} -var _nls; -(function (_nls) { - _nls.moduleToNLSKeys = {}; - _nls.moduleToNLSMessages = {}; - _nls.allNLSMessages = []; - _nls.allNLSModulesAndKeys = []; - let allNLSMessagesIndex = 0; - function fileFrom(file, contents, path = file.path) { - return new vinyl_1.default({ - contents: Buffer.from(contents), - base: file.base, - cwd: file.cwd, - path: path - }); - } - function mappedPositionFrom(source, lc) { - return { source, line: lc.line + 1, column: lc.character }; - } - function lcFrom(position) { - return { line: position.line - 1, character: position.column }; - } - class SingleFileServiceHost { - options; - filename; - file; - lib; - constructor(ts, options, filename, contents) { - this.options = options; - this.filename = filename; - this.file = ts.ScriptSnapshot.fromString(contents); - this.lib = ts.ScriptSnapshot.fromString(''); - } - getCompilationSettings = () => this.options; - getScriptFileNames = () => [this.filename]; - getScriptVersion = () => '1'; - getScriptSnapshot = (name) => name === this.filename ? this.file : this.lib; - getCurrentDirectory = () => ''; - getDefaultLibFileName = () => 'lib.d.ts'; - readFile(path, _encoding) { - if (path === this.filename) { - return this.file.getText(0, this.file.getLength()); - } - return undefined; - } - fileExists(path) { - return path === this.filename; - } - } - function isCallExpressionWithinTextSpanCollectStep(ts, textSpan, node) { - if (!ts.textSpanContainsTextSpan({ start: node.pos, length: node.end - node.pos }, textSpan)) { - return CollectStepResult.No; - } - return node.kind === ts.SyntaxKind.CallExpression ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse; - } - function analyze(ts, contents, functionName, options = {}) { - const filename = 'file.ts'; - const serviceHost = new SingleFileServiceHost(ts, Object.assign(clone(options), { noResolve: true }), filename, contents); - const service = ts.createLanguageService(serviceHost); - const sourceFile = ts.createSourceFile(filename, contents, ts.ScriptTarget.ES5, true); - // all imports - const imports = (0, lazy_js_1.default)(collect(ts, sourceFile, n => isImportNode(ts, n) ? CollectStepResult.YesAndRecurse : CollectStepResult.NoAndRecurse)); - // import nls = require('vs/nls'); - const importEqualsDeclarations = imports - .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) - .map(n => n) - .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) - .filter(d => d.moduleReference.expression.getText().endsWith(`/nls.js'`)); - // import ... from 'vs/nls'; - const importDeclarations = imports - .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) - .map(n => n) - .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) - .filter(d => d.moduleSpecifier.getText().endsWith(`/nls.js'`)) - .filter(d => !!d.importClause && !!d.importClause.namedBindings); - // `nls.localize(...)` calls - const nlsLocalizeCallExpressions = importDeclarations - .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) - .map(d => d.importClause.namedBindings.name) - .concat(importEqualsDeclarations.map(d => d.name)) - // find read-only references to `nls` - .map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess) - // find the deepest call expressions AST nodes that contain those references - .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => (0, lazy_js_1.default)(a).last()) - .filter(n => !!n) - .map(n => n) - // only `localize` calls - .filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && n.expression.name.getText() === functionName); - // `localize` named imports - const allLocalizeImportDeclarations = importDeclarations - .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) - .map(d => [].concat(d.importClause.namedBindings.elements)) - .flatten(); - // `localize` read-only references - const localizeReferences = allLocalizeImportDeclarations - .filter(d => d.name.getText() === functionName) - .map(n => service.getReferencesAtPosition(filename, n.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess); - // custom named `localize` read-only references - const namedLocalizeReferences = allLocalizeImportDeclarations - .filter(d => d.propertyName && d.propertyName.getText() === functionName) - .map(n => service.getReferencesAtPosition(filename, n.name.pos + 1) ?? []) - .flatten() - .filter(r => !r.isWriteAccess); - // find the deepest call expressions AST nodes that contain those references - const localizeCallExpressions = localizeReferences - .concat(namedLocalizeReferences) - .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) - .map(a => (0, lazy_js_1.default)(a).last()) - .filter(n => !!n) - .map(n => n); - // collect everything - const localizeCalls = nlsLocalizeCallExpressions - .concat(localizeCallExpressions) - .map(e => e.arguments) - .filter(a => a.length > 1) - .sort((a, b) => a[0].getStart() - b[0].getStart()) - .map(a => ({ - keySpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[0].getEnd()) }, - key: a[0].getText(), - valueSpan: { start: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getStart()), end: ts.getLineAndCharacterOfPosition(sourceFile, a[1].getEnd()) }, - value: a[1].getText() - })); - return { - localizeCalls: localizeCalls.toArray() - }; - } - class TextModel { - lines; - lineEndings; - constructor(contents) { - const regex = /\r\n|\r|\n/g; - let index = 0; - let match; - this.lines = []; - this.lineEndings = []; - while (match = regex.exec(contents)) { - this.lines.push(contents.substring(index, match.index)); - this.lineEndings.push(match[0]); - index = regex.lastIndex; - } - if (contents.length > 0) { - this.lines.push(contents.substring(index, contents.length)); - this.lineEndings.push(''); - } - } - get(index) { - return this.lines[index]; - } - set(index, line) { - this.lines[index] = line; - } - get lineCount() { - return this.lines.length; - } - /** - * Applies patch(es) to the model. - * Multiple patches must be ordered. - * Does not support patches spanning multiple lines. - */ - apply(patch) { - const startLineNumber = patch.span.start.line; - const endLineNumber = patch.span.end.line; - const startLine = this.lines[startLineNumber] || ''; - const endLine = this.lines[endLineNumber] || ''; - this.lines[startLineNumber] = [ - startLine.substring(0, patch.span.start.character), - patch.content, - endLine.substring(patch.span.end.character) - ].join(''); - for (let i = startLineNumber + 1; i <= endLineNumber; i++) { - this.lines[i] = ''; - } - } - toString() { - return (0, lazy_js_1.default)(this.lines).zip(this.lineEndings) - .flatten().toArray().join(''); - } - } - function patchJavascript(patches, contents) { - const model = new TextModel(contents); - // patch the localize calls - (0, lazy_js_1.default)(patches).reverse().each(p => model.apply(p)); - return model.toString(); - } - function patchSourcemap(patches, rsm, smc) { - const smg = new source_map_1.default.SourceMapGenerator({ - file: rsm.file, - sourceRoot: rsm.sourceRoot - }); - patches = patches.reverse(); - let currentLine = -1; - let currentLineDiff = 0; - let source = null; - smc.eachMapping(m => { - const patch = patches[patches.length - 1]; - const original = { line: m.originalLine, column: m.originalColumn }; - const generated = { line: m.generatedLine, column: m.generatedColumn }; - if (currentLine !== generated.line) { - currentLineDiff = 0; - } - currentLine = generated.line; - generated.column += currentLineDiff; - if (patch && m.generatedLine - 1 === patch.span.end.line && m.generatedColumn === patch.span.end.character) { - const originalLength = patch.span.end.character - patch.span.start.character; - const modifiedLength = patch.content.length; - const lengthDiff = modifiedLength - originalLength; - currentLineDiff += lengthDiff; - generated.column += lengthDiff; - patches.pop(); - } - source = rsm.sourceRoot ? path_1.default.relative(rsm.sourceRoot, m.source) : m.source; - source = source.replace(/\\/g, '/'); - smg.addMapping({ source, name: m.name, original, generated }); - }, null, source_map_1.default.SourceMapConsumer.GENERATED_ORDER); - if (source) { - smg.setSourceContent(source, smc.sourceContentFor(source)); - } - return JSON.parse(smg.toString()); - } - function parseLocalizeKeyOrValue(sourceExpression) { - // sourceValue can be "foo", 'foo', `foo` or { .... } - // in its evalulated form - // we want to return either the string or the object - // eslint-disable-next-line no-eval - return eval(`(${sourceExpression})`); - } - function patch(ts, typescript, javascript, sourcemap, options) { - const { localizeCalls } = analyze(ts, typescript, 'localize'); - const { localizeCalls: localize2Calls } = analyze(ts, typescript, 'localize2'); - if (localizeCalls.length === 0 && localize2Calls.length === 0) { - return { javascript, sourcemap }; - } - const nlsKeys = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.key)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.key))); - const nlsMessages = localizeCalls.map(lc => parseLocalizeKeyOrValue(lc.value)).concat(localize2Calls.map(lc => parseLocalizeKeyOrValue(lc.value))); - const smc = new source_map_1.default.SourceMapConsumer(sourcemap); - const positionFrom = mappedPositionFrom.bind(null, sourcemap.sources[0]); - // build patches - const toPatch = (c) => { - const start = lcFrom(smc.generatedPositionFor(positionFrom(c.range.start))); - const end = lcFrom(smc.generatedPositionFor(positionFrom(c.range.end))); - return { span: { start, end }, content: c.content }; - }; - const localizePatches = (0, lazy_js_1.default)(localizeCalls) - .map(lc => (options.preserveEnglish ? [ - { range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize('key', "message") => localize(, "message") - ] : [ - { range: lc.keySpan, content: `${allNLSMessagesIndex++}` }, // localize('key', "message") => localize(, null) - { range: lc.valueSpan, content: 'null' } - ])) - .flatten() - .map(toPatch); - const localize2Patches = (0, lazy_js_1.default)(localize2Calls) - .map(lc => ({ range: lc.keySpan, content: `${allNLSMessagesIndex++}` } // localize2('key', "message") => localize(, "message") - )) - .map(toPatch); - // Sort patches by their start position - const patches = localizePatches.concat(localize2Patches).toArray().sort((a, b) => { - if (a.span.start.line < b.span.start.line) { - return -1; - } - else if (a.span.start.line > b.span.start.line) { - return 1; - } - else if (a.span.start.character < b.span.start.character) { - return -1; - } - else if (a.span.start.character > b.span.start.character) { - return 1; - } - else { - return 0; - } - }); - javascript = patchJavascript(patches, javascript); - sourcemap = patchSourcemap(patches, sourcemap, smc); - return { javascript, sourcemap, nlsKeys, nlsMessages }; - } - function patchFile(javascriptFile, typescript, options) { - const ts = require('typescript'); - // hack? - const moduleId = javascriptFile.relative - .replace(/\.js$/, '') - .replace(/\\/g, '/'); - const { javascript, sourcemap, nlsKeys, nlsMessages } = patch(ts, typescript, javascriptFile.contents.toString(), javascriptFile.sourceMap, options); - const result = fileFrom(javascriptFile, javascript); - result.sourceMap = sourcemap; - if (nlsKeys) { - _nls.moduleToNLSKeys[moduleId] = nlsKeys; - _nls.allNLSModulesAndKeys.push([moduleId, nlsKeys.map(nlsKey => typeof nlsKey === 'string' ? nlsKey : nlsKey.key)]); - } - if (nlsMessages) { - _nls.moduleToNLSMessages[moduleId] = nlsMessages; - _nls.allNLSMessages.push(...nlsMessages); - } - return result; - } - _nls.patchFile = patchFile; -})(_nls || (_nls = {})); -//# sourceMappingURL=nls.js.map \ No newline at end of file diff --git a/build/lib/nls.ts b/build/lib/nls.ts index 1cfb1cbd580..39cc07d9d01 100644 --- a/build/lib/nls.ts +++ b/build/lib/nls.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as ts from 'typescript'; +import * as ts from 'typescript'; import lazy from 'lazy.js'; -import { duplex, through } from 'event-stream'; +import eventStream from 'event-stream'; import File from 'vinyl'; import sm from 'source-map'; import path from 'path'; @@ -13,12 +13,14 @@ import sort from 'gulp-sort'; type FileWithSourcemap = File & { sourceMap: sm.RawSourceMap }; -enum CollectStepResult { - Yes, - YesAndRecurse, - No, - NoAndRecurse -} +const CollectStepResult = Object.freeze({ + Yes: 'Yes', + YesAndRecurse: 'YesAndRecurse', + No: 'No', + NoAndRecurse: 'NoAndRecurse' +}); + +type CollectStepResult = typeof CollectStepResult[keyof typeof CollectStepResult]; function collect(ts: typeof import('typescript'), node: ts.Node, fn: (node: ts.Node) => CollectStepResult): ts.Node[] { const result: ts.Node[] = []; @@ -52,10 +54,10 @@ function clone(object: T): T { */ export function nls(options: { preserveEnglish: boolean }): NodeJS.ReadWriteStream { let base: string; - const input = through(); + const input = eventStream.through(); const output = input .pipe(sort()) // IMPORTANT: to ensure stable NLS metadata generation, we must sort the files because NLS messages are globally extracted and indexed across all files - .pipe(through(function (f: FileWithSourcemap) { + .pipe(eventStream.through(function (f: FileWithSourcemap) { if (!f.sourceMap) { return this.emit('error', new Error(`File ${f.relative} does not have sourcemaps.`)); } @@ -112,19 +114,19 @@ globalThis._VSCODE_NLS_MESSAGES=${JSON.stringify(_nls.allNLSMessages)};`), this.emit('end'); })); - return duplex(input, output); + return eventStream.duplex(input, output); } function isImportNode(ts: typeof import('typescript'), node: ts.Node): boolean { return node.kind === ts.SyntaxKind.ImportDeclaration || node.kind === ts.SyntaxKind.ImportEqualsDeclaration; } -module _nls { +const _nls = (() => { - export const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; - export const moduleToNLSMessages: { [name: string /* module ID */]: string[] /* messages */ } = {}; - export const allNLSMessages: string[] = []; - export const allNLSModulesAndKeys: Array<[string /* module ID */, string[] /* keys */]> = []; + const moduleToNLSKeys: { [name: string /* module ID */]: ILocalizeKey[] /* keys */ } = {}; + const moduleToNLSMessages: { [name: string /* module ID */]: string[] /* messages */ } = {}; + const allNLSMessages: string[] = []; + const allNLSModulesAndKeys: Array<[string /* module ID */, string[] /* keys */]> = []; let allNLSMessagesIndex = 0; type ILocalizeKey = string | { key: string }; // key might contain metadata for translators and then is not just a string @@ -178,8 +180,12 @@ module _nls { private file: ts.IScriptSnapshot; private lib: ts.IScriptSnapshot; + private options: ts.CompilerOptions; + private filename: string; - constructor(ts: typeof import('typescript'), private options: ts.CompilerOptions, private filename: string, contents: string) { + constructor(ts: typeof import('typescript'), options: ts.CompilerOptions, filename: string, contents: string) { + this.options = options; + this.filename = filename; this.file = ts.ScriptSnapshot.fromString(contents); this.lib = ts.ScriptSnapshot.fromString(''); } @@ -227,14 +233,14 @@ module _nls { // import nls = require('vs/nls'); const importEqualsDeclarations = imports .filter(n => n.kind === ts.SyntaxKind.ImportEqualsDeclaration) - .map(n => n) + .map(n => n as ts.ImportEqualsDeclaration) .filter(d => d.moduleReference.kind === ts.SyntaxKind.ExternalModuleReference) - .filter(d => (d.moduleReference).expression.getText().endsWith(`/nls.js'`)); + .filter(d => (d.moduleReference as ts.ExternalModuleReference).expression.getText().endsWith(`/nls.js'`)); // import ... from 'vs/nls'; const importDeclarations = imports .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration) - .map(n => n) + .map(n => n as ts.ImportDeclaration) .filter(d => d.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) .filter(d => d.moduleSpecifier.getText().endsWith(`/nls.js'`)) .filter(d => !!d.importClause && !!d.importClause.namedBindings); @@ -242,7 +248,7 @@ module _nls { // `nls.localize(...)` calls const nlsLocalizeCallExpressions = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport)) - .map(d => (d.importClause!.namedBindings).name) + .map(d => (d.importClause!.namedBindings as ts.NamespaceImport).name) .concat(importEqualsDeclarations.map(d => d.name)) // find read-only references to `nls` @@ -254,15 +260,15 @@ module _nls { .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) - .map(n => n) + .map(n => n as ts.CallExpression) // only `localize` calls - .filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && (n.expression).name.getText() === functionName); + .filter(n => n.expression.kind === ts.SyntaxKind.PropertyAccessExpression && (n.expression as ts.PropertyAccessExpression).name.getText() === functionName); // `localize` named imports const allLocalizeImportDeclarations = importDeclarations .filter(d => !!(d.importClause && d.importClause.namedBindings && d.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) - .map(d => ([] as any[]).concat((d.importClause!.namedBindings!).elements)) + .map(d => (d.importClause!.namedBindings! as ts.NamedImports).elements) .flatten(); // `localize` read-only references @@ -274,7 +280,7 @@ module _nls { // custom named `localize` read-only references const namedLocalizeReferences = allLocalizeImportDeclarations - .filter(d => d.propertyName && d.propertyName.getText() === functionName) + .filter(d => !!d.propertyName && d.propertyName.getText() === functionName) .map(n => service.getReferencesAtPosition(filename, n.name.pos + 1) ?? []) .flatten() .filter(r => !r.isWriteAccess); @@ -285,7 +291,7 @@ module _nls { .map(r => collect(ts, sourceFile, n => isCallExpressionWithinTextSpanCollectStep(ts, r.textSpan, n))) .map(a => lazy(a).last()) .filter(n => !!n) - .map(n => n); + .map(n => n as ts.CallExpression); // collect everything const localizeCalls = nlsLocalizeCallExpressions @@ -492,8 +498,7 @@ module _nls { return { javascript, sourcemap, nlsKeys, nlsMessages }; } - export function patchFile(javascriptFile: File, typescript: string, options: { preserveEnglish: boolean }): File { - const ts = require('typescript') as typeof import('typescript'); + function patchFile(javascriptFile: File, typescript: string, options: { preserveEnglish: boolean }): File { // hack? const moduleId = javascriptFile.relative .replace(/\.js$/, '') @@ -522,4 +527,12 @@ module _nls { return result; } -} + + return { + moduleToNLSKeys, + moduleToNLSMessages, + allNLSMessages, + allNLSModulesAndKeys, + patchFile + }; +})(); diff --git a/build/lib/node.js b/build/lib/node.js deleted file mode 100644 index 01a381183ff..00000000000 --- a/build/lib/node.js +++ /dev/null @@ -1,21 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const npmrcPath = path_1.default.join(root, 'remote', '.npmrc'); -const npmrc = fs_1.default.readFileSync(npmrcPath, 'utf8'); -const version = /^target="(.*)"$/m.exec(npmrc)[1]; -const platform = process.platform; -const arch = process.arch; -const node = platform === 'win32' ? 'node.exe' : 'node'; -const nodePath = path_1.default.join(root, '.build', 'node', `v${version}`, `${platform}-${arch}`, node); -console.log(nodePath); -//# sourceMappingURL=node.js.map \ No newline at end of file diff --git a/build/lib/node.ts b/build/lib/node.ts index a2fdc361aa1..1825546deb9 100644 --- a/build/lib/node.ts +++ b/build/lib/node.ts @@ -6,10 +6,14 @@ import path from 'path'; import fs from 'fs'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); const npmrcPath = path.join(root, 'remote', '.npmrc'); const npmrc = fs.readFileSync(npmrcPath, 'utf8'); -const version = /^target="(.*)"$/m.exec(npmrc)![1]; +const version = /^target="(.*)"$/m.exec(npmrc)?.[1]; + +if (!version) { + throw new Error('Failed to extract Node version from .npmrc'); +} const platform = process.platform; const arch = process.arch; diff --git a/build/lib/optimize.js b/build/lib/optimize.js deleted file mode 100644 index 2ba72a97159..00000000000 --- a/build/lib/optimize.js +++ /dev/null @@ -1,231 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.bundleTask = bundleTask; -exports.minifyTask = minifyTask; -const event_stream_1 = __importDefault(require("event-stream")); -const gulp_1 = __importDefault(require("gulp")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const pump_1 = __importDefault(require("pump")); -const vinyl_1 = __importDefault(require("vinyl")); -const bundle = __importStar(require("./bundle")); -const esbuild_1 = __importDefault(require("esbuild")); -const gulp_sourcemaps_1 = __importDefault(require("gulp-sourcemaps")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const tsconfigUtils_1 = require("./tsconfigUtils"); -const REPO_ROOT_PATH = path_1.default.join(__dirname, '../..'); -const DEFAULT_FILE_HEADER = [ - '/*!--------------------------------------------------------', - ' * Copyright (C) Microsoft Corporation. All rights reserved.', - ' *--------------------------------------------------------*/' -].join('\n'); -function bundleESMTask(opts) { - const resourcesStream = event_stream_1.default.through(); // this stream will contain the resources - const bundlesStream = event_stream_1.default.through(); // this stream will contain the bundled files - const target = getBuildTarget(); - const entryPoints = opts.entryPoints.map(entryPoint => { - if (typeof entryPoint === 'string') { - return { name: path_1.default.parse(entryPoint).name }; - } - return entryPoint; - }); - const bundleAsync = async () => { - const files = []; - const tasks = []; - for (const entryPoint of entryPoints) { - (0, fancy_log_1.default)(`Bundled entry point: ${ansi_colors_1.default.yellow(entryPoint.name)}...`); - // support for 'dest' via esbuild#in/out - const dest = entryPoint.dest?.replace(/\.[^/.]+$/, '') ?? entryPoint.name; - // banner contents - const banner = { - js: DEFAULT_FILE_HEADER, - css: DEFAULT_FILE_HEADER - }; - // TS Boilerplate - if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { - const tslibPath = path_1.default.join(require.resolve('tslib'), '../tslib.es6.js'); - banner.js += await fs_1.default.promises.readFile(tslibPath, 'utf-8'); - } - const contentsMapper = { - name: 'contents-mapper', - setup(build) { - build.onLoad({ filter: /\.js$/ }, async ({ path }) => { - const contents = await fs_1.default.promises.readFile(path, 'utf-8'); - // TS Boilerplate - let newContents; - if (!opts.skipTSBoilerplateRemoval?.(entryPoint.name)) { - newContents = bundle.removeAllTSBoilerplate(contents); - } - else { - newContents = contents; - } - // File Content Mapper - const mapper = opts.fileContentMapper?.(path.replace(/\\/g, '/')); - if (mapper) { - newContents = await mapper(newContents); - } - return { contents: newContents }; - }); - } - }; - const externalOverride = { - name: 'external-override', - setup(build) { - // We inline selected modules that are we depend on on startup without - // a conditional `await import(...)` by hooking into the resolution. - build.onResolve({ filter: /^minimist$/ }, () => { - return { path: path_1.default.join(REPO_ROOT_PATH, 'node_modules', 'minimist', 'index.js'), external: false }; - }); - }, - }; - const task = esbuild_1.default.build({ - bundle: true, - packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages - platform: 'neutral', // makes esm - format: 'esm', - sourcemap: 'external', - plugins: [contentsMapper, externalOverride], - target: [target], - loader: { - '.ttf': 'file', - '.svg': 'file', - '.png': 'file', - '.sh': 'file', - }, - assetNames: 'media/[name]', // moves media assets into a sub-folder "media" - banner: entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner, // TODO@esm remove line when we stop supporting web-amd-esm-bridge - entryPoints: [ - { - in: path_1.default.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), - out: dest, - } - ], - outdir: path_1.default.join(REPO_ROOT_PATH, opts.src), - write: false, // enables res.outputFiles - metafile: true, // enables res.metafile - // minify: NOT enabled because we have a separate minify task that takes care of the TSLib banner as well - }).then(res => { - for (const file of res.outputFiles) { - let sourceMapFile = undefined; - if (file.path.endsWith('.js')) { - sourceMapFile = res.outputFiles.find(f => f.path === `${file.path}.map`); - } - const fileProps = { - contents: Buffer.from(file.contents), - sourceMap: sourceMapFile ? JSON.parse(sourceMapFile.text) : undefined, // support gulp-sourcemaps - path: file.path, - base: path_1.default.join(REPO_ROOT_PATH, opts.src) - }; - files.push(new vinyl_1.default(fileProps)); - } - }); - tasks.push(task); - } - await Promise.all(tasks); - return { files }; - }; - bundleAsync().then((output) => { - // bundle output (JS, CSS, SVG...) - event_stream_1.default.readArray(output.files).pipe(bundlesStream); - // forward all resources - gulp_1.default.src(opts.resources ?? [], { base: `${opts.src}`, allowEmpty: true }).pipe(resourcesStream); - }); - const result = event_stream_1.default.merge(bundlesStream, resourcesStream); - return result - .pipe(gulp_sourcemaps_1.default.write('./', { - sourceRoot: undefined, - addComment: true, - includeContent: true - })); -} -function bundleTask(opts) { - return function () { - return bundleESMTask(opts.esm).pipe(gulp_1.default.dest(opts.out)); - }; -} -function minifyTask(src, sourceMapBaseUrl) { - const sourceMappingURL = sourceMapBaseUrl ? ((f) => `${sourceMapBaseUrl}/${f.relative}.map`) : undefined; - const target = getBuildTarget(); - return cb => { - const svgmin = require('gulp-svgmin'); - const esbuildFilter = (0, gulp_filter_1.default)('**/*.{js,css}', { restore: true }); - const svgFilter = (0, gulp_filter_1.default)('**/*.svg', { restore: true }); - (0, pump_1.default)(gulp_1.default.src([src + '/**', '!' + src + '/**/*.map']), esbuildFilter, gulp_sourcemaps_1.default.init({ loadMaps: true }), event_stream_1.default.map((f, cb) => { - esbuild_1.default.build({ - entryPoints: [f.path], - minify: true, - sourcemap: 'external', - outdir: '.', - packages: 'external', // "external all the things", see https://esbuild.github.io/api/#packages - platform: 'neutral', // makes esm - target: [target], - write: false, - }).then(res => { - const jsOrCSSFile = res.outputFiles.find(f => /\.(js|css)$/.test(f.path)); - const sourceMapFile = res.outputFiles.find(f => /\.(js|css)\.map$/.test(f.path)); - const contents = Buffer.from(jsOrCSSFile.contents); - const unicodeMatch = contents.toString().match(/[^\x00-\xFF]+/g); - if (unicodeMatch) { - cb(new Error(`Found non-ascii character ${unicodeMatch[0]} in the minified output of ${f.path}. Non-ASCII characters in the output can cause performance problems when loading. Please review if you have introduced a regular expression that esbuild is not automatically converting and convert it to using unicode escape sequences.`)); - } - else { - f.contents = contents; - f.sourceMap = JSON.parse(sourceMapFile.text); - cb(undefined, f); - } - }, cb); - }), esbuildFilter.restore, svgFilter, svgmin(), svgFilter.restore, gulp_sourcemaps_1.default.write('./', { - sourceMappingURL, - sourceRoot: undefined, - includeContent: true, - addComment: true - }), gulp_1.default.dest(src + '-min'), (err) => cb(err)); - }; -} -function getBuildTarget() { - const tsconfigPath = path_1.default.join(REPO_ROOT_PATH, 'src', 'tsconfig.base.json'); - return (0, tsconfigUtils_1.getTargetStringFromTsConfig)(tsconfigPath); -} -//# sourceMappingURL=optimize.js.map \ No newline at end of file diff --git a/build/lib/optimize.ts b/build/lib/optimize.ts index 1e824a54106..f5e812e2890 100644 --- a/build/lib/optimize.ts +++ b/build/lib/optimize.ts @@ -10,12 +10,16 @@ import path from 'path'; import fs from 'fs'; import pump from 'pump'; import VinylFile from 'vinyl'; -import * as bundle from './bundle'; +import * as bundle from './bundle.ts'; import esbuild from 'esbuild'; import sourcemaps from 'gulp-sourcemaps'; import fancyLog from 'fancy-log'; import ansiColors from 'ansi-colors'; -import { getTargetStringFromTsConfig } from './tsconfigUtils'; +import { getTargetStringFromTsConfig } from './tsconfigUtils.ts'; +import svgmin from 'gulp-svgmin'; +import { createRequire } from 'module'; + +const require = createRequire(import.meta.url); declare module 'gulp-sourcemaps' { interface WriteOptions { @@ -28,7 +32,7 @@ declare module 'gulp-sourcemaps' { } } -const REPO_ROOT_PATH = path.join(__dirname, '../..'); +const REPO_ROOT_PATH = path.join(import.meta.dirname, '../..'); export interface IBundleESMTaskOpts { /** @@ -148,7 +152,7 @@ function bundleESMTask(opts: IBundleESMTaskOpts): NodeJS.ReadWriteStream { '.sh': 'file', }, assetNames: 'media/[name]', // moves media assets into a sub-folder "media" - banner: entryPoint.name === 'vs/workbench/workbench.web.main' ? undefined : banner, // TODO@esm remove line when we stop supporting web-amd-esm-bridge + banner, entryPoints: [ { in: path.join(REPO_ROOT_PATH, opts.src, `${entryPoint.name}.js`), @@ -205,7 +209,7 @@ function bundleESMTask(opts: IBundleESMTaskOpts): NodeJS.ReadWriteStream { })); } -export interface IBundleESMTaskOpts { +export interface IBundleTaskOpts { /** * Destination folder for the bundled files. */ @@ -216,7 +220,7 @@ export interface IBundleESMTaskOpts { esm: IBundleESMTaskOpts; } -export function bundleTask(opts: IBundleESMTaskOpts): () => NodeJS.ReadWriteStream { +export function bundleTask(opts: IBundleTaskOpts): () => NodeJS.ReadWriteStream { return function () { return bundleESMTask(opts.esm).pipe(gulp.dest(opts.out)); }; @@ -227,7 +231,6 @@ export function minifyTask(src: string, sourceMapBaseUrl?: string): (cb: any) => const target = getBuildTarget(); return cb => { - const svgmin = require('gulp-svgmin') as typeof import('gulp-svgmin'); const esbuildFilter = filter('**/*.{js,css}', { restore: true }); const svgFilter = filter('**/*.svg', { restore: true }); diff --git a/build/lib/policies/basePolicy.js b/build/lib/policies/basePolicy.js deleted file mode 100644 index 5c1b919d428..00000000000 --- a/build/lib/policies/basePolicy.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BasePolicy = void 0; -const render_1 = require("./render"); -class BasePolicy { - type; - name; - category; - minimumVersion; - description; - moduleName; - constructor(type, name, category, minimumVersion, description, moduleName) { - this.type = type; - this.name = name; - this.category = category; - this.minimumVersion = minimumVersion; - this.description = description; - this.moduleName = moduleName; - } - renderADMLString(nlsString, translations) { - return (0, render_1.renderADMLString)(this.name, this.moduleName, nlsString, translations); - } - renderADMX(regKey) { - return [ - ``, - ` `, - ` `, - ` `, - ...this.renderADMXElements(), - ` `, - `` - ]; - } - renderADMLStrings(translations) { - return [ - `${this.name}`, - this.renderADMLString(this.description, translations) - ]; - } - renderADMLPresentation() { - return `${this.renderADMLPresentationContents()}`; - } - renderProfile() { - return [`${this.name}`, this.renderProfileValue()]; - } - renderProfileManifest(translations) { - return ` -${this.renderProfileManifestValue(translations)} -`; - } -} -exports.BasePolicy = BasePolicy; -//# sourceMappingURL=basePolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/basePolicy.ts b/build/lib/policies/basePolicy.ts index f0477d244f0..7f650ba7b2e 100644 --- a/build/lib/policies/basePolicy.ts +++ b/build/lib/policies/basePolicy.ts @@ -3,18 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { renderADMLString } from './render'; -import { Category, LanguageTranslations, NlsString, Policy, PolicyType } from './types'; +import { renderADMLString } from './render.ts'; +import type { Category, LanguageTranslations, NlsString, Policy, PolicyType } from './types.ts'; export abstract class BasePolicy implements Policy { + readonly type: PolicyType; + readonly name: string; + readonly category: Category; + readonly minimumVersion: string; + protected description: NlsString; + protected moduleName: string; + constructor( - readonly type: PolicyType, - readonly name: string, - readonly category: Category, - readonly minimumVersion: string, - protected description: NlsString, - protected moduleName: string, - ) { } + type: PolicyType, + name: string, + category: Category, + minimumVersion: string, + description: NlsString, + moduleName: string, + ) { + this.type = type; + this.name = name; + this.category = category; + this.minimumVersion = minimumVersion; + this.description = description; + this.moduleName = moduleName; + } protected renderADMLString(nlsString: NlsString, translations?: LanguageTranslations): string { return renderADMLString(this.name, this.moduleName, nlsString, translations); diff --git a/build/lib/policies/booleanPolicy.js b/build/lib/policies/booleanPolicy.js deleted file mode 100644 index 77ea3d9a42e..00000000000 --- a/build/lib/policies/booleanPolicy.js +++ /dev/null @@ -1,52 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.BooleanPolicy = void 0; -const basePolicy_1 = require("./basePolicy"); -const render_1 = require("./render"); -const types_1 = require("./types"); -class BooleanPolicy extends basePolicy_1.BasePolicy { - static from(category, policy) { - const { name, minimumVersion, localization, type } = policy; - if (type !== 'boolean') { - return undefined; - } - return new BooleanPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, ''); - } - constructor(name, category, minimumVersion, description, moduleName) { - super(types_1.PolicyType.Boolean, name, category, minimumVersion, description, moduleName); - } - renderADMXElements() { - return [ - ``, - ` `, - `` - ]; - } - renderADMLPresentationContents() { - return `${this.name}`; - } - renderJsonValue() { - return false; - } - renderProfileValue() { - return ``; - } - renderProfileManifestValue(translations) { - return `pfm_default - -pfm_description -${(0, render_1.renderProfileString)(this.name, this.moduleName, this.description, translations)} -pfm_name -${this.name} -pfm_title -${this.name} -pfm_type -boolean`; - } -} -exports.BooleanPolicy = BooleanPolicy; -//# sourceMappingURL=booleanPolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/booleanPolicy.ts b/build/lib/policies/booleanPolicy.ts index 538140b3db2..59e2402eb3c 100644 --- a/build/lib/policies/booleanPolicy.ts +++ b/build/lib/policies/booleanPolicy.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePolicy } from './basePolicy'; -import { CategoryDto, PolicyDto } from './policyDto'; -import { renderProfileString } from './render'; -import { Category, NlsString, PolicyType, LanguageTranslations } from './types'; +import { BasePolicy } from './basePolicy.ts'; +import type { CategoryDto, PolicyDto } from './policyDto.ts'; +import { renderProfileString } from './render.ts'; +import { type Category, type NlsString, PolicyType, type LanguageTranslations } from './types.ts'; export class BooleanPolicy extends BasePolicy { diff --git a/build/lib/policies/copyPolicyDto.js b/build/lib/policies/copyPolicyDto.js deleted file mode 100644 index a223bb4c0ef..00000000000 --- a/build/lib/policies/copyPolicyDto.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); -const sourceFile = path.join(__dirname, '../../../src/vs/workbench/contrib/policyExport/common/policyDto.ts'); -const destFile = path.join(__dirname, 'policyDto.ts'); -try { - // Check if source file exists - if (!fs.existsSync(sourceFile)) { - console.error(`Error: Source file not found: ${sourceFile}`); - console.error('Please ensure policyDto.ts exists in src/vs/workbench/contrib/policyExport/common/'); - process.exit(1); - } - // Copy the file - fs.copyFileSync(sourceFile, destFile); -} -catch (error) { - console.error(`Error copying policyDto.ts: ${error.message}`); - process.exit(1); -} -//# sourceMappingURL=copyPolicyDto.js.map \ No newline at end of file diff --git a/build/lib/policies/copyPolicyDto.ts b/build/lib/policies/copyPolicyDto.ts index 4fb74456837..6bf8cd88802 100644 --- a/build/lib/policies/copyPolicyDto.ts +++ b/build/lib/policies/copyPolicyDto.ts @@ -6,8 +6,8 @@ import * as fs from 'fs'; import * as path from 'path'; -const sourceFile = path.join(__dirname, '../../../src/vs/workbench/contrib/policyExport/common/policyDto.ts'); -const destFile = path.join(__dirname, 'policyDto.ts'); +const sourceFile = path.join(import.meta.dirname, '../../../src/vs/workbench/contrib/policyExport/common/policyDto.ts'); +const destFile = path.join(import.meta.dirname, 'policyDto.ts'); try { // Check if source file exists diff --git a/build/lib/policies/numberPolicy.js b/build/lib/policies/numberPolicy.js deleted file mode 100644 index 3bc0b98d19a..00000000000 --- a/build/lib/policies/numberPolicy.js +++ /dev/null @@ -1,56 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.NumberPolicy = void 0; -const basePolicy_1 = require("./basePolicy"); -const render_1 = require("./render"); -const types_1 = require("./types"); -class NumberPolicy extends basePolicy_1.BasePolicy { - defaultValue; - static from(category, policy) { - const { type, default: defaultValue, name, minimumVersion, localization } = policy; - if (type !== 'number') { - return undefined; - } - if (typeof defaultValue !== 'number') { - throw new Error(`Missing required 'default' property.`); - } - return new NumberPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, '', defaultValue); - } - constructor(name, category, minimumVersion, description, moduleName, defaultValue) { - super(types_1.PolicyType.Number, name, category, minimumVersion, description, moduleName); - this.defaultValue = defaultValue; - } - renderADMXElements() { - return [ - `` - // `` - ]; - } - renderADMLPresentationContents() { - return `${this.name}`; - } - renderJsonValue() { - return this.defaultValue; - } - renderProfileValue() { - return `${this.defaultValue}`; - } - renderProfileManifestValue(translations) { - return `pfm_default -${this.defaultValue} -pfm_description -${(0, render_1.renderProfileString)(this.name, this.moduleName, this.description, translations)} -pfm_name -${this.name} -pfm_title -${this.name} -pfm_type -integer`; - } -} -exports.NumberPolicy = NumberPolicy; -//# sourceMappingURL=numberPolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/numberPolicy.ts b/build/lib/policies/numberPolicy.ts index db4143e1f7f..3091e004677 100644 --- a/build/lib/policies/numberPolicy.ts +++ b/build/lib/policies/numberPolicy.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePolicy } from './basePolicy'; -import { CategoryDto, PolicyDto } from './policyDto'; -import { renderProfileString } from './render'; -import { Category, NlsString, PolicyType, LanguageTranslations } from './types'; +import { BasePolicy } from './basePolicy.ts'; +import type { CategoryDto, PolicyDto } from './policyDto.ts'; +import { renderProfileString } from './render.ts'; +import { type Category, type NlsString, PolicyType, type LanguageTranslations } from './types.ts'; export class NumberPolicy extends BasePolicy { @@ -24,15 +24,18 @@ export class NumberPolicy extends BasePolicy { return new NumberPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, '', defaultValue); } + protected readonly defaultValue: number; + private constructor( name: string, category: Category, minimumVersion: string, description: NlsString, moduleName: string, - protected readonly defaultValue: number, + defaultValue: number, ) { super(PolicyType.Number, name, category, minimumVersion, description, moduleName); + this.defaultValue = defaultValue; } protected renderADMXElements(): string[] { diff --git a/build/lib/policies/objectPolicy.js b/build/lib/policies/objectPolicy.js deleted file mode 100644 index 43a7aaa3fc9..00000000000 --- a/build/lib/policies/objectPolicy.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ObjectPolicy = void 0; -const basePolicy_1 = require("./basePolicy"); -const render_1 = require("./render"); -const types_1 = require("./types"); -class ObjectPolicy extends basePolicy_1.BasePolicy { - static from(category, policy) { - const { type, name, minimumVersion, localization } = policy; - if (type !== 'object' && type !== 'array') { - return undefined; - } - return new ObjectPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, ''); - } - constructor(name, category, minimumVersion, description, moduleName) { - super(types_1.PolicyType.Object, name, category, minimumVersion, description, moduleName); - } - renderADMXElements() { - return [``]; - } - renderADMLPresentationContents() { - return ``; - } - renderJsonValue() { - return ''; - } - renderProfileValue() { - return ``; - } - renderProfileManifestValue(translations) { - return `pfm_default - -pfm_description -${(0, render_1.renderProfileString)(this.name, this.moduleName, this.description, translations)} -pfm_name -${this.name} -pfm_title -${this.name} -pfm_type -string -`; - } -} -exports.ObjectPolicy = ObjectPolicy; -//# sourceMappingURL=objectPolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/objectPolicy.ts b/build/lib/policies/objectPolicy.ts index 3bbc916636f..b565b06e8bb 100644 --- a/build/lib/policies/objectPolicy.ts +++ b/build/lib/policies/objectPolicy.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePolicy } from './basePolicy'; -import { CategoryDto, PolicyDto } from './policyDto'; -import { renderProfileString } from './render'; -import { Category, NlsString, PolicyType, LanguageTranslations } from './types'; +import { BasePolicy } from './basePolicy.ts'; +import type { CategoryDto, PolicyDto } from './policyDto.ts'; +import { renderProfileString } from './render.ts'; +import { type Category, type NlsString, PolicyType, type LanguageTranslations } from './types.ts'; export class ObjectPolicy extends BasePolicy { diff --git a/build/lib/policies/policyData.jsonc b/build/lib/policies/policyData.jsonc index 3b8f1cdf0f5..53387c4fa34 100644 --- a/build/lib/policies/policyData.jsonc +++ b/build/lib/policies/policyData.jsonc @@ -74,7 +74,7 @@ "localization": { "description": { "key": "extensions.allowed.policy", - "value": "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. More information: https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions" + "value": "Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. More information: https://aka.ms/vscode/enterprise/extensions/allowed" } }, "type": "object", @@ -94,6 +94,20 @@ "type": "boolean", "default": false }, + { + "key": "chat.tools.eligibleForAutoApproval", + "name": "ChatToolsEligibleForAutoApproval", + "category": "InteractiveSession", + "minimumVersion": "1.107", + "localization": { + "description": { + "key": "chat.tools.eligibleForAutoApproval", + "value": "Controls which tools are eligible for automatic approval. Tools set to 'false' will always present a confirmation and will never offer the option to auto-approve. The default behavior (or setting a tool to 'true') may result in the tool offering auto-approval options." + } + }, + "type": "object", + "default": {} + }, { "key": "chat.mcp.access", "name": "ChatMCP", @@ -149,7 +163,7 @@ "localization": { "description": { "key": "chat.agent.enabled.description", - "value": "Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view." + "value": "When enabled, agent mode can be activated from chat and tools in agentic contexts with side effects can be used." } }, "type": "boolean", diff --git a/build/lib/policies/policyGenerator.js b/build/lib/policies/policyGenerator.js deleted file mode 100644 index 132e55873da..00000000000 --- a/build/lib/policies/policyGenerator.js +++ /dev/null @@ -1,243 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const minimist_1 = __importDefault(require("minimist")); -const fs_1 = require("fs"); -const path_1 = __importDefault(require("path")); -const JSONC = __importStar(require("jsonc-parser")); -const booleanPolicy_1 = require("./booleanPolicy"); -const numberPolicy_1 = require("./numberPolicy"); -const objectPolicy_1 = require("./objectPolicy"); -const stringEnumPolicy_1 = require("./stringEnumPolicy"); -const stringPolicy_1 = require("./stringPolicy"); -const types_1 = require("./types"); -const render_1 = require("./render"); -const product = require('../../../product.json'); -const packageJson = require('../../../package.json'); -async function getSpecificNLS(resourceUrlTemplate, languageId, version) { - const resource = { - publisher: 'ms-ceintl', - name: `vscode-language-pack-${languageId}`, - version: `${version[0]}.${version[1]}.${version[2]}`, - path: 'extension/translations/main.i18n.json' - }; - const url = resourceUrlTemplate.replace(/\{([^}]+)\}/g, (_, key) => resource[key]); - const res = await fetch(url); - if (res.status !== 200) { - throw new Error(`[${res.status}] Error downloading language pack ${languageId}@${version}`); - } - const { contents: result } = await res.json(); - // TODO: support module namespacing - // Flatten all moduleName keys to empty string - const flattened = { '': {} }; - for (const moduleName in result) { - for (const nlsKey in result[moduleName]) { - flattened[''][nlsKey] = result[moduleName][nlsKey]; - } - } - return flattened; -} -function parseVersion(version) { - const [, major, minor, patch] = /^(\d+)\.(\d+)\.(\d+)/.exec(version); - return [parseInt(major), parseInt(minor), parseInt(patch)]; -} -function compareVersions(a, b) { - if (a[0] !== b[0]) { - return a[0] - b[0]; - } - if (a[1] !== b[1]) { - return a[1] - b[1]; - } - return a[2] - b[2]; -} -async function queryVersions(serviceUrl, languageId) { - const res = await fetch(`${serviceUrl}/extensionquery`, { - method: 'POST', - headers: { - 'Accept': 'application/json;api-version=3.0-preview.1', - 'Content-Type': 'application/json', - 'User-Agent': 'VS Code Build', - }, - body: JSON.stringify({ - filters: [{ criteria: [{ filterType: 7, value: `ms-ceintl.vscode-language-pack-${languageId}` }] }], - flags: 0x1 - }) - }); - if (res.status !== 200) { - throw new Error(`[${res.status}] Error querying for extension: ${languageId}`); - } - const result = await res.json(); - return result.results[0].extensions[0].versions.map(v => parseVersion(v.version)).sort(compareVersions); -} -async function getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) { - const versions = await queryVersions(extensionGalleryServiceUrl, languageId); - const nextMinor = [version[0], version[1] + 1, 0]; - const compatibleVersions = versions.filter(v => compareVersions(v, nextMinor) < 0); - const latestCompatibleVersion = compatibleVersions.at(-1); // order is newest to oldest - if (!latestCompatibleVersion) { - throw new Error(`No compatible language pack found for ${languageId} for version ${version}`); - } - return await getSpecificNLS(resourceUrlTemplate, languageId, latestCompatibleVersion); -} -// TODO: add more policy types -const PolicyTypes = [ - booleanPolicy_1.BooleanPolicy, - numberPolicy_1.NumberPolicy, - stringEnumPolicy_1.StringEnumPolicy, - stringPolicy_1.StringPolicy, - objectPolicy_1.ObjectPolicy -]; -async function parsePolicies(policyDataFile) { - const contents = JSONC.parse(await fs_1.promises.readFile(policyDataFile, { encoding: 'utf8' })); - const categories = new Map(); - for (const category of contents.categories) { - categories.set(category.key, category); - } - const policies = []; - for (const policy of contents.policies) { - const category = categories.get(policy.category); - if (!category) { - throw new Error(`Unknown category: ${policy.category}`); - } - let result; - for (const policyType of PolicyTypes) { - if (result = policyType.from(category, policy)) { - break; - } - } - if (!result) { - throw new Error(`Unsupported policy type: ${policy.type} for policy ${policy.name}`); - } - policies.push(result); - } - // Sort policies first by category name, then by policy name - policies.sort((a, b) => { - const categoryCompare = a.category.name.value.localeCompare(b.category.name.value); - if (categoryCompare !== 0) { - return categoryCompare; - } - return a.name.localeCompare(b.name); - }); - return policies; -} -async function getTranslations() { - const extensionGalleryServiceUrl = product.extensionsGallery?.serviceUrl; - if (!extensionGalleryServiceUrl) { - console.warn(`Skipping policy localization: No 'extensionGallery.serviceUrl' found in 'product.json'.`); - return []; - } - const resourceUrlTemplate = product.extensionsGallery?.resourceUrlTemplate; - if (!resourceUrlTemplate) { - console.warn(`Skipping policy localization: No 'resourceUrlTemplate' found in 'product.json'.`); - return []; - } - const version = parseVersion(packageJson.version); - const languageIds = Object.keys(types_1.Languages); - return await Promise.all(languageIds.map(languageId => getNLS(extensionGalleryServiceUrl, resourceUrlTemplate, languageId, version) - .then(languageTranslations => ({ languageId, languageTranslations })))); -} -async function windowsMain(policies, translations) { - const root = '.build/policies/win32'; - const { admx, adml } = (0, render_1.renderGP)(product, policies, translations); - await fs_1.promises.rm(root, { recursive: true, force: true }); - await fs_1.promises.mkdir(root, { recursive: true }); - await fs_1.promises.writeFile(path_1.default.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); - for (const { languageId, contents } of adml) { - const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : types_1.Languages[languageId]); - await fs_1.promises.mkdir(languagePath, { recursive: true }); - await fs_1.promises.writeFile(path_1.default.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); - } -} -async function darwinMain(policies, translations) { - const bundleIdentifier = product.darwinBundleIdentifier; - if (!bundleIdentifier || !product.darwinProfilePayloadUUID || !product.darwinProfileUUID) { - throw new Error(`Missing required product information.`); - } - const root = '.build/policies/darwin'; - const { profile, manifests } = (0, render_1.renderMacOSPolicy)(product, policies, translations); - await fs_1.promises.rm(root, { recursive: true, force: true }); - await fs_1.promises.mkdir(root, { recursive: true }); - await fs_1.promises.writeFile(path_1.default.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); - for (const { languageId, contents } of manifests) { - const languagePath = path_1.default.join(root, languageId === 'en-us' ? 'en-us' : types_1.Languages[languageId]); - await fs_1.promises.mkdir(languagePath, { recursive: true }); - await fs_1.promises.writeFile(path_1.default.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); - } -} -async function linuxMain(policies) { - const root = '.build/policies/linux'; - const policyFileContents = JSON.stringify((0, render_1.renderJsonPolicies)(policies), undefined, 4); - await fs_1.promises.rm(root, { recursive: true, force: true }); - await fs_1.promises.mkdir(root, { recursive: true }); - const jsonPath = path_1.default.join(root, `policy.json`); - await fs_1.promises.writeFile(jsonPath, policyFileContents.replace(/\r?\n/g, '\n')); -} -async function main() { - const args = (0, minimist_1.default)(process.argv.slice(2)); - if (args._.length !== 2) { - console.error(`Usage: node build/lib/policies `); - process.exit(1); - } - const policyDataFile = args._[0]; - const platform = args._[1]; - const [policies, translations] = await Promise.all([parsePolicies(policyDataFile), getTranslations()]); - if (platform === 'darwin') { - await darwinMain(policies, translations); - } - else if (platform === 'win32') { - await windowsMain(policies, translations); - } - else if (platform === 'linux') { - await linuxMain(policies); - } - else { - console.error(`Usage: node build/lib/policies `); - process.exit(1); - } -} -if (require.main === module) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=policyGenerator.js.map \ No newline at end of file diff --git a/build/lib/policies/policyGenerator.ts b/build/lib/policies/policyGenerator.ts index 50ea96b1280..e0de81f4d32 100644 --- a/build/lib/policies/policyGenerator.ts +++ b/build/lib/policies/policyGenerator.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import minimist from 'minimist'; -import { promises as fs } from 'fs'; +import * as fs from 'fs'; import path from 'path'; -import { CategoryDto, ExportedPolicyDataDto } from './policyDto'; +import { type CategoryDto, type ExportedPolicyDataDto } from './policyDto.ts'; import * as JSONC from 'jsonc-parser'; -import { BooleanPolicy } from './booleanPolicy'; -import { NumberPolicy } from './numberPolicy'; -import { ObjectPolicy } from './objectPolicy'; -import { StringEnumPolicy } from './stringEnumPolicy'; -import { StringPolicy } from './stringPolicy'; -import { Version, LanguageTranslations, Policy, Translations, Languages, ProductJson } from './types'; -import { renderGP, renderJsonPolicies, renderMacOSPolicy } from './render'; +import { BooleanPolicy } from './booleanPolicy.ts'; +import { NumberPolicy } from './numberPolicy.ts'; +import { ObjectPolicy } from './objectPolicy.ts'; +import { StringEnumPolicy } from './stringEnumPolicy.ts'; +import { StringPolicy } from './stringPolicy.ts'; +import { type Version, type LanguageTranslations, type Policy, type Translations, Languages, type ProductJson } from './types.ts'; +import { renderGP, renderJsonPolicies, renderMacOSPolicy } from './render.ts'; -const product = require('../../../product.json') as ProductJson; -const packageJson = require('../../../package.json'); +const product: ProductJson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../product.json'), 'utf8')); +const packageJson = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../../../package.json'), 'utf8')); async function getSpecificNLS(resourceUrlTemplate: string, languageId: string, version: Version): Promise { const resource = { @@ -104,7 +104,7 @@ const PolicyTypes = [ ]; async function parsePolicies(policyDataFile: string): Promise { - const contents = JSONC.parse(await fs.readFile(policyDataFile, { encoding: 'utf8' })) as ExportedPolicyDataDto; + const contents = JSONC.parse(await fs.promises.readFile(policyDataFile, { encoding: 'utf8' })) as ExportedPolicyDataDto; const categories = new Map(); for (const category of contents.categories) { categories.set(category.key, category); @@ -171,15 +171,15 @@ async function windowsMain(policies: Policy[], translations: Translations) { const root = '.build/policies/win32'; const { admx, adml } = renderGP(product, policies, translations); - await fs.rm(root, { recursive: true, force: true }); - await fs.mkdir(root, { recursive: true }); + await fs.promises.rm(root, { recursive: true, force: true }); + await fs.promises.mkdir(root, { recursive: true }); - await fs.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); + await fs.promises.writeFile(path.join(root, `${product.win32RegValueName}.admx`), admx.replace(/\r?\n/g, '\n')); for (const { languageId, contents } of adml) { const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]); - await fs.mkdir(languagePath, { recursive: true }); - await fs.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); + await fs.promises.mkdir(languagePath, { recursive: true }); + await fs.promises.writeFile(path.join(languagePath, `${product.win32RegValueName}.adml`), contents.replace(/\r?\n/g, '\n')); } } @@ -191,14 +191,14 @@ async function darwinMain(policies: Policy[], translations: Translations) { const root = '.build/policies/darwin'; const { profile, manifests } = renderMacOSPolicy(product, policies, translations); - await fs.rm(root, { recursive: true, force: true }); - await fs.mkdir(root, { recursive: true }); - await fs.writeFile(path.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); + await fs.promises.rm(root, { recursive: true, force: true }); + await fs.promises.mkdir(root, { recursive: true }); + await fs.promises.writeFile(path.join(root, `${bundleIdentifier}.mobileconfig`), profile.replace(/\r?\n/g, '\n')); for (const { languageId, contents } of manifests) { const languagePath = path.join(root, languageId === 'en-us' ? 'en-us' : Languages[languageId as keyof typeof Languages]); - await fs.mkdir(languagePath, { recursive: true }); - await fs.writeFile(path.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); + await fs.promises.mkdir(languagePath, { recursive: true }); + await fs.promises.writeFile(path.join(languagePath, `${bundleIdentifier}.plist`), contents.replace(/\r?\n/g, '\n')); } } @@ -206,11 +206,11 @@ async function linuxMain(policies: Policy[]) { const root = '.build/policies/linux'; const policyFileContents = JSON.stringify(renderJsonPolicies(policies), undefined, 4); - await fs.rm(root, { recursive: true, force: true }); - await fs.mkdir(root, { recursive: true }); + await fs.promises.rm(root, { recursive: true, force: true }); + await fs.promises.mkdir(root, { recursive: true }); const jsonPath = path.join(root, `policy.json`); - await fs.writeFile(jsonPath, policyFileContents.replace(/\r?\n/g, '\n')); + await fs.promises.writeFile(jsonPath, policyFileContents.replace(/\r?\n/g, '\n')); } async function main() { @@ -236,7 +236,7 @@ async function main() { } } -if (require.main === module) { +if (import.meta.main) { main().catch(err => { console.error(err); process.exit(1); diff --git a/build/lib/policies/render.js b/build/lib/policies/render.js deleted file mode 100644 index 8661dab9154..00000000000 --- a/build/lib/policies/render.js +++ /dev/null @@ -1,283 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.renderADMLString = renderADMLString; -exports.renderProfileString = renderProfileString; -exports.renderADMX = renderADMX; -exports.renderADML = renderADML; -exports.renderProfileManifest = renderProfileManifest; -exports.renderMacOSPolicy = renderMacOSPolicy; -exports.renderGP = renderGP; -exports.renderJsonPolicies = renderJsonPolicies; -function renderADMLString(prefix, moduleName, nlsString, translations) { - let value; - if (translations) { - const moduleTranslations = translations[moduleName]; - if (moduleTranslations) { - value = moduleTranslations[nlsString.nlsKey]; - } - } - if (!value) { - value = nlsString.value; - } - return `${value}`; -} -function renderProfileString(_prefix, moduleName, nlsString, translations) { - let value; - if (translations) { - const moduleTranslations = translations[moduleName]; - if (moduleTranslations) { - value = moduleTranslations[nlsString.nlsKey]; - } - } - if (!value) { - value = nlsString.value; - } - return value; -} -function renderADMX(regKey, versions, categories, policies) { - versions = versions.map(v => v.replace(/\./g, '_')); - return ` - - - - - - - - ${versions.map(v => ``).join(`\n `)} - - - - - ${categories.map(c => ``).join(`\n `)} - - - ${policies.map(p => p.renderADMX(regKey)).flat().join(`\n `)} - - -`; -} -function renderADML(appName, versions, categories, policies, translations) { - return ` - - - - - - ${appName} - ${versions.map(v => `${appName} >= ${v}`).join(`\n `)} - ${categories.map(c => renderADMLString('Category', c.moduleName, c.name, translations)).join(`\n `)} - ${policies.map(p => p.renderADMLStrings(translations)).flat().join(`\n `)} - - - ${policies.map(p => p.renderADMLPresentation()).join(`\n `)} - - - -`; -} -function renderProfileManifest(appName, bundleIdentifier, _versions, _categories, policies, translations) { - const requiredPayloadFields = ` - - pfm_default - Configure ${appName} - pfm_name - PayloadDescription - pfm_title - Payload Description - pfm_type - string - - - pfm_default - ${appName} - pfm_name - PayloadDisplayName - pfm_require - always - pfm_title - Payload Display Name - pfm_type - string - - - pfm_default - ${bundleIdentifier} - pfm_name - PayloadIdentifier - pfm_require - always - pfm_title - Payload Identifier - pfm_type - string - - - pfm_default - ${bundleIdentifier} - pfm_name - PayloadType - pfm_require - always - pfm_title - Payload Type - pfm_type - string - - - pfm_default - - pfm_name - PayloadUUID - pfm_require - always - pfm_title - Payload UUID - pfm_type - string - - - pfm_default - 1 - pfm_name - PayloadVersion - pfm_range_list - - 1 - - pfm_require - always - pfm_title - Payload Version - pfm_type - integer - - - pfm_default - Microsoft - pfm_name - PayloadOrganization - pfm_title - Payload Organization - pfm_type - string - `; - const profileManifestSubkeys = policies.map(policy => { - return policy.renderProfileManifest(translations); - }).join(''); - return ` - - - - pfm_app_url - https://code.visualstudio.com/ - pfm_description - ${appName} Managed Settings - pfm_documentation_url - https://code.visualstudio.com/docs/setup/enterprise - pfm_domain - ${bundleIdentifier} - pfm_format_version - 1 - pfm_interaction - combined - pfm_last_modified - ${new Date().toISOString().replace(/\.\d+Z$/, 'Z')} - pfm_platforms - - macOS - - pfm_subkeys - - ${requiredPayloadFields} - ${profileManifestSubkeys} - - pfm_title - ${appName} - pfm_unique - - pfm_version - 1 - -`; -} -function renderMacOSPolicy(product, policies, translations) { - const appName = product.nameLong; - const bundleIdentifier = product.darwinBundleIdentifier; - const payloadUUID = product.darwinProfilePayloadUUID; - const UUID = product.darwinProfileUUID; - const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); - const categories = [...new Set(policies.map(p => p.category))]; - const policyEntries = policies.map(policy => policy.renderProfile()) - .flat() - .map(entry => `\t\t\t\t${entry}`) - .join('\n'); - return { - profile: ` - - - - PayloadContent - - - PayloadDisplayName - ${appName} - PayloadIdentifier - ${bundleIdentifier}.${UUID} - PayloadType - ${bundleIdentifier} - PayloadUUID - ${UUID} - PayloadVersion - 1 -${policyEntries} - - - PayloadDescription - This profile manages ${appName}. For more information see https://code.visualstudio.com/docs/setup/enterprise - PayloadDisplayName - ${appName} - PayloadIdentifier - ${bundleIdentifier} - PayloadOrganization - Microsoft - PayloadType - Configuration - PayloadUUID - ${payloadUUID} - PayloadVersion - 1 - TargetDeviceType - 5 - -`, - manifests: [{ languageId: 'en-us', contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies) }, - ...translations.map(({ languageId, languageTranslations }) => ({ languageId, contents: renderProfileManifest(appName, bundleIdentifier, versions, categories, policies, languageTranslations) })) - ] - }; -} -function renderGP(product, policies, translations) { - const appName = product.nameLong; - const regKey = product.win32RegValueName; - const versions = [...new Set(policies.map(p => p.minimumVersion)).values()].sort(); - const categories = [...Object.values(policies.reduce((acc, p) => ({ ...acc, [p.category.name.nlsKey]: p.category }), {}))]; - return { - admx: renderADMX(regKey, versions, categories, policies), - adml: [ - { languageId: 'en-us', contents: renderADML(appName, versions, categories, policies) }, - ...translations.map(({ languageId, languageTranslations }) => ({ languageId, contents: renderADML(appName, versions, categories, policies, languageTranslations) })) - ] - }; -} -function renderJsonPolicies(policies) { - const policyObject = {}; - for (const policy of policies) { - policyObject[policy.name] = policy.renderJsonValue(); - } - return policyObject; -} -//# sourceMappingURL=render.js.map \ No newline at end of file diff --git a/build/lib/policies/render.ts b/build/lib/policies/render.ts index 8aa4181753d..47b485d1bf0 100644 --- a/build/lib/policies/render.ts +++ b/build/lib/policies/render.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { NlsString, LanguageTranslations, Category, Policy, Translations, ProductJson } from './types'; +import type { NlsString, LanguageTranslations, Category, Policy, Translations, ProductJson } from './types.ts'; export function renderADMLString(prefix: string, moduleName: string, nlsString: NlsString, translations?: LanguageTranslations): string { let value: string | undefined; diff --git a/build/lib/policies/stringEnumPolicy.js b/build/lib/policies/stringEnumPolicy.js deleted file mode 100644 index 20403b3590a..00000000000 --- a/build/lib/policies/stringEnumPolicy.js +++ /dev/null @@ -1,74 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StringEnumPolicy = void 0; -const basePolicy_1 = require("./basePolicy"); -const render_1 = require("./render"); -const types_1 = require("./types"); -class StringEnumPolicy extends basePolicy_1.BasePolicy { - enum_; - enumDescriptions; - static from(category, policy) { - const { type, name, minimumVersion, enum: enumValue, localization } = policy; - if (type !== 'string') { - return undefined; - } - const enum_ = enumValue; - if (!enum_) { - return undefined; - } - if (!localization.enumDescriptions || !Array.isArray(localization.enumDescriptions) || localization.enumDescriptions.length !== enum_.length) { - throw new Error(`Invalid policy data: enumDescriptions must exist and have the same length as enum_ for policy "${name}".`); - } - const enumDescriptions = localization.enumDescriptions.map((e) => ({ nlsKey: e.key, value: e.value })); - return new StringEnumPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, '', enum_, enumDescriptions); - } - constructor(name, category, minimumVersion, description, moduleName, enum_, enumDescriptions) { - super(types_1.PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); - this.enum_ = enum_; - this.enumDescriptions = enumDescriptions; - } - renderADMXElements() { - return [ - ``, - ...this.enum_.map((value, index) => ` ${value}`), - `` - ]; - } - renderADMLStrings(translations) { - return [ - ...super.renderADMLStrings(translations), - ...this.enumDescriptions.map(e => this.renderADMLString(e, translations)) - ]; - } - renderADMLPresentationContents() { - return ``; - } - renderJsonValue() { - return this.enum_[0]; - } - renderProfileValue() { - return `${this.enum_[0]}`; - } - renderProfileManifestValue(translations) { - return `pfm_default -${this.enum_[0]} -pfm_description -${(0, render_1.renderProfileString)(this.name, this.moduleName, this.description, translations)} -pfm_name -${this.name} -pfm_title -${this.name} -pfm_type -string -pfm_range_list - - ${this.enum_.map(e => `${e}`).join('\n ')} -`; - } -} -exports.StringEnumPolicy = StringEnumPolicy; -//# sourceMappingURL=stringEnumPolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/stringEnumPolicy.ts b/build/lib/policies/stringEnumPolicy.ts index c4adabdace7..b590abcc87b 100644 --- a/build/lib/policies/stringEnumPolicy.ts +++ b/build/lib/policies/stringEnumPolicy.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePolicy } from './basePolicy'; -import { CategoryDto, PolicyDto } from './policyDto'; -import { renderProfileString } from './render'; -import { Category, NlsString, PolicyType, LanguageTranslations } from './types'; +import { BasePolicy } from './basePolicy.ts'; +import type { CategoryDto, PolicyDto } from './policyDto.ts'; +import { renderProfileString } from './render.ts'; +import { type Category, type NlsString, PolicyType, type LanguageTranslations } from './types.ts'; export class StringEnumPolicy extends BasePolicy { @@ -38,22 +38,27 @@ export class StringEnumPolicy extends BasePolicy { ); } + protected enum_: string[]; + protected enumDescriptions: NlsString[]; + private constructor( name: string, category: Category, minimumVersion: string, description: NlsString, moduleName: string, - protected enum_: string[], - protected enumDescriptions: NlsString[], + enum_: string[], + enumDescriptions: NlsString[], ) { super(PolicyType.StringEnum, name, category, minimumVersion, description, moduleName); + this.enum_ = enum_; + this.enumDescriptions = enumDescriptions; } protected renderADMXElements(): string[] { return [ ``, - ...this.enum_.map((value, index) => ` ${value}`), + ...this.enum_.map((value, index) => ` ${value}`), `` ]; } diff --git a/build/lib/policies/stringPolicy.js b/build/lib/policies/stringPolicy.js deleted file mode 100644 index 1db9e53649b..00000000000 --- a/build/lib/policies/stringPolicy.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.StringPolicy = void 0; -const basePolicy_1 = require("./basePolicy"); -const render_1 = require("./render"); -const types_1 = require("./types"); -class StringPolicy extends basePolicy_1.BasePolicy { - static from(category, policy) { - const { type, name, minimumVersion, localization } = policy; - if (type !== 'string') { - return undefined; - } - return new StringPolicy(name, { moduleName: '', name: { nlsKey: category.name.key, value: category.name.value } }, minimumVersion, { nlsKey: localization.description.key, value: localization.description.value }, ''); - } - constructor(name, category, minimumVersion, description, moduleName) { - super(types_1.PolicyType.String, name, category, minimumVersion, description, moduleName); - } - renderADMXElements() { - return [``]; - } - renderJsonValue() { - return ''; - } - renderADMLPresentationContents() { - return ``; - } - renderProfileValue() { - return ``; - } - renderProfileManifestValue(translations) { - return `pfm_default - -pfm_description -${(0, render_1.renderProfileString)(this.name, this.moduleName, this.description, translations)} -pfm_name -${this.name} -pfm_title -${this.name} -pfm_type -string`; - } -} -exports.StringPolicy = StringPolicy; -//# sourceMappingURL=stringPolicy.js.map \ No newline at end of file diff --git a/build/lib/policies/stringPolicy.ts b/build/lib/policies/stringPolicy.ts index e318a6165d8..e4e07e42c69 100644 --- a/build/lib/policies/stringPolicy.ts +++ b/build/lib/policies/stringPolicy.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BasePolicy } from './basePolicy'; -import { CategoryDto, PolicyDto } from './policyDto'; -import { renderProfileString } from './render'; -import { Category, NlsString, PolicyType, LanguageTranslations } from './types'; +import { BasePolicy } from './basePolicy.ts'; +import type { CategoryDto, PolicyDto } from './policyDto.ts'; +import { renderProfileString } from './render.ts'; +import { PolicyType, type Category, type LanguageTranslations, type NlsString } from './types.ts'; export class StringPolicy extends BasePolicy { diff --git a/build/lib/policies/types.js b/build/lib/policies/types.js deleted file mode 100644 index 9eab676dec5..00000000000 --- a/build/lib/policies/types.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Languages = exports.PolicyType = void 0; -var PolicyType; -(function (PolicyType) { - PolicyType["Boolean"] = "boolean"; - PolicyType["Number"] = "number"; - PolicyType["Object"] = "object"; - PolicyType["String"] = "string"; - PolicyType["StringEnum"] = "stringEnum"; -})(PolicyType || (exports.PolicyType = PolicyType = {})); -exports.Languages = { - 'fr': 'fr-fr', - 'it': 'it-it', - 'de': 'de-de', - 'es': 'es-es', - 'ru': 'ru-ru', - 'zh-hans': 'zh-cn', - 'zh-hant': 'zh-tw', - 'ja': 'ja-jp', - 'ko': 'ko-kr', - 'cs': 'cs-cz', - 'pt-br': 'pt-br', - 'tr': 'tr-tr', - 'pl': 'pl-pl', -}; -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/build/lib/policies/types.ts b/build/lib/policies/types.ts index 861b5205f69..4fe801c23d6 100644 --- a/build/lib/policies/types.ts +++ b/build/lib/policies/types.ts @@ -36,13 +36,14 @@ export interface Category { readonly name: NlsString; } -export enum PolicyType { - Boolean = 'boolean', - Number = 'number', - Object = 'object', - String = 'string', - StringEnum = 'stringEnum', -} +export const PolicyType = Object.freeze({ + Boolean: 'boolean', + Number: 'number', + Object: 'object', + String: 'string', + StringEnum: 'stringEnum', +}); +export type PolicyType = typeof PolicyType[keyof typeof PolicyType]; export const Languages = { 'fr': 'fr-fr', diff --git a/build/lib/preLaunch.js b/build/lib/preLaunch.js deleted file mode 100644 index 3ebe7b1d7a2..00000000000 --- a/build/lib/preLaunch.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -// @ts-check -const path = __importStar(require("path")); -const child_process_1 = require("child_process"); -const fs = __importStar(require("fs")); -const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const rootDir = path.resolve(__dirname, '..', '..'); -function runProcess(command, args = []) { - return new Promise((resolve, reject) => { - const child = (0, child_process_1.spawn)(command, args, { cwd: rootDir, stdio: 'inherit', env: process.env, shell: process.platform === 'win32' }); - child.on('exit', err => !err ? resolve() : process.exit(err ?? 1)); - child.on('error', reject); - }); -} -async function exists(subdir) { - try { - await fs.promises.stat(path.join(rootDir, subdir)); - return true; - } - catch { - return false; - } -} -async function ensureNodeModules() { - if (!(await exists('node_modules'))) { - await runProcess(npm, ['ci']); - } -} -async function getElectron() { - await runProcess(npm, ['run', 'electron']); -} -async function ensureCompiled() { - if (!(await exists('out'))) { - await runProcess(npm, ['run', 'compile']); - } -} -async function main() { - await ensureNodeModules(); - await getElectron(); - await ensureCompiled(); - // Can't require this until after dependencies are installed - const { getBuiltInExtensions } = require('./builtInExtensions'); - await getBuiltInExtensions(); -} -if (require.main === module) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=preLaunch.js.map \ No newline at end of file diff --git a/build/lib/preLaunch.ts b/build/lib/preLaunch.ts index 9611a7e07b4..5e175afde28 100644 --- a/build/lib/preLaunch.ts +++ b/build/lib/preLaunch.ts @@ -2,15 +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 * as path from 'path'; +import path from 'path'; import { spawn } from 'child_process'; -import * as fs from 'fs'; +import { promises as fs } from 'fs'; const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const rootDir = path.resolve(__dirname, '..', '..'); +const rootDir = path.resolve(import.meta.dirname, '..', '..'); function runProcess(command: string, args: ReadonlyArray = []) { return new Promise((resolve, reject) => { @@ -22,7 +19,7 @@ function runProcess(command: string, args: ReadonlyArray = []) { async function exists(subdir: string) { try { - await fs.promises.stat(path.join(rootDir, subdir)); + await fs.stat(path.join(rootDir, subdir)); return true; } catch { return false; @@ -51,11 +48,11 @@ async function main() { await ensureCompiled(); // Can't require this until after dependencies are installed - const { getBuiltInExtensions } = require('./builtInExtensions'); + const { getBuiltInExtensions } = await import('./builtInExtensions.ts'); await getBuiltInExtensions(); } -if (require.main === module) { +if (import.meta.main) { main().catch(err => { console.error(err); process.exit(1); diff --git a/build/lib/propertyInitOrderChecker.js b/build/lib/propertyInitOrderChecker.js deleted file mode 100644 index 58921645599..00000000000 --- a/build/lib/propertyInitOrderChecker.js +++ /dev/null @@ -1,249 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -const ts = __importStar(require("typescript")); -const path = __importStar(require("path")); -const fs = __importStar(require("fs")); -const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); -// -// ############################################################################################# -// -// A custom typescript checker that ensure constructor properties are NOT used to initialize -// defined properties. This is needed for the times when `useDefineForClassFields` is gone. -// -// see https://github.com/microsoft/vscode/issues/243049, https://github.com/microsoft/vscode/issues/186726, -// https://github.com/microsoft/vscode/pull/241544 -// -// ############################################################################################# -// -var EntryKind; -(function (EntryKind) { - EntryKind[EntryKind["Span"] = 0] = "Span"; - EntryKind[EntryKind["Node"] = 1] = "Node"; - EntryKind[EntryKind["StringLiteral"] = 2] = "StringLiteral"; - EntryKind[EntryKind["SearchedLocalFoundProperty"] = 3] = "SearchedLocalFoundProperty"; - EntryKind[EntryKind["SearchedPropertyFoundLocal"] = 4] = "SearchedPropertyFoundLocal"; -})(EntryKind || (EntryKind = {})); -const cancellationToken = { - isCancellationRequested: () => false, - throwIfCancellationRequested: () => { }, -}; -const seenFiles = new Set(); -let errorCount = 0; -function createProgram(tsconfigPath) { - const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); - const configHostParser = { fileExists: fs.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs.readFileSync(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; - const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path.resolve(path.dirname(tsconfigPath)), { noEmit: true }); - const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); - return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); -} -const program = createProgram(TS_CONFIG_PATH); -program.getTypeChecker(); -for (const file of program.getSourceFiles()) { - if (!file || file.isDeclarationFile) { - continue; - } - visit(file); -} -if (seenFiles.size) { - console.log(); - console.log(`Found ${errorCount} error${errorCount === 1 ? '' : 's'} in ${seenFiles.size} file${seenFiles.size === 1 ? '' : 's'}.`); - process.exit(errorCount); -} -function visit(node) { - if (ts.isParameter(node) && ts.isParameterPropertyDeclaration(node, node.parent)) { - checkParameterPropertyDeclaration(node); - } - ts.forEachChild(node, visit); -} -function checkParameterPropertyDeclaration(param) { - const uses = [...collectReferences(param.name, [])]; - if (!uses.length) { - return; - } - const sourceFile = param.getSourceFile(); - if (!seenFiles.has(sourceFile)) { - if (seenFiles.size) { - console.log(``); - } - console.log(`${formatFileName(param)}:`); - seenFiles.add(sourceFile); - } - else { - console.log(``); - } - console.log(` Parameter property '${param.name.getText()}' is used before its declaration.`); - for (const { stack, container } of uses) { - const use = stack[stack.length - 1]; - console.log(` at ${formatLocation(use)}: ${formatMember(container)} -> ${formatStack(stack)}`); - errorCount++; - } -} -function* collectReferences(node, stack, requiresInvocationDepth = 0, seen = new Set()) { - for (const use of findAllReferencesInClass(node)) { - const container = findContainer(use); - if (!container || seen.has(container) || ts.isConstructorDeclaration(container)) { - continue; - } - seen.add(container); - const nextStack = [...stack, use]; - let nextRequiresInvocationDepth = requiresInvocationDepth; - if (isInvocation(use) && nextRequiresInvocationDepth > 0) { - nextRequiresInvocationDepth--; - } - if (ts.isPropertyDeclaration(container) && nextRequiresInvocationDepth === 0) { - yield { stack: nextStack, container }; - } - else if (requiresInvocation(container)) { - nextRequiresInvocationDepth++; - } - yield* collectReferences(container.name ?? container, nextStack, nextRequiresInvocationDepth, seen); - } -} -function requiresInvocation(definition) { - return ts.isMethodDeclaration(definition) || ts.isFunctionDeclaration(definition) || ts.isFunctionExpression(definition) || ts.isArrowFunction(definition); -} -function isInvocation(use) { - let location = use; - if (ts.isPropertyAccessExpression(location.parent) && location.parent.name === location) { - location = location.parent; - } - else if (ts.isElementAccessExpression(location.parent) && location.parent.argumentExpression === location) { - location = location.parent; - } - return ts.isCallExpression(location.parent) && location.parent.expression === location - || ts.isTaggedTemplateExpression(location.parent) && location.parent.tag === location; -} -function formatFileName(node) { - const sourceFile = node.getSourceFile(); - return path.resolve(sourceFile.fileName); -} -function formatLocation(node) { - const sourceFile = node.getSourceFile(); - const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, node.pos); - return `${formatFileName(sourceFile)}(${line + 1},${character + 1})`; -} -function formatStack(stack) { - return stack.slice().reverse().map((use) => formatUse(use)).join(' -> '); -} -function formatMember(container) { - const name = container.name?.getText(); - if (name) { - const className = findClass(container)?.name?.getText(); - if (className) { - return `${className}.${name}`; - } - return name; - } - return ''; -} -function formatUse(use) { - let text = use.getText(); - if (use.parent && ts.isPropertyAccessExpression(use.parent) && use.parent.name === use) { - if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { - text = `this.${text}`; - } - use = use.parent; - } - else if (use.parent && ts.isElementAccessExpression(use.parent) && use.parent.argumentExpression === use) { - if (use.parent.expression.kind === ts.SyntaxKind.ThisKeyword) { - text = `this['${text}']`; - } - use = use.parent; - } - if (ts.isCallExpression(use.parent)) { - text = `${text}(...)`; - } - return text; -} -function findContainer(node) { - return ts.findAncestor(node, ancestor => { - switch (ancestor.kind) { - case ts.SyntaxKind.PropertyDeclaration: - case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.GetAccessor: - case ts.SyntaxKind.SetAccessor: - case ts.SyntaxKind.Constructor: - case ts.SyntaxKind.ClassStaticBlockDeclaration: - case ts.SyntaxKind.ArrowFunction: - case ts.SyntaxKind.FunctionExpression: - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.Parameter: - return true; - } - return false; - }); -} -function findClass(node) { - return ts.findAncestor(node, ts.isClassLike); -} -function* findAllReferencesInClass(node) { - const classDecl = findClass(node); - if (!classDecl) { - return []; - } - for (const ref of findAllReferences(node)) { - for (const entry of ref.references) { - if (entry.kind !== EntryKind.Node || entry.node === node) { - continue; - } - if (findClass(entry.node) === classDecl) { - yield entry.node; - } - } - } -} -function findAllReferences(node) { - const sourceFile = node.getSourceFile(); - const position = node.getStart(); - const tsInternal = ts; - const name = tsInternal.getTouchingPropertyName(sourceFile, position); - const options = { use: tsInternal.FindAllReferences.FindReferencesUse.References }; - return tsInternal.FindAllReferences.Core.getReferencedSymbolsForNode(position, name, program, [sourceFile], cancellationToken, options) ?? []; -} -var DefinitionKind; -(function (DefinitionKind) { - DefinitionKind[DefinitionKind["Symbol"] = 0] = "Symbol"; - DefinitionKind[DefinitionKind["Label"] = 1] = "Label"; - DefinitionKind[DefinitionKind["Keyword"] = 2] = "Keyword"; - DefinitionKind[DefinitionKind["This"] = 3] = "This"; - DefinitionKind[DefinitionKind["String"] = 4] = "String"; - DefinitionKind[DefinitionKind["TripleSlashReference"] = 5] = "TripleSlashReference"; -})(DefinitionKind || (DefinitionKind = {})); -//# sourceMappingURL=propertyInitOrderChecker.js.map \ No newline at end of file diff --git a/build/lib/propertyInitOrderChecker.ts b/build/lib/propertyInitOrderChecker.ts index eab53477e11..2c07f9c8757 100644 --- a/build/lib/propertyInitOrderChecker.ts +++ b/build/lib/propertyInitOrderChecker.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import * as path from 'path'; import * as fs from 'fs'; -const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); +const TS_CONFIG_PATH = path.join(import.meta.dirname, '../../', 'src', 'tsconfig.json'); // // ############################################################################################# @@ -22,13 +22,15 @@ const TS_CONFIG_PATH = path.join(__dirname, '../../', 'src', 'tsconfig.json'); // ############################################################################################# // -enum EntryKind { - Span, - Node, - StringLiteral, - SearchedLocalFoundProperty, - SearchedPropertyFoundLocal, -} +const EntryKind = Object.freeze({ + Span: 'Span', + Node: 'Node', + StringLiteral: 'StringLiteral', + SearchedLocalFoundProperty: 'SearchedLocalFoundProperty', + SearchedPropertyFoundLocal: 'SearchedPropertyFoundLocal' +}); + +type EntryKind = typeof EntryKind[keyof typeof EntryKind]; const cancellationToken: ts.CancellationToken = { isCancellationRequested: () => false, @@ -281,24 +283,25 @@ interface SymbolAndEntries { readonly references: readonly Entry[]; } -const enum DefinitionKind { - Symbol, - Label, - Keyword, - This, - String, - TripleSlashReference, -} +const DefinitionKind = Object.freeze({ + Symbol: 0, + Label: 1, + Keyword: 2, + This: 3, + String: 4, + TripleSlashReference: 5, +}); +type DefinitionKind = typeof DefinitionKind[keyof typeof DefinitionKind]; type Definition = - | { readonly type: DefinitionKind.Symbol; readonly symbol: ts.Symbol } - | { readonly type: DefinitionKind.Label; readonly node: ts.Identifier } - | { readonly type: DefinitionKind.Keyword; readonly node: ts.Node } - | { readonly type: DefinitionKind.This; readonly node: ts.Node } - | { readonly type: DefinitionKind.String; readonly node: ts.StringLiteralLike } - | { readonly type: DefinitionKind.TripleSlashReference; readonly reference: ts.FileReference; readonly file: ts.SourceFile }; - -type NodeEntryKind = EntryKind.Node | EntryKind.StringLiteral | EntryKind.SearchedLocalFoundProperty | EntryKind.SearchedPropertyFoundLocal; + | { readonly type: DefinitionKind; readonly symbol: ts.Symbol } + | { readonly type: DefinitionKind; readonly node: ts.Identifier } + | { readonly type: DefinitionKind; readonly node: ts.Node } + | { readonly type: DefinitionKind; readonly node: ts.Node } + | { readonly type: DefinitionKind; readonly node: ts.StringLiteralLike } + | { readonly type: DefinitionKind; readonly reference: ts.FileReference; readonly file: ts.SourceFile }; + +type NodeEntryKind = typeof EntryKind.Node | typeof EntryKind.StringLiteral | typeof EntryKind.SearchedLocalFoundProperty | typeof EntryKind.SearchedPropertyFoundLocal; type Entry = NodeEntry | SpanEntry; interface ContextWithStartAndEndNode { start: ts.Node; @@ -311,7 +314,7 @@ interface NodeEntry { readonly context?: ContextNode; } interface SpanEntry { - readonly kind: EntryKind.Span; + readonly kind: typeof EntryKind.Span; readonly fileName: string; readonly textSpan: ts.TextSpan; } diff --git a/build/lib/reporter.js b/build/lib/reporter.js deleted file mode 100644 index cb7fd272d5d..00000000000 --- a/build/lib/reporter.js +++ /dev/null @@ -1,107 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createReporter = createReporter; -const event_stream_1 = __importDefault(require("event-stream")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -class ErrorLog { - id; - constructor(id) { - this.id = id; - } - allErrors = []; - startTime = null; - count = 0; - onStart() { - if (this.count++ > 0) { - return; - } - this.startTime = new Date().getTime(); - (0, fancy_log_1.default)(`Starting ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''}...`); - } - onEnd() { - if (--this.count > 0) { - return; - } - this.log(); - } - log() { - const errors = this.allErrors.flat(); - const seen = new Set(); - errors.map(err => { - if (!seen.has(err)) { - seen.add(err); - (0, fancy_log_1.default)(`${ansi_colors_1.default.red('Error')}: ${err}`); - } - }); - (0, fancy_log_1.default)(`Finished ${ansi_colors_1.default.green('compilation')}${this.id ? ansi_colors_1.default.blue(` ${this.id}`) : ''} with ${errors.length} errors after ${ansi_colors_1.default.magenta((new Date().getTime() - this.startTime) + ' ms')}`); - const regex = /^([^(]+)\((\d+),(\d+)\): (.*)$/s; - const messages = errors - .map(err => regex.exec(err)) - .filter(match => !!match) - .map(x => x) - .map(([, path, line, column, message]) => ({ path, line: parseInt(line), column: parseInt(column), message })); - try { - const logFileName = 'log' + (this.id ? `_${this.id}` : ''); - fs_1.default.writeFileSync(path_1.default.join(buildLogFolder, logFileName), JSON.stringify(messages)); - } - catch (err) { - //noop - } - } -} -const errorLogsById = new Map(); -function getErrorLog(id = '') { - let errorLog = errorLogsById.get(id); - if (!errorLog) { - errorLog = new ErrorLog(id); - errorLogsById.set(id, errorLog); - } - return errorLog; -} -const buildLogFolder = path_1.default.join(path_1.default.dirname(path_1.default.dirname(__dirname)), '.build'); -try { - fs_1.default.mkdirSync(buildLogFolder); -} -catch (err) { - // ignore -} -class ReporterError extends Error { - __reporter__ = true; -} -function createReporter(id) { - const errorLog = getErrorLog(id); - const errors = []; - errorLog.allErrors.push(errors); - const result = (err) => errors.push(err); - result.hasErrors = () => errors.length > 0; - result.end = (emitError) => { - errors.length = 0; - errorLog.onStart(); - return event_stream_1.default.through(undefined, function () { - errorLog.onEnd(); - if (emitError && errors.length > 0) { - if (!errors.__logged__) { - errorLog.log(); - } - errors.__logged__ = true; - const err = new ReporterError(`Found ${errors.length} errors`); - this.emit('error', err); - } - else { - this.emit('end'); - } - }); - }; - return result; -} -//# sourceMappingURL=reporter.js.map \ No newline at end of file diff --git a/build/lib/reporter.ts b/build/lib/reporter.ts index 5ea8cb14e74..31a0cb3945d 100644 --- a/build/lib/reporter.ts +++ b/build/lib/reporter.ts @@ -10,7 +10,10 @@ import fs from 'fs'; import path from 'path'; class ErrorLog { - constructor(public id: string) { + public id: string; + + constructor(id: string) { + this.id = id; } allErrors: string[][] = []; startTime: number | null = null; @@ -73,7 +76,7 @@ function getErrorLog(id: string = '') { return errorLog; } -const buildLogFolder = path.join(path.dirname(path.dirname(__dirname)), '.build'); +const buildLogFolder = path.join(path.dirname(path.dirname(import.meta.dirname)), '.build'); try { fs.mkdirSync(buildLogFolder); diff --git a/build/lib/snapshotLoader.js b/build/lib/snapshotLoader.js deleted file mode 100644 index 7d9b3f154f1..00000000000 --- a/build/lib/snapshotLoader.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.snaps = void 0; -var snaps; -(function (snaps) { - const fs = require('fs'); - const path = require('path'); - const os = require('os'); - const cp = require('child_process'); - const mksnapshot = path.join(__dirname, `../../node_modules/.bin/${process.platform === 'win32' ? 'mksnapshot.cmd' : 'mksnapshot'}`); - const product = require('../../product.json'); - const arch = (process.argv.join('').match(/--arch=(.*)/) || [])[1]; - // - let loaderFilepath; - let startupBlobFilepath; - switch (process.platform) { - case 'darwin': - loaderFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Resources/app/out/vs/loader.js`; - startupBlobFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin`; - break; - case 'win32': - case 'linux': - loaderFilepath = `VSCode-${process.platform}-${arch}/resources/app/out/vs/loader.js`; - startupBlobFilepath = `VSCode-${process.platform}-${arch}/snapshot_blob.bin`; - break; - default: - throw new Error('Unknown platform'); - } - loaderFilepath = path.join(__dirname, '../../../', loaderFilepath); - startupBlobFilepath = path.join(__dirname, '../../../', startupBlobFilepath); - snapshotLoader(loaderFilepath, startupBlobFilepath); - function snapshotLoader(loaderFilepath, startupBlobFilepath) { - const inputFile = fs.readFileSync(loaderFilepath); - const wrappedInputFile = ` - var Monaco_Loader_Init; - (function() { - var doNotInitLoader = true; - ${inputFile.toString()}; - Monaco_Loader_Init = function() { - AMDLoader.init(); - CSSLoaderPlugin.init(); - NLSLoaderPlugin.init(); - - return { define, require }; - } - })(); - `; - const wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); - console.log(wrappedInputFilepath); - fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); - cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); - } -})(snaps || (exports.snaps = snaps = {})); -//# sourceMappingURL=snapshotLoader.js.map \ No newline at end of file diff --git a/build/lib/snapshotLoader.ts b/build/lib/snapshotLoader.ts deleted file mode 100644 index 3cb2191144d..00000000000 --- a/build/lib/snapshotLoader.ts +++ /dev/null @@ -1,65 +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 namespace snaps { - - const fs = require('fs'); - const path = require('path'); - const os = require('os'); - const cp = require('child_process'); - - const mksnapshot = path.join(__dirname, `../../node_modules/.bin/${process.platform === 'win32' ? 'mksnapshot.cmd' : 'mksnapshot'}`); - const product = require('../../product.json'); - const arch = (process.argv.join('').match(/--arch=(.*)/) || [])[1]; - - // - let loaderFilepath: string; - let startupBlobFilepath: string; - - switch (process.platform) { - case 'darwin': - loaderFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Resources/app/out/vs/loader.js`; - startupBlobFilepath = `VSCode-darwin/${product.nameLong}.app/Contents/Frameworks/Electron Framework.framework/Resources/snapshot_blob.bin`; - break; - - case 'win32': - case 'linux': - loaderFilepath = `VSCode-${process.platform}-${arch}/resources/app/out/vs/loader.js`; - startupBlobFilepath = `VSCode-${process.platform}-${arch}/snapshot_blob.bin`; - break; - - default: - throw new Error('Unknown platform'); - } - - loaderFilepath = path.join(__dirname, '../../../', loaderFilepath); - startupBlobFilepath = path.join(__dirname, '../../../', startupBlobFilepath); - - snapshotLoader(loaderFilepath, startupBlobFilepath); - - function snapshotLoader(loaderFilepath: string, startupBlobFilepath: string): void { - - const inputFile = fs.readFileSync(loaderFilepath); - const wrappedInputFile = ` - var Monaco_Loader_Init; - (function() { - var doNotInitLoader = true; - ${inputFile.toString()}; - Monaco_Loader_Init = function() { - AMDLoader.init(); - CSSLoaderPlugin.init(); - NLSLoaderPlugin.init(); - - return { define, require }; - } - })(); - `; - const wrappedInputFilepath = path.join(os.tmpdir(), 'wrapped-loader.js'); - console.log(wrappedInputFilepath); - fs.writeFileSync(wrappedInputFilepath, wrappedInputFile); - - cp.execFileSync(mksnapshot, [wrappedInputFilepath, `--startup_blob`, startupBlobFilepath]); - } -} diff --git a/build/lib/standalone.js b/build/lib/standalone.js deleted file mode 100644 index 9d38b863b51..00000000000 --- a/build/lib/standalone.js +++ /dev/null @@ -1,209 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.extractEditor = extractEditor; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const tss = __importStar(require("./treeshaking")); -const dirCache = {}; -function writeFile(filePath, contents) { - function ensureDirs(dirPath) { - if (dirCache[dirPath]) { - return; - } - dirCache[dirPath] = true; - ensureDirs(path_1.default.dirname(dirPath)); - if (fs_1.default.existsSync(dirPath)) { - return; - } - fs_1.default.mkdirSync(dirPath); - } - ensureDirs(path_1.default.dirname(filePath)); - fs_1.default.writeFileSync(filePath, contents); -} -function extractEditor(options) { - const ts = require('typescript'); - const tsConfig = JSON.parse(fs_1.default.readFileSync(path_1.default.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); - let compilerOptions; - if (tsConfig.extends) { - compilerOptions = Object.assign({}, require(path_1.default.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); - delete tsConfig.extends; - } - else { - compilerOptions = tsConfig.compilerOptions; - } - tsConfig.compilerOptions = compilerOptions; - tsConfig.compilerOptions.sourceMap = true; - tsConfig.compilerOptions.outDir = options.tsOutDir; - compilerOptions.noEmit = false; - compilerOptions.noUnusedLocals = false; - compilerOptions.preserveConstEnums = false; - compilerOptions.declaration = false; - options.compilerOptions = compilerOptions; - console.log(`Running tree shaker with shakeLevel ${tss.toStringShakeLevel(options.shakeLevel)}`); - // Take the extra included .d.ts files from `tsconfig.monaco.json` - options.typings = tsConfig.include.filter(includedFile => /\.d\.ts$/.test(includedFile)); - const result = tss.shake(options); - for (const fileName in result) { - if (result.hasOwnProperty(fileName)) { - const relativePath = path_1.default.relative(options.sourcesRoot, fileName); - writeFile(path_1.default.join(options.destRoot, relativePath), result[fileName]); - } - } - const copied = {}; - const copyFile = (fileName, toFileName) => { - if (copied[fileName]) { - return; - } - copied[fileName] = true; - if (path_1.default.isAbsolute(fileName)) { - const relativePath = path_1.default.relative(options.sourcesRoot, fileName); - const dstPath = path_1.default.join(options.destRoot, toFileName ?? relativePath); - writeFile(dstPath, fs_1.default.readFileSync(fileName)); - } - else { - const srcPath = path_1.default.join(options.sourcesRoot, fileName); - const dstPath = path_1.default.join(options.destRoot, toFileName ?? fileName); - writeFile(dstPath, fs_1.default.readFileSync(srcPath)); - } - }; - const writeOutputFile = (fileName, contents) => { - const relativePath = path_1.default.isAbsolute(fileName) ? path_1.default.relative(options.sourcesRoot, fileName) : fileName; - writeFile(path_1.default.join(options.destRoot, relativePath), contents); - }; - for (const fileName in result) { - if (result.hasOwnProperty(fileName)) { - const fileContents = result[fileName]; - const info = ts.preProcessFile(fileContents); - for (let i = info.importedFiles.length - 1; i >= 0; i--) { - const importedFileName = info.importedFiles[i].fileName; - let importedFilePath = importedFileName; - if (/(^\.\/)|(^\.\.\/)/.test(importedFilePath)) { - importedFilePath = path_1.default.join(path_1.default.dirname(fileName), importedFilePath); - } - if (/\.css$/.test(importedFilePath)) { - transportCSS(importedFilePath, copyFile, writeOutputFile); - } - else { - const pathToCopy = path_1.default.join(options.sourcesRoot, importedFilePath); - if (fs_1.default.existsSync(pathToCopy) && !fs_1.default.statSync(pathToCopy).isDirectory()) { - copyFile(importedFilePath); - } - } - } - } - } - delete tsConfig.compilerOptions.moduleResolution; - writeOutputFile('tsconfig.json', JSON.stringify(tsConfig, null, '\t')); - options.additionalFilesToCopyOut?.forEach((file) => { - copyFile(file); - }); - copyFile('vs/loader.js'); - copyFile('typings/css.d.ts'); - copyFile('../node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts', '@vscode/tree-sitter-wasm.d.ts'); -} -function transportCSS(module, enqueue, write) { - if (!/\.css/.test(module)) { - return false; - } - const fileContents = fs_1.default.readFileSync(module).toString(); - const inlineResources = 'base64'; // see https://github.com/microsoft/monaco-editor/issues/148 - const newContents = _rewriteOrInlineUrls(fileContents, inlineResources === 'base64'); - write(module, newContents); - return true; - function _rewriteOrInlineUrls(contents, forceBase64) { - return _replaceURL(contents, (url) => { - const fontMatch = url.match(/^(.*).ttf\?(.*)$/); - if (fontMatch) { - const relativeFontPath = `${fontMatch[1]}.ttf`; // trim the query parameter - const fontPath = path_1.default.join(path_1.default.dirname(module), relativeFontPath); - enqueue(fontPath); - return relativeFontPath; - } - const imagePath = path_1.default.join(path_1.default.dirname(module), url); - const fileContents = fs_1.default.readFileSync(imagePath); - const MIME = /\.svg$/.test(url) ? 'image/svg+xml' : 'image/png'; - let DATA = ';base64,' + fileContents.toString('base64'); - if (!forceBase64 && /\.svg$/.test(url)) { - // .svg => url encode as explained at https://codepen.io/tigt/post/optimizing-svgs-in-data-uris - const newText = fileContents.toString() - .replace(/"/g, '\'') - .replace(//g, '%3E') - .replace(/&/g, '%26') - .replace(/#/g, '%23') - .replace(/\s+/g, ' '); - const encodedData = ',' + newText; - if (encodedData.length < DATA.length) { - DATA = encodedData; - } - } - return '"data:' + MIME + DATA + '"'; - }); - } - function _replaceURL(contents, replacer) { - // Use ")" as the terminator as quotes are oftentimes not used at all - return contents.replace(/url\(\s*([^\)]+)\s*\)?/g, (_, ...matches) => { - let url = matches[0]; - // Eliminate starting quotes (the initial whitespace is not captured) - if (url.charAt(0) === '"' || url.charAt(0) === '\'') { - url = url.substring(1); - } - // The ending whitespace is captured - while (url.length > 0 && (url.charAt(url.length - 1) === ' ' || url.charAt(url.length - 1) === '\t')) { - url = url.substring(0, url.length - 1); - } - // Eliminate ending quotes - if (url.charAt(url.length - 1) === '"' || url.charAt(url.length - 1) === '\'') { - url = url.substring(0, url.length - 1); - } - if (!_startsWith(url, 'data:') && !_startsWith(url, 'http://') && !_startsWith(url, 'https://')) { - url = replacer(url); - } - return 'url(' + url + ')'; - }); - } - function _startsWith(haystack, needle) { - return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; - } -} -//# sourceMappingURL=standalone.js.map \ No newline at end of file diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 5f2104cb4c6..967ff0108bf 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -5,7 +5,8 @@ import fs from 'fs'; import path from 'path'; -import * as tss from './treeshaking'; +import * as tss from './treeshaking.ts'; +import ts from 'typescript'; const dirCache: { [dir: string]: boolean } = {}; @@ -27,12 +28,11 @@ function writeFile(filePath: string, contents: Buffer | string): void { } export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: string; tsOutDir: string; additionalFilesToCopyOut?: string[] }): void { - const ts = require('typescript') as typeof import('typescript'); - const tsConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, 'tsconfig.monaco.json')).toString()); let compilerOptions: { [key: string]: any }; if (tsConfig.extends) { - compilerOptions = Object.assign({}, require(path.join(options.sourcesRoot, tsConfig.extends)).compilerOptions, tsConfig.compilerOptions); + const extendedConfig = JSON.parse(fs.readFileSync(path.join(options.sourcesRoot, tsConfig.extends)).toString()); + compilerOptions = Object.assign({}, extendedConfig.compilerOptions, tsConfig.compilerOptions); delete tsConfig.extends; } else { compilerOptions = tsConfig.compilerOptions; @@ -52,13 +52,19 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str console.log(`Running tree shaker with shakeLevel ${tss.toStringShakeLevel(options.shakeLevel)}`); // Take the extra included .d.ts files from `tsconfig.monaco.json` - options.typings = (tsConfig.include).filter(includedFile => /\.d\.ts$/.test(includedFile)); + options.typings = (tsConfig.include as string[]).filter(includedFile => /\.d\.ts$/.test(includedFile)); const result = tss.shake(options); for (const fileName in result) { if (result.hasOwnProperty(fileName)) { + let fileContents = result[fileName]; + // Replace .ts? with .js? in new URL() patterns + fileContents = fileContents.replace( + /(new\s+URL\s*\(\s*['"`][^'"`]*?)\.ts(\?[^'"`]*['"`])/g, + '$1.js$2' + ); const relativePath = path.relative(options.sourcesRoot, fileName); - writeFile(path.join(options.destRoot, relativePath), result[fileName]); + writeFile(path.join(options.destRoot, relativePath), fileContents); } } const copied: { [fileName: string]: boolean } = {}; @@ -114,7 +120,6 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str copyFile(file); }); - copyFile('vs/loader.js'); copyFile('typings/css.d.ts'); copyFile('../node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts', '@vscode/tree-sitter-wasm.d.ts'); } diff --git a/build/lib/stats.js b/build/lib/stats.js deleted file mode 100644 index 3f6d953ae40..00000000000 --- a/build/lib/stats.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.createStatsStream = createStatsStream; -const event_stream_1 = __importDefault(require("event-stream")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -class Entry { - name; - totalCount; - totalSize; - constructor(name, totalCount, totalSize) { - this.name = name; - this.totalCount = totalCount; - this.totalSize = totalSize; - } - toString(pretty) { - if (!pretty) { - if (this.totalCount === 1) { - return `${this.name}: ${this.totalSize} bytes`; - } - else { - return `${this.name}: ${this.totalCount} files with ${this.totalSize} bytes`; - } - } - else { - if (this.totalCount === 1) { - return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${Math.round(this.totalSize / 1204)}KB`; - } - else { - const count = this.totalCount < 100 - ? ansi_colors_1.default.green(this.totalCount.toString()) - : ansi_colors_1.default.red(this.totalCount.toString()); - return `Stats for '${ansi_colors_1.default.grey(this.name)}': ${count} files, ${Math.round(this.totalSize / 1204)}KB`; - } - } - } -} -const _entries = new Map(); -function createStatsStream(group, log) { - const entry = new Entry(group, 0, 0); - _entries.set(entry.name, entry); - return event_stream_1.default.through(function (data) { - const file = data; - if (typeof file.path === 'string') { - entry.totalCount += 1; - if (Buffer.isBuffer(file.contents)) { - entry.totalSize += file.contents.length; - } - else if (file.stat && typeof file.stat.size === 'number') { - entry.totalSize += file.stat.size; - } - else { - // funky file... - } - } - this.emit('data', data); - }, function () { - if (log) { - if (entry.totalCount === 1) { - (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${Math.round(entry.totalSize / 1204)}KB`); - } - else { - const count = entry.totalCount < 100 - ? ansi_colors_1.default.green(entry.totalCount.toString()) - : ansi_colors_1.default.red(entry.totalCount.toString()); - (0, fancy_log_1.default)(`Stats for '${ansi_colors_1.default.grey(entry.name)}': ${count} files, ${Math.round(entry.totalSize / 1204)}KB`); - } - } - this.emit('end'); - }); -} -//# sourceMappingURL=stats.js.map \ No newline at end of file diff --git a/build/lib/stats.ts b/build/lib/stats.ts index 8db55d3e777..83bf0a4a7ae 100644 --- a/build/lib/stats.ts +++ b/build/lib/stats.ts @@ -9,7 +9,15 @@ import ansiColors from 'ansi-colors'; import File from 'vinyl'; class Entry { - constructor(readonly name: string, public totalCount: number, public totalSize: number) { } + readonly name: string; + public totalCount: number; + public totalSize: number; + + constructor(name: string, totalCount: number, totalSize: number) { + this.name = name; + this.totalCount = totalCount; + this.totalSize = totalSize; + } toString(pretty?: boolean): string { if (!pretty) { diff --git a/build/lib/stylelint/validateVariableNames.js b/build/lib/stylelint/validateVariableNames.js deleted file mode 100644 index 5644fddf954..00000000000 --- a/build/lib/stylelint/validateVariableNames.js +++ /dev/null @@ -1,67 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVariableNameValidator = getVariableNameValidator; -const fs = __importStar(require("fs")); -const path = __importStar(require("path")); -const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; -let knownVariables; -function getKnownVariableNames() { - if (!knownVariables) { - const knownVariablesFileContent = fs.readFileSync(path.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); - const knownVariablesInfo = JSON.parse(knownVariablesFileContent); - knownVariables = new Set([...knownVariablesInfo.colors, ...knownVariablesInfo.others]); - } - return knownVariables; -} -const iconVariable = /^--vscode-icon-.+-(content|font-family)$/; -function getVariableNameValidator() { - const allVariables = getKnownVariableNames(); - return (value, report) => { - RE_VAR_PROP.lastIndex = 0; // reset lastIndex just to be sure - let match; - while (match = RE_VAR_PROP.exec(value)) { - const variableName = match[1]; - if (variableName && !allVariables.has(variableName) && !iconVariable.test(variableName)) { - report(variableName); - } - } - }; -} -//# sourceMappingURL=validateVariableNames.js.map \ No newline at end of file diff --git a/build/lib/stylelint/validateVariableNames.ts b/build/lib/stylelint/validateVariableNames.ts index 50f3e347da0..3cab12ac98d 100644 --- a/build/lib/stylelint/validateVariableNames.ts +++ b/build/lib/stylelint/validateVariableNames.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; +import { readFileSync } from 'fs'; +import path from 'path'; const RE_VAR_PROP = /var\(\s*(--([\w\-\.]+))/g; let knownVariables: Set | undefined; function getKnownVariableNames() { if (!knownVariables) { - const knownVariablesFileContent = fs.readFileSync(path.join(__dirname, './vscode-known-variables.json'), 'utf8').toString(); + const knownVariablesFileContent = readFileSync(path.join(import.meta.dirname, './vscode-known-variables.json'), 'utf8').toString(); const knownVariablesInfo = JSON.parse(knownVariablesFileContent); - knownVariables = new Set([...knownVariablesInfo.colors, ...knownVariablesInfo.others] as string[]); + knownVariables = new Set([...knownVariablesInfo.colors, ...knownVariablesInfo.others, ...(knownVariablesInfo.sizes || [])] as string[]); } return knownVariables; } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index e76ccc928a2..5dee2dcd04f 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -21,6 +21,9 @@ "--vscode-activityErrorBadge-foreground", "--vscode-activityWarningBadge-background", "--vscode-activityWarningBadge-foreground", + "--vscode-agentSessionReadIndicator-foreground", + "--vscode-agentSessionSelectedBadge-border", + "--vscode-agentSessionSelectedUnfocusedBadge-border", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -36,6 +39,7 @@ "--vscode-button-foreground", "--vscode-button-hoverBackground", "--vscode-button-secondaryBackground", + "--vscode-button-secondaryBorder", "--vscode-button-secondaryForeground", "--vscode-button-secondaryHoverBackground", "--vscode-button-separator", @@ -63,6 +67,7 @@ "--vscode-chat-requestCodeBorder", "--vscode-chat-slashCommandBackground", "--vscode-chat-slashCommandForeground", + "--vscode-chat-thinkingShimmer", "--vscode-chatManagement-sashBorder", "--vscode-checkbox-background", "--vscode-checkbox-border", @@ -159,6 +164,7 @@ "--vscode-editor-foldPlaceholderForeground", "--vscode-editor-foreground", "--vscode-editor-hoverHighlightBackground", + "--vscode-editor-inactiveLineHighlightBackground", "--vscode-editor-inactiveSelectionBackground", "--vscode-editor-inlineValuesBackground", "--vscode-editor-inlineValuesForeground", @@ -199,6 +205,7 @@ "--vscode-editorBracketHighlight-unexpectedBracket-foreground", "--vscode-editorBracketMatch-background", "--vscode-editorBracketMatch-border", + "--vscode-editorBracketMatch-foreground", "--vscode-editorBracketPairGuide-activeBackground1", "--vscode-editorBracketPairGuide-activeBackground2", "--vscode-editorBracketPairGuide-activeBackground3", @@ -239,6 +246,7 @@ "--vscode-editorGutter-addedBackground", "--vscode-editorGutter-addedSecondaryBackground", "--vscode-editorGutter-background", + "--vscode-editorGutter-commentDraftGlyphForeground", "--vscode-editorGutter-commentGlyphForeground", "--vscode-editorGutter-commentRangeForeground", "--vscode-editorGutter-commentUnresolvedGlyphForeground", @@ -302,6 +310,7 @@ "--vscode-editorOverviewRuler-background", "--vscode-editorOverviewRuler-border", "--vscode-editorOverviewRuler-bracketMatchForeground", + "--vscode-editorOverviewRuler-commentDraftForeground", "--vscode-editorOverviewRuler-commentForeground", "--vscode-editorOverviewRuler-commentUnresolvedForeground", "--vscode-editorOverviewRuler-commonContentForeground", @@ -343,7 +352,6 @@ "--vscode-editorWarning-background", "--vscode-editorWarning-border", "--vscode-editorWarning-foreground", - "--vscode-editorWatermark-foreground", "--vscode-editorWhitespace-foreground", "--vscode-editorWidget-background", "--vscode-editorWidget-border", @@ -353,6 +361,7 @@ "--vscode-extensionBadge-remoteBackground", "--vscode-extensionBadge-remoteForeground", "--vscode-extensionButton-background", + "--vscode-extensionButton-border", "--vscode-extensionButton-foreground", "--vscode-extensionButton-hoverBackground", "--vscode-extensionButton-prominentBackground", @@ -377,8 +386,8 @@ "--vscode-inlineChat-background", "--vscode-inlineChat-border", "--vscode-inlineChat-foreground", - "--vscode-inlineChat-regionHighlight", "--vscode-inlineChat-shadow", + "--vscode-inlineChat-regionHighlight", "--vscode-inlineChatDiff-inserted", "--vscode-inlineChatDiff-removed", "--vscode-inlineChatInput-background", @@ -864,6 +873,7 @@ "--vscode-textLink-activeForeground", "--vscode-textLink-foreground", "--vscode-textPreformat-background", + "--vscode-textPreformat-border", "--vscode-textPreformat-foreground", "--vscode-textSeparator-foreground", "--vscode-titleBar-activeBackground", @@ -889,57 +899,15 @@ "--vscode-widget-border", "--vscode-widget-shadow", "--vscode-window-activeBorder", - "--vscode-window-inactiveBorder", - "--cortex-accent", - "--cortex-brand", - "--cortex-brand-soft", - "--cortex-border-strong", - "--cortex-border-weak", - "--cortex-danger", - "--cortex-overlay", - "--cortex-ring-color", - "--cortex-shadow-hairline", - "--cortex-shadow-soft", - "--cortex-success", - "--cortex-surface-0", - "--cortex-surface-1", - "--cortex-surface-1-alt", - "--cortex-surface-2", - "--cortex-surface-2-alt", - "--cortex-surface-3", - "--cortex-surface-4", - "--cortex-text-base", - "--cortex-text-muted", - "--cortex-text-strong", - "--cortex-text-subtle", - "--cortex-warning", - "--void-bg-1", - "--void-bg-1-alt", - "--void-bg-2", - "--void-bg-2-alt", - "--void-bg-3", - "--void-border-2", - "--void-border-3", - "--void-fg-0", - "--void-fg-1", - "--void-link-color", - "--void-ring-color", - "--void-warning", - "--vscode-void-greenBG", - "--vscode-void-highlightBG", - "--vscode-void-redBG", - "--vscode-void-sweepBG", - "--vscode-void-sweepIdxBG" + "--vscode-window-inactiveBorder" ], "others": [ + "--editor-font-size", "--background-dark", "--background-light", "--chat-editing-last-edit-shift", "--chat-current-response-min-height", - "--dropdown-padding-bottom", - "--dropdown-padding-top", "--inline-chat-frame-progress", - "--inline-chat-hint-progress", "--insert-border-color", "--last-tab-margin-right", "--monaco-monospace-font", @@ -1028,27 +996,26 @@ "--vscode-chat-font-size-body-xl", "--vscode-chat-font-size-body-xs", "--vscode-chat-font-size-body-xxl", - "--cortex-font-body", - "--cortex-font-label", - "--cortex-font-micro", - "--cortex-font-subtitle", - "--cortex-font-title", - "--cortex-logo-url", - "--cortex-radius-lg", - "--cortex-radius-md", - "--cortex-radius-sm", - "--cortex-radius-xs", - "--cortex-space-2xl", - "--cortex-space-2xs", - "--cortex-space-lg", - "--cortex-space-md", - "--cortex-space-sm", - "--cortex-space-xl", - "--cortex-space-xs", - "--cortex-transition-base", - "--cortex-transition-fast", - "--cortex-transition-slow", - "--scrollbar-horizontal-height", - "--scrollbar-vertical-width" + "--vscode-toolbar-action-min-width", + "--comment-thread-editor-font-family", + "--comment-thread-editor-font-weight", + "--comment-thread-state-color", + "--comment-thread-state-background-color", + "--inline-edit-border-radius", + "--chat-subagent-last-item-height", + "--vscode-inline-chat-affordance-height" + ], + "sizes": [ + "--vscode-bodyFontSize", + "--vscode-bodyFontSize-small", + "--vscode-bodyFontSize-xSmall", + "--vscode-codiconFontSize", + "--vscode-cornerRadius-circle", + "--vscode-cornerRadius-large", + "--vscode-cornerRadius-medium", + "--vscode-cornerRadius-small", + "--vscode-cornerRadius-xLarge", + "--vscode-cornerRadius-xSmall", + "--vscode-strokeThickness" ] } diff --git a/build/lib/task.js b/build/lib/task.js deleted file mode 100644 index a30b65b288c..00000000000 --- a/build/lib/task.js +++ /dev/null @@ -1,97 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.series = series; -exports.parallel = parallel; -exports.define = define; -const fancy_log_1 = __importDefault(require("fancy-log")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -function _isPromise(p) { - return typeof p.then === 'function'; -} -function _renderTime(time) { - return `${Math.round(time)} ms`; -} -async function _execute(task) { - const name = task.taskName || task.displayName || ``; - if (!task._tasks) { - (0, fancy_log_1.default)('Starting', ansi_colors_1.default.cyan(name), '...'); - } - const startTime = process.hrtime(); - await _doExecute(task); - const elapsedArr = process.hrtime(startTime); - const elapsedNanoseconds = (elapsedArr[0] * 1e9 + elapsedArr[1]); - if (!task._tasks) { - (0, fancy_log_1.default)(`Finished`, ansi_colors_1.default.cyan(name), 'after', ansi_colors_1.default.magenta(_renderTime(elapsedNanoseconds / 1e6))); - } -} -async function _doExecute(task) { - // Always invoke as if it were a callback task - return new Promise((resolve, reject) => { - if (task.length === 1) { - // this is a callback task - task((err) => { - if (err) { - return reject(err); - } - resolve(); - }); - return; - } - const taskResult = task(); - if (typeof taskResult === 'undefined') { - // this is a sync task - resolve(); - return; - } - if (_isPromise(taskResult)) { - // this is a promise returning task - taskResult.then(resolve, reject); - return; - } - // this is a stream returning task - taskResult.on('end', _ => resolve()); - taskResult.on('error', err => reject(err)); - }); -} -function series(...tasks) { - const result = async () => { - for (let i = 0; i < tasks.length; i++) { - await _execute(tasks[i]); - } - }; - result._tasks = tasks; - return result; -} -function parallel(...tasks) { - const result = async () => { - await Promise.all(tasks.map(t => _execute(t))); - }; - result._tasks = tasks; - return result; -} -function define(name, task) { - if (task._tasks) { - // This is a composite task - const lastTask = task._tasks[task._tasks.length - 1]; - if (lastTask._tasks || lastTask.taskName) { - // This is a composite task without a real task function - // => generate a fake task function - return define(name, series(task, () => Promise.resolve())); - } - lastTask.taskName = name; - task.displayName = name; - return task; - } - // This is a simple task - task.taskName = name; - task.displayName = name; - return task; -} -//# sourceMappingURL=task.js.map \ No newline at end of file diff --git a/build/lib/task.ts b/build/lib/task.ts index 76c2002296b..085843a93b7 100644 --- a/build/lib/task.ts +++ b/build/lib/task.ts @@ -18,7 +18,7 @@ export interface StreamTask extends BaseTask { (): NodeJS.ReadWriteStream; } export interface CallbackTask extends BaseTask { - (cb?: (err?: any) => void): void; + (cb?: (err?: Error) => void): void; } export type Task = PromiseTask | StreamTask | CallbackTask; diff --git a/build/lib/test/booleanPolicy.test.js b/build/lib/test/booleanPolicy.test.js deleted file mode 100644 index 944916c3d76..00000000000 --- a/build/lib/test/booleanPolicy.test.js +++ /dev/null @@ -1,126 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const booleanPolicy_js_1 = require("../policies/booleanPolicy.js"); -const types_js_1 = require("../policies/types.js"); -suite('BooleanPolicy', () => { - const mockCategory = { - key: 'test.category', - name: { value: 'Category1', key: 'test.category' }, - }; - const mockPolicy = { - key: 'test.boolean.policy', - name: 'TestBooleanPolicy', - category: 'Category1', - minimumVersion: '1.0', - type: 'boolean', - localization: { - description: { key: 'test.policy.description', value: 'Test policy description' } - } - }; - test('should create BooleanPolicy from factory method', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - assert_1.default.strictEqual(policy.name, 'TestBooleanPolicy'); - assert_1.default.strictEqual(policy.minimumVersion, '1.0'); - assert_1.default.strictEqual(policy.category.name.nlsKey, mockCategory.name.key); - assert_1.default.strictEqual(policy.category.name.value, mockCategory.name.value); - assert_1.default.strictEqual(policy.type, types_js_1.PolicyType.Boolean); - }); - test('should render ADMX elements correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admx = policy.renderADMX('TestKey'); - assert_1.default.deepStrictEqual(admx, [ - '', - '\t', - '\t', - '\t', - '', - '\t', - '', - '\t', - '' - ]); - }); - test('should render ADML strings correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admlStrings = policy.renderADMLStrings(); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestBooleanPolicy', - 'Test policy description' - ]); - }); - test('should render ADML strings with translations', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated description' - } - }; - const admlStrings = policy.renderADMLStrings(translations); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestBooleanPolicy', - 'Translated description' - ]); - }); - test('should render ADML presentation correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const presentation = policy.renderADMLPresentation(); - assert_1.default.strictEqual(presentation, 'TestBooleanPolicy'); - }); - test('should render JSON value correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const jsonValue = policy.renderJsonValue(); - assert_1.default.strictEqual(jsonValue, false); - }); - test('should render profile value correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profileValue = policy.renderProfileValue(); - assert_1.default.strictEqual(profileValue, ''); - }); - test('should render profile correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profile = policy.renderProfile(); - assert_1.default.strictEqual(profile.length, 2); - assert_1.default.strictEqual(profile[0], 'TestBooleanPolicy'); - assert_1.default.strictEqual(profile[1], ''); - }); - test('should render profile manifest value correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifestValue = policy.renderProfileManifestValue(); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTest policy description\npfm_name\nTestBooleanPolicy\npfm_title\nTestBooleanPolicy\npfm_type\nboolean'); - }); - test('should render profile manifest value with translations', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated manifest description' - } - }; - const manifestValue = policy.renderProfileManifestValue(translations); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTranslated manifest description\npfm_name\nTestBooleanPolicy\npfm_title\nTestBooleanPolicy\npfm_type\nboolean'); - }); - test('should render profile manifest correctly', () => { - const policy = booleanPolicy_js_1.BooleanPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifest = policy.renderProfileManifest(); - assert_1.default.strictEqual(manifest, '\npfm_default\n\npfm_description\nTest policy description\npfm_name\nTestBooleanPolicy\npfm_title\nTestBooleanPolicy\npfm_type\nboolean\n'); - }); -}); -//# sourceMappingURL=booleanPolicy.test.js.map \ No newline at end of file diff --git a/build/lib/test/booleanPolicy.test.ts b/build/lib/test/booleanPolicy.test.ts index 8da223530b9..d64f9fff646 100644 --- a/build/lib/test/booleanPolicy.test.ts +++ b/build/lib/test/booleanPolicy.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { BooleanPolicy } from '../policies/booleanPolicy.js'; -import { LanguageTranslations, PolicyType } from '../policies/types.js'; -import { CategoryDto, PolicyDto } from '../policies/policyDto.js'; +import { BooleanPolicy } from '../policies/booleanPolicy.ts'; +import { type LanguageTranslations, PolicyType } from '../policies/types.ts'; +import type { CategoryDto, PolicyDto } from '../policies/policyDto.ts'; suite('BooleanPolicy', () => { const mockCategory: CategoryDto = { diff --git a/build/lib/test/fixtures/policies/win32/CodeOSS.admx b/build/lib/test/fixtures/policies/win32/CodeOSS.admx index fee094c56c5..c78eaebaf28 100644 --- a/build/lib/test/fixtures/policies/win32/CodeOSS.admx +++ b/build/lib/test/fixtures/policies/win32/CodeOSS.admx @@ -45,9 +45,9 @@ - none - registry - all + none + registry + all @@ -113,10 +113,10 @@ - all - error - crash - off + all + error + crash + off diff --git a/build/lib/test/i18n.test.js b/build/lib/test/i18n.test.js deleted file mode 100644 index 41aa8a7f668..00000000000 --- a/build/lib/test/i18n.test.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const i18n = __importStar(require("../i18n")); -suite('XLF Parser Tests', () => { - const sampleXlf = 'Key #1Key #2 &'; - const sampleTranslatedXlf = 'Key #1Кнопка #1Key #2 &Кнопка #2 &'; - const name = 'vs/base/common/keybinding'; - const keys = ['key1', 'key2']; - const messages = ['Key #1', 'Key #2 &']; - const translatedMessages = { key1: 'Кнопка #1', key2: 'Кнопка #2 &' }; - test('Keys & messages to XLF conversion', () => { - const xlf = new i18n.XLF('vscode-workbench'); - xlf.addFile(name, keys, messages); - const xlfString = xlf.toString(); - assert_1.default.strictEqual(xlfString.replace(/\s{2,}/g, ''), sampleXlf); - }); - test('XLF to keys & messages conversion', () => { - i18n.XLF.parse(sampleTranslatedXlf).then(function (resolvedFiles) { - assert_1.default.deepStrictEqual(resolvedFiles[0].messages, translatedMessages); - assert_1.default.strictEqual(resolvedFiles[0].name, name); - }); - }); - test('JSON file source path to Transifex resource match', () => { - const editorProject = 'vscode-editor', workbenchProject = 'vscode-workbench'; - const platform = { name: 'vs/platform', project: editorProject }, editorContrib = { name: 'vs/editor/contrib', project: editorProject }, editor = { name: 'vs/editor', project: editorProject }, base = { name: 'vs/base', project: editorProject }, code = { name: 'vs/code', project: workbenchProject }, workbenchParts = { name: 'vs/workbench/contrib/html', project: workbenchProject }, workbenchServices = { name: 'vs/workbench/services/textfile', project: workbenchProject }, workbench = { name: 'vs/workbench', project: workbenchProject }; - assert_1.default.deepStrictEqual(i18n.getResource('vs/platform/actions/browser/menusExtensionPoint'), platform); - assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/contrib/clipboard/browser/clipboard'), editorContrib); - assert_1.default.deepStrictEqual(i18n.getResource('vs/editor/common/modes/modesRegistry'), editor); - assert_1.default.deepStrictEqual(i18n.getResource('vs/base/common/errorMessage'), base); - assert_1.default.deepStrictEqual(i18n.getResource('vs/code/electron-main/window'), code); - assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/contrib/html/browser/webview'), workbenchParts); - assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/services/textfile/node/testFileService'), workbenchServices); - assert_1.default.deepStrictEqual(i18n.getResource('vs/workbench/browser/parts/panel/panelActions'), workbench); - }); -}); -//# sourceMappingURL=i18n.test.js.map \ No newline at end of file diff --git a/build/lib/test/i18n.test.ts b/build/lib/test/i18n.test.ts index 4e4545548b8..7d5bb0433fe 100644 --- a/build/lib/test/i18n.test.ts +++ b/build/lib/test/i18n.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import * as i18n from '../i18n'; +import * as i18n from '../i18n.ts'; suite('XLF Parser Tests', () => { const sampleXlf = 'Key #1Key #2 &'; diff --git a/build/lib/test/numberPolicy.test.js b/build/lib/test/numberPolicy.test.js deleted file mode 100644 index 312ec7587ee..00000000000 --- a/build/lib/test/numberPolicy.test.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const numberPolicy_js_1 = require("../policies/numberPolicy.js"); -const types_js_1 = require("../policies/types.js"); -suite('NumberPolicy', () => { - const mockCategory = { - key: 'test.category', - name: { value: 'Category1', key: 'test.category' }, - }; - const mockPolicy = { - key: 'test.number.policy', - name: 'TestNumberPolicy', - category: 'Category1', - minimumVersion: '1.0', - type: 'number', - default: 42, - localization: { - description: { key: 'test.policy.description', value: 'Test number policy description' } - } - }; - test('should create NumberPolicy from factory method', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - assert_1.default.strictEqual(policy.name, 'TestNumberPolicy'); - assert_1.default.strictEqual(policy.minimumVersion, '1.0'); - assert_1.default.strictEqual(policy.category.name.nlsKey, mockCategory.name.key); - assert_1.default.strictEqual(policy.category.name.value, mockCategory.name.value); - assert_1.default.strictEqual(policy.type, types_js_1.PolicyType.Number); - }); - test('should render ADMX elements correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admx = policy.renderADMX('TestKey'); - assert_1.default.deepStrictEqual(admx, [ - '', - '\t', - '\t', - '\t', - '', - '\t', - '' - ]); - }); - test('should render ADML strings correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admlStrings = policy.renderADMLStrings(); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestNumberPolicy', - 'Test number policy description' - ]); - }); - test('should render ADML strings with translations', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated description' - } - }; - const admlStrings = policy.renderADMLStrings(translations); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestNumberPolicy', - 'Translated description' - ]); - }); - test('should render ADML presentation correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const presentation = policy.renderADMLPresentation(); - assert_1.default.strictEqual(presentation, 'TestNumberPolicy'); - }); - test('should render JSON value correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const jsonValue = policy.renderJsonValue(); - assert_1.default.strictEqual(jsonValue, 42); - }); - test('should render profile value correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profileValue = policy.renderProfileValue(); - assert_1.default.strictEqual(profileValue, '42'); - }); - test('should render profile correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profile = policy.renderProfile(); - assert_1.default.strictEqual(profile.length, 2); - assert_1.default.strictEqual(profile[0], 'TestNumberPolicy'); - assert_1.default.strictEqual(profile[1], '42'); - }); - test('should render profile manifest value correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifestValue = policy.renderProfileManifestValue(); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n42\npfm_description\nTest number policy description\npfm_name\nTestNumberPolicy\npfm_title\nTestNumberPolicy\npfm_type\ninteger'); - }); - test('should render profile manifest value with translations', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated manifest description' - } - }; - const manifestValue = policy.renderProfileManifestValue(translations); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n42\npfm_description\nTranslated manifest description\npfm_name\nTestNumberPolicy\npfm_title\nTestNumberPolicy\npfm_type\ninteger'); - }); - test('should render profile manifest correctly', () => { - const policy = numberPolicy_js_1.NumberPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifest = policy.renderProfileManifest(); - assert_1.default.strictEqual(manifest, '\npfm_default\n42\npfm_description\nTest number policy description\npfm_name\nTestNumberPolicy\npfm_title\nTestNumberPolicy\npfm_type\ninteger\n'); - }); -}); -//# sourceMappingURL=numberPolicy.test.js.map \ No newline at end of file diff --git a/build/lib/test/numberPolicy.test.ts b/build/lib/test/numberPolicy.test.ts index dfb6276e34e..503403ca5c0 100644 --- a/build/lib/test/numberPolicy.test.ts +++ b/build/lib/test/numberPolicy.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { NumberPolicy } from '../policies/numberPolicy.js'; -import { LanguageTranslations, PolicyType } from '../policies/types.js'; -import { CategoryDto, PolicyDto } from '../policies/policyDto.js'; +import { NumberPolicy } from '../policies/numberPolicy.ts'; +import { type LanguageTranslations, PolicyType } from '../policies/types.ts'; +import type { CategoryDto, PolicyDto } from '../policies/policyDto.ts'; suite('NumberPolicy', () => { const mockCategory: CategoryDto = { diff --git a/build/lib/test/objectPolicy.test.js b/build/lib/test/objectPolicy.test.js deleted file mode 100644 index a34d71383d2..00000000000 --- a/build/lib/test/objectPolicy.test.js +++ /dev/null @@ -1,124 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const objectPolicy_js_1 = require("../policies/objectPolicy.js"); -const types_js_1 = require("../policies/types.js"); -suite('ObjectPolicy', () => { - const mockCategory = { - key: 'test.category', - name: { value: 'Category1', key: 'test.category' }, - }; - const mockPolicy = { - key: 'test.object.policy', - name: 'TestObjectPolicy', - category: 'Category1', - minimumVersion: '1.0', - type: 'object', - localization: { - description: { key: 'test.policy.description', value: 'Test policy description' } - } - }; - test('should create ObjectPolicy from factory method', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - assert_1.default.strictEqual(policy.name, 'TestObjectPolicy'); - assert_1.default.strictEqual(policy.minimumVersion, '1.0'); - assert_1.default.strictEqual(policy.category.name.nlsKey, mockCategory.name.key); - assert_1.default.strictEqual(policy.category.name.value, mockCategory.name.value); - assert_1.default.strictEqual(policy.type, types_js_1.PolicyType.Object); - }); - test('should render ADMX elements correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admx = policy.renderADMX('TestKey'); - assert_1.default.deepStrictEqual(admx, [ - '', - '\t', - '\t', - '\t', - '', - '\t', - '' - ]); - }); - test('should render ADML strings correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admlStrings = policy.renderADMLStrings(); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestObjectPolicy', - 'Test policy description' - ]); - }); - test('should render ADML strings with translations', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated description' - } - }; - const admlStrings = policy.renderADMLStrings(translations); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestObjectPolicy', - 'Translated description' - ]); - }); - test('should render ADML presentation correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const presentation = policy.renderADMLPresentation(); - assert_1.default.strictEqual(presentation, ''); - }); - test('should render JSON value correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const jsonValue = policy.renderJsonValue(); - assert_1.default.strictEqual(jsonValue, ''); - }); - test('should render profile value correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profileValue = policy.renderProfileValue(); - assert_1.default.strictEqual(profileValue, ''); - }); - test('should render profile correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profile = policy.renderProfile(); - assert_1.default.strictEqual(profile.length, 2); - assert_1.default.strictEqual(profile[0], 'TestObjectPolicy'); - assert_1.default.strictEqual(profile[1], ''); - }); - test('should render profile manifest value correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifestValue = policy.renderProfileManifestValue(); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTest policy description\npfm_name\nTestObjectPolicy\npfm_title\nTestObjectPolicy\npfm_type\nstring\n'); - }); - test('should render profile manifest value with translations', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated manifest description' - } - }; - const manifestValue = policy.renderProfileManifestValue(translations); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTranslated manifest description\npfm_name\nTestObjectPolicy\npfm_title\nTestObjectPolicy\npfm_type\nstring\n'); - }); - test('should render profile manifest correctly', () => { - const policy = objectPolicy_js_1.ObjectPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifest = policy.renderProfileManifest(); - assert_1.default.strictEqual(manifest, '\npfm_default\n\npfm_description\nTest policy description\npfm_name\nTestObjectPolicy\npfm_title\nTestObjectPolicy\npfm_type\nstring\n\n'); - }); -}); -//# sourceMappingURL=objectPolicy.test.js.map \ No newline at end of file diff --git a/build/lib/test/objectPolicy.test.ts b/build/lib/test/objectPolicy.test.ts index 6012b8012da..8e688d19b8f 100644 --- a/build/lib/test/objectPolicy.test.ts +++ b/build/lib/test/objectPolicy.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ObjectPolicy } from '../policies/objectPolicy.js'; -import { LanguageTranslations, PolicyType } from '../policies/types.js'; -import { CategoryDto, PolicyDto } from '../policies/policyDto.js'; +import { ObjectPolicy } from '../policies/objectPolicy.ts'; +import { type LanguageTranslations, PolicyType } from '../policies/types.ts'; +import type { CategoryDto, PolicyDto } from '../policies/policyDto.ts'; suite('ObjectPolicy', () => { const mockCategory: CategoryDto = { diff --git a/build/lib/test/policyConversion.test.js b/build/lib/test/policyConversion.test.js deleted file mode 100644 index 6fc735f1127..00000000000 --- a/build/lib/test/policyConversion.test.js +++ /dev/null @@ -1,465 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const fs_1 = require("fs"); -const path_1 = __importDefault(require("path")); -const booleanPolicy_1 = require("../policies/booleanPolicy"); -const numberPolicy_1 = require("../policies/numberPolicy"); -const objectPolicy_1 = require("../policies/objectPolicy"); -const stringEnumPolicy_1 = require("../policies/stringEnumPolicy"); -const stringPolicy_1 = require("../policies/stringPolicy"); -const render_1 = require("../policies/render"); -const PolicyTypes = [ - booleanPolicy_1.BooleanPolicy, - numberPolicy_1.NumberPolicy, - stringEnumPolicy_1.StringEnumPolicy, - stringPolicy_1.StringPolicy, - objectPolicy_1.ObjectPolicy -]; -function parsePolicies(policyData) { - const categories = new Map(); - for (const category of policyData.categories) { - categories.set(category.key, category); - } - const policies = []; - for (const policy of policyData.policies) { - const category = categories.get(policy.category); - if (!category) { - throw new Error(`Unknown category: ${policy.category}`); - } - let result; - for (const policyType of PolicyTypes) { - if (result = policyType.from(category, policy)) { - break; - } - } - if (!result) { - throw new Error(`Unsupported policy type: ${policy.type} for policy ${policy.name}`); - } - policies.push(result); - } - // Sort policies first by category name, then by policy name - policies.sort((a, b) => { - const categoryCompare = a.category.name.value.localeCompare(b.category.name.value); - if (categoryCompare !== 0) { - return categoryCompare; - } - return a.name.localeCompare(b.name); - }); - return policies; -} -/** - * This is a snapshot of the data taken on Oct. 20 2025 as part of the - * policy refactor effort. Let's make sure that nothing has regressed. - */ -const policies = { - categories: [ - { - key: 'Extensions', - name: { - key: 'extensionsConfigurationTitle', - value: 'Extensions' - } - }, - { - key: 'IntegratedTerminal', - name: { - key: 'terminalIntegratedConfigurationTitle', - value: 'Integrated Terminal' - } - }, - { - key: 'InteractiveSession', - name: { - key: 'interactiveSessionConfigurationTitle', - value: 'Chat' - } - }, - { - key: 'Telemetry', - name: { - key: 'telemetryConfigurationTitle', - value: 'Telemetry' - } - }, - { - key: 'Update', - name: { - key: 'updateConfigurationTitle', - value: 'Update' - } - } - ], - policies: [ - { - key: 'chat.mcp.gallery.serviceUrl', - name: 'McpGalleryServiceUrl', - category: 'InteractiveSession', - minimumVersion: '1.101', - localization: { - description: { - key: 'mcp.gallery.serviceUrl', - value: 'Configure the MCP Gallery service URL to connect to' - } - }, - type: 'string', - default: '' - }, - { - key: 'extensions.gallery.serviceUrl', - name: 'ExtensionGalleryServiceUrl', - category: 'Extensions', - minimumVersion: '1.99', - localization: { - description: { - key: 'extensions.gallery.serviceUrl', - value: 'Configure the Marketplace service URL to connect to' - } - }, - type: 'string', - default: '' - }, - { - key: 'extensions.allowed', - name: 'AllowedExtensions', - category: 'Extensions', - minimumVersion: '1.96', - localization: { - description: { - key: 'extensions.allowed.policy', - value: 'Specify a list of extensions that are allowed to use. This helps maintain a secure and consistent development environment by restricting the use of unauthorized extensions. More information: https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions' - } - }, - type: 'object', - default: '*' - }, - { - key: 'chat.tools.global.autoApprove', - name: 'ChatToolsAutoApprove', - category: 'InteractiveSession', - minimumVersion: '1.99', - localization: { - description: { - key: 'autoApprove2.description', - value: 'Global auto approve also known as "YOLO mode" disables manual approval completely for all tools in all workspaces, allowing the agent to act fully autonomously. This is extremely dangerous and is *never* recommended, even containerized environments like Codespaces and Dev Containers have user keys forwarded into the container that could be compromised.\n\nThis feature disables critical security protections and makes it much easier for an attacker to compromise the machine.' - } - }, - type: 'boolean', - default: false - }, - { - key: 'chat.mcp.access', - name: 'ChatMCP', - category: 'InteractiveSession', - minimumVersion: '1.99', - localization: { - description: { - key: 'chat.mcp.access', - value: 'Controls access to installed Model Context Protocol servers.' - }, - enumDescriptions: [ - { - key: 'chat.mcp.access.none', - value: 'No access to MCP servers.' - }, - { - key: 'chat.mcp.access.registry', - value: 'Allows access to MCP servers installed from the registry that VS Code is connected to.' - }, - { - key: 'chat.mcp.access.any', - value: 'Allow access to any installed MCP server.' - } - ] - }, - type: 'string', - default: 'all', - enum: [ - 'none', - 'registry', - 'all' - ] - }, - { - key: 'chat.extensionTools.enabled', - name: 'ChatAgentExtensionTools', - category: 'InteractiveSession', - minimumVersion: '1.99', - localization: { - description: { - key: 'chat.extensionToolsEnabled', - value: 'Enable using tools contributed by third-party extensions.' - } - }, - type: 'boolean', - default: true - }, - { - key: 'chat.agent.enabled', - name: 'ChatAgentMode', - category: 'InteractiveSession', - minimumVersion: '1.99', - localization: { - description: { - key: 'chat.agent.enabled.description', - value: 'Enable agent mode for chat. When this is enabled, agent mode can be activated via the dropdown in the view.' - } - }, - type: 'boolean', - default: true - }, - { - key: 'chat.promptFiles', - name: 'ChatPromptFiles', - category: 'InteractiveSession', - minimumVersion: '1.99', - localization: { - description: { - key: 'chat.promptFiles.policy', - value: 'Enables reusable prompt and instruction files in Chat sessions.' - } - }, - type: 'boolean', - default: true - }, - { - key: 'chat.tools.terminal.enableAutoApprove', - name: 'ChatToolsTerminalEnableAutoApprove', - category: 'IntegratedTerminal', - minimumVersion: '1.104', - localization: { - description: { - key: 'autoApproveMode.description', - value: 'Controls whether to allow auto approval in the run in terminal tool.' - } - }, - type: 'boolean', - default: true - }, - { - key: 'update.mode', - name: 'UpdateMode', - category: 'Update', - minimumVersion: '1.67', - localization: { - description: { - key: 'updateMode', - value: 'Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service.' - }, - enumDescriptions: [ - { - key: 'none', - value: 'Disable updates.' - }, - { - key: 'manual', - value: 'Disable automatic background update checks. Updates will be available if you manually check for updates.' - }, - { - key: 'start', - value: 'Check for updates only on startup. Disable automatic background update checks.' - }, - { - key: 'default', - value: 'Enable automatic update checks. Code will check for updates automatically and periodically.' - } - ] - }, - type: 'string', - default: 'default', - enum: [ - 'none', - 'manual', - 'start', - 'default' - ] - }, - { - key: 'telemetry.telemetryLevel', - name: 'TelemetryLevel', - category: 'Telemetry', - minimumVersion: '1.99', - localization: { - description: { - key: 'telemetry.telemetryLevel.policyDescription', - value: 'Controls the level of telemetry.' - }, - enumDescriptions: [ - { - key: 'telemetry.telemetryLevel.default', - value: 'Sends usage data, errors, and crash reports.' - }, - { - key: 'telemetry.telemetryLevel.error', - value: 'Sends general error telemetry and crash reports.' - }, - { - key: 'telemetry.telemetryLevel.crash', - value: 'Sends OS level crash reports.' - }, - { - key: 'telemetry.telemetryLevel.off', - value: 'Disables all product telemetry.' - } - ] - }, - type: 'string', - default: 'all', - enum: [ - 'all', - 'error', - 'crash', - 'off' - ] - }, - { - key: 'telemetry.feedback.enabled', - name: 'EnableFeedback', - category: 'Telemetry', - minimumVersion: '1.99', - localization: { - description: { - key: 'telemetry.feedback.enabled', - value: 'Enable feedback mechanisms such as the issue reporter, surveys, and other feedback options.' - } - }, - type: 'boolean', - default: true - } - ] -}; -const mockProduct = { - nameLong: 'Code - OSS', - darwinBundleIdentifier: 'com.visualstudio.code.oss', - darwinProfilePayloadUUID: 'CF808BE7-53F3-46C6-A7E2-7EDB98A5E959', - darwinProfileUUID: '47827DD9-4734-49A0-AF80-7E19B11495CC', - win32RegValueName: 'CodeOSS' -}; -const frenchTranslations = [ - { - languageId: 'fr-fr', - languageTranslations: { - '': { - 'interactiveSessionConfigurationTitle': 'Session interactive', - 'extensionsConfigurationTitle': 'Extensions', - 'terminalIntegratedConfigurationTitle': 'Terminal intégré', - 'telemetryConfigurationTitle': 'Télémétrie', - 'updateConfigurationTitle': 'Mettre à jour', - 'chat.extensionToolsEnabled': 'Autorisez l’utilisation d’outils fournis par des extensions tierces.', - 'chat.agent.enabled.description': 'Activez le mode Assistant pour la conversation. Lorsque cette option est activée, le mode Assistant peut être activé via la liste déroulante de la vue.', - 'chat.mcp.access': 'Contrôle l’accès aux serveurs de protocole de contexte du modèle.', - 'chat.mcp.access.none': 'Aucun accès aux serveurs MCP.', - 'chat.mcp.access.registry': `Autorise l’accès aux serveurs MCP installés à partir du registre auquel VS Code est connecté.`, - 'chat.mcp.access.any': 'Autorisez l’accès à tout serveur MCP installé.', - 'chat.promptFiles.policy': 'Active les fichiers d’instruction et de requête réutilisables dans les sessions Conversation.', - 'autoApprove2.description': `L’approbation automatique globale, également appelée « mode YOLO », désactive complètement l’approbation manuelle pour tous les outils dans tous les espaces de travail, permettant à l’agent d’agir de manière totalement autonome. Ceci est extrêmement dangereux et est *jamais* recommandé, même dans des environnements conteneurisés comme [Codespaces](https://github.com/features/codespaces) et [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers), où des clés utilisateur sont transférées dans le conteneur et pourraient être compromises. - -Cette fonctionnalité désactive [les protections de sécurité critiques](https://code.visualstudio.com/docs/copilot/security) et facilite considérablement la compromission de la machine par un attaquant.`, - 'mcp.gallery.serviceUrl': 'Configurer l’URL du service de la galerie MCP à laquelle se connecter', - 'extensions.allowed.policy': 'Spécifiez une liste d’extensions autorisées. Cela permet de maintenir un environnement de développement sécurisé et cohérent en limitant l’utilisation d’extensions non autorisées. Plus d’informations : https://code.visualstudio.com/docs/setup/enterprise#_configure-allowed-extensions', - 'extensions.gallery.serviceUrl': 'Configurer l’URL du service Place de marché à laquelle se connecter', - 'autoApproveMode.description': 'Contrôle s’il faut autoriser l’approbation automatique lors de l’exécution dans l’outil terminal.', - 'telemetry.feedback.enabled': 'Activez les mécanismes de commentaires tels que le système de rapport de problèmes, les sondages et autres options de commentaires.', - 'telemetry.telemetryLevel.policyDescription': 'Contrôle le niveau de télémétrie.', - 'telemetry.telemetryLevel.default': `Envoie les données d'utilisation, les erreurs et les rapports d'erreur.`, - 'telemetry.telemetryLevel.error': `Envoie la télémétrie d'erreur générale et les rapports de plantage.`, - 'telemetry.telemetryLevel.crash': `Envoie des rapports de plantage au niveau du système d'exploitation.`, - 'telemetry.telemetryLevel.off': 'Désactive toutes les données de télémétrie du produit.', - 'updateMode': `Choisissez si vous voulez recevoir des mises à jour automatiques. Nécessite un redémarrage après le changement. Les mises à jour sont récupérées auprès d'un service en ligne Microsoft.`, - 'none': 'Aucun', - 'manual': 'Désactivez la recherche de mises à jour automatique en arrière-plan. Les mises à jour sont disponibles si vous les rechercher manuellement.', - 'start': 'Démarrer', - 'default': 'Système' - } - } - } -]; -suite('Policy E2E conversion', () => { - test('should render macOS policy profile from policies list', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderMacOSPolicy)(mockProduct, parsedPolicies, []); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'darwin', 'com.visualstudio.code.oss.mobileconfig'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Compare the rendered profile with the fixture - assert_1.default.strictEqual(result.profile, expectedContent, 'macOS policy profile should match the fixture'); - }); - test('should render macOS manifest from policies list', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderMacOSPolicy)(mockProduct, parsedPolicies, []); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'darwin', 'en-us', 'com.visualstudio.code.oss.plist'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Find the en-us manifest - const enUsManifest = result.manifests.find(m => m.languageId === 'en-us'); - assert_1.default.ok(enUsManifest, 'en-us manifest should exist'); - // Compare the rendered manifest with the fixture, ignoring the timestamp - // The pfm_last_modified field contains a timestamp that will differ each time - const normalizeTimestamp = (content) => content.replace(/.*?<\/date>/, 'TIMESTAMP'); - assert_1.default.strictEqual(normalizeTimestamp(enUsManifest.contents), normalizeTimestamp(expectedContent), 'macOS manifest should match the fixture (ignoring timestamp)'); - }); - test('should render Windows ADMX from policies list', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderGP)(mockProduct, parsedPolicies, []); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'win32', 'CodeOSS.admx'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Compare the rendered ADMX with the fixture - assert_1.default.strictEqual(result.admx, expectedContent, 'Windows ADMX should match the fixture'); - }); - test('should render Windows ADML from policies list', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderGP)(mockProduct, parsedPolicies, []); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'win32', 'en-us', 'CodeOSS.adml'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Find the en-us ADML - const enUsAdml = result.adml.find(a => a.languageId === 'en-us'); - assert_1.default.ok(enUsAdml, 'en-us ADML should exist'); - // Compare the rendered ADML with the fixture - assert_1.default.strictEqual(enUsAdml.contents, expectedContent, 'Windows ADML should match the fixture'); - }); - test('should render macOS manifest with fr-fr locale', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderMacOSPolicy)(mockProduct, parsedPolicies, frenchTranslations); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'darwin', 'fr-fr', 'com.visualstudio.code.oss.plist'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Find the fr-fr manifest - const frFrManifest = result.manifests.find(m => m.languageId === 'fr-fr'); - assert_1.default.ok(frFrManifest, 'fr-fr manifest should exist'); - // Compare the rendered manifest with the fixture, ignoring the timestamp - const normalizeTimestamp = (content) => content.replace(/.*?<\/date>/, 'TIMESTAMP'); - assert_1.default.strictEqual(normalizeTimestamp(frFrManifest.contents), normalizeTimestamp(expectedContent), 'macOS fr-fr manifest should match the fixture (ignoring timestamp)'); - }); - test('should render Windows ADML with fr-fr locale', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderGP)(mockProduct, parsedPolicies, frenchTranslations); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'win32', 'fr-fr', 'CodeOSS.adml'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - // Find the fr-fr ADML - const frFrAdml = result.adml.find(a => a.languageId === 'fr-fr'); - assert_1.default.ok(frFrAdml, 'fr-fr ADML should exist'); - // Compare the rendered ADML with the fixture - assert_1.default.strictEqual(frFrAdml.contents, expectedContent, 'Windows fr-fr ADML should match the fixture'); - }); - test('should render Linux policy JSON from policies list', async () => { - const parsedPolicies = parsePolicies(policies); - const result = (0, render_1.renderJsonPolicies)(parsedPolicies); - // Load the expected fixture file - const fixturePath = path_1.default.join(__dirname, 'fixtures', 'policies', 'linux', 'policy.json'); - const expectedContent = await fs_1.promises.readFile(fixturePath, 'utf-8'); - const expectedJson = JSON.parse(expectedContent); - // Compare the rendered JSON with the fixture - assert_1.default.deepStrictEqual(result, expectedJson, 'Linux policy JSON should match the fixture'); - }); -}); -//# sourceMappingURL=policyConversion.test.js.map \ No newline at end of file diff --git a/build/lib/test/policyConversion.test.ts b/build/lib/test/policyConversion.test.ts index 0610b0cd980..bb4036a7ab9 100644 --- a/build/lib/test/policyConversion.test.ts +++ b/build/lib/test/policyConversion.test.ts @@ -6,14 +6,14 @@ import assert from 'assert'; import { promises as fs } from 'fs'; import path from 'path'; -import { ExportedPolicyDataDto, CategoryDto } from '../policies/policyDto'; -import { BooleanPolicy } from '../policies/booleanPolicy'; -import { NumberPolicy } from '../policies/numberPolicy'; -import { ObjectPolicy } from '../policies/objectPolicy'; -import { StringEnumPolicy } from '../policies/stringEnumPolicy'; -import { StringPolicy } from '../policies/stringPolicy'; -import { Policy, ProductJson } from '../policies/types'; -import { renderGP, renderMacOSPolicy, renderJsonPolicies } from '../policies/render'; +import type { ExportedPolicyDataDto, CategoryDto } from '../policies/policyDto.ts'; +import { BooleanPolicy } from '../policies/booleanPolicy.ts'; +import { NumberPolicy } from '../policies/numberPolicy.ts'; +import { ObjectPolicy } from '../policies/objectPolicy.ts'; +import { StringEnumPolicy } from '../policies/stringEnumPolicy.ts'; +import { StringPolicy } from '../policies/stringPolicy.ts'; +import type { Policy, ProductJson } from '../policies/types.ts'; +import { renderGP, renderMacOSPolicy, renderJsonPolicies } from '../policies/render.ts'; const PolicyTypes = [ BooleanPolicy, @@ -398,7 +398,7 @@ suite('Policy E2E conversion', () => { const result = renderMacOSPolicy(mockProduct, parsedPolicies, []); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'darwin', 'com.visualstudio.code.oss.mobileconfig'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'darwin', 'com.visualstudio.code.oss.mobileconfig'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Compare the rendered profile with the fixture @@ -410,7 +410,7 @@ suite('Policy E2E conversion', () => { const result = renderMacOSPolicy(mockProduct, parsedPolicies, []); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'darwin', 'en-us', 'com.visualstudio.code.oss.plist'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'darwin', 'en-us', 'com.visualstudio.code.oss.plist'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Find the en-us manifest @@ -432,7 +432,7 @@ suite('Policy E2E conversion', () => { const result = renderGP(mockProduct, parsedPolicies, []); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'win32', 'CodeOSS.admx'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'win32', 'CodeOSS.admx'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Compare the rendered ADMX with the fixture @@ -444,7 +444,7 @@ suite('Policy E2E conversion', () => { const result = renderGP(mockProduct, parsedPolicies, []); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'win32', 'en-us', 'CodeOSS.adml'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'win32', 'en-us', 'CodeOSS.adml'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Find the en-us ADML @@ -460,7 +460,7 @@ suite('Policy E2E conversion', () => { const result = renderMacOSPolicy(mockProduct, parsedPolicies, frenchTranslations); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'darwin', 'fr-fr', 'com.visualstudio.code.oss.plist'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'darwin', 'fr-fr', 'com.visualstudio.code.oss.plist'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Find the fr-fr manifest @@ -481,7 +481,7 @@ suite('Policy E2E conversion', () => { const result = renderGP(mockProduct, parsedPolicies, frenchTranslations); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'win32', 'fr-fr', 'CodeOSS.adml'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'win32', 'fr-fr', 'CodeOSS.adml'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); // Find the fr-fr ADML @@ -497,7 +497,7 @@ suite('Policy E2E conversion', () => { const result = renderJsonPolicies(parsedPolicies); // Load the expected fixture file - const fixturePath = path.join(__dirname, 'fixtures', 'policies', 'linux', 'policy.json'); + const fixturePath = path.join(import.meta.dirname, 'fixtures', 'policies', 'linux', 'policy.json'); const expectedContent = await fs.readFile(fixturePath, 'utf-8'); const expectedJson = JSON.parse(expectedContent); diff --git a/build/lib/test/render.test.js b/build/lib/test/render.test.js deleted file mode 100644 index 87c7fa14621..00000000000 --- a/build/lib/test/render.test.js +++ /dev/null @@ -1,855 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const render_js_1 = require("../policies/render.js"); -const types_js_1 = require("../policies/types.js"); -suite('Render Functions', () => { - suite('renderADMLString', () => { - test('should render ADML string without translations', () => { - const nlsString = { - value: 'Test description', - nlsKey: 'test.description' - }; - const result = (0, render_js_1.renderADMLString)('TestPrefix', 'testModule', nlsString); - assert_1.default.strictEqual(result, 'Test description'); - }); - test('should replace dots with underscores in nls key', () => { - const nlsString = { - value: 'Test value', - nlsKey: 'my.test.nls.key' - }; - const result = (0, render_js_1.renderADMLString)('Prefix', 'testModule', nlsString); - assert_1.default.ok(result.includes('id="Prefix_my_test_nls_key"')); - }); - test('should use translation when available', () => { - const nlsString = { - value: 'Original value', - nlsKey: 'test.key' - }; - const translations = { - 'testModule': { - 'test.key': 'Translated value' - } - }; - const result = (0, render_js_1.renderADMLString)('TestPrefix', 'testModule', nlsString, translations); - assert_1.default.ok(result.includes('>Translated value')); - }); - test('should fallback to original value when translation not found', () => { - const nlsString = { - value: 'Original value', - nlsKey: 'test.key' - }; - const translations = { - 'testModule': { - 'other.key': 'Other translation' - } - }; - const result = (0, render_js_1.renderADMLString)('TestPrefix', 'testModule', nlsString, translations); - assert_1.default.ok(result.includes('>Original value')); - }); - }); - suite('renderProfileString', () => { - test('should render profile string without translations', () => { - const nlsString = { - value: 'Profile description', - nlsKey: 'profile.description' - }; - const result = (0, render_js_1.renderProfileString)('ProfilePrefix', 'testModule', nlsString); - assert_1.default.strictEqual(result, 'Profile description'); - }); - test('should use translation when available', () => { - const nlsString = { - value: 'Original profile value', - nlsKey: 'profile.key' - }; - const translations = { - 'testModule': { - 'profile.key': 'Translated profile value' - } - }; - const result = (0, render_js_1.renderProfileString)('ProfilePrefix', 'testModule', nlsString, translations); - assert_1.default.strictEqual(result, 'Translated profile value'); - }); - test('should fallback to original value when translation not found', () => { - const nlsString = { - value: 'Original profile value', - nlsKey: 'profile.key' - }; - const translations = { - 'testModule': { - 'other.key': 'Other translation' - } - }; - const result = (0, render_js_1.renderProfileString)('ProfilePrefix', 'testModule', nlsString, translations); - assert_1.default.strictEqual(result, 'Original profile value'); - }); - }); - suite('renderADMX', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - const mockPolicy = { - name: 'TestPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.85', - renderADMX: (regKey) => [ - ``, - ` `, - `` - ], - renderADMLStrings: () => ['Test Policy'], - renderADMLPresentation: () => '', - renderProfile: () => ['TestPolicy', ''], - renderProfileManifest: () => 'pfm_nameTestPolicy', - renderJsonValue: () => null - }; - test('should render ADMX with correct XML structure', () => { - const result = (0, render_js_1.renderADMX)('VSCode', ['1.85'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - }); - test('should include policy namespaces with regKey', () => { - const result = (0, render_js_1.renderADMX)('TestApp', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes(' { - const result = (0, render_js_1.renderADMX)('VSCode', ['1.85.0', '1.90.1'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('Supported_1_85_0')); - assert_1.default.ok(result.includes('Supported_1_90_1')); - assert_1.default.ok(!result.includes('Supported_1.85.0')); - }); - test('should include categories in correct structure', () => { - const result = (0, render_js_1.renderADMX)('VSCode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - }); - test('should include policies section', () => { - const result = (0, render_js_1.renderADMX)('VSCode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('TestPolicy')); - assert_1.default.ok(result.includes('')); - }); - test('should handle multiple versions', () => { - const result = (0, render_js_1.renderADMX)('VSCode', ['1.0', '1.5', '2.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('Supported_1_0')); - assert_1.default.ok(result.includes('Supported_1_5')); - assert_1.default.ok(result.includes('Supported_2_0')); - }); - test('should handle multiple categories', () => { - const category1 = { moduleName: 'testModule', name: { value: 'Cat1', nlsKey: 'cat1' } }; - const category2 = { moduleName: 'testModule', name: { value: 'Cat2', nlsKey: 'cat2' } }; - const result = (0, render_js_1.renderADMX)('VSCode', ['1.0'], [category1, category2], [mockPolicy]); - assert_1.default.ok(result.includes('Category_cat1')); - assert_1.default.ok(result.includes('Category_cat2')); - }); - test('should handle multiple policies', () => { - const policy2 = { - name: 'TestPolicy2', - type: types_js_1.PolicyType.String, - category: mockCategory, - minimumVersion: '1.85', - renderADMX: (regKey) => [ - ``, - ` `, - `` - ], - renderADMLStrings: () => ['Test Policy 2'], - renderADMLPresentation: () => '', - renderProfile: () => ['TestPolicy2', ''], - renderProfileManifest: () => 'pfm_nameTestPolicy2', - renderJsonValue: () => null - }; - const result = (0, render_js_1.renderADMX)('VSCode', ['1.0'], [mockCategory], [mockPolicy, policy2]); - assert_1.default.ok(result.includes('TestPolicy')); - assert_1.default.ok(result.includes('TestPolicy2')); - }); - }); - suite('renderADML', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - const mockPolicy = { - name: 'TestPolicy', - type: types_js_1.PolicyType.String, - category: mockCategory, - minimumVersion: '1.85', - renderADMX: () => [], - renderADMLStrings: (translations) => [ - `Test Policy ${translations?.['testModule']?.['test.policy'] || 'Default'}` - ], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => null - }; - test('should render ADML with correct XML structure', () => { - const result = (0, render_js_1.renderADML)('VS Code', ['1.85'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - }); - test('should include application name', () => { - const result = (0, render_js_1.renderADML)('My Application', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('My Application')); - }); - test('should include supported versions with escaped greater-than', () => { - const result = (0, render_js_1.renderADML)('VS Code', ['1.85', '1.90'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('VS Code >= 1.85')); - assert_1.default.ok(result.includes('VS Code >= 1.90')); - }); - test('should include category strings', () => { - const result = (0, render_js_1.renderADML)('VS Code', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('Category_test_category')); - }); - test('should include policy strings', () => { - const result = (0, render_js_1.renderADML)('VS Code', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('TestPolicy')); - assert_1.default.ok(result.includes('Test Policy Default')); - }); - test('should include policy presentations', () => { - const result = (0, render_js_1.renderADML)('VS Code', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - }); - test('should pass translations to policy strings', () => { - const translations = { - 'testModule': { - 'test.policy': 'Translated' - } - }; - const result = (0, render_js_1.renderADML)('VS Code', ['1.0'], [mockCategory], [mockPolicy], translations); - assert_1.default.ok(result.includes('Test Policy Translated')); - }); - test('should handle multiple categories', () => { - const category1 = { moduleName: 'testModule', name: { value: 'Cat1', nlsKey: 'cat1' } }; - const category2 = { moduleName: 'testModule', name: { value: 'Cat2', nlsKey: 'cat2' } }; - const result = (0, render_js_1.renderADML)('VS Code', ['1.0'], [category1, category2], [mockPolicy]); - assert_1.default.ok(result.includes('Category_cat1')); - assert_1.default.ok(result.includes('Category_cat2')); - }); - }); - suite('renderProfileManifest', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - const mockPolicy = { - name: 'TestPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: (translations) => ` -pfm_name -TestPolicy -pfm_description -${translations?.['testModule']?.['test.desc'] || 'Default Desc'} -`, - renderJsonValue: () => null - }; - test('should render profile manifest with correct XML structure', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - assert_1.default.ok(result.includes('')); - }); - test('should include app name', () => { - const result = (0, render_js_1.renderProfileManifest)('My App', 'com.example.myapp', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('My App Managed Settings')); - assert_1.default.ok(result.includes('My App')); - }); - test('should include bundle identifier', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('com.microsoft.vscode')); - }); - test('should include required payload fields', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('PayloadDescription')); - assert_1.default.ok(result.includes('PayloadDisplayName')); - assert_1.default.ok(result.includes('PayloadIdentifier')); - assert_1.default.ok(result.includes('PayloadType')); - assert_1.default.ok(result.includes('PayloadUUID')); - assert_1.default.ok(result.includes('PayloadVersion')); - assert_1.default.ok(result.includes('PayloadOrganization')); - }); - test('should include policy manifests in subkeys', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_subkeys')); - assert_1.default.ok(result.includes('TestPolicy')); - assert_1.default.ok(result.includes('Default Desc')); - }); - test('should pass translations to policy manifests', () => { - const translations = { - 'testModule': { - 'test.desc': 'Translated Description' - } - }; - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy], translations); - assert_1.default.ok(result.includes('Translated Description')); - }); - test('should include VS Code specific URLs', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('https://code.visualstudio.com/')); - assert_1.default.ok(result.includes('https://code.visualstudio.com/docs/setup/enterprise')); - }); - test('should include last modified date', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_last_modified')); - assert_1.default.ok(result.includes('')); - }); - test('should mark manifest as unique', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_unique')); - assert_1.default.ok(result.includes('')); - }); - test('should handle multiple policies', () => { - const policy2 = { - ...mockPolicy, - name: 'TestPolicy2', - renderProfileManifest: () => ` -pfm_name -TestPolicy2 -` - }; - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy, policy2]); - assert_1.default.ok(result.includes('TestPolicy')); - assert_1.default.ok(result.includes('TestPolicy2')); - }); - test('should set format version to 1', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_format_version')); - assert_1.default.ok(result.includes('1')); - }); - test('should set interaction to combined', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_interaction')); - assert_1.default.ok(result.includes('combined')); - }); - test('should set platform to macOS', () => { - const result = (0, render_js_1.renderProfileManifest)('VS Code', 'com.microsoft.vscode', ['1.0'], [mockCategory], [mockPolicy]); - assert_1.default.ok(result.includes('pfm_platforms')); - assert_1.default.ok(result.includes('macOS')); - }); - }); - suite('renderMacOSPolicy', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - const mockPolicy = { - name: 'TestPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => ['TestPolicy', ''], - renderProfileManifest: (translations) => ` -pfm_name -TestPolicy -pfm_description -${translations?.['testModule']?.['test.desc'] || 'Default Desc'} -`, - renderJsonValue: () => null - }; - test('should render complete macOS policy profile', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], []); - const expected = ` - - - - PayloadContent - - - PayloadDisplayName - VS Code - PayloadIdentifier - com.microsoft.vscode.uuid - PayloadType - com.microsoft.vscode - PayloadUUID - uuid - PayloadVersion - 1 - TestPolicy - - - - PayloadDescription - This profile manages VS Code. For more information see https://code.visualstudio.com/docs/setup/enterprise - PayloadDisplayName - VS Code - PayloadIdentifier - com.microsoft.vscode - PayloadOrganization - Microsoft - PayloadType - Configuration - PayloadUUID - payload-uuid - PayloadVersion - 1 - TargetDeviceType - 5 - -`; - assert_1.default.strictEqual(result.profile, expected); - }); - test('should include en-us manifest by default', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], []); - assert_1.default.strictEqual(result.manifests.length, 1); - assert_1.default.strictEqual(result.manifests[0].languageId, 'en-us'); - assert_1.default.ok(result.manifests[0].contents.includes('VS Code Managed Settings')); - }); - test('should include translations', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const translations = [ - { languageId: 'fr-fr', languageTranslations: { 'testModule': { 'test.desc': 'Description Française' } } }, - { languageId: 'de-de', languageTranslations: { 'testModule': { 'test.desc': 'Deutsche Beschreibung' } } } - ]; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], translations); - assert_1.default.strictEqual(result.manifests.length, 3); // en-us + 2 translations - assert_1.default.strictEqual(result.manifests[0].languageId, 'en-us'); - assert_1.default.strictEqual(result.manifests[1].languageId, 'fr-fr'); - assert_1.default.strictEqual(result.manifests[2].languageId, 'de-de'); - assert_1.default.ok(result.manifests[1].contents.includes('Description Française')); - assert_1.default.ok(result.manifests[2].contents.includes('Deutsche Beschreibung')); - }); - test('should handle multiple policies with correct indentation', () => { - const policy2 = { - ...mockPolicy, - name: 'TestPolicy2', - renderProfile: () => ['TestPolicy2', 'test value'] - }; - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy, policy2], []); - assert_1.default.ok(result.profile.includes('TestPolicy')); - assert_1.default.ok(result.profile.includes('')); - assert_1.default.ok(result.profile.includes('TestPolicy2')); - assert_1.default.ok(result.profile.includes('test value')); - }); - test('should use provided UUIDs in profile', () => { - const product = { - nameLong: 'My App', - darwinBundleIdentifier: 'com.example.app', - darwinProfilePayloadUUID: 'custom-payload-uuid', - darwinProfileUUID: 'custom-uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], []); - assert_1.default.ok(result.profile.includes('custom-payload-uuid')); - assert_1.default.ok(result.profile.includes('custom-uuid')); - assert_1.default.ok(result.profile.includes('com.example.app.custom-uuid')); - }); - test('should include enterprise documentation link', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], []); - assert_1.default.ok(result.profile.includes('https://code.visualstudio.com/docs/setup/enterprise')); - }); - test('should set TargetDeviceType to 5', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderMacOSPolicy)(product, [mockPolicy], []); - assert_1.default.ok(result.profile.includes('TargetDeviceType')); - assert_1.default.ok(result.profile.includes('5')); - }); - }); - suite('renderGP', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - const mockPolicy = { - name: 'TestPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.85', - renderADMX: (regKey) => [ - ``, - ` `, - `` - ], - renderADMLStrings: (translations) => [ - `${translations?.['testModule']?.['test.policy'] || 'Test Policy'}` - ], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => null - }; - test('should render complete GP with ADMX and ADML', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.admx); - assert_1.default.ok(result.adml); - assert_1.default.ok(Array.isArray(result.adml)); - }); - test('should include regKey in ADMX', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'CustomRegKey' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.admx.includes('CustomRegKey')); - assert_1.default.ok(result.admx.includes('Software\\Policies\\Microsoft\\CustomRegKey')); - }); - test('should include en-us ADML by default', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.strictEqual(result.adml.length, 1); - assert_1.default.strictEqual(result.adml[0].languageId, 'en-us'); - assert_1.default.ok(result.adml[0].contents.includes('VS Code')); - }); - test('should include translations in ADML', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const translations = [ - { languageId: 'fr-fr', languageTranslations: { 'testModule': { 'test.policy': 'Politique de test' } } }, - { languageId: 'de-de', languageTranslations: { 'testModule': { 'test.policy': 'Testrichtlinie' } } } - ]; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], translations); - assert_1.default.strictEqual(result.adml.length, 3); // en-us + 2 translations - assert_1.default.strictEqual(result.adml[0].languageId, 'en-us'); - assert_1.default.strictEqual(result.adml[1].languageId, 'fr-fr'); - assert_1.default.strictEqual(result.adml[2].languageId, 'de-de'); - assert_1.default.ok(result.adml[1].contents.includes('Politique de test')); - assert_1.default.ok(result.adml[2].contents.includes('Testrichtlinie')); - }); - test('should pass versions to ADMX', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.admx.includes('Supported_1_85')); - }); - test('should pass versions to ADML', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.adml[0].contents.includes('VS Code >= 1.85')); - }); - test('should pass categories to ADMX', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.admx.includes('test.category')); - }); - test('should pass categories to ADML', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.adml[0].contents.includes('Category_test_category')); - }); - test('should handle multiple policies', () => { - const policy2 = { - ...mockPolicy, - name: 'TestPolicy2', - renderADMX: (regKey) => [ - ``, - ` `, - `` - ], - renderADMLStrings: () => ['Test Policy 2'] - }; - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy, policy2], []); - assert_1.default.ok(result.admx.includes('TestPolicy')); - assert_1.default.ok(result.admx.includes('TestPolicy2')); - assert_1.default.ok(result.adml[0].contents.includes('TestPolicy')); - assert_1.default.ok(result.adml[0].contents.includes('TestPolicy2')); - }); - test('should include app name in ADML', () => { - const product = { - nameLong: 'My Custom App', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok(result.adml[0].contents.includes('My Custom App')); - }); - test('should return structured result with admx and adml properties', () => { - const product = { - nameLong: 'VS Code', - darwinBundleIdentifier: 'com.microsoft.vscode', - darwinProfilePayloadUUID: 'payload-uuid', - darwinProfileUUID: 'uuid', - win32RegValueName: 'VSCode' - }; - const result = (0, render_js_1.renderGP)(product, [mockPolicy], []); - assert_1.default.ok('admx' in result); - assert_1.default.ok('adml' in result); - assert_1.default.strictEqual(typeof result.admx, 'string'); - assert_1.default.ok(Array.isArray(result.adml)); - }); - }); - suite('renderJsonPolicies', () => { - const mockCategory = { - moduleName: 'testModule', - name: { value: 'Test Category', nlsKey: 'test.category' } - }; - test('should render boolean policy JSON value', () => { - const booleanPolicy = { - name: 'BooleanPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => false - }; - const result = (0, render_js_1.renderJsonPolicies)([booleanPolicy]); - assert_1.default.deepStrictEqual(result, { BooleanPolicy: false }); - }); - test('should render number policy JSON value', () => { - const numberPolicy = { - name: 'NumberPolicy', - type: types_js_1.PolicyType.Number, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => 42 - }; - const result = (0, render_js_1.renderJsonPolicies)([numberPolicy]); - assert_1.default.deepStrictEqual(result, { NumberPolicy: 42 }); - }); - test('should render string policy JSON value', () => { - const stringPolicy = { - name: 'StringPolicy', - type: types_js_1.PolicyType.String, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => '' - }; - const result = (0, render_js_1.renderJsonPolicies)([stringPolicy]); - assert_1.default.deepStrictEqual(result, { StringPolicy: '' }); - }); - test('should render string enum policy JSON value', () => { - const stringEnumPolicy = { - name: 'StringEnumPolicy', - type: types_js_1.PolicyType.StringEnum, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => 'auto' - }; - const result = (0, render_js_1.renderJsonPolicies)([stringEnumPolicy]); - assert_1.default.deepStrictEqual(result, { StringEnumPolicy: 'auto' }); - }); - test('should render object policy JSON value', () => { - const objectPolicy = { - name: 'ObjectPolicy', - type: types_js_1.PolicyType.Object, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => '' - }; - const result = (0, render_js_1.renderJsonPolicies)([objectPolicy]); - assert_1.default.deepStrictEqual(result, { ObjectPolicy: '' }); - }); - test('should render multiple policies', () => { - const booleanPolicy = { - name: 'BooleanPolicy', - type: types_js_1.PolicyType.Boolean, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => true - }; - const numberPolicy = { - name: 'NumberPolicy', - type: types_js_1.PolicyType.Number, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => 100 - }; - const stringPolicy = { - name: 'StringPolicy', - type: types_js_1.PolicyType.String, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => 'test-value' - }; - const result = (0, render_js_1.renderJsonPolicies)([booleanPolicy, numberPolicy, stringPolicy]); - assert_1.default.deepStrictEqual(result, { - BooleanPolicy: true, - NumberPolicy: 100, - StringPolicy: 'test-value' - }); - }); - test('should handle empty policies array', () => { - const result = (0, render_js_1.renderJsonPolicies)([]); - assert_1.default.deepStrictEqual(result, {}); - }); - test('should handle null JSON value', () => { - const nullPolicy = { - name: 'NullPolicy', - type: types_js_1.PolicyType.String, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => null - }; - const result = (0, render_js_1.renderJsonPolicies)([nullPolicy]); - assert_1.default.deepStrictEqual(result, { NullPolicy: null }); - }); - test('should handle object JSON value', () => { - const objectPolicy = { - name: 'ComplexObjectPolicy', - type: types_js_1.PolicyType.Object, - category: mockCategory, - minimumVersion: '1.0', - renderADMX: () => [], - renderADMLStrings: () => [], - renderADMLPresentation: () => '', - renderProfile: () => [], - renderProfileManifest: () => '', - renderJsonValue: () => ({ nested: { value: 123 } }) - }; - const result = (0, render_js_1.renderJsonPolicies)([objectPolicy]); - assert_1.default.deepStrictEqual(result, { ComplexObjectPolicy: { nested: { value: 123 } } }); - }); - }); -}); -//# sourceMappingURL=render.test.js.map \ No newline at end of file diff --git a/build/lib/test/render.test.ts b/build/lib/test/render.test.ts index 325831247c4..130bbc78132 100644 --- a/build/lib/test/render.test.ts +++ b/build/lib/test/render.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { renderADMLString, renderProfileString, renderADMX, renderADML, renderProfileManifest, renderMacOSPolicy, renderGP, renderJsonPolicies } from '../policies/render.js'; -import { NlsString, LanguageTranslations, Category, Policy, PolicyType } from '../policies/types.js'; +import { renderADMLString, renderProfileString, renderADMX, renderADML, renderProfileManifest, renderMacOSPolicy, renderGP, renderJsonPolicies } from '../policies/render.ts'; +import { type NlsString, type LanguageTranslations, type Category, type Policy, PolicyType } from '../policies/types.ts'; suite('Render Functions', () => { diff --git a/build/lib/test/stringEnumPolicy.test.js b/build/lib/test/stringEnumPolicy.test.js deleted file mode 100644 index d1700730544..00000000000 --- a/build/lib/test/stringEnumPolicy.test.js +++ /dev/null @@ -1,142 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const stringEnumPolicy_js_1 = require("../policies/stringEnumPolicy.js"); -const types_js_1 = require("../policies/types.js"); -suite('StringEnumPolicy', () => { - const mockCategory = { - key: 'test.category', - name: { value: 'Category1', key: 'test.category' }, - }; - const mockPolicy = { - key: 'test.stringenum.policy', - name: 'TestStringEnumPolicy', - category: 'Category1', - minimumVersion: '1.0', - type: 'string', - localization: { - description: { key: 'test.policy.description', value: 'Test policy description' }, - enumDescriptions: [ - { key: 'test.option.one', value: 'Option One' }, - { key: 'test.option.two', value: 'Option Two' }, - { key: 'test.option.three', value: 'Option Three' } - ] - }, - enum: ['auto', 'manual', 'disabled'] - }; - test('should create StringEnumPolicy from factory method', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - assert_1.default.strictEqual(policy.name, 'TestStringEnumPolicy'); - assert_1.default.strictEqual(policy.minimumVersion, '1.0'); - assert_1.default.strictEqual(policy.category.name.nlsKey, mockCategory.name.key); - assert_1.default.strictEqual(policy.category.name.value, mockCategory.name.value); - assert_1.default.strictEqual(policy.type, types_js_1.PolicyType.StringEnum); - }); - test('should render ADMX elements correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admx = policy.renderADMX('TestKey'); - assert_1.default.deepStrictEqual(admx, [ - '', - '\t', - '\t', - '\t', - '', - '\tauto', - '\tmanual', - '\tdisabled', - '', - '\t', - '' - ]); - }); - test('should render ADML strings correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admlStrings = policy.renderADMLStrings(); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestStringEnumPolicy', - 'Test policy description', - 'Option One', - 'Option Two', - 'Option Three' - ]); - }); - test('should render ADML strings with translations', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated description', - 'test.option.one': 'Translated Option One', - 'test.option.two': 'Translated Option Two' - } - }; - const admlStrings = policy.renderADMLStrings(translations); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestStringEnumPolicy', - 'Translated description', - 'Translated Option One', - 'Translated Option Two', - 'Option Three' - ]); - }); - test('should render ADML presentation correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const presentation = policy.renderADMLPresentation(); - assert_1.default.strictEqual(presentation, ''); - }); - test('should render JSON value correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const jsonValue = policy.renderJsonValue(); - assert_1.default.strictEqual(jsonValue, 'auto'); - }); - test('should render profile value correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profileValue = policy.renderProfileValue(); - assert_1.default.strictEqual(profileValue, 'auto'); - }); - test('should render profile correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profile = policy.renderProfile(); - assert_1.default.strictEqual(profile.length, 2); - assert_1.default.strictEqual(profile[0], 'TestStringEnumPolicy'); - assert_1.default.strictEqual(profile[1], 'auto'); - }); - test('should render profile manifest value correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifestValue = policy.renderProfileManifestValue(); - assert_1.default.strictEqual(manifestValue, 'pfm_default\nauto\npfm_description\nTest policy description\npfm_name\nTestStringEnumPolicy\npfm_title\nTestStringEnumPolicy\npfm_type\nstring\npfm_range_list\n\n\tauto\n\tmanual\n\tdisabled\n'); - }); - test('should render profile manifest value with translations', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated manifest description' - } - }; - const manifestValue = policy.renderProfileManifestValue(translations); - assert_1.default.strictEqual(manifestValue, 'pfm_default\nauto\npfm_description\nTranslated manifest description\npfm_name\nTestStringEnumPolicy\npfm_title\nTestStringEnumPolicy\npfm_type\nstring\npfm_range_list\n\n\tauto\n\tmanual\n\tdisabled\n'); - }); - test('should render profile manifest correctly', () => { - const policy = stringEnumPolicy_js_1.StringEnumPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifest = policy.renderProfileManifest(); - assert_1.default.strictEqual(manifest, '\npfm_default\nauto\npfm_description\nTest policy description\npfm_name\nTestStringEnumPolicy\npfm_title\nTestStringEnumPolicy\npfm_type\nstring\npfm_range_list\n\n\tauto\n\tmanual\n\tdisabled\n\n'); - }); -}); -//# sourceMappingURL=stringEnumPolicy.test.js.map \ No newline at end of file diff --git a/build/lib/test/stringEnumPolicy.test.ts b/build/lib/test/stringEnumPolicy.test.ts index 3ee3856afd7..f27f9ec0a17 100644 --- a/build/lib/test/stringEnumPolicy.test.ts +++ b/build/lib/test/stringEnumPolicy.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { StringEnumPolicy } from '../policies/stringEnumPolicy.js'; -import { LanguageTranslations, PolicyType } from '../policies/types.js'; -import { CategoryDto, PolicyDto } from '../policies/policyDto.js'; +import { StringEnumPolicy } from '../policies/stringEnumPolicy.ts'; +import { PolicyType, type LanguageTranslations } from '../policies/types.ts'; +import type { CategoryDto, PolicyDto } from '../policies/policyDto.ts'; suite('StringEnumPolicy', () => { const mockCategory: CategoryDto = { @@ -55,9 +55,9 @@ suite('StringEnumPolicy', () => { '\t', '\t', '', - '\tauto', - '\tmanual', - '\tdisabled', + '\tauto', + '\tmanual', + '\tdisabled', '', '\t', '' diff --git a/build/lib/test/stringPolicy.test.js b/build/lib/test/stringPolicy.test.js deleted file mode 100644 index 6919da78f88..00000000000 --- a/build/lib/test/stringPolicy.test.js +++ /dev/null @@ -1,125 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const assert_1 = __importDefault(require("assert")); -const stringPolicy_js_1 = require("../policies/stringPolicy.js"); -const types_js_1 = require("../policies/types.js"); -suite('StringPolicy', () => { - const mockCategory = { - key: 'test.category', - name: { value: 'Category1', key: 'test.category' }, - }; - const mockPolicy = { - key: 'test.string.policy', - name: 'TestStringPolicy', - category: 'Category1', - minimumVersion: '1.0', - type: 'string', - default: '', - localization: { - description: { key: 'test.policy.description', value: 'Test string policy description' } - } - }; - test('should create StringPolicy from factory method', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - assert_1.default.strictEqual(policy.name, 'TestStringPolicy'); - assert_1.default.strictEqual(policy.minimumVersion, '1.0'); - assert_1.default.strictEqual(policy.category.name.nlsKey, mockCategory.name.key); - assert_1.default.strictEqual(policy.category.name.value, mockCategory.name.value); - assert_1.default.strictEqual(policy.type, types_js_1.PolicyType.String); - }); - test('should render ADMX elements correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admx = policy.renderADMX('TestKey'); - assert_1.default.deepStrictEqual(admx, [ - '', - '\t', - '\t', - '\t', - '', - '\t', - '' - ]); - }); - test('should render ADML strings correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const admlStrings = policy.renderADMLStrings(); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestStringPolicy', - 'Test string policy description' - ]); - }); - test('should render ADML strings with translations', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated description' - } - }; - const admlStrings = policy.renderADMLStrings(translations); - assert_1.default.deepStrictEqual(admlStrings, [ - 'TestStringPolicy', - 'Translated description' - ]); - }); - test('should render ADML presentation correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const presentation = policy.renderADMLPresentation(); - assert_1.default.strictEqual(presentation, ''); - }); - test('should render JSON value correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const jsonValue = policy.renderJsonValue(); - assert_1.default.strictEqual(jsonValue, ''); - }); - test('should render profile value correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profileValue = policy.renderProfileValue(); - assert_1.default.strictEqual(profileValue, ''); - }); - test('should render profile correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const profile = policy.renderProfile(); - assert_1.default.strictEqual(profile.length, 2); - assert_1.default.strictEqual(profile[0], 'TestStringPolicy'); - assert_1.default.strictEqual(profile[1], ''); - }); - test('should render profile manifest value correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifestValue = policy.renderProfileManifestValue(); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTest string policy description\npfm_name\nTestStringPolicy\npfm_title\nTestStringPolicy\npfm_type\nstring'); - }); - test('should render profile manifest value with translations', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const translations = { - '': { - 'test.policy.description': 'Translated manifest description' - } - }; - const manifestValue = policy.renderProfileManifestValue(translations); - assert_1.default.strictEqual(manifestValue, 'pfm_default\n\npfm_description\nTranslated manifest description\npfm_name\nTestStringPolicy\npfm_title\nTestStringPolicy\npfm_type\nstring'); - }); - test('should render profile manifest correctly', () => { - const policy = stringPolicy_js_1.StringPolicy.from(mockCategory, mockPolicy); - assert_1.default.ok(policy); - const manifest = policy.renderProfileManifest(); - assert_1.default.strictEqual(manifest, '\npfm_default\n\npfm_description\nTest string policy description\npfm_name\nTestStringPolicy\npfm_title\nTestStringPolicy\npfm_type\nstring\n'); - }); -}); -//# sourceMappingURL=stringPolicy.test.js.map \ No newline at end of file diff --git a/build/lib/test/stringPolicy.test.ts b/build/lib/test/stringPolicy.test.ts index a76c38c7dcb..7f69da33869 100644 --- a/build/lib/test/stringPolicy.test.ts +++ b/build/lib/test/stringPolicy.test.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { StringPolicy } from '../policies/stringPolicy.js'; -import { LanguageTranslations, PolicyType } from '../policies/types.js'; -import { CategoryDto, PolicyDto } from '../policies/policyDto.js'; +import { StringPolicy } from '../policies/stringPolicy.ts'; +import { PolicyType, type LanguageTranslations } from '../policies/types.ts'; +import type { CategoryDto, PolicyDto } from '../policies/policyDto.ts'; suite('StringPolicy', () => { const mockCategory: CategoryDto = { diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js deleted file mode 100644 index feca811d9f9..00000000000 --- a/build/lib/treeshaking.js +++ /dev/null @@ -1,778 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.toStringShakeLevel = toStringShakeLevel; -exports.shake = shake; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const typeScriptLanguageServiceHost_1 = require("./typeScriptLanguageServiceHost"); -var ShakeLevel; -(function (ShakeLevel) { - ShakeLevel[ShakeLevel["Files"] = 0] = "Files"; - ShakeLevel[ShakeLevel["InnerFile"] = 1] = "InnerFile"; - ShakeLevel[ShakeLevel["ClassMembers"] = 2] = "ClassMembers"; -})(ShakeLevel || (ShakeLevel = {})); -function toStringShakeLevel(shakeLevel) { - switch (shakeLevel) { - case ShakeLevel.Files: - return 'Files (0)'; - case ShakeLevel.InnerFile: - return 'InnerFile (1)'; - case ShakeLevel.ClassMembers: - return 'ClassMembers (2)'; - } -} -function printDiagnostics(options, diagnostics) { - for (const diag of diagnostics) { - let result = ''; - if (diag.file) { - result += `${path_1.default.join(options.sourcesRoot, diag.file.fileName)}`; - } - if (diag.file && diag.start) { - const location = diag.file.getLineAndCharacterOfPosition(diag.start); - result += `:${location.line + 1}:${location.character}`; - } - result += ` - ` + JSON.stringify(diag.messageText); - console.log(result); - } -} -function shake(options) { - const ts = require('typescript'); - const languageService = createTypeScriptLanguageService(ts, options); - const program = languageService.getProgram(); - const globalDiagnostics = program.getGlobalDiagnostics(); - if (globalDiagnostics.length > 0) { - printDiagnostics(options, globalDiagnostics); - throw new Error(`Compilation Errors encountered.`); - } - const syntacticDiagnostics = program.getSyntacticDiagnostics(); - if (syntacticDiagnostics.length > 0) { - printDiagnostics(options, syntacticDiagnostics); - throw new Error(`Compilation Errors encountered.`); - } - const semanticDiagnostics = program.getSemanticDiagnostics(); - if (semanticDiagnostics.length > 0) { - printDiagnostics(options, semanticDiagnostics); - throw new Error(`Compilation Errors encountered.`); - } - markNodes(ts, languageService, options); - return generateResult(ts, languageService, options.shakeLevel); -} -//#region Discovery, LanguageService & Setup -function createTypeScriptLanguageService(ts, options) { - // Discover referenced files - const FILES = new Map(); - // Add entrypoints - options.entryPoints.forEach(entryPoint => { - const filePath = path_1.default.join(options.sourcesRoot, entryPoint); - FILES.set(path_1.default.normalize(filePath), fs_1.default.readFileSync(filePath).toString()); - }); - // Add fake usage files - options.inlineEntryPoints.forEach((inlineEntryPoint, index) => { - FILES.set(path_1.default.normalize(path_1.default.join(options.sourcesRoot, `inlineEntryPoint.${index}.ts`)), inlineEntryPoint); - }); - // Add additional typings - options.typings.forEach((typing) => { - const filePath = path_1.default.join(options.sourcesRoot, typing); - FILES.set(path_1.default.normalize(filePath), fs_1.default.readFileSync(filePath).toString()); - }); - const basePath = path_1.default.join(options.sourcesRoot, '..'); - const compilerOptions = ts.convertCompilerOptionsFromJson(options.compilerOptions, basePath).options; - const host = new typeScriptLanguageServiceHost_1.TypeScriptLanguageServiceHost(ts, FILES, compilerOptions); - return ts.createLanguageService(host); -} -//#endregion -//#region Tree Shaking -var NodeColor; -(function (NodeColor) { - NodeColor[NodeColor["White"] = 0] = "White"; - NodeColor[NodeColor["Gray"] = 1] = "Gray"; - NodeColor[NodeColor["Black"] = 2] = "Black"; -})(NodeColor || (NodeColor = {})); -function getColor(node) { - return node.$$$color || 0 /* NodeColor.White */; -} -function setColor(node, color) { - node.$$$color = color; -} -function markNeededSourceFile(node) { - node.$$$neededSourceFile = true; -} -function isNeededSourceFile(node) { - return Boolean(node.$$$neededSourceFile); -} -function nodeOrParentIsBlack(node) { - while (node) { - const color = getColor(node); - if (color === 2 /* NodeColor.Black */) { - return true; - } - node = node.parent; - } - return false; -} -function nodeOrChildIsBlack(node) { - if (getColor(node) === 2 /* NodeColor.Black */) { - return true; - } - for (const child of node.getChildren()) { - if (nodeOrChildIsBlack(child)) { - return true; - } - } - return false; -} -function isSymbolWithDeclarations(symbol) { - return !!(symbol && symbol.declarations); -} -function isVariableStatementWithSideEffects(ts, node) { - if (!ts.isVariableStatement(node)) { - return false; - } - let hasSideEffects = false; - const visitNode = (node) => { - if (hasSideEffects) { - // no need to go on - return; - } - if (ts.isCallExpression(node) || ts.isNewExpression(node)) { - // TODO: assuming `createDecorator` and `refineServiceDecorator` calls are side-effect free - const isSideEffectFree = /(createDecorator|refineServiceDecorator)/.test(node.expression.getText()); - if (!isSideEffectFree) { - hasSideEffects = true; - } - } - node.forEachChild(visitNode); - }; - node.forEachChild(visitNode); - return hasSideEffects; -} -function isStaticMemberWithSideEffects(ts, node) { - if (!ts.isPropertyDeclaration(node)) { - return false; - } - if (!node.modifiers) { - return false; - } - if (!node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) { - return false; - } - let hasSideEffects = false; - const visitNode = (node) => { - if (hasSideEffects) { - // no need to go on - return; - } - if (ts.isCallExpression(node) || ts.isNewExpression(node)) { - hasSideEffects = true; - } - node.forEachChild(visitNode); - }; - node.forEachChild(visitNode); - return hasSideEffects; -} -function markNodes(ts, languageService, options) { - const program = languageService.getProgram(); - if (!program) { - throw new Error('Could not get program from language service'); - } - if (options.shakeLevel === ShakeLevel.Files) { - // Mark all source files Black - program.getSourceFiles().forEach((sourceFile) => { - setColor(sourceFile, 2 /* NodeColor.Black */); - }); - return; - } - const black_queue = []; - const gray_queue = []; - const export_import_queue = []; - const sourceFilesLoaded = {}; - function enqueueTopLevelModuleStatements(sourceFile) { - sourceFile.forEachChild((node) => { - if (ts.isImportDeclaration(node)) { - if (!node.importClause && ts.isStringLiteral(node.moduleSpecifier)) { - setColor(node, 2 /* NodeColor.Black */); - enqueueImport(node, node.moduleSpecifier.text); - } - return; - } - if (ts.isExportDeclaration(node)) { - if (!node.exportClause && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { - // export * from "foo"; - setColor(node, 2 /* NodeColor.Black */); - enqueueImport(node, node.moduleSpecifier.text); - } - if (node.exportClause && ts.isNamedExports(node.exportClause)) { - for (const exportSpecifier of node.exportClause.elements) { - export_import_queue.push(exportSpecifier); - } - } - return; - } - if (isVariableStatementWithSideEffects(ts, node)) { - enqueue_black(node); - } - if (ts.isExpressionStatement(node) - || ts.isIfStatement(node) - || ts.isIterationStatement(node, true) - || ts.isExportAssignment(node)) { - enqueue_black(node); - } - if (ts.isImportEqualsDeclaration(node)) { - if (/export/.test(node.getFullText(sourceFile))) { - // e.g. "export import Severity = BaseSeverity;" - enqueue_black(node); - } - } - }); - } - /** - * Return the parent of `node` which is an ImportDeclaration - */ - function findParentImportDeclaration(node) { - let _node = node; - do { - if (ts.isImportDeclaration(_node)) { - return _node; - } - _node = _node.parent; - } while (_node); - return null; - } - function enqueue_gray(node) { - if (nodeOrParentIsBlack(node) || getColor(node) === 1 /* NodeColor.Gray */) { - return; - } - setColor(node, 1 /* NodeColor.Gray */); - gray_queue.push(node); - } - function enqueue_black(node) { - const previousColor = getColor(node); - if (previousColor === 2 /* NodeColor.Black */) { - return; - } - if (previousColor === 1 /* NodeColor.Gray */) { - // remove from gray queue - gray_queue.splice(gray_queue.indexOf(node), 1); - setColor(node, 0 /* NodeColor.White */); - // add to black queue - enqueue_black(node); - // move from one queue to the other - // black_queue.push(node); - // setColor(node, NodeColor.Black); - return; - } - if (nodeOrParentIsBlack(node)) { - return; - } - const fileName = node.getSourceFile().fileName; - if (/^defaultLib:/.test(fileName) || /\.d\.ts$/.test(fileName)) { - setColor(node, 2 /* NodeColor.Black */); - return; - } - const sourceFile = node.getSourceFile(); - if (!sourceFilesLoaded[sourceFile.fileName]) { - sourceFilesLoaded[sourceFile.fileName] = true; - enqueueTopLevelModuleStatements(sourceFile); - } - if (ts.isSourceFile(node)) { - return; - } - setColor(node, 2 /* NodeColor.Black */); - black_queue.push(node); - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) || ts.isPropertySignature(node) || ts.isPropertyDeclaration(node) || ts.isGetAccessor(node) || ts.isSetAccessor(node))) { - const references = languageService.getReferencesAtPosition(node.getSourceFile().fileName, node.name.pos + node.name.getLeadingTriviaWidth()); - if (references) { - for (let i = 0, len = references.length; i < len; i++) { - const reference = references[i]; - const referenceSourceFile = program.getSourceFile(reference.fileName); - if (!referenceSourceFile) { - continue; - } - const referenceNode = getTokenAtPosition(ts, referenceSourceFile, reference.textSpan.start, false, false); - if (ts.isMethodDeclaration(referenceNode.parent) - || ts.isPropertyDeclaration(referenceNode.parent) - || ts.isGetAccessor(referenceNode.parent) - || ts.isSetAccessor(referenceNode.parent)) { - enqueue_gray(referenceNode.parent); - } - } - } - } - } - function enqueueFile(filename) { - const sourceFile = program.getSourceFile(filename); - if (!sourceFile) { - console.warn(`Cannot find source file ${filename}`); - return; - } - // This source file should survive even if it is empty - markNeededSourceFile(sourceFile); - enqueue_black(sourceFile); - } - function enqueueImport(node, importText) { - if (options.importIgnorePattern.test(importText)) { - // this import should be ignored - return; - } - const nodeSourceFile = node.getSourceFile(); - let fullPath; - if (/(^\.\/)|(^\.\.\/)/.test(importText)) { - if (importText.endsWith('.js')) { // ESM: code imports require to be relative and to have a '.js' file extension - importText = importText.substr(0, importText.length - 3); - } - fullPath = path_1.default.join(path_1.default.dirname(nodeSourceFile.fileName), importText); - } - else { - fullPath = importText; - } - if (fs_1.default.existsSync(fullPath + '.ts')) { - fullPath = fullPath + '.ts'; - } - else { - fullPath = fullPath + '.js'; - } - enqueueFile(fullPath); - } - options.entryPoints.forEach(moduleId => enqueueFile(path_1.default.join(options.sourcesRoot, moduleId))); - // Add fake usage files - options.inlineEntryPoints.forEach((_, index) => enqueueFile(path_1.default.join(options.sourcesRoot, `inlineEntryPoint.${index}.ts`))); - let step = 0; - const checker = program.getTypeChecker(); - while (black_queue.length > 0 || gray_queue.length > 0) { - ++step; - let node; - if (step % 100 === 0) { - console.log(`Treeshaking - ${Math.floor(100 * step / (step + black_queue.length + gray_queue.length))}% - ${step}/${step + black_queue.length + gray_queue.length} (${black_queue.length}, ${gray_queue.length})`); - } - if (black_queue.length === 0) { - for (let i = 0; i < gray_queue.length; i++) { - const node = gray_queue[i]; - const nodeParent = node.parent; - if ((ts.isClassDeclaration(nodeParent) || ts.isInterfaceDeclaration(nodeParent)) && nodeOrChildIsBlack(nodeParent)) { - gray_queue.splice(i, 1); - black_queue.push(node); - setColor(node, 2 /* NodeColor.Black */); - i--; - } - } - } - if (black_queue.length > 0) { - node = black_queue.shift(); - } - else { - // only gray nodes remaining... - break; - } - const nodeSourceFile = node.getSourceFile(); - const loop = (node) => { - const symbols = getRealNodeSymbol(ts, checker, node); - for (const { symbol, symbolImportNode } of symbols) { - if (symbolImportNode) { - setColor(symbolImportNode, 2 /* NodeColor.Black */); - const importDeclarationNode = findParentImportDeclaration(symbolImportNode); - if (importDeclarationNode && ts.isStringLiteral(importDeclarationNode.moduleSpecifier)) { - enqueueImport(importDeclarationNode, importDeclarationNode.moduleSpecifier.text); - } - } - if (isSymbolWithDeclarations(symbol) && !nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol)) { - for (let i = 0, len = symbol.declarations.length; i < len; i++) { - const declaration = symbol.declarations[i]; - if (ts.isSourceFile(declaration)) { - // Do not enqueue full source files - // (they can be the declaration of a module import) - continue; - } - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration)) { - enqueue_black(declaration.name); - for (let j = 0; j < declaration.members.length; j++) { - const member = declaration.members[j]; - const memberName = member.name ? member.name.getText() : null; - if (ts.isConstructorDeclaration(member) - || ts.isConstructSignatureDeclaration(member) - || ts.isIndexSignatureDeclaration(member) - || ts.isCallSignatureDeclaration(member) - || memberName === '[Symbol.iterator]' - || memberName === '[Symbol.toStringTag]' - || memberName === 'toJSON' - || memberName === 'toString' - || memberName === 'dispose' // TODO: keeping all `dispose` methods - || /^_(.*)Brand$/.test(memberName || '') // TODO: keeping all members ending with `Brand`... - ) { - enqueue_black(member); - } - if (isStaticMemberWithSideEffects(ts, member)) { - enqueue_black(member); - } - } - // queue the heritage clauses - if (declaration.heritageClauses) { - for (const heritageClause of declaration.heritageClauses) { - enqueue_black(heritageClause); - } - } - } - else { - enqueue_black(declaration); - } - } - } - } - node.forEachChild(loop); - }; - node.forEachChild(loop); - } - while (export_import_queue.length > 0) { - const node = export_import_queue.shift(); - if (nodeOrParentIsBlack(node)) { - continue; - } - if (!node.symbol) { - continue; - } - const aliased = checker.getAliasedSymbol(node.symbol); - if (aliased.declarations && aliased.declarations.length > 0) { - if (nodeOrParentIsBlack(aliased.declarations[0]) || nodeOrChildIsBlack(aliased.declarations[0])) { - setColor(node, 2 /* NodeColor.Black */); - } - } - } -} -function nodeIsInItsOwnDeclaration(nodeSourceFile, node, symbol) { - for (let i = 0, len = symbol.declarations.length; i < len; i++) { - const declaration = symbol.declarations[i]; - const declarationSourceFile = declaration.getSourceFile(); - if (nodeSourceFile === declarationSourceFile) { - if (declaration.pos <= node.pos && node.end <= declaration.end) { - return true; - } - } - } - return false; -} -function generateResult(ts, languageService, shakeLevel) { - const program = languageService.getProgram(); - if (!program) { - throw new Error('Could not get program from language service'); - } - const result = {}; - const writeFile = (filePath, contents) => { - result[filePath] = contents; - }; - program.getSourceFiles().forEach((sourceFile) => { - const fileName = sourceFile.fileName; - if (/^defaultLib:/.test(fileName)) { - return; - } - const destination = fileName; - if (/\.d\.ts$/.test(fileName)) { - if (nodeOrChildIsBlack(sourceFile)) { - writeFile(destination, sourceFile.text); - } - return; - } - const text = sourceFile.text; - let result = ''; - function keep(node) { - result += text.substring(node.pos, node.end); - } - function write(data) { - result += data; - } - function writeMarkedNodes(node) { - if (getColor(node) === 2 /* NodeColor.Black */) { - return keep(node); - } - // Always keep certain top-level statements - if (ts.isSourceFile(node.parent)) { - if (ts.isExpressionStatement(node) && ts.isStringLiteral(node.expression) && node.expression.text === 'use strict') { - return keep(node); - } - if (ts.isVariableStatement(node) && nodeOrChildIsBlack(node)) { - return keep(node); - } - } - // Keep the entire import in import * as X cases - if (ts.isImportDeclaration(node)) { - if (node.importClause && node.importClause.namedBindings) { - if (ts.isNamespaceImport(node.importClause.namedBindings)) { - if (getColor(node.importClause.namedBindings) === 2 /* NodeColor.Black */) { - return keep(node); - } - } - else { - const survivingImports = []; - for (const importNode of node.importClause.namedBindings.elements) { - if (getColor(importNode) === 2 /* NodeColor.Black */) { - survivingImports.push(importNode.getFullText(sourceFile)); - } - } - const leadingTriviaWidth = node.getLeadingTriviaWidth(); - const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); - if (survivingImports.length > 0) { - if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* NodeColor.Black */) { - return write(`${leadingTrivia}import ${node.importClause.name.text}, {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); - } - return write(`${leadingTrivia}import {${survivingImports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); - } - else { - if (node.importClause && node.importClause.name && getColor(node.importClause) === 2 /* NodeColor.Black */) { - return write(`${leadingTrivia}import ${node.importClause.name.text} from${node.moduleSpecifier.getFullText(sourceFile)};`); - } - } - } - } - else { - if (node.importClause && getColor(node.importClause) === 2 /* NodeColor.Black */) { - return keep(node); - } - } - } - if (ts.isExportDeclaration(node)) { - if (node.exportClause && node.moduleSpecifier && ts.isNamedExports(node.exportClause)) { - const survivingExports = []; - for (const exportSpecifier of node.exportClause.elements) { - if (getColor(exportSpecifier) === 2 /* NodeColor.Black */) { - survivingExports.push(exportSpecifier.getFullText(sourceFile)); - } - } - const leadingTriviaWidth = node.getLeadingTriviaWidth(); - const leadingTrivia = sourceFile.text.substr(node.pos, leadingTriviaWidth); - if (survivingExports.length > 0) { - return write(`${leadingTrivia}export {${survivingExports.join(',')} } from${node.moduleSpecifier.getFullText(sourceFile)};`); - } - } - } - if (shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) && nodeOrChildIsBlack(node)) { - let toWrite = node.getFullText(); - for (let i = node.members.length - 1; i >= 0; i--) { - const member = node.members[i]; - if (getColor(member) === 2 /* NodeColor.Black */ || !member.name) { - // keep method - continue; - } - const pos = member.pos - node.pos; - const end = member.end - node.pos; - toWrite = toWrite.substring(0, pos) + toWrite.substring(end); - } - return write(toWrite); - } - if (ts.isFunctionDeclaration(node)) { - // Do not go inside functions if they haven't been marked - return; - } - node.forEachChild(writeMarkedNodes); - } - if (getColor(sourceFile) !== 2 /* NodeColor.Black */) { - if (!nodeOrChildIsBlack(sourceFile)) { - // none of the elements are reachable - if (isNeededSourceFile(sourceFile)) { - // this source file must be written, even if nothing is used from it - // because there is an import somewhere for it. - // However, TS complains with empty files with the error "x" is not a module, - // so we will export a dummy variable - result = 'export const __dummy = 0;'; - } - else { - // don't write this file at all! - return; - } - } - else { - sourceFile.forEachChild(writeMarkedNodes); - result += sourceFile.endOfFileToken.getFullText(sourceFile); - } - } - else { - result = text; - } - writeFile(destination, result); - }); - return result; -} -//#endregion -//#region Utils -function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(ts, program, checker, declaration) { - if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { - for (const heritageClause of declaration.heritageClauses) { - for (const type of heritageClause.types) { - const symbol = findSymbolFromHeritageType(ts, checker, type); - if (symbol) { - const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); - if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { - return true; - } - } - } - } - } - return false; -} -function findSymbolFromHeritageType(ts, checker, type) { - if (ts.isExpressionWithTypeArguments(type)) { - return findSymbolFromHeritageType(ts, checker, type.expression); - } - if (ts.isIdentifier(type)) { - const tmp = getRealNodeSymbol(ts, checker, type); - return (tmp.length > 0 ? tmp[0].symbol : null); - } - if (ts.isPropertyAccessExpression(type)) { - return findSymbolFromHeritageType(ts, checker, type.name); - } - return null; -} -class SymbolImportTuple { - symbol; - symbolImportNode; - constructor(symbol, symbolImportNode) { - this.symbol = symbol; - this.symbolImportNode = symbolImportNode; - } -} -/** - * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) - */ -function getRealNodeSymbol(ts, checker, node) { - // Go to the original declaration for cases: - // - // (1) when the aliased symbol was declared in the location(parent). - // (2) when the aliased symbol is originating from an import. - // - function shouldSkipAlias(node, declaration) { - if (!ts.isShorthandPropertyAssignment(node) && node.kind !== ts.SyntaxKind.Identifier) { - return false; - } - if (node.parent === declaration) { - return true; - } - switch (declaration.kind) { - case ts.SyntaxKind.ImportClause: - case ts.SyntaxKind.ImportEqualsDeclaration: - return true; - case ts.SyntaxKind.ImportSpecifier: - return declaration.parent.kind === ts.SyntaxKind.NamedImports; - default: - return false; - } - } - if (!ts.isShorthandPropertyAssignment(node)) { - if (node.getChildCount() !== 0) { - return []; - } - } - const { parent } = node; - let symbol = (ts.isShorthandPropertyAssignment(node) - ? checker.getShorthandAssignmentValueSymbol(node) - : checker.getSymbolAtLocation(node)); - let importNode = null; - // If this is an alias, and the request came at the declaration location - // get the aliased symbol instead. This allows for goto def on an import e.g. - // import {A, B} from "mod"; - // to jump to the implementation directly. - if (symbol && symbol.flags & ts.SymbolFlags.Alias && symbol.declarations && shouldSkipAlias(node, symbol.declarations[0])) { - const aliased = checker.getAliasedSymbol(symbol); - if (aliased.declarations) { - // We should mark the import as visited - importNode = symbol.declarations[0]; - symbol = aliased; - } - } - if (symbol) { - // Because name in short-hand property assignment has two different meanings: property name and property value, - // using go-to-definition at such position should go to the variable declaration of the property value rather than - // go to the declaration of the property name (in this case stay at the same position). However, if go-to-definition - // is performed at the location of property access, we would like to go to definition of the property in the short-hand - // assignment. This case and others are handled by the following code. - if (node.parent.kind === ts.SyntaxKind.ShorthandPropertyAssignment) { - symbol = checker.getShorthandAssignmentValueSymbol(symbol.valueDeclaration); - } - // If the node is the name of a BindingElement within an ObjectBindingPattern instead of just returning the - // declaration the symbol (which is itself), we should try to get to the original type of the ObjectBindingPattern - // and return the property declaration for the referenced property. - // For example: - // import('./foo').then(({ b/*goto*/ar }) => undefined); => should get use to the declaration in file "./foo" - // - // function bar(onfulfilled: (value: T) => void) { //....} - // interface Test { - // pr/*destination*/op1: number - // } - // bar(({pr/*goto*/op1})=>{}); - if (ts.isPropertyName(node) && ts.isBindingElement(parent) && ts.isObjectBindingPattern(parent.parent) && - (node === (parent.propertyName || parent.name))) { - const name = ts.getNameFromPropertyName(node); - const type = checker.getTypeAtLocation(parent.parent); - if (name && type) { - if (type.isUnion()) { - return generateMultipleSymbols(type, name, importNode); - } - else { - const prop = type.getProperty(name); - if (prop) { - symbol = prop; - } - } - } - } - // If the current location we want to find its definition is in an object literal, try to get the contextual type for the - // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. - // For example - // interface Props{ - // /*first*/prop1: number - // prop2: boolean - // } - // function Foo(arg: Props) {} - // Foo( { pr/*1*/op1: 10, prop2: false }) - const element = ts.getContainingObjectLiteralElement(node); - if (element) { - const contextualType = element && checker.getContextualType(element.parent); - if (contextualType) { - const propertySymbols = ts.getPropertySymbolsFromContextualType(element, checker, contextualType, /*unionSymbolOk*/ false); - if (propertySymbols) { - symbol = propertySymbols[0]; - } - } - } - } - if (symbol && symbol.declarations) { - return [new SymbolImportTuple(symbol, importNode)]; - } - return []; - function generateMultipleSymbols(type, name, importNode) { - const result = []; - for (const t of type.types) { - const prop = t.getProperty(name); - if (prop && prop.declarations) { - result.push(new SymbolImportTuple(prop, importNode)); - } - } - return result; - } -} -/** Get the token whose text contains the position */ -function getTokenAtPosition(ts, sourceFile, position, allowPositionInLeadingTrivia, includeEndPosition) { - let current = sourceFile; - outer: while (true) { - // find the child that contains 'position' - for (const child of current.getChildren()) { - const start = allowPositionInLeadingTrivia ? child.getFullStart() : child.getStart(sourceFile, /*includeJsDoc*/ true); - if (start > position) { - // If this child begins after position, then all subsequent children will as well. - break; - } - const end = child.getEnd(); - if (position < end || (position === end && (child.kind === ts.SyntaxKind.EndOfFileToken || includeEndPosition))) { - current = child; - continue outer; - } - } - return current; - } -} -//#endregion -//# sourceMappingURL=treeshaking.js.map \ No newline at end of file diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 3d1e785e073..463e701f73f 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -5,14 +5,16 @@ import fs from 'fs'; import path from 'path'; -import type * as ts from 'typescript'; -import { IFileMap, TypeScriptLanguageServiceHost } from './typeScriptLanguageServiceHost'; +import * as ts from 'typescript'; +import { type IFileMap, TypeScriptLanguageServiceHost } from './typeScriptLanguageServiceHost.ts'; -enum ShakeLevel { - Files = 0, - InnerFile = 1, - ClassMembers = 2 -} +const ShakeLevel = Object.freeze({ + Files: 0, + InnerFile: 1, + ClassMembers: 2 +}); + +type ShakeLevel = typeof ShakeLevel[keyof typeof ShakeLevel]; export function toStringShakeLevel(shakeLevel: ShakeLevel): string { switch (shakeLevel) { @@ -77,7 +79,6 @@ function printDiagnostics(options: ITreeShakingOptions, diagnostics: ReadonlyArr } export function shake(options: ITreeShakingOptions): ITreeShakingResult { - const ts = require('typescript') as typeof import('typescript'); const languageService = createTypeScriptLanguageService(ts, options); const program = languageService.getProgram()!; @@ -136,11 +137,12 @@ function createTypeScriptLanguageService(ts: typeof import('typescript'), option //#region Tree Shaking -const enum NodeColor { - White = 0, - Gray = 1, - Black = 2 -} +const NodeColor = Object.freeze({ + White: 0, + Gray: 1, + Black: 2 +}); +type NodeColor = typeof NodeColor[keyof typeof NodeColor]; type ObjectLiteralElementWithName = ts.ObjectLiteralElement & { name: ts.PropertyName; parent: ts.ObjectLiteralExpression | ts.JsxAttributes }; @@ -755,10 +757,16 @@ function findSymbolFromHeritageType(ts: typeof import('typescript'), checker: ts } class SymbolImportTuple { + public readonly symbol: ts.Symbol | null; + public readonly symbolImportNode: ts.Declaration | null; + constructor( - public readonly symbol: ts.Symbol | null, - public readonly symbolImportNode: ts.Declaration | null - ) { } + symbol: ts.Symbol | null, + symbolImportNode: ts.Declaration | null + ) { + this.symbol = symbol; + this.symbolImportNode = symbolImportNode; + } } /** diff --git a/build/lib/tsb/builder.js b/build/lib/tsb/builder.js deleted file mode 100644 index eb8e7bca1b3..00000000000 --- a/build/lib/tsb/builder.js +++ /dev/null @@ -1,664 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CancellationToken = void 0; -exports.createTypeScriptBuilder = createTypeScriptBuilder; -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const crypto_1 = __importDefault(require("crypto")); -const utils = __importStar(require("./utils")); -const ansi_colors_1 = __importDefault(require("ansi-colors")); -const typescript_1 = __importDefault(require("typescript")); -const vinyl_1 = __importDefault(require("vinyl")); -const source_map_1 = require("source-map"); -var CancellationToken; -(function (CancellationToken) { - CancellationToken.None = { - isCancellationRequested() { return false; } - }; -})(CancellationToken || (exports.CancellationToken = CancellationToken = {})); -function normalize(path) { - return path.replace(/\\/g, '/'); -} -function createTypeScriptBuilder(config, projectFile, cmd) { - const _log = config.logFn; - const host = new LanguageServiceHost(cmd, projectFile, _log); - const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log); - const toBeCheckedForCycles = []; - const service = typescript_1.default.createLanguageService(host, typescript_1.default.createDocumentRegistry()); - const lastBuildVersion = Object.create(null); - const lastDtsHash = Object.create(null); - const userWantsDeclarations = cmd.options.declaration; - let oldErrors = Object.create(null); - let headUsed = process.memoryUsage().heapUsed; - let emitSourceMapsInStream = true; - // always emit declaraction files - host.getCompilationSettings().declaration = true; - function file(file) { - // support gulp-sourcemaps - if (file.sourceMap) { - emitSourceMapsInStream = false; - } - if (!file.contents) { - host.removeScriptSnapshot(file.path); - delete lastBuildVersion[normalize(file.path)]; - } - else { - host.addScriptSnapshot(file.path, new VinylScriptSnapshot(file)); - } - } - function baseFor(snapshot) { - if (snapshot instanceof VinylScriptSnapshot) { - return cmd.options.outDir || snapshot.getBase(); - } - else { - return ''; - } - } - function isExternalModule(sourceFile) { - return !!sourceFile.externalModuleIndicator - || /declare\s+module\s+('|")(.+)\1/.test(sourceFile.getText()); - } - function build(out, onError, token = CancellationToken.None) { - function checkSyntaxSoon(fileName) { - return new Promise(resolve => { - process.nextTick(function () { - if (!host.getScriptSnapshot(fileName, false)) { - resolve([]); // no script, no problems - } - else { - resolve(service.getSyntacticDiagnostics(fileName)); - } - }); - }); - } - function checkSemanticsSoon(fileName) { - return new Promise(resolve => { - process.nextTick(function () { - if (!host.getScriptSnapshot(fileName, false)) { - resolve([]); // no script, no problems - } - else { - resolve(service.getSemanticDiagnostics(fileName)); - } - }); - }); - } - function emitSoon(fileName) { - return new Promise(resolve => { - process.nextTick(function () { - if (/\.d\.ts$/.test(fileName)) { - // if it's already a d.ts file just emit it signature - const snapshot = host.getScriptSnapshot(fileName); - const signature = crypto_1.default.createHash('sha256') - .update(snapshot.getText(0, snapshot.getLength())) - .digest('base64'); - return resolve({ - fileName, - signature, - files: [] - }); - } - const output = service.getEmitOutput(fileName); - const files = []; - let signature; - for (const file of output.outputFiles) { - if (!emitSourceMapsInStream && /\.js\.map$/.test(file.name)) { - continue; - } - if (/\.d\.ts$/.test(file.name)) { - signature = crypto_1.default.createHash('sha256') - .update(file.text) - .digest('base64'); - if (!userWantsDeclarations) { - // don't leak .d.ts files if users don't want them - continue; - } - } - const vinyl = new vinyl_1.default({ - path: file.name, - contents: Buffer.from(file.text), - base: !config._emitWithoutBasePath && baseFor(host.getScriptSnapshot(fileName)) || undefined - }); - if (!emitSourceMapsInStream && /\.js$/.test(file.name)) { - const sourcemapFile = output.outputFiles.filter(f => /\.js\.map$/.test(f.name))[0]; - if (sourcemapFile) { - const extname = path_1.default.extname(vinyl.relative); - const basename = path_1.default.basename(vinyl.relative, extname); - const dirname = path_1.default.dirname(vinyl.relative); - const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts'; - let sourceMap = JSON.parse(sourcemapFile.text); - sourceMap.sources[0] = tsname.replace(/\\/g, '/'); - // check for an "input source" map and combine them - // in step 1 we extract all line edit from the input source map, and - // in step 2 we apply the line edits to the typescript source map - const snapshot = host.getScriptSnapshot(fileName); - if (snapshot instanceof VinylScriptSnapshot && snapshot.sourceMap) { - const inputSMC = new source_map_1.SourceMapConsumer(snapshot.sourceMap); - const tsSMC = new source_map_1.SourceMapConsumer(sourceMap); - let didChange = false; - const smg = new source_map_1.SourceMapGenerator({ - file: sourceMap.file, - sourceRoot: sourceMap.sourceRoot - }); - // step 1 - const lineEdits = new Map(); - inputSMC.eachMapping(m => { - if (m.originalLine === m.generatedLine) { - // same line mapping - let array = lineEdits.get(m.originalLine); - if (!array) { - array = []; - lineEdits.set(m.originalLine, array); - } - array.push([m.originalColumn, m.generatedColumn]); - } - else { - // NOT SUPPORTED - } - }); - // step 2 - tsSMC.eachMapping(m => { - didChange = true; - const edits = lineEdits.get(m.originalLine); - let originalColumnDelta = 0; - if (edits) { - for (const [from, to] of edits) { - if (to >= m.originalColumn) { - break; - } - originalColumnDelta = from - to; - } - } - smg.addMapping({ - source: m.source, - name: m.name, - generated: { line: m.generatedLine, column: m.generatedColumn }, - original: { line: m.originalLine, column: m.originalColumn + originalColumnDelta } - }); - }); - if (didChange) { - [tsSMC, inputSMC].forEach((consumer) => { - consumer.sources.forEach((sourceFile) => { - smg._sources.add(sourceFile); - const sourceContent = consumer.sourceContentFor(sourceFile); - if (sourceContent !== null) { - smg.setSourceContent(sourceFile, sourceContent); - } - }); - }); - sourceMap = JSON.parse(smg.toString()); - // const filename = '/Users/jrieken/Code/vscode/src2/' + vinyl.relative + '.map'; - // fs.promises.mkdir(path.dirname(filename), { recursive: true }).then(async () => { - // await fs.promises.writeFile(filename, smg.toString()); - // await fs.promises.writeFile('/Users/jrieken/Code/vscode/src2/' + vinyl.relative, vinyl.contents); - // }); - } - } - vinyl.sourceMap = sourceMap; - } - } - files.push(vinyl); - } - resolve({ - fileName, - signature, - files - }); - }); - }); - } - const newErrors = Object.create(null); - const t1 = Date.now(); - const toBeEmitted = []; - const toBeCheckedSyntactically = []; - const toBeCheckedSemantically = []; - const filesWithChangedSignature = []; - const dependentFiles = []; - const newLastBuildVersion = new Map(); - for (const fileName of host.getScriptFileNames()) { - if (lastBuildVersion[fileName] !== host.getScriptVersion(fileName)) { - toBeEmitted.push(fileName); - toBeCheckedSyntactically.push(fileName); - toBeCheckedSemantically.push(fileName); - } - } - return new Promise(resolve => { - const semanticCheckInfo = new Map(); - const seenAsDependentFile = new Set(); - function workOnNext() { - let promise; - // let fileName: string; - // someone told us to stop this - if (token.isCancellationRequested()) { - _log('[CANCEL]', '>>This compile run was cancelled<<'); - newLastBuildVersion.clear(); - resolve(); - return; - } - // (1st) emit code - else if (toBeEmitted.length) { - const fileName = toBeEmitted.pop(); - promise = emitSoon(fileName).then(value => { - for (const file of value.files) { - _log('[emit code]', file.path); - out(file); - } - // remember when this was build - newLastBuildVersion.set(fileName, host.getScriptVersion(fileName)); - // remeber the signature - if (value.signature && lastDtsHash[fileName] !== value.signature) { - lastDtsHash[fileName] = value.signature; - filesWithChangedSignature.push(fileName); - } - // line up for cycle check - const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js')); - if (jsValue) { - outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date())); - toBeCheckedForCycles.push(normalize(jsValue.path)); - } - }).catch(e => { - // can't just skip this or make a result up.. - host.error(`ERROR emitting ${fileName}`); - host.error(e); - }); - } - // (2nd) check syntax - else if (toBeCheckedSyntactically.length) { - const fileName = toBeCheckedSyntactically.pop(); - _log('[check syntax]', fileName); - promise = checkSyntaxSoon(fileName).then(diagnostics => { - delete oldErrors[fileName]; - if (diagnostics.length > 0) { - diagnostics.forEach(d => onError(d)); - newErrors[fileName] = diagnostics; - // stop the world when there are syntax errors - toBeCheckedSyntactically.length = 0; - toBeCheckedSemantically.length = 0; - filesWithChangedSignature.length = 0; - } - }); - } - // (3rd) check semantics - else if (toBeCheckedSemantically.length) { - let fileName = toBeCheckedSemantically.pop(); - while (fileName && semanticCheckInfo.has(fileName)) { - fileName = toBeCheckedSemantically.pop(); - } - if (fileName) { - _log('[check semantics]', fileName); - promise = checkSemanticsSoon(fileName).then(diagnostics => { - delete oldErrors[fileName]; - semanticCheckInfo.set(fileName, diagnostics.length); - if (diagnostics.length > 0) { - diagnostics.forEach(d => onError(d)); - newErrors[fileName] = diagnostics; - } - }); - } - } - // (4th) check dependents - else if (filesWithChangedSignature.length) { - while (filesWithChangedSignature.length) { - const fileName = filesWithChangedSignature.pop(); - if (!isExternalModule(service.getProgram().getSourceFile(fileName))) { - _log('[check semantics*]', fileName + ' is an internal module and it has changed shape -> check whatever hasn\'t been checked yet'); - toBeCheckedSemantically.push(...host.getScriptFileNames()); - filesWithChangedSignature.length = 0; - dependentFiles.length = 0; - break; - } - host.collectDependents(fileName, dependentFiles); - } - } - // (5th) dependents contd - else if (dependentFiles.length) { - let fileName = dependentFiles.pop(); - while (fileName && seenAsDependentFile.has(fileName)) { - fileName = dependentFiles.pop(); - } - if (fileName) { - seenAsDependentFile.add(fileName); - const value = semanticCheckInfo.get(fileName); - if (value === 0) { - // already validated successfully -> look at dependents next - host.collectDependents(fileName, dependentFiles); - } - else if (typeof value === 'undefined') { - // first validate -> look at dependents next - dependentFiles.push(fileName); - toBeCheckedSemantically.push(fileName); - } - } - } - // (last) done - else { - resolve(); - return; - } - if (!promise) { - promise = Promise.resolve(); - } - promise.then(function () { - // change to change - process.nextTick(workOnNext); - }).catch(err => { - console.error(err); - }); - } - workOnNext(); - }).then(() => { - // check for cyclic dependencies - const cycles = outHost.getCyclicDependencies(toBeCheckedForCycles); - toBeCheckedForCycles.length = 0; - for (const [filename, error] of cycles) { - const cyclicDepErrors = []; - if (error) { - cyclicDepErrors.push({ - category: typescript_1.default.DiagnosticCategory.Error, - code: 1, - file: undefined, - start: undefined, - length: undefined, - messageText: `CYCLIC dependency: ${error}` - }); - } - delete oldErrors[filename]; - newErrors[filename] = cyclicDepErrors; - cyclicDepErrors.forEach(d => onError(d)); - } - }).then(() => { - // store the build versions to not rebuilt the next time - newLastBuildVersion.forEach((value, key) => { - lastBuildVersion[key] = value; - }); - // print old errors and keep them - for (const [key, value] of Object.entries(oldErrors)) { - value.forEach(diag => onError(diag)); - newErrors[key] = value; - } - oldErrors = newErrors; - // print stats - const headNow = process.memoryUsage().heapUsed; - const MB = 1024 * 1024; - _log('[tsb]', `time: ${ansi_colors_1.default.yellow((Date.now() - t1) + 'ms')} + \nmem: ${ansi_colors_1.default.cyan(Math.ceil(headNow / MB) + 'MB')} ${ansi_colors_1.default.bgCyan('delta: ' + Math.ceil((headNow - headUsed) / MB))}`); - headUsed = headNow; - }); - } - return { - file, - build, - languageService: service - }; -} -class ScriptSnapshot { - _text; - _mtime; - constructor(text, mtime) { - this._text = text; - this._mtime = mtime; - } - getVersion() { - return this._mtime.toUTCString(); - } - getText(start, end) { - return this._text.substring(start, end); - } - getLength() { - return this._text.length; - } - getChangeRange(_oldSnapshot) { - return undefined; - } -} -class VinylScriptSnapshot extends ScriptSnapshot { - _base; - sourceMap; - constructor(file) { - super(file.contents.toString(), file.stat.mtime); - this._base = file.base; - this.sourceMap = file.sourceMap; - } - getBase() { - return this._base; - } -} -class LanguageServiceHost { - _cmdLine; - _projectPath; - _log; - _snapshots; - _filesInProject; - _filesAdded; - _dependencies; - _dependenciesRecomputeList; - _fileNameToDeclaredModule; - _projectVersion; - constructor(_cmdLine, _projectPath, _log) { - this._cmdLine = _cmdLine; - this._projectPath = _projectPath; - this._log = _log; - this._snapshots = Object.create(null); - this._filesInProject = new Set(_cmdLine.fileNames); - this._filesAdded = new Set(); - this._dependencies = new utils.graph.Graph(); - this._dependenciesRecomputeList = []; - this._fileNameToDeclaredModule = Object.create(null); - this._projectVersion = 1; - } - log(_s) { - // console.log(s); - } - trace(_s) { - // console.log(s); - } - error(s) { - console.error(s); - } - getCompilationSettings() { - return this._cmdLine.options; - } - getProjectVersion() { - return String(this._projectVersion); - } - getScriptFileNames() { - const res = Object.keys(this._snapshots).filter(path => this._filesInProject.has(path) || this._filesAdded.has(path)); - return res; - } - getScriptVersion(filename) { - filename = normalize(filename); - const result = this._snapshots[filename]; - if (result) { - return result.getVersion(); - } - return 'UNKNWON_FILE_' + Math.random().toString(16).slice(2); - } - getScriptSnapshot(filename, resolve = true) { - filename = normalize(filename); - let result = this._snapshots[filename]; - if (!result && resolve) { - try { - result = new VinylScriptSnapshot(new vinyl_1.default({ - path: filename, - contents: fs_1.default.readFileSync(filename), - base: this.getCompilationSettings().outDir, - stat: fs_1.default.statSync(filename) - })); - this.addScriptSnapshot(filename, result); - } - catch (e) { - // ignore - } - } - return result; - } - static _declareModule = /declare\s+module\s+('|")(.+)\1/g; - addScriptSnapshot(filename, snapshot) { - this._projectVersion++; - filename = normalize(filename); - const old = this._snapshots[filename]; - if (!old && !this._filesInProject.has(filename) && !filename.endsWith('.d.ts')) { - // ^^^^^^^^^^^^^^^^^^^^^^^^^^ - // not very proper! - this._filesAdded.add(filename); - } - if (!old || old.getVersion() !== snapshot.getVersion()) { - this._dependenciesRecomputeList.push(filename); - // (cheap) check for declare module - LanguageServiceHost._declareModule.lastIndex = 0; - let match; - while ((match = LanguageServiceHost._declareModule.exec(snapshot.getText(0, snapshot.getLength())))) { - let declaredModules = this._fileNameToDeclaredModule[filename]; - if (!declaredModules) { - this._fileNameToDeclaredModule[filename] = declaredModules = []; - } - declaredModules.push(match[2]); - } - } - this._snapshots[filename] = snapshot; - return old; - } - removeScriptSnapshot(filename) { - filename = normalize(filename); - this._log('removeScriptSnapshot', filename); - this._filesInProject.delete(filename); - this._filesAdded.delete(filename); - this._projectVersion++; - delete this._fileNameToDeclaredModule[filename]; - return delete this._snapshots[filename]; - } - getCurrentDirectory() { - return path_1.default.dirname(this._projectPath); - } - getDefaultLibFileName(options) { - return typescript_1.default.getDefaultLibFilePath(options); - } - directoryExists = typescript_1.default.sys.directoryExists; - getDirectories = typescript_1.default.sys.getDirectories; - fileExists = typescript_1.default.sys.fileExists; - readFile = typescript_1.default.sys.readFile; - readDirectory = typescript_1.default.sys.readDirectory; - // ---- dependency management - collectDependents(filename, target) { - while (this._dependenciesRecomputeList.length) { - this._processFile(this._dependenciesRecomputeList.pop()); - } - filename = normalize(filename); - const node = this._dependencies.lookup(filename); - if (node) { - node.incoming.forEach(entry => target.push(entry.data)); - } - } - getCyclicDependencies(filenames) { - // Ensure dependencies are up to date - while (this._dependenciesRecomputeList.length) { - this._processFile(this._dependenciesRecomputeList.pop()); - } - const cycles = this._dependencies.findCycles(filenames.sort((a, b) => a.localeCompare(b))); - const result = new Map(); - for (const [key, value] of cycles) { - result.set(key, value?.join(' -> ')); - } - return result; - } - _processFile(filename) { - if (filename.match(/.*\.d\.ts$/)) { - return; - } - filename = normalize(filename); - const snapshot = this.getScriptSnapshot(filename); - if (!snapshot) { - this._log('processFile', `Missing snapshot for: ${filename}`); - return; - } - const info = typescript_1.default.preProcessFile(snapshot.getText(0, snapshot.getLength()), true); - // (0) clear out old dependencies - this._dependencies.resetNode(filename); - // (1) ///-references - info.referencedFiles.forEach(ref => { - const resolvedPath = path_1.default.resolve(path_1.default.dirname(filename), ref.fileName); - const normalizedPath = normalize(resolvedPath); - this._dependencies.inertEdge(filename, normalizedPath); - }); - // (2) import-require statements - info.importedFiles.forEach(ref => { - if (!ref.fileName.startsWith('.')) { - // node module? - return; - } - if (ref.fileName.endsWith('.css')) { - return; - } - const stopDirname = normalize(this.getCurrentDirectory()); - let dirname = filename; - let found = false; - while (!found && dirname.indexOf(stopDirname) === 0) { - dirname = path_1.default.dirname(dirname); - let resolvedPath = path_1.default.resolve(dirname, ref.fileName); - if (resolvedPath.endsWith('.js')) { - resolvedPath = resolvedPath.slice(0, -3); - } - const normalizedPath = normalize(resolvedPath); - if (this.getScriptSnapshot(normalizedPath + '.ts')) { - this._dependencies.inertEdge(filename, normalizedPath + '.ts'); - found = true; - } - else if (this.getScriptSnapshot(normalizedPath + '.d.ts')) { - this._dependencies.inertEdge(filename, normalizedPath + '.d.ts'); - found = true; - } - else if (this.getScriptSnapshot(normalizedPath + '.js')) { - this._dependencies.inertEdge(filename, normalizedPath + '.js'); - found = true; - } - } - if (!found) { - for (const key in this._fileNameToDeclaredModule) { - if (this._fileNameToDeclaredModule[key] && ~this._fileNameToDeclaredModule[key].indexOf(ref.fileName)) { - this._dependencies.inertEdge(filename, key); - } - } - } - }); - } -} -//# sourceMappingURL=builder.js.map \ No newline at end of file diff --git a/build/lib/tsb/builder.ts b/build/lib/tsb/builder.ts index 64081ac4797..628afc05427 100644 --- a/build/lib/tsb/builder.ts +++ b/build/lib/tsb/builder.ts @@ -6,11 +6,11 @@ import fs from 'fs'; import path from 'path'; import crypto from 'crypto'; -import * as utils from './utils'; +import * as utils from './utils.ts'; import colors from 'ansi-colors'; import ts from 'typescript'; import Vinyl from 'vinyl'; -import { RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'; +import { type RawSourceMap, SourceMapConsumer, SourceMapGenerator } from 'source-map'; export interface IConfiguration { logFn: (topic: string, message: string) => void; @@ -21,11 +21,11 @@ export interface CancellationToken { isCancellationRequested(): boolean; } -export namespace CancellationToken { - export const None: CancellationToken = { +export const CancellationToken = new class { + None: CancellationToken = { isCancellationRequested() { return false; } }; -} +}; export interface ITypeScriptBuilder { build(out: (file: Vinyl) => void, onError: (err: ts.Diagnostic) => void, token?: CancellationToken): Promise; @@ -167,7 +167,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str const dirname = path.dirname(vinyl.relative); const tsname = (dirname === '.' ? '' : dirname + '/') + basename + '.ts'; - let sourceMap = JSON.parse(sourcemapFile.text); + let sourceMap = JSON.parse(sourcemapFile.text) as RawSourceMap; sourceMap.sources[0] = tsname.replace(/\\/g, '/'); // check for an "input source" map and combine them @@ -227,7 +227,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str } [tsSMC, inputSMC].forEach((consumer) => { - (consumer).sources.forEach((sourceFile: string) => { + (consumer as SourceMapConsumer & { sources: string[] }).sources.forEach((sourceFile: string) => { (smg as SourceMapGeneratorWithSources)._sources.add(sourceFile); const sourceContent = consumer.sourceContentFor(sourceFile); if (sourceContent !== null) { @@ -529,19 +529,25 @@ class LanguageServiceHost implements ts.LanguageServiceHost { private readonly _snapshots: { [path: string]: ScriptSnapshot }; private readonly _filesInProject: Set; private readonly _filesAdded: Set; - private readonly _dependencies: utils.graph.Graph; + private readonly _dependencies: InstanceType>; private readonly _dependenciesRecomputeList: string[]; private readonly _fileNameToDeclaredModule: { [path: string]: string[] }; private _projectVersion: number; + private readonly _cmdLine: ts.ParsedCommandLine; + private readonly _projectPath: string; + private readonly _log: (topic: string, message: string) => void; constructor( - private readonly _cmdLine: ts.ParsedCommandLine, - private readonly _projectPath: string, - private readonly _log: (topic: string, message: string) => void + cmdLine: ts.ParsedCommandLine, + projectPath: string, + log: (topic: string, message: string) => void ) { + this._cmdLine = cmdLine; + this._projectPath = projectPath; + this._log = log; this._snapshots = Object.create(null); - this._filesInProject = new Set(_cmdLine.fileNames); + this._filesInProject = new Set(this._cmdLine.fileNames); this._filesAdded = new Set(); this._dependencies = new utils.graph.Graph(); this._dependenciesRecomputeList = []; @@ -665,7 +671,7 @@ class LanguageServiceHost implements ts.LanguageServiceHost { filename = normalize(filename); const node = this._dependencies.lookup(filename); if (node) { - node.incoming.forEach(entry => target.push(entry.data)); + node.incoming.forEach((entry: any) => target.push(entry.data)); } } diff --git a/build/lib/tsb/index.js b/build/lib/tsb/index.js deleted file mode 100644 index 3bad38fa52f..00000000000 --- a/build/lib/tsb/index.js +++ /dev/null @@ -1,171 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.create = create; -const vinyl_1 = __importDefault(require("vinyl")); -const through_1 = __importDefault(require("through")); -const builder = __importStar(require("./builder")); -const typescript_1 = __importDefault(require("typescript")); -const stream_1 = require("stream"); -const path_1 = require("path"); -const utils_1 = require("./utils"); -const fs = __importStar(require("fs")); -const fancy_log_1 = __importDefault(require("fancy-log")); -const transpiler_1 = require("./transpiler"); -const colors = require("ansi-colors"); -class EmptyDuplex extends stream_1.Duplex { - _write(_chunk, _encoding, callback) { callback(); } - _read() { this.push(null); } -} -function createNullCompiler() { - const result = function () { return new EmptyDuplex(); }; - result.src = () => new EmptyDuplex(); - return result; -} -const _defaultOnError = (err) => console.log(JSON.stringify(err, null, 4)); -function create(projectPath, existingOptions, config, onError = _defaultOnError) { - function printDiagnostic(diag) { - if (diag instanceof Error) { - onError(diag.message); - } - else if (!diag.file || !diag.start) { - onError(typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n')); - } - else { - const lineAndCh = diag.file.getLineAndCharacterOfPosition(diag.start); - onError(utils_1.strings.format('{0}({1},{2}): {3}', diag.file.fileName, lineAndCh.line + 1, lineAndCh.character + 1, typescript_1.default.flattenDiagnosticMessageText(diag.messageText, '\n'))); - } - } - const parsed = typescript_1.default.readConfigFile(projectPath, typescript_1.default.sys.readFile); - if (parsed.error) { - printDiagnostic(parsed.error); - return createNullCompiler(); - } - const cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, (0, path_1.dirname)(projectPath), existingOptions); - if (cmdLine.errors.length > 0) { - cmdLine.errors.forEach(printDiagnostic); - return createNullCompiler(); - } - function logFn(topic, message) { - if (config.verbose) { - (0, fancy_log_1.default)(colors.cyan(topic), message); - } - } - // FULL COMPILE stream doing transpile, syntax and semantic diagnostics - function createCompileStream(builder, token) { - return (0, through_1.default)(function (file) { - // give the file to the compiler - if (file.isStream()) { - this.emit('error', 'no support for streams'); - return; - } - builder.file(file); - }, function () { - // start the compilation process - builder.build(file => this.queue(file), printDiagnostic, token).catch(e => console.error(e)).then(() => this.queue(null)); - }); - } - // TRANSPILE ONLY stream doing just TS to JS conversion - function createTranspileStream(transpiler) { - return (0, through_1.default)(function (file) { - // give the file to the compiler - if (file.isStream()) { - this.emit('error', 'no support for streams'); - return; - } - if (!file.contents) { - return; - } - if (!config.transpileOnlyIncludesDts && file.path.endsWith('.d.ts')) { - return; - } - if (!transpiler.onOutfile) { - transpiler.onOutfile = file => this.queue(file); - } - transpiler.transpile(file); - }, function () { - transpiler.join().then(() => { - this.queue(null); - transpiler.onOutfile = undefined; - }); - }); - } - let result; - if (config.transpileOnly) { - const transpiler = !config.transpileWithEsbuild - ? new transpiler_1.TscTranspiler(logFn, printDiagnostic, projectPath, cmdLine) - : new transpiler_1.ESBuildTranspiler(logFn, printDiagnostic, projectPath, cmdLine); - result = (() => createTranspileStream(transpiler)); - } - else { - const _builder = builder.createTypeScriptBuilder({ logFn }, projectPath, cmdLine); - result = ((token) => createCompileStream(_builder, token)); - } - result.src = (opts) => { - let _pos = 0; - const _fileNames = cmdLine.fileNames.slice(0); - return new class extends stream_1.Readable { - constructor() { - super({ objectMode: true }); - } - _read() { - let more = true; - let path; - for (; more && _pos < _fileNames.length; _pos++) { - path = _fileNames[_pos]; - more = this.push(new vinyl_1.default({ - path, - contents: fs.readFileSync(path), - stat: fs.statSync(path), - cwd: opts && opts.cwd, - base: opts && opts.base || (0, path_1.dirname)(projectPath) - })); - } - if (_pos >= _fileNames.length) { - this.push(null); - } - } - }; - }; - return result; -} -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/build/lib/tsb/index.ts b/build/lib/tsb/index.ts index e45e458c020..31c1c3f15f8 100644 --- a/build/lib/tsb/index.ts +++ b/build/lib/tsb/index.ts @@ -5,15 +5,15 @@ import Vinyl from 'vinyl'; import through from 'through'; -import * as builder from './builder'; +import * as builder from './builder.ts'; import ts from 'typescript'; import { Readable, Writable, Duplex } from 'stream'; import { dirname } from 'path'; -import { strings } from './utils'; -import * as fs from 'fs'; +import { strings } from './utils.ts'; +import { readFileSync, statSync } from 'fs'; import log from 'fancy-log'; -import { ESBuildTranspiler, ITranspiler, TscTranspiler } from './transpiler'; -import colors = require('ansi-colors'); +import { ESBuildTranspiler, type ITranspiler, TscTranspiler } from './transpiler.ts'; +import colors from 'ansi-colors'; export interface IncrementalCompiler { (token?: any): Readable & Writable; @@ -151,8 +151,8 @@ export function create( path = _fileNames[_pos]; more = this.push(new Vinyl({ path, - contents: fs.readFileSync(path), - stat: fs.statSync(path), + contents: readFileSync(path), + stat: statSync(path), cwd: opts && opts.cwd, base: opts && opts.base || dirname(projectPath) })); @@ -164,5 +164,5 @@ export function create( }; }; - return result; + return result as IncrementalCompiler; } diff --git a/build/lib/tsb/transpiler.js b/build/lib/tsb/transpiler.js deleted file mode 100644 index 07c19c5bae2..00000000000 --- a/build/lib/tsb/transpiler.js +++ /dev/null @@ -1,306 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ESBuildTranspiler = exports.TscTranspiler = void 0; -const esbuild_1 = __importDefault(require("esbuild")); -const typescript_1 = __importDefault(require("typescript")); -const node_worker_threads_1 = __importDefault(require("node:worker_threads")); -const vinyl_1 = __importDefault(require("vinyl")); -const node_os_1 = require("node:os"); -const tsconfigUtils_1 = require("../tsconfigUtils"); -function transpile(tsSrc, options) { - const isAmd = /\n(import|export)/m.test(tsSrc); - if (!isAmd && options.compilerOptions?.module === typescript_1.default.ModuleKind.AMD) { - // enforce NONE module-system for not-amd cases - options = { ...options, ...{ compilerOptions: { ...options.compilerOptions, module: typescript_1.default.ModuleKind.None } } }; - } - const out = typescript_1.default.transpileModule(tsSrc, options); - return { - jsSrc: out.outputText, - diag: out.diagnostics ?? [] - }; -} -if (!node_worker_threads_1.default.isMainThread) { - // WORKER - node_worker_threads_1.default.parentPort?.addListener('message', (req) => { - const res = { - jsSrcs: [], - diagnostics: [] - }; - for (const tsSrc of req.tsSrcs) { - const out = transpile(tsSrc, req.options); - res.jsSrcs.push(out.jsSrc); - res.diagnostics.push(out.diag); - } - node_worker_threads_1.default.parentPort.postMessage(res); - }); -} -class OutputFileNameOracle { - getOutputFileName; - constructor(cmdLine, configFilePath) { - this.getOutputFileName = (file) => { - try { - // windows: path-sep normalizing - file = typescript_1.default.normalizePath(file); - if (!cmdLine.options.configFilePath) { - // this is needed for the INTERNAL getOutputFileNames-call below... - cmdLine.options.configFilePath = configFilePath; - } - const isDts = file.endsWith('.d.ts'); - if (isDts) { - file = file.slice(0, -5) + '.ts'; - cmdLine.fileNames.push(file); - } - const outfile = typescript_1.default.getOutputFileNames(cmdLine, file, true)[0]; - if (isDts) { - cmdLine.fileNames.pop(); - } - return outfile; - } - catch (err) { - console.error(file, cmdLine.fileNames); - console.error(err); - throw err; - } - }; - } -} -class TranspileWorker { - static pool = 1; - id = TranspileWorker.pool++; - _worker = new node_worker_threads_1.default.Worker(__filename); - _pending; - _durations = []; - constructor(outFileFn) { - this._worker.addListener('message', (res) => { - if (!this._pending) { - console.error('RECEIVING data WITHOUT request'); - return; - } - const [resolve, reject, files, options, t1] = this._pending; - const outFiles = []; - const diag = []; - for (let i = 0; i < res.jsSrcs.length; i++) { - // inputs and outputs are aligned across the arrays - const file = files[i]; - const jsSrc = res.jsSrcs[i]; - const diag = res.diagnostics[i]; - if (diag.length > 0) { - diag.push(...diag); - continue; - } - let SuffixTypes; - (function (SuffixTypes) { - SuffixTypes[SuffixTypes["Dts"] = 5] = "Dts"; - SuffixTypes[SuffixTypes["Ts"] = 3] = "Ts"; - SuffixTypes[SuffixTypes["Unknown"] = 0] = "Unknown"; - })(SuffixTypes || (SuffixTypes = {})); - const suffixLen = file.path.endsWith('.d.ts') ? 5 /* SuffixTypes.Dts */ : file.path.endsWith('.ts') ? 3 /* SuffixTypes.Ts */ : 0 /* SuffixTypes.Unknown */; - // check if output of a DTS-files isn't just "empty" and iff so - // skip this file - if (suffixLen === 5 /* SuffixTypes.Dts */ && _isDefaultEmpty(jsSrc)) { - continue; - } - const outBase = options.compilerOptions?.outDir ?? file.base; - const outPath = outFileFn(file.path); - outFiles.push(new vinyl_1.default({ - path: outPath, - base: outBase, - contents: Buffer.from(jsSrc), - })); - } - this._pending = undefined; - this._durations.push(Date.now() - t1); - if (diag.length > 0) { - reject(diag); - } - else { - resolve(outFiles); - } - }); - } - terminate() { - // console.log(`Worker#${this.id} ENDS after ${this._durations.length} jobs (total: ${this._durations.reduce((p, c) => p + c, 0)}, avg: ${this._durations.reduce((p, c) => p + c, 0) / this._durations.length})`); - this._worker.terminate(); - } - get isBusy() { - return this._pending !== undefined; - } - next(files, options) { - if (this._pending !== undefined) { - throw new Error('BUSY'); - } - return new Promise((resolve, reject) => { - this._pending = [resolve, reject, files, options, Date.now()]; - const req = { - options, - tsSrcs: files.map(file => String(file.contents)) - }; - this._worker.postMessage(req); - }); - } -} -class TscTranspiler { - _onError; - _cmdLine; - static P = Math.floor((0, node_os_1.cpus)().length * .5); - _outputFileNames; - onOutfile; - _workerPool = []; - _queue = []; - _allJobs = []; - constructor(logFn, _onError, configFilePath, _cmdLine) { - this._onError = _onError; - this._cmdLine = _cmdLine; - logFn('Transpile', `will use ${TscTranspiler.P} transpile worker`); - this._outputFileNames = new OutputFileNameOracle(_cmdLine, configFilePath); - } - async join() { - // wait for all penindg jobs - this._consumeQueue(); - await Promise.allSettled(this._allJobs); - this._allJobs.length = 0; - // terminate all worker - this._workerPool.forEach(w => w.terminate()); - this._workerPool.length = 0; - } - transpile(file) { - if (this._cmdLine.options.noEmit) { - // not doing ANYTHING here - return; - } - const newLen = this._queue.push(file); - if (newLen > TscTranspiler.P ** 2) { - this._consumeQueue(); - } - } - _consumeQueue() { - if (this._queue.length === 0) { - // no work... - return; - } - // kinda LAZYily create workers - if (this._workerPool.length === 0) { - for (let i = 0; i < TscTranspiler.P; i++) { - this._workerPool.push(new TranspileWorker(file => this._outputFileNames.getOutputFileName(file))); - } - } - const freeWorker = this._workerPool.filter(w => !w.isBusy); - if (freeWorker.length === 0) { - // OK, they will pick up work themselves - return; - } - for (const worker of freeWorker) { - if (this._queue.length === 0) { - break; - } - const job = new Promise(resolve => { - const consume = () => { - const files = this._queue.splice(0, TscTranspiler.P); - if (files.length === 0) { - // DONE - resolve(undefined); - return; - } - // work on the NEXT file - // const [inFile, outFn] = req; - worker.next(files, { compilerOptions: this._cmdLine.options }).then(outFiles => { - if (this.onOutfile) { - outFiles.map(this.onOutfile, this); - } - consume(); - }).catch(err => { - this._onError(err); - }); - }; - consume(); - }); - this._allJobs.push(job); - } - } -} -exports.TscTranspiler = TscTranspiler; -class ESBuildTranspiler { - _logFn; - _onError; - _cmdLine; - _outputFileNames; - _jobs = []; - onOutfile; - _transformOpts; - constructor(_logFn, _onError, configFilePath, _cmdLine) { - this._logFn = _logFn; - this._onError = _onError; - this._cmdLine = _cmdLine; - _logFn('Transpile', `will use ESBuild to transpile source files`); - this._outputFileNames = new OutputFileNameOracle(_cmdLine, configFilePath); - const isExtension = configFilePath.includes('extensions'); - const target = (0, tsconfigUtils_1.getTargetStringFromTsConfig)(configFilePath); - this._transformOpts = { - target: [target], - format: isExtension ? 'cjs' : 'esm', - platform: isExtension ? 'node' : undefined, - loader: 'ts', - sourcemap: 'inline', - tsconfigRaw: JSON.stringify({ - compilerOptions: { - ...this._cmdLine.options, - ...{ - module: isExtension ? typescript_1.default.ModuleKind.CommonJS : undefined - } - } - }), - supported: { - 'class-static-blocks': false, // SEE https://github.com/evanw/esbuild/issues/3823, - 'dynamic-import': !isExtension, // see https://github.com/evanw/esbuild/issues/1281 - 'class-field': !isExtension - } - }; - } - async join() { - const jobs = this._jobs.slice(); - this._jobs.length = 0; - await Promise.allSettled(jobs); - } - transpile(file) { - if (!(file.contents instanceof Buffer)) { - throw Error('file.contents must be a Buffer'); - } - const t1 = Date.now(); - this._jobs.push(esbuild_1.default.transform(file.contents, { - ...this._transformOpts, - sourcefile: file.path, - }).then(result => { - // check if output of a DTS-files isn't just "empty" and iff so - // skip this file - if (file.path.endsWith('.d.ts') && _isDefaultEmpty(result.code)) { - return; - } - const outBase = this._cmdLine.options.outDir ?? file.base; - const outPath = this._outputFileNames.getOutputFileName(file.path); - this.onOutfile(new vinyl_1.default({ - path: outPath, - base: outBase, - contents: Buffer.from(result.code), - })); - this._logFn('Transpile', `esbuild took ${Date.now() - t1}ms for ${file.path}`); - }).catch(err => { - this._onError(err); - })); - } -} -exports.ESBuildTranspiler = ESBuildTranspiler; -function _isDefaultEmpty(src) { - return src - .replace('"use strict";', '') - .replace(/\/\/# sourceMappingURL.*^/, '') - .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1') - .trim().length === 0; -} -//# sourceMappingURL=transpiler.js.map \ No newline at end of file diff --git a/build/lib/tsb/transpiler.ts b/build/lib/tsb/transpiler.ts index f81039d70b6..72883a2ab0c 100644 --- a/build/lib/tsb/transpiler.ts +++ b/build/lib/tsb/transpiler.ts @@ -8,7 +8,7 @@ import ts from 'typescript'; import threads from 'node:worker_threads'; import Vinyl from 'vinyl'; import { cpus } from 'node:os'; -import { getTargetStringFromTsConfig } from '../tsconfigUtils'; +import { getTargetStringFromTsConfig } from '../tsconfigUtils.ts'; interface TranspileReq { readonly tsSrcs: string[]; @@ -65,7 +65,7 @@ class OutputFileNameOracle { try { // windows: path-sep normalizing - file = (ts).normalizePath(file); + file = (ts as InternalTsApi).normalizePath(file); if (!cmdLine.options.configFilePath) { // this is needed for the INTERNAL getOutputFileNames-call below... @@ -76,7 +76,7 @@ class OutputFileNameOracle { file = file.slice(0, -5) + '.ts'; cmdLine.fileNames.push(file); } - const outfile = (ts).getOutputFileNames(cmdLine, file, true)[0]; + const outfile = (ts as InternalTsApi).getOutputFileNames(cmdLine, file, true)[0]; if (isDts) { cmdLine.fileNames.pop(); } @@ -97,7 +97,7 @@ class TranspileWorker { readonly id = TranspileWorker.pool++; - private _worker = new threads.Worker(__filename); + private _worker = new threads.Worker(import.meta.filename); private _pending?: [resolve: Function, reject: Function, file: Vinyl[], options: ts.TranspileOptions, t1: number]; private _durations: number[] = []; @@ -124,11 +124,11 @@ class TranspileWorker { diag.push(...diag); continue; } - const enum SuffixTypes { - Dts = 5, - Ts = 3, - Unknown = 0 - } + const SuffixTypes = { + Dts: 5, + Ts: 3, + Unknown: 0 + } as const; const suffixLen = file.path.endsWith('.d.ts') ? SuffixTypes.Dts : file.path.endsWith('.ts') ? SuffixTypes.Ts : SuffixTypes.Unknown; @@ -203,14 +203,21 @@ export class TscTranspiler implements ITranspiler { private _queue: Vinyl[] = []; private _allJobs: Promise[] = []; + private readonly _logFn: (topic: string, message: string) => void; + private readonly _onError: (err: any) => void; + private readonly _cmdLine: ts.ParsedCommandLine; + constructor( logFn: (topic: string, message: string) => void, - private readonly _onError: (err: any) => void, + onError: (err: any) => void, configFilePath: string, - private readonly _cmdLine: ts.ParsedCommandLine + cmdLine: ts.ParsedCommandLine ) { - logFn('Transpile', `will use ${TscTranspiler.P} transpile worker`); - this._outputFileNames = new OutputFileNameOracle(_cmdLine, configFilePath); + this._logFn = logFn; + this._onError = onError; + this._cmdLine = cmdLine; + this._logFn('Transpile', `will use ${TscTranspiler.P} transpile worker`); + this._outputFileNames = new OutputFileNameOracle(this._cmdLine, configFilePath); } async join() { @@ -300,15 +307,21 @@ export class ESBuildTranspiler implements ITranspiler { onOutfile?: ((file: Vinyl) => void) | undefined; private readonly _transformOpts: esbuild.TransformOptions; + private readonly _logFn: (topic: string, message: string) => void; + private readonly _onError: (err: any) => void; + private readonly _cmdLine: ts.ParsedCommandLine; constructor( - private readonly _logFn: (topic: string, message: string) => void, - private readonly _onError: (err: any) => void, + logFn: (topic: string, message: string) => void, + onError: (err: any) => void, configFilePath: string, - private readonly _cmdLine: ts.ParsedCommandLine + cmdLine: ts.ParsedCommandLine ) { - _logFn('Transpile', `will use ESBuild to transpile source files`); - this._outputFileNames = new OutputFileNameOracle(_cmdLine, configFilePath); + this._logFn = logFn; + this._onError = onError; + this._cmdLine = cmdLine; + this._logFn('Transpile', `will use ESBuild to transpile source files`); + this._outputFileNames = new OutputFileNameOracle(this._cmdLine, configFilePath); const isExtension = configFilePath.includes('extensions'); diff --git a/build/lib/tsb/utils.js b/build/lib/tsb/utils.js deleted file mode 100644 index 2ea820c6e6b..00000000000 --- a/build/lib/tsb/utils.js +++ /dev/null @@ -1,96 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.graph = exports.strings = void 0; -var strings; -(function (strings) { - function format(value, ...rest) { - return value.replace(/({\d+})/g, function (match) { - const index = Number(match.substring(1, match.length - 1)); - return String(rest[index]) || match; - }); - } - strings.format = format; -})(strings || (exports.strings = strings = {})); -var graph; -(function (graph) { - class Node { - data; - incoming = new Map(); - outgoing = new Map(); - constructor(data) { - this.data = data; - } - } - graph.Node = Node; - class Graph { - _nodes = new Map(); - inertEdge(from, to) { - const fromNode = this.lookupOrInsertNode(from); - const toNode = this.lookupOrInsertNode(to); - fromNode.outgoing.set(toNode.data, toNode); - toNode.incoming.set(fromNode.data, fromNode); - } - resetNode(data) { - const node = this._nodes.get(data); - if (!node) { - return; - } - for (const outDep of node.outgoing.values()) { - outDep.incoming.delete(node.data); - } - node.outgoing.clear(); - } - lookupOrInsertNode(data) { - let node = this._nodes.get(data); - if (!node) { - node = new Node(data); - this._nodes.set(data, node); - } - return node; - } - lookup(data) { - return this._nodes.get(data) ?? null; - } - findCycles(allData) { - const result = new Map(); - const checked = new Set(); - for (const data of allData) { - const node = this.lookup(data); - if (!node) { - continue; - } - const r = this._findCycle(node, checked, new Set()); - result.set(node.data, r); - } - return result; - } - _findCycle(node, checked, seen) { - if (checked.has(node.data)) { - return undefined; - } - let result; - for (const child of node.outgoing.values()) { - if (seen.has(child.data)) { - const seenArr = Array.from(seen); - const idx = seenArr.indexOf(child.data); - seenArr.push(child.data); - return idx > 0 ? seenArr.slice(idx) : seenArr; - } - seen.add(child.data); - result = this._findCycle(child, checked, seen); - seen.delete(child.data); - if (result) { - break; - } - } - checked.add(node.data); - return result; - } - } - graph.Graph = Graph; -})(graph || (exports.graph = graph = {})); -//# sourceMappingURL=utils.js.map \ No newline at end of file diff --git a/build/lib/tsb/utils.ts b/build/lib/tsb/utils.ts index 7f0bbdd5f23..4c5abb3e9c6 100644 --- a/build/lib/tsb/utils.ts +++ b/build/lib/tsb/utils.ts @@ -3,29 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export namespace strings { +export const strings = (() => { - export function format(value: string, ...rest: unknown[]): string { - return value.replace(/({\d+})/g, function (match) { + function format(value: string, ...rest: unknown[]): string { + return value.replace(/(\{\d+\})/g, function (match) { const index = Number(match.substring(1, match.length - 1)); return String(rest[index]) || match; }); } -} -export namespace graph { + return { format }; +})(); - export class Node { +export const graph = (() => { + + class Node { readonly incoming = new Map>(); readonly outgoing = new Map>(); + readonly data: T; - constructor(readonly data: T) { - + constructor(data: T) { + this.data = data; } } - export class Graph { + class Graph { private _nodes = new Map>(); @@ -103,4 +106,5 @@ export namespace graph { } } -} + return { Node, Graph }; +})(); diff --git a/build/lib/tsconfig.js b/build/lib/tsconfig.js deleted file mode 100644 index 929d74e3b57..00000000000 --- a/build/lib/tsconfig.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTargetStringFromTsConfig = getTargetStringFromTsConfig; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const posix_1 = require("path/posix"); -const typescript_1 = __importDefault(require("typescript")); -/** - * Get the target (e.g. 'ES2024') from a tsconfig.json file. - */ -function getTargetStringFromTsConfig(configFilePath) { - const parsed = typescript_1.default.readConfigFile(configFilePath, typescript_1.default.sys.readFile); - if (parsed.error) { - throw new Error(`Cannot determine target from ${configFilePath}. TS error: ${parsed.error.messageText}`); - } - const cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, (0, posix_1.dirname)(configFilePath), {}); - const resolved = typeof cmdLine.options.target !== 'undefined' ? typescript_1.default.ScriptTarget[cmdLine.options.target] : undefined; - if (!resolved) { - throw new Error(`Could not resolve target in ${configFilePath}`); - } - return resolved; -} -//# sourceMappingURL=tsconfig.js.map \ No newline at end of file diff --git a/build/lib/tsconfigUtils.js b/build/lib/tsconfigUtils.js deleted file mode 100644 index a20e2d6f77d..00000000000 --- a/build/lib/tsconfigUtils.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getTargetStringFromTsConfig = getTargetStringFromTsConfig; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -const path_1 = require("path"); -const typescript_1 = __importDefault(require("typescript")); -/** - * Get the target (e.g. 'ES2024') from a tsconfig.json file. - */ -function getTargetStringFromTsConfig(configFilePath) { - const parsed = typescript_1.default.readConfigFile(configFilePath, typescript_1.default.sys.readFile); - if (parsed.error) { - throw new Error(`Cannot determine target from ${configFilePath}. TS error: ${parsed.error.messageText}`); - } - const cmdLine = typescript_1.default.parseJsonConfigFileContent(parsed.config, typescript_1.default.sys, (0, path_1.dirname)(configFilePath), {}); - const resolved = typeof cmdLine.options.target !== 'undefined' ? typescript_1.default.ScriptTarget[cmdLine.options.target] : undefined; - if (!resolved) { - throw new Error(`Could not resolve target in ${configFilePath}`); - } - return resolved; -} -//# sourceMappingURL=tsconfigUtils.js.map \ No newline at end of file diff --git a/build/lib/typeScriptLanguageServiceHost.js b/build/lib/typeScriptLanguageServiceHost.js deleted file mode 100644 index 6ba0802102d..00000000000 --- a/build/lib/typeScriptLanguageServiceHost.js +++ /dev/null @@ -1,79 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.TypeScriptLanguageServiceHost = void 0; -const typescript_1 = __importDefault(require("typescript")); -const node_fs_1 = __importDefault(require("node:fs")); -const node_path_1 = require("node:path"); -function normalizePath(filePath) { - return (0, node_path_1.normalize)(filePath); -} -/** - * A TypeScript language service host - */ -class TypeScriptLanguageServiceHost { - ts; - topLevelFiles; - compilerOptions; - constructor(ts, topLevelFiles, compilerOptions) { - this.ts = ts; - this.topLevelFiles = topLevelFiles; - this.compilerOptions = compilerOptions; - } - // --- language service host --------------- - getCompilationSettings() { - return this.compilerOptions; - } - getScriptFileNames() { - return [ - ...this.topLevelFiles.keys(), - this.ts.getDefaultLibFilePath(this.compilerOptions) - ]; - } - getScriptVersion(_fileName) { - return '1'; - } - getProjectVersion() { - return '1'; - } - getScriptSnapshot(fileName) { - fileName = normalizePath(fileName); - if (this.topLevelFiles.has(fileName)) { - return this.ts.ScriptSnapshot.fromString(this.topLevelFiles.get(fileName)); - } - else { - return typescript_1.default.ScriptSnapshot.fromString(node_fs_1.default.readFileSync(fileName).toString()); - } - } - getScriptKind(_fileName) { - return this.ts.ScriptKind.TS; - } - getCurrentDirectory() { - return ''; - } - getDefaultLibFileName(options) { - return this.ts.getDefaultLibFilePath(options); - } - readFile(path, encoding) { - path = normalizePath(path); - if (this.topLevelFiles.get(path)) { - return this.topLevelFiles.get(path); - } - return typescript_1.default.sys.readFile(path, encoding); - } - fileExists(path) { - path = normalizePath(path); - if (this.topLevelFiles.has(path)) { - return true; - } - return typescript_1.default.sys.fileExists(path); - } -} -exports.TypeScriptLanguageServiceHost = TypeScriptLanguageServiceHost; -//# sourceMappingURL=typeScriptLanguageServiceHost.js.map \ No newline at end of file diff --git a/build/lib/typeScriptLanguageServiceHost.ts b/build/lib/typeScriptLanguageServiceHost.ts index f3bacd617d5..94c304fe094 100644 --- a/build/lib/typeScriptLanguageServiceHost.ts +++ b/build/lib/typeScriptLanguageServiceHost.ts @@ -18,11 +18,19 @@ function normalizePath(filePath: string): string { */ export class TypeScriptLanguageServiceHost implements ts.LanguageServiceHost { + private readonly ts: typeof import('typescript'); + private readonly topLevelFiles: IFileMap; + private readonly compilerOptions: ts.CompilerOptions; + constructor( - private readonly ts: typeof import('typescript'), - private readonly topLevelFiles: IFileMap, - private readonly compilerOptions: ts.CompilerOptions, - ) { } + ts: typeof import('typescript'), + topLevelFiles: IFileMap, + compilerOptions: ts.CompilerOptions, + ) { + this.ts = ts; + this.topLevelFiles = topLevelFiles; + this.compilerOptions = compilerOptions; + } // --- language service host --------------- getCompilationSettings(): ts.CompilerOptions { diff --git a/build/lib/typings/@vscode/gulp-electron.d.ts b/build/lib/typings/@vscode/gulp-electron.d.ts new file mode 100644 index 00000000000..aaf1b861a87 --- /dev/null +++ b/build/lib/typings/@vscode/gulp-electron.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module '@vscode/gulp-electron' { + + interface MainFunction { + (options: any): NodeJS.ReadWriteStream; + dest(destination: string, options: any): NodeJS.ReadWriteStream; + } + + const main: MainFunction; + export default main; +} diff --git a/build/lib/typings/asar.d.ts b/build/lib/typings/asar.d.ts new file mode 100644 index 00000000000..cdb5b6395c5 --- /dev/null +++ b/build/lib/typings/asar.d.ts @@ -0,0 +1,9 @@ +declare module 'asar/lib/filesystem.js' { + + export default class AsarFilesystem { + readonly header: unknown; + constructor(src: string); + insertDirectory(path: string, shouldUnpack?: boolean): unknown; + insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number } }, options: {}): Promise; + } +} diff --git a/build/lib/typings/chromium-pickle-js.d.ts b/build/lib/typings/chromium-pickle-js.d.ts new file mode 100644 index 00000000000..e2fcd8dc096 --- /dev/null +++ b/build/lib/typings/chromium-pickle-js.d.ts @@ -0,0 +1,10 @@ +declare module 'chromium-pickle-js' { + export interface Pickle { + writeString(value: string): void; + writeUInt32(value: number): void; + + toBuffer(): Buffer; + } + + export function createEmpty(): Pickle; +} diff --git a/build/lib/typings/gulp-azure-storage.d.ts b/build/lib/typings/gulp-azure-storage.d.ts new file mode 100644 index 00000000000..4e9f560c8f2 --- /dev/null +++ b/build/lib/typings/gulp-azure-storage.d.ts @@ -0,0 +1,5 @@ +declare module 'gulp-azure-storage' { + import { ThroughStream } from 'event-stream'; + + export function upload(options: any): ThroughStream; +} diff --git a/build/lib/typings/gulp-gunzip.d.ts b/build/lib/typings/gulp-gunzip.d.ts new file mode 100644 index 00000000000..a68a350d5e7 --- /dev/null +++ b/build/lib/typings/gulp-gunzip.d.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'gulp-gunzip' { + import type { Transform } from 'stream'; + + /** + * Gunzip plugin for gulp + */ + function gunzip(): Transform; + + export = gunzip; +} diff --git a/build/lib/typings/gulp-vinyl-zip.d.ts b/build/lib/typings/gulp-vinyl-zip.d.ts new file mode 100644 index 00000000000..d28166ffa77 --- /dev/null +++ b/build/lib/typings/gulp-vinyl-zip.d.ts @@ -0,0 +1,4 @@ + +declare module 'gulp-vinyl-zip' { + export function src(): NodeJS.ReadWriteStream; +} diff --git a/build/lib/typings/rcedit.d.ts b/build/lib/typings/rcedit.d.ts new file mode 100644 index 00000000000..e18d3f93584 --- /dev/null +++ b/build/lib/typings/rcedit.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'rcedit' { + export default function rcedit(exePath, options, cb): Promise; +} diff --git a/build/lib/typings/vscode-gulp-watch.d.ts b/build/lib/typings/vscode-gulp-watch.d.ts new file mode 100644 index 00000000000..24316c07f16 --- /dev/null +++ b/build/lib/typings/vscode-gulp-watch.d.ts @@ -0,0 +1,3 @@ +declare module 'vscode-gulp-watch' { + export default function watch(...args: any[]): any; +} diff --git a/build/lib/util.js b/build/lib/util.js deleted file mode 100644 index 7a11c742064..00000000000 --- a/build/lib/util.js +++ /dev/null @@ -1,398 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.VinylStat = void 0; -exports.incremental = incremental; -exports.debounce = debounce; -exports.fixWin32DirectoryPermissions = fixWin32DirectoryPermissions; -exports.setExecutableBit = setExecutableBit; -exports.toFileUri = toFileUri; -exports.skipDirectories = skipDirectories; -exports.cleanNodeModules = cleanNodeModules; -exports.loadSourcemaps = loadSourcemaps; -exports.stripSourceMappingURL = stripSourceMappingURL; -exports.$if = $if; -exports.appendOwnPathSourceURL = appendOwnPathSourceURL; -exports.rewriteSourceMappingURL = rewriteSourceMappingURL; -exports.rimraf = rimraf; -exports.rreddir = rreddir; -exports.ensureDir = ensureDir; -exports.rebase = rebase; -exports.filter = filter; -exports.streamToPromise = streamToPromise; -exports.getElectronVersion = getElectronVersion; -const es = __importStar(require("event-stream")); -const debounce_1 = __importDefault(require("debounce")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const gulp_rename_1 = __importDefault(require("gulp-rename")); -const path_1 = __importDefault(require("path")); -const fs_1 = __importDefault(require("fs")); -const url_1 = require("url"); -const ternary_stream_1 = __importDefault(require("ternary-stream")); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -// Use require for rimraf 2.2.8 (CommonJS module, no default export) -const rimrafModule = require('rimraf'); -const NoCancellationToken = { isCancellationRequested: () => false }; -function incremental(streamProvider, initial, supportsCancellation) { - const input = es.through(); - const output = es.through(); - let state = 'idle'; - let buffer = Object.create(null); - const token = !supportsCancellation ? undefined : { isCancellationRequested: () => Object.keys(buffer).length > 0 }; - const run = (input, isCancellable) => { - state = 'running'; - const stream = !supportsCancellation ? streamProvider() : streamProvider(isCancellable ? token : NoCancellationToken); - input - .pipe(stream) - .pipe(es.through(undefined, () => { - state = 'idle'; - eventuallyRun(); - })) - .pipe(output); - }; - if (initial) { - run(initial, false); - } - const eventuallyRun = (0, debounce_1.default)(() => { - const paths = Object.keys(buffer); - if (paths.length === 0) { - return; - } - const data = paths.map(path => buffer[path]); - buffer = Object.create(null); - run(es.readArray(data), true); - }, 500); - input.on('data', (f) => { - buffer[f.path] = f; - if (state === 'idle') { - eventuallyRun(); - } - }); - return es.duplex(input, output); -} -function debounce(task, duration = 500) { - const input = es.through(); - const output = es.through(); - let state = 'idle'; - const run = () => { - state = 'running'; - task() - .pipe(es.through(undefined, () => { - const shouldRunAgain = state === 'stale'; - state = 'idle'; - if (shouldRunAgain) { - eventuallyRun(); - } - })) - .pipe(output); - }; - run(); - const eventuallyRun = (0, debounce_1.default)(() => run(), duration); - input.on('data', () => { - if (state === 'idle') { - eventuallyRun(); - } - else { - state = 'stale'; - } - }); - return es.duplex(input, output); -} -function fixWin32DirectoryPermissions() { - if (!/win32/.test(process.platform)) { - return es.through(); - } - return es.mapSync(f => { - if (f.stat && f.stat.isDirectory && f.stat.isDirectory()) { - f.stat.mode = 16877; - } - return f; - }); -} -function setExecutableBit(pattern) { - const setBit = es.mapSync(f => { - if (!f.stat) { - const stat = { isFile() { return true; }, mode: 0 }; - f.stat = stat; - } - f.stat.mode = /* 100755 */ 33261; - return f; - }); - if (!pattern) { - return setBit; - } - const input = es.through(); - const filter = (0, gulp_filter_1.default)(pattern, { restore: true }); - const output = input - .pipe(filter) - .pipe(setBit) - .pipe(filter.restore); - return es.duplex(input, output); -} -function toFileUri(filePath) { - const match = filePath.match(/^([a-z])\:(.*)$/i); - if (match) { - filePath = '/' + match[1].toUpperCase() + ':' + match[2]; - } - return 'file://' + filePath.replace(/\\/g, '/'); -} -function skipDirectories() { - return es.mapSync(f => { - if (!f.isDirectory()) { - return f; - } - }); -} -function cleanNodeModules(rulePath) { - const rules = fs_1.default.readFileSync(rulePath, 'utf8') - .split(/\r?\n/g) - .map(line => line.trim()) - .filter(line => line && !/^#/.test(line)); - const excludes = rules.filter(line => !/^!/.test(line)).map(line => `!**/node_modules/${line}`); - const includes = rules.filter(line => /^!/.test(line)).map(line => `**/node_modules/${line.substr(1)}`); - const input = es.through(); - const output = es.merge(input.pipe((0, gulp_filter_1.default)(['**', ...excludes])), input.pipe((0, gulp_filter_1.default)(includes))); - return es.duplex(input, output); -} -function loadSourcemaps() { - const input = es.through(); - const output = input - .pipe(es.map((f, cb) => { - if (f.sourceMap) { - cb(undefined, f); - return; - } - if (!f.contents) { - cb(undefined, f); - return; - } - const contents = f.contents.toString('utf8'); - const reg = /\/\/# sourceMappingURL=(.*)$/g; - let lastMatch = null; - let match = null; - while (match = reg.exec(contents)) { - lastMatch = match; - } - if (!lastMatch) { - f.sourceMap = { - version: '3', - names: [], - mappings: '', - sources: [f.relative.replace(/\\/g, '/')], - sourcesContent: [contents] - }; - cb(undefined, f); - return; - } - f.contents = Buffer.from(contents.replace(/\/\/# sourceMappingURL=(.*)$/g, ''), 'utf8'); - fs_1.default.readFile(path_1.default.join(path_1.default.dirname(f.path), lastMatch[1]), 'utf8', (err, contents) => { - if (err) { - return cb(err); - } - f.sourceMap = JSON.parse(contents); - cb(undefined, f); - }); - })); - return es.duplex(input, output); -} -function stripSourceMappingURL() { - const input = es.through(); - const output = input - .pipe(es.mapSync(f => { - const contents = f.contents.toString('utf8'); - f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); - return f; - })); - return es.duplex(input, output); -} -/** Splits items in the stream based on the predicate, sending them to onTrue if true, or onFalse otherwise */ -function $if(test, onTrue, onFalse = es.through()) { - if (typeof test === 'boolean') { - return test ? onTrue : onFalse; - } - return (0, ternary_stream_1.default)(test, onTrue, onFalse); -} -/** Operator that appends the js files' original path a sourceURL, so debug locations map */ -function appendOwnPathSourceURL() { - const input = es.through(); - const output = input - .pipe(es.mapSync(f => { - if (!(f.contents instanceof Buffer)) { - throw new Error(`contents of ${f.path} are not a buffer`); - } - f.contents = Buffer.concat([f.contents, Buffer.from(`\n//# sourceURL=${(0, url_1.pathToFileURL)(f.path)}`)]); - return f; - })); - return es.duplex(input, output); -} -function rewriteSourceMappingURL(sourceMappingURLBase) { - const input = es.through(); - const output = input - .pipe(es.mapSync(f => { - const contents = f.contents.toString('utf8'); - const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path_1.default.dirname(f.relative).replace(/\\/g, '/')}/$1`; - f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str)); - return f; - })); - return es.duplex(input, output); -} -function rimraf(dir) { - const result = () => new Promise((c, e) => { - let retries = 0; - const retry = () => { - rimrafModule(dir, { maxBusyTries: 1 }, (err) => { - if (!err) { - return c(); - } - if (err.code === 'ENOTEMPTY' && ++retries < 5) { - return setTimeout(() => retry(), 10); - } - return e(err); - }); - }; - retry(); - }); - result.taskName = `clean-${path_1.default.basename(dir).toLowerCase()}`; - return result; -} -function _rreaddir(dirPath, prepend, result) { - const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true }); - for (const entry of entries) { - if (entry.isDirectory()) { - _rreaddir(path_1.default.join(dirPath, entry.name), `${prepend}/${entry.name}`, result); - } - else { - result.push(`${prepend}/${entry.name}`); - } - } -} -function rreddir(dirPath) { - const result = []; - _rreaddir(dirPath, '', result); - return result; -} -function ensureDir(dirPath) { - if (fs_1.default.existsSync(dirPath)) { - return; - } - ensureDir(path_1.default.dirname(dirPath)); - fs_1.default.mkdirSync(dirPath); -} -function rebase(count) { - return (0, gulp_rename_1.default)(f => { - const parts = f.dirname ? f.dirname.split(/[\/\\]/) : []; - f.dirname = parts.slice(count).join(path_1.default.sep); - }); -} -function filter(fn) { - const result = es.through(function (data) { - if (fn(data)) { - this.emit('data', data); - } - else { - result.restore.push(data); - } - }); - result.restore = es.through(); - return result; -} -function streamToPromise(stream) { - return new Promise((c, e) => { - stream.on('error', err => e(err)); - stream.on('end', () => c()); - }); -} -function getElectronVersion() { - const npmrc = fs_1.default.readFileSync(path_1.default.join(root, '.npmrc'), 'utf8'); - const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; - const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; - return { electronVersion, msBuildId }; -} -class VinylStat { - dev; - ino; - mode; - nlink; - uid; - gid; - rdev; - size; - blksize; - blocks; - atimeMs; - mtimeMs; - ctimeMs; - birthtimeMs; - atime; - mtime; - ctime; - birthtime; - constructor(stat) { - this.dev = stat.dev ?? 0; - this.ino = stat.ino ?? 0; - this.mode = stat.mode ?? 0; - this.nlink = stat.nlink ?? 0; - this.uid = stat.uid ?? 0; - this.gid = stat.gid ?? 0; - this.rdev = stat.rdev ?? 0; - this.size = stat.size ?? 0; - this.blksize = stat.blksize ?? 0; - this.blocks = stat.blocks ?? 0; - this.atimeMs = stat.atimeMs ?? 0; - this.mtimeMs = stat.mtimeMs ?? 0; - this.ctimeMs = stat.ctimeMs ?? 0; - this.birthtimeMs = stat.birthtimeMs ?? 0; - this.atime = stat.atime ?? new Date(0); - this.mtime = stat.mtime ?? new Date(0); - this.ctime = stat.ctime ?? new Date(0); - this.birthtime = stat.birthtime ?? new Date(0); - } - isFile() { return true; } - isDirectory() { return false; } - isBlockDevice() { return false; } - isCharacterDevice() { return false; } - isSymbolicLink() { return false; } - isFIFO() { return false; } - isSocket() { return false; } -} -exports.VinylStat = VinylStat; -//# sourceMappingURL=util.js.map \ No newline at end of file diff --git a/build/lib/util.ts b/build/lib/util.ts index e8b10b1a648..792a932a916 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -11,12 +11,14 @@ import path from 'path'; import fs from 'fs'; import VinylFile from 'vinyl'; -import { ThroughStream } from 'through'; +import through from 'through'; import sm from 'source-map'; import { pathToFileURL } from 'url'; import ternaryStream from 'ternary-stream'; +import type { Transform } from 'stream'; +import * as tar from 'tar'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); // Use require for rimraf 2.2.8 (CommonJS module, no default export) const rimrafModule = require('rimraf'); @@ -206,8 +208,7 @@ export function loadSourcemaps(): NodeJS.ReadWriteStream { return; } - const contents = (f.contents).toString('utf8'); - + const contents = (f.contents as Buffer).toString('utf8'); const reg = /\/\/# sourceMappingURL=(.*)$/g; let lastMatch: RegExpExecArray | null = null; let match: RegExpExecArray | null = null; @@ -247,7 +248,7 @@ export function stripSourceMappingURL(): NodeJS.ReadWriteStream { const output = input .pipe(es.mapSync(f => { - const contents = (f.contents).toString('utf8'); + const contents = (f.contents as Buffer).toString('utf8'); f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, ''), 'utf8'); return f; })); @@ -286,7 +287,7 @@ export function rewriteSourceMappingURL(sourceMappingURLBase: string): NodeJS.Re const output = input .pipe(es.mapSync(f => { - const contents = (f.contents).toString('utf8'); + const contents = (f.contents as Buffer).toString('utf8'); const str = `//# sourceMappingURL=${sourceMappingURLBase}/${path.dirname(f.relative).replace(/\\/g, '/')}/$1`; f.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, str)); return f; @@ -353,7 +354,7 @@ export function rebase(count: number): NodeJS.ReadWriteStream { } export interface FilterStream extends NodeJS.ReadWriteStream { - restore: ThroughStream; + restore: through.ThroughStream; } export function filter(fn: (data: any) => boolean): FilterStream { @@ -433,3 +434,39 @@ export class VinylStat implements fs.Stats { isFIFO(): boolean { return false; } isSocket(): boolean { return false; } } + +export function untar(): Transform { + return es.through(function (this: through.ThroughStream, f: VinylFile) { + if (!f.contents || !Buffer.isBuffer(f.contents)) { + this.emit('error', new Error('Expected file with Buffer contents')); + return; + } + + const self = this; + const parser = new tar.Parser(); + + parser.on('entry', (entry: tar.ReadEntry) => { + if (entry.type === 'File') { + const chunks: Buffer[] = []; + entry.on('data', (chunk: Buffer) => chunks.push(chunk)); + entry.on('end', () => { + const file = new VinylFile({ + path: entry.path, + contents: Buffer.concat(chunks), + stat: new VinylStat({ + mode: entry.mode, + mtime: entry.mtime, + size: entry.size + }) + }); + self.emit('data', file); + }); + } else { + entry.resume(); + } + }); + + parser.on('error', (err: Error) => self.emit('error', err)); + parser.end(f.contents); + }) as Transform; +} diff --git a/build/lib/watch/index.js b/build/lib/watch/index.js deleted file mode 100644 index 69eca78fd70..00000000000 --- a/build/lib/watch/index.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); -module.exports = function () { - return watch.apply(null, arguments); -}; -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/build/lib/watch/index.ts b/build/lib/watch/index.ts index ce4bdfd75ed..763cacc6d89 100644 --- a/build/lib/watch/index.ts +++ b/build/lib/watch/index.ts @@ -2,9 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { createRequire } from 'node:module'; -const watch = process.platform === 'win32' ? require('./watch-win32') : require('vscode-gulp-watch'); +const require = createRequire(import.meta.url); +const watch = process.platform === 'win32' ? require('./watch-win32.ts').default : require('vscode-gulp-watch'); -module.exports = function () { - return watch.apply(null, arguments); -}; +export default function (...args: any[]): ReturnType { + return watch.apply(null, args); +} diff --git a/build/lib/watch/watch-win32.js b/build/lib/watch/watch-win32.js deleted file mode 100644 index 7b77981d620..00000000000 --- a/build/lib/watch/watch-win32.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const path_1 = __importDefault(require("path")); -const child_process_1 = __importDefault(require("child_process")); -const fs_1 = __importDefault(require("fs")); -const vinyl_1 = __importDefault(require("vinyl")); -const event_stream_1 = __importDefault(require("event-stream")); -const gulp_filter_1 = __importDefault(require("gulp-filter")); -const watcherPath = path_1.default.join(__dirname, 'watcher.exe'); -function toChangeType(type) { - switch (type) { - case '0': return 'change'; - case '1': return 'add'; - default: return 'unlink'; - } -} -function watch(root) { - const result = event_stream_1.default.through(); - let child = child_process_1.default.spawn(watcherPath, [root]); - child.stdout.on('data', function (data) { - const lines = data.toString('utf8').split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - if (line.length === 0) { - continue; - } - const changeType = line[0]; - const changePath = line.substr(2); - // filter as early as possible - if (/^\.git/.test(changePath) || /(^|\\)out($|\\)/.test(changePath)) { - continue; - } - const changePathFull = path_1.default.join(root, changePath); - const file = new vinyl_1.default({ - path: changePathFull, - base: root - }); - file.event = toChangeType(changeType); - result.emit('data', file); - } - }); - child.stderr.on('data', function (data) { - result.emit('error', data); - }); - child.on('exit', function (code) { - result.emit('error', 'Watcher died with code ' + code); - child = null; - }); - process.once('SIGTERM', function () { process.exit(0); }); - process.once('SIGTERM', function () { process.exit(0); }); - process.once('exit', function () { if (child) { - child.kill(); - } }); - return result; -} -const cache = Object.create(null); -module.exports = function (pattern, options) { - options = options || {}; - const cwd = path_1.default.normalize(options.cwd || process.cwd()); - let watcher = cache[cwd]; - if (!watcher) { - watcher = cache[cwd] = watch(cwd); - } - const rebase = !options.base ? event_stream_1.default.through() : event_stream_1.default.mapSync(function (f) { - f.base = options.base; - return f; - }); - return watcher - .pipe((0, gulp_filter_1.default)(['**', '!.git{,/**}'], { dot: options.dot })) // ignore all things git - .pipe((0, gulp_filter_1.default)(pattern, { dot: options.dot })) - .pipe(event_stream_1.default.map(function (file, cb) { - fs_1.default.stat(file.path, function (err, stat) { - if (err && err.code === 'ENOENT') { - return cb(undefined, file); - } - if (err) { - return cb(); - } - if (!stat.isFile()) { - return cb(); - } - fs_1.default.readFile(file.path, function (err, contents) { - if (err && err.code === 'ENOENT') { - return cb(undefined, file); - } - if (err) { - return cb(); - } - file.contents = contents; - file.stat = stat; - cb(undefined, file); - }); - }); - })) - .pipe(rebase); -}; -//# sourceMappingURL=watch-win32.js.map \ No newline at end of file diff --git a/build/lib/watch/watch-win32.ts b/build/lib/watch/watch-win32.ts index 38cbdea80b2..12b8ffc0ac3 100644 --- a/build/lib/watch/watch-win32.ts +++ b/build/lib/watch/watch-win32.ts @@ -11,7 +11,7 @@ import es from 'event-stream'; import filter from 'gulp-filter'; import { Stream } from 'stream'; -const watcherPath = path.join(__dirname, 'watcher.exe'); +const watcherPath = path.join(import.meta.dirname, 'watcher.exe'); function toChangeType(type: '0' | '1' | '2'): 'change' | 'add' | 'unlink' { switch (type) { @@ -33,7 +33,7 @@ function watch(root: string): Stream { continue; } - const changeType = <'0' | '1' | '2'>line[0]; + const changeType = line[0] as '0' | '1' | '2'; const changePath = line.substr(2); // filter as early as possible @@ -70,7 +70,7 @@ function watch(root: string): Stream { const cache: { [cwd: string]: Stream } = Object.create(null); -module.exports = function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; dot?: boolean }) { +export default function (pattern: string | string[] | filter.FileFunction, options?: { cwd?: string; base?: string; dot?: boolean }) { options = options || {}; const cwd = path.normalize(options.cwd || process.cwd()); @@ -105,4 +105,4 @@ module.exports = function (pattern: string | string[] | filter.FileFunction, opt }); })) .pipe(rebase); -}; +} diff --git a/build/linux/debian/calculate-deps.js b/build/linux/debian/calculate-deps.js deleted file mode 100644 index 34276ce7705..00000000000 --- a/build/linux/debian/calculate-deps.js +++ /dev/null @@ -1,89 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = generatePackageDeps; -const child_process_1 = require("child_process"); -const fs_1 = require("fs"); -const os_1 = require("os"); -const path_1 = __importDefault(require("path")); -const cgmanifest_json_1 = __importDefault(require("../../../cgmanifest.json")); -const dep_lists_1 = require("./dep-lists"); -function generatePackageDeps(files, arch, chromiumSysroot, vscodeSysroot) { - const dependencies = files.map(file => calculatePackageDeps(file, arch, chromiumSysroot, vscodeSysroot)); - const additionalDepsSet = new Set(dep_lists_1.additionalDeps); - dependencies.push(additionalDepsSet); - return dependencies; -} -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/calculate_package_deps.py. -function calculatePackageDeps(binaryPath, arch, chromiumSysroot, vscodeSysroot) { - try { - if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) { - throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); - } - } - catch (e) { - // The package might not exist. Don't re-throw the error here. - console.error('Tried to stat ' + binaryPath + ' but failed.'); - } - // Get the Chromium dpkg-shlibdeps file. - const chromiumManifest = cgmanifest_json_1.default.registrations.filter(registration => { - return registration.component.type === 'git' && registration.component.git.name === 'chromium'; - }); - const dpkgShlibdepsUrl = `https://raw.githubusercontent.com/chromium/chromium/${chromiumManifest[0].version}/third_party/dpkg-shlibdeps/dpkg-shlibdeps.pl`; - const dpkgShlibdepsScriptLocation = `${(0, os_1.tmpdir)()}/dpkg-shlibdeps.pl`; - const result = (0, child_process_1.spawnSync)('curl', [dpkgShlibdepsUrl, '-o', dpkgShlibdepsScriptLocation]); - if (result.status !== 0) { - throw new Error('Cannot retrieve dpkg-shlibdeps. Stderr:\n' + result.stderr); - } - const cmd = [dpkgShlibdepsScriptLocation, '--ignore-weak-undefined']; - switch (arch) { - case 'amd64': - cmd.push(`-l${chromiumSysroot}/usr/lib/x86_64-linux-gnu`, `-l${chromiumSysroot}/lib/x86_64-linux-gnu`, `-l${vscodeSysroot}/usr/lib/x86_64-linux-gnu`, `-l${vscodeSysroot}/lib/x86_64-linux-gnu`); - break; - case 'armhf': - cmd.push(`-l${chromiumSysroot}/usr/lib/arm-linux-gnueabihf`, `-l${chromiumSysroot}/lib/arm-linux-gnueabihf`, `-l${vscodeSysroot}/usr/lib/arm-linux-gnueabihf`, `-l${vscodeSysroot}/lib/arm-linux-gnueabihf`); - break; - case 'arm64': - cmd.push(`-l${chromiumSysroot}/usr/lib/aarch64-linux-gnu`, `-l${chromiumSysroot}/lib/aarch64-linux-gnu`, `-l${vscodeSysroot}/usr/lib/aarch64-linux-gnu`, `-l${vscodeSysroot}/lib/aarch64-linux-gnu`); - break; - } - cmd.push(`-l${chromiumSysroot}/usr/lib`); - cmd.push(`-L${vscodeSysroot}/debian/libxkbfile1/DEBIAN/shlibs`); - cmd.push('-O', '-e', path_1.default.resolve(binaryPath)); - const dpkgShlibdepsResult = (0, child_process_1.spawnSync)('perl', cmd, { cwd: chromiumSysroot }); - if (dpkgShlibdepsResult.status !== 0) { - throw new Error(`dpkg-shlibdeps failed with exit code ${dpkgShlibdepsResult.status}. stderr:\n${dpkgShlibdepsResult.stderr} `); - } - const shlibsDependsPrefix = 'shlibs:Depends='; - const requiresList = dpkgShlibdepsResult.stdout.toString('utf-8').trimEnd().split('\n'); - let depsStr = ''; - for (const line of requiresList) { - if (line.startsWith(shlibsDependsPrefix)) { - depsStr = line.substring(shlibsDependsPrefix.length); - } - } - // Refs https://chromium-review.googlesource.com/c/chromium/src/+/3572926 - // Chromium depends on libgcc_s, is from the package libgcc1. However, in - // Bullseye, the package was renamed to libgcc-s1. To avoid adding a dep - // on the newer package, this hack skips the dep. This is safe because - // libgcc-s1 is a dependency of libc6. This hack can be removed once - // support for Debian Buster and Ubuntu Bionic are dropped. - // - // Remove kerberos native module related dependencies as the versions - // computed from sysroot will not satisfy the minimum supported distros - // Refs https://github.com/microsoft/vscode/issues/188881. - // TODO(deepak1556): remove this workaround in favor of computing the - // versions from build container for native modules. - const filteredDeps = depsStr.split(', ').filter(dependency => { - return !dependency.startsWith('libgcc-s1'); - }).sort(); - const requires = new Set(filteredDeps); - return requires; -} -//# sourceMappingURL=calculate-deps.js.map \ No newline at end of file diff --git a/build/linux/debian/calculate-deps.ts b/build/linux/debian/calculate-deps.ts index addc38696a8..98a96302e19 100644 --- a/build/linux/debian/calculate-deps.ts +++ b/build/linux/debian/calculate-deps.ts @@ -7,9 +7,9 @@ import { spawnSync } from 'child_process'; import { constants, statSync } from 'fs'; import { tmpdir } from 'os'; import path from 'path'; -import manifests from '../../../cgmanifest.json'; -import { additionalDeps } from './dep-lists'; -import { DebianArchString } from './types'; +import manifests from '../../../cgmanifest.json' with { type: 'json' }; +import { additionalDeps } from './dep-lists.ts'; +import type { DebianArchString } from './types.ts'; export function generatePackageDeps(files: string[], arch: DebianArchString, chromiumSysroot: string, vscodeSysroot: string): Set[] { const dependencies: Set[] = files.map(file => calculatePackageDeps(file, arch, chromiumSysroot, vscodeSysroot)); diff --git a/build/linux/debian/dep-lists.js b/build/linux/debian/dep-lists.js deleted file mode 100644 index d30f9bc51f0..00000000000 --- a/build/linux/debian/dep-lists.js +++ /dev/null @@ -1,313 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.referenceGeneratedDepsByArch = exports.recommendedDeps = exports.additionalDeps = void 0; -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/additional_deps -// Additional dependencies not in the dpkg-shlibdeps output. -exports.additionalDeps = [ - 'ca-certificates', // Make sure users have SSL certificates. - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnss3 (>= 3.26)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', // For Breakpad crash reports. - 'xdg-utils (>= 1.0.2)', // OS integration -]; -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/manual_recommends -// Dependencies that we can only recommend -// for now since some of the older distros don't support them. -exports.recommendedDeps = [ - 'libvulkan1' // Move to additionalDeps once support for Trusty and Jessie are dropped. -]; -exports.referenceGeneratedDepsByArch = { - 'amd64': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.14)', - 'libc6 (>= 2.16)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libcairo2 (>= 1.6.0)', - 'libc6 (>= 2.15)', - 'libc6 (>= 2.29)', - 'libc6 (>= 2.30)', - 'libc6 (>= 2.4)', - 'libcups2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' - ], - 'armhf': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.16)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libc6 (>= 2.4)', - 'libc6 (>= 2.9)', - 'libcairo2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 4.1.1)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 5.2)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)' - ], - 'arm64': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.16)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libc6 (>= 2.29)', - 'libc6 (>= 2.30)', - 'libcairo2 (>= 1.6.0)', - 'libcups2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)' - ], - 'ppc64el': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libcairo2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 4.1.1)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 5.2)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' - ], - 'riscv64': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libcairo2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 4.1.1)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 5.2)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' - ], - 'loong64': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libcairo2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 4.1.1)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 5.2)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' - ], - 's390x': [ - 'ca-certificates', - 'libasound2 (>= 1.0.17)', - 'libatk-bridge2.0-0 (>= 2.5.3)', - 'libatk1.0-0 (>= 2.11.90)', - 'libatspi2.0-0 (>= 2.9.90)', - 'libc6 (>= 2.17)', - 'libc6 (>= 2.25)', - 'libc6 (>= 2.28)', - 'libcairo2 (>= 1.6.0)', - 'libcurl3-gnutls | libcurl3-nss | libcurl4 | libcurl3', - 'libdbus-1-3 (>= 1.9.14)', - 'libexpat1 (>= 2.1~beta3)', - 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', - 'libgtk-3-0 (>= 3.9.10)', - 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', - 'libnspr4 (>= 2:4.9-2~)', - 'libnss3 (>= 2:3.30)', - 'libnss3 (>= 3.26)', - 'libpango-1.0-0 (>= 1.14.0)', - 'libstdc++6 (>= 4.1.1)', - 'libstdc++6 (>= 5)', - 'libstdc++6 (>= 5.2)', - 'libstdc++6 (>= 6)', - 'libstdc++6 (>= 9)', - 'libudev1 (>= 183)', - 'libx11-6', - 'libx11-6 (>= 2:1.4.99.1)', - 'libxcb1 (>= 1.9.2)', - 'libxcomposite1 (>= 1:0.4.4-1)', - 'libxdamage1 (>= 1:1.1)', - 'libxext6', - 'libxfixes3', - 'libxkbcommon0 (>= 0.5.0)', - 'libxkbfile1 (>= 1:1.1.0)', - 'libxrandr2', - 'xdg-utils (>= 1.0.2)', - 'zlib1g (>= 1:1.2.3.4)' - ] -}; -//# sourceMappingURL=dep-lists.js.map \ No newline at end of file diff --git a/build/linux/debian/dep-lists.ts b/build/linux/debian/dep-lists.ts index 6aa16198ab1..f2cf2844a0c 100644 --- a/build/linux/debian/dep-lists.ts +++ b/build/linux/debian/dep-lists.ts @@ -32,6 +32,7 @@ export const referenceGeneratedDepsByArch = { 'libc6 (>= 2.17)', 'libc6 (>= 2.25)', 'libc6 (>= 2.28)', + 'libc6 (>= 2.4)', 'libcairo2 (>= 1.6.0)', 'libc6 (>= 2.15)', 'libc6 (>= 2.29)', @@ -42,7 +43,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', + 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', 'libnspr4 (>= 2:4.9-2~)', @@ -83,7 +84,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', + 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', 'libnspr4 (>= 2:4.9-2~)', @@ -163,7 +164,7 @@ export const referenceGeneratedDepsByArch = { 'libdbus-1-3 (>= 1.9.14)', 'libexpat1 (>= 2.1~beta3)', 'libgbm1 (>= 17.1.0~rc2)', - 'libglib2.0-0 (>= 2.37.3)', + 'libglib2.0-0 (>= 2.39.4)', 'libgtk-3-0 (>= 3.9.10)', 'libgtk-3-0 (>= 3.9.10) | libgtk-4-1', 'libnspr4 (>= 2:4.9-2~)', diff --git a/build/linux/debian/install-sysroot.js b/build/linux/debian/install-sysroot.js deleted file mode 100644 index 38a466b2482..00000000000 --- a/build/linux/debian/install-sysroot.js +++ /dev/null @@ -1,231 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.getVSCodeSysroot = getVSCodeSysroot; -exports.getChromiumSysroot = getChromiumSysroot; -const child_process_1 = require("child_process"); -const os_1 = require("os"); -const fs_1 = __importDefault(require("fs")); -const https_1 = __importDefault(require("https")); -const path_1 = __importDefault(require("path")); -const crypto_1 = require("crypto"); -// Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. -const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; -const URL_PATH = 'sysroots/toolchain'; -const REPO_ROOT = path_1.default.dirname(path_1.default.dirname(path_1.default.dirname(__dirname))); -const ghApiHeaders = { - Accept: 'application/vnd.github.v3+json', - 'User-Agent': 'VSCode Build', -}; -if (process.env.GITHUB_TOKEN) { - ghApiHeaders.Authorization = 'Basic ' + Buffer.from(process.env.GITHUB_TOKEN).toString('base64'); -} -const ghDownloadHeaders = { - ...ghApiHeaders, - Accept: 'application/octet-stream', -}; -function getElectronVersion() { - const npmrc = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, '.npmrc'), 'utf8'); - const electronVersion = /^target="(.*)"$/m.exec(npmrc)[1]; - const msBuildId = /^ms_build_id="(.*)"$/m.exec(npmrc)[1]; - return { electronVersion, msBuildId }; -} -function getSha(filename) { - const hash = (0, crypto_1.createHash)('sha256'); - // Read file 1 MB at a time - const fd = fs_1.default.openSync(filename, 'r'); - const buffer = Buffer.alloc(1024 * 1024); - let position = 0; - let bytesRead = 0; - while ((bytesRead = fs_1.default.readSync(fd, buffer, 0, buffer.length, position)) === buffer.length) { - hash.update(buffer); - position += bytesRead; - } - hash.update(buffer.slice(0, bytesRead)); - return hash.digest('hex'); -} -function getVSCodeSysrootChecksum(expectedName) { - const checksums = fs_1.default.readFileSync(path_1.default.join(REPO_ROOT, 'build', 'checksums', 'vscode-sysroot.txt'), 'utf8'); - for (const line of checksums.split('\n')) { - const [checksum, name] = line.split(/\s+/); - if (name === expectedName) { - return checksum; - } - } - return undefined; -} -/* - * Do not use the fetch implementation from build/lib/fetch as it relies on vinyl streams - * and vinyl-fs breaks the symlinks in the compiler toolchain sysroot. We use the native - * tar implementation for that reason. - */ -async function fetchUrl(options, retries = 10, retryDelay = 1000) { - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 30 * 1000); - const version = '20250407-330404'; - try { - const response = await fetch(`https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`, { - headers: ghApiHeaders, - signal: controller.signal - }); - if (response.ok && (response.status >= 200 && response.status < 300)) { - console.log(`Fetch completed: Status ${response.status}.`); - const contents = Buffer.from(await response.arrayBuffer()); - const asset = JSON.parse(contents.toString()).assets.find((a) => a.name === options.assetName); - if (!asset) { - throw new Error(`Could not find asset in release of Microsoft/vscode-linux-build-agent @ ${version}`); - } - console.log(`Found asset ${options.assetName} @ ${asset.url}.`); - const assetResponse = await fetch(asset.url, { - headers: ghDownloadHeaders - }); - if (assetResponse.ok && (assetResponse.status >= 200 && assetResponse.status < 300)) { - const assetContents = Buffer.from(await assetResponse.arrayBuffer()); - console.log(`Fetched response body buffer: ${assetContents.byteLength} bytes`); - if (options.checksumSha256) { - const actualSHA256Checksum = (0, crypto_1.createHash)('sha256').update(assetContents).digest('hex'); - if (actualSHA256Checksum !== options.checksumSha256) { - throw new Error(`Checksum mismatch for ${asset.url} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum}))`); - } - } - console.log(`Verified SHA256 checksums match for ${asset.url}`); - const tarCommand = `tar -xz -C ${options.dest}`; - (0, child_process_1.execSync)(tarCommand, { input: assetContents }); - console.log(`Fetch complete!`); - return; - } - throw new Error(`Request ${asset.url} failed with status code: ${assetResponse.status}`); - } - throw new Error(`Request https://api.github.com failed with status code: ${response.status}`); - } - finally { - clearTimeout(timeout); - } - } - catch (e) { - if (retries > 0) { - console.log(`Fetching failed: ${e}`); - await new Promise(resolve => setTimeout(resolve, retryDelay)); - return fetchUrl(options, retries - 1, retryDelay); - } - throw e; - } -} -async function getVSCodeSysroot(arch, isMusl = false) { - let expectedName; - let triple; - const prefix = process.env['VSCODE_SYSROOT_PREFIX'] ?? '-glibc-2.28-gcc-10.5.0'; - switch (arch) { - case 'amd64': - expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; - triple = 'x86_64-linux-gnu'; - break; - case 'arm64': - if (isMusl) { - expectedName = 'aarch64-linux-musl-gcc-10.3.0.tar.gz'; - triple = 'aarch64-linux-musl'; - } - else { - expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; - triple = 'aarch64-linux-gnu'; - } - break; - case 'armhf': - expectedName = `arm-rpi-linux-gnueabihf${prefix}.tar.gz`; - triple = 'arm-rpi-linux-gnueabihf'; - break; - case 's390x': - expectedName = `s390x-linux-gnu${prefix}.tar.gz`; - triple = 's390x-linux-gnu'; - break; - } - console.log(`Fetching ${expectedName} for ${triple}`); - const checksumSha256 = getVSCodeSysrootChecksum(expectedName); - if (!checksumSha256) { - throw new Error(`Could not find checksum for ${expectedName}`); - } - const sysroot = process.env['VSCODE_SYSROOT_DIR'] ?? path_1.default.join((0, os_1.tmpdir)(), `vscode-${arch}-sysroot`); - const stamp = path_1.default.join(sysroot, '.stamp'); - let result = `${sysroot}/${triple}/${triple}/sysroot`; - if (isMusl) { - result = `${sysroot}/output/${triple}`; - } - if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === expectedName) { - return result; - } - console.log(`Installing ${arch} root image: ${sysroot}`); - fs_1.default.rmSync(sysroot, { recursive: true, force: true }); - fs_1.default.mkdirSync(sysroot, { recursive: true }); - await fetchUrl({ - checksumSha256, - assetName: expectedName, - dest: sysroot - }); - fs_1.default.writeFileSync(stamp, expectedName); - return result; -} -async function getChromiumSysroot(arch) { - const sysrootJSONUrl = `https://raw.githubusercontent.com/electron/electron/v${getElectronVersion().electronVersion}/script/sysroots.json`; - const sysrootDictLocation = `${(0, os_1.tmpdir)()}/sysroots.json`; - const result = (0, child_process_1.spawnSync)('curl', [sysrootJSONUrl, '-o', sysrootDictLocation]); - if (result.status !== 0) { - throw new Error('Cannot retrieve sysroots.json. Stderr:\n' + result.stderr); - } - const sysrootInfo = require(sysrootDictLocation); - const sysrootArch = `bullseye_${arch}`; - const sysrootDict = sysrootInfo[sysrootArch]; - const tarballFilename = sysrootDict['Tarball']; - const tarballSha = sysrootDict['Sha256Sum']; - const sysroot = path_1.default.join((0, os_1.tmpdir)(), sysrootDict['SysrootDir']); - const url = [URL_PREFIX, URL_PATH, tarballSha].join('/'); - const stamp = path_1.default.join(sysroot, '.stamp'); - if (fs_1.default.existsSync(stamp) && fs_1.default.readFileSync(stamp).toString() === url) { - return sysroot; - } - console.log(`Installing Debian ${arch} root image: ${sysroot}`); - fs_1.default.rmSync(sysroot, { recursive: true, force: true }); - fs_1.default.mkdirSync(sysroot); - const tarball = path_1.default.join(sysroot, tarballFilename); - console.log(`Downloading ${url}`); - let downloadSuccess = false; - for (let i = 0; i < 3 && !downloadSuccess; i++) { - fs_1.default.writeFileSync(tarball, ''); - await new Promise((c) => { - https_1.default.get(url, (res) => { - res.on('data', (chunk) => { - fs_1.default.appendFileSync(tarball, chunk); - }); - res.on('end', () => { - downloadSuccess = true; - c(); - }); - }).on('error', (err) => { - console.error('Encountered an error during the download attempt: ' + err.message); - c(); - }); - }); - } - if (!downloadSuccess) { - fs_1.default.rmSync(tarball); - throw new Error('Failed to download ' + url); - } - const sha = getSha(tarball); - if (sha !== tarballSha) { - throw new Error(`Tarball sha1sum is wrong. Expected ${tarballSha}, actual ${sha}`); - } - const proc = (0, child_process_1.spawnSync)('tar', ['xf', tarball, '-C', sysroot]); - if (proc.status) { - throw new Error('Tarball extraction failed with code ' + proc.status); - } - fs_1.default.rmSync(tarball); - fs_1.default.writeFileSync(stamp, url); - return sysroot; -} -//# sourceMappingURL=install-sysroot.js.map \ No newline at end of file diff --git a/build/linux/debian/install-sysroot.ts b/build/linux/debian/install-sysroot.ts index d05be917872..be90bc07791 100644 --- a/build/linux/debian/install-sysroot.ts +++ b/build/linux/debian/install-sysroot.ts @@ -9,12 +9,12 @@ import fs from 'fs'; import https from 'https'; import path from 'path'; import { createHash } from 'crypto'; -import { DebianArchString } from './types'; +import type { DebianArchString } from './types.ts'; // Based on https://source.chromium.org/chromium/chromium/src/+/main:build/linux/sysroot_scripts/install-sysroot.py. const URL_PREFIX = 'https://msftelectronbuild.z5.web.core.windows.net'; const URL_PATH = 'sysroots/toolchain'; -const REPO_ROOT = path.dirname(path.dirname(path.dirname(__dirname))); +const REPO_ROOT = path.dirname(path.dirname(path.dirname(import.meta.dirname))); const ghApiHeaders: Record = { Accept: 'application/vnd.github.v3+json', @@ -192,7 +192,7 @@ export async function getChromiumSysroot(arch: DebianArchString): Promise { - return !bundledDeps.some(bundledDep => dependency.startsWith(bundledDep)); - }).sort(); - const referenceGeneratedDeps = packageType === 'deb' ? - dep_lists_1.referenceGeneratedDepsByArch[arch] : - dep_lists_2.referenceGeneratedDepsByArch[arch]; - if (JSON.stringify(sortedDependencies) !== JSON.stringify(referenceGeneratedDeps)) { - const failMessage = 'The dependencies list has changed.' - + '\nOld:\n' + referenceGeneratedDeps.join('\n') - + '\nNew:\n' + sortedDependencies.join('\n'); - if (FAIL_BUILD_FOR_NEW_DEPENDENCIES) { - throw new Error(failMessage); - } - else { - console.warn(failMessage); - } - } - return sortedDependencies; -} -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/merge_package_deps.py. -function mergePackageDeps(inputDeps) { - const requires = new Set(); - for (const depSet of inputDeps) { - for (const dep of depSet) { - const trimmedDependency = dep.trim(); - if (trimmedDependency.length && !trimmedDependency.startsWith('#')) { - requires.add(trimmedDependency); - } - } - } - return requires; -} -//# sourceMappingURL=dependencies-generator.js.map \ No newline at end of file diff --git a/build/linux/dependencies-generator.ts b/build/linux/dependencies-generator.ts index 46c6d6c099a..874c8026c40 100644 --- a/build/linux/dependencies-generator.ts +++ b/build/linux/dependencies-generator.ts @@ -2,19 +2,16 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -'use strict'; - import { spawnSync } from 'child_process'; import path from 'path'; -import { getChromiumSysroot, getVSCodeSysroot } from './debian/install-sysroot'; -import { generatePackageDeps as generatePackageDepsDebian } from './debian/calculate-deps'; -import { generatePackageDeps as generatePackageDepsRpm } from './rpm/calculate-deps'; -import { referenceGeneratedDepsByArch as debianGeneratedDeps } from './debian/dep-lists'; -import { referenceGeneratedDepsByArch as rpmGeneratedDeps } from './rpm/dep-lists'; -import { DebianArchString, isDebianArchString } from './debian/types'; -import { isRpmArchString, RpmArchString } from './rpm/types'; -import product = require('../../product.json'); +import { getChromiumSysroot, getVSCodeSysroot } from './debian/install-sysroot.ts'; +import { generatePackageDeps as generatePackageDepsDebian } from './debian/calculate-deps.ts'; +import { generatePackageDeps as generatePackageDepsRpm } from './rpm/calculate-deps.ts'; +import { referenceGeneratedDepsByArch as debianGeneratedDeps } from './debian/dep-lists.ts'; +import { referenceGeneratedDepsByArch as rpmGeneratedDeps } from './rpm/dep-lists.ts'; +import { type DebianArchString, isDebianArchString } from './debian/types.ts'; +import { isRpmArchString, type RpmArchString } from './rpm/types.ts'; +import product from '../../product.json' with { type: 'json' }; // A flag that can easily be toggled. // Make sure to compile the build directory after toggling the value. @@ -25,7 +22,7 @@ import product = require('../../product.json'); // are valid, are in dep-lists.ts const FAIL_BUILD_FOR_NEW_DEPENDENCIES: boolean = true; -// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/138.0.7204.251:chrome/installer/linux/BUILD.gn;l=64-80 +// Based on https://source.chromium.org/chromium/chromium/src/+/refs/tags/142.0.7444.265:chrome/installer/linux/BUILD.gn;l=64-80 // and the Linux Archive build // Shared library dependencies that we already bundle. const bundledDeps = [ diff --git a/build/linux/libcxx-fetcher.js b/build/linux/libcxx-fetcher.js deleted file mode 100644 index d6c998e5aea..00000000000 --- a/build/linux/libcxx-fetcher.js +++ /dev/null @@ -1,73 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadLibcxxHeaders = downloadLibcxxHeaders; -exports.downloadLibcxxObjects = downloadLibcxxObjects; -// Can be removed once https://github.com/electron/electron-rebuild/pull/703 is available. -const fs_1 = __importDefault(require("fs")); -const path_1 = __importDefault(require("path")); -const debug_1 = __importDefault(require("debug")); -const extract_zip_1 = __importDefault(require("extract-zip")); -const get_1 = require("@electron/get"); -const root = path_1.default.dirname(path_1.default.dirname(__dirname)); -const d = (0, debug_1.default)('libcxx-fetcher'); -async function downloadLibcxxHeaders(outDir, electronVersion, lib_name) { - if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'include'))) { - return; - } - if (!await fs_1.default.existsSync(outDir)) { - await fs_1.default.mkdirSync(outDir, { recursive: true }); - } - d(`downloading ${lib_name}_headers`); - const headers = await (0, get_1.downloadArtifact)({ - version: electronVersion, - isGeneric: true, - artifactName: `${lib_name}_headers.zip`, - }); - d(`unpacking ${lib_name}_headers from ${headers}`); - await (0, extract_zip_1.default)(headers, { dir: outDir }); -} -async function downloadLibcxxObjects(outDir, electronVersion, targetArch = 'x64') { - if (await fs_1.default.existsSync(path_1.default.resolve(outDir, 'libc++.a'))) { - return; - } - if (!await fs_1.default.existsSync(outDir)) { - await fs_1.default.mkdirSync(outDir, { recursive: true }); - } - d(`downloading libcxx-objects-linux-${targetArch}`); - const objects = await (0, get_1.downloadArtifact)({ - version: electronVersion, - platform: 'linux', - artifactName: 'libcxx-objects', - arch: targetArch, - }); - d(`unpacking libcxx-objects from ${objects}`); - await (0, extract_zip_1.default)(objects, { dir: outDir }); -} -async function main() { - const libcxxObjectsDirPath = process.env['VSCODE_LIBCXX_OBJECTS_DIR']; - const libcxxHeadersDownloadDir = process.env['VSCODE_LIBCXX_HEADERS_DIR']; - const libcxxabiHeadersDownloadDir = process.env['VSCODE_LIBCXXABI_HEADERS_DIR']; - const arch = process.env['VSCODE_ARCH']; - const packageJSON = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, 'package.json'), 'utf8')); - const electronVersion = packageJSON.devDependencies.electron; - if (!libcxxObjectsDirPath || !libcxxHeadersDownloadDir || !libcxxabiHeadersDownloadDir) { - throw new Error('Required build env not set'); - } - await downloadLibcxxObjects(libcxxObjectsDirPath, electronVersion, arch); - await downloadLibcxxHeaders(libcxxHeadersDownloadDir, electronVersion, 'libcxx'); - await downloadLibcxxHeaders(libcxxabiHeadersDownloadDir, electronVersion, 'libcxxabi'); -} -if (require.main === module) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=libcxx-fetcher.js.map \ No newline at end of file diff --git a/build/linux/libcxx-fetcher.ts b/build/linux/libcxx-fetcher.ts index 6bdbd8a4f30..981fbd3392e 100644 --- a/build/linux/libcxx-fetcher.ts +++ b/build/linux/libcxx-fetcher.ts @@ -11,7 +11,7 @@ import debug from 'debug'; import extract from 'extract-zip'; import { downloadArtifact } from '@electron/get'; -const root = path.dirname(path.dirname(__dirname)); +const root = path.dirname(path.dirname(import.meta.dirname)); const d = debug('libcxx-fetcher'); @@ -71,7 +71,7 @@ async function main(): Promise { await downloadLibcxxHeaders(libcxxabiHeadersDownloadDir, electronVersion, 'libcxxabi'); } -if (require.main === module) { +if (import.meta.main) { main().catch(err => { console.error(err); process.exit(1); diff --git a/build/linux/rpm/calculate-deps.js b/build/linux/rpm/calculate-deps.js deleted file mode 100644 index b19e26f1854..00000000000 --- a/build/linux/rpm/calculate-deps.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generatePackageDeps = generatePackageDeps; -const child_process_1 = require("child_process"); -const fs_1 = require("fs"); -const dep_lists_1 = require("./dep-lists"); -function generatePackageDeps(files) { - const dependencies = files.map(file => calculatePackageDeps(file)); - const additionalDepsSet = new Set(dep_lists_1.additionalDeps); - dependencies.push(additionalDepsSet); - return dependencies; -} -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/calculate_package_deps.py. -function calculatePackageDeps(binaryPath) { - try { - if (!((0, fs_1.statSync)(binaryPath).mode & fs_1.constants.S_IXUSR)) { - throw new Error(`Binary ${binaryPath} needs to have an executable bit set.`); - } - } - catch (e) { - // The package might not exist. Don't re-throw the error here. - console.error('Tried to stat ' + binaryPath + ' but failed.'); - } - const findRequiresResult = (0, child_process_1.spawnSync)('/usr/lib/rpm/find-requires', { input: binaryPath + '\n' }); - if (findRequiresResult.status !== 0) { - throw new Error(`find-requires failed with exit code ${findRequiresResult.status}.\nstderr: ${findRequiresResult.stderr}`); - } - const requires = new Set(findRequiresResult.stdout.toString('utf-8').trimEnd().split('\n')); - return requires; -} -//# sourceMappingURL=calculate-deps.js.map \ No newline at end of file diff --git a/build/linux/rpm/calculate-deps.ts b/build/linux/rpm/calculate-deps.ts index 4be2200c018..0a1f0107594 100644 --- a/build/linux/rpm/calculate-deps.ts +++ b/build/linux/rpm/calculate-deps.ts @@ -5,7 +5,7 @@ import { spawnSync } from 'child_process'; import { constants, statSync } from 'fs'; -import { additionalDeps } from './dep-lists'; +import { additionalDeps } from './dep-lists.ts'; export function generatePackageDeps(files: string[]): Set[] { const dependencies: Set[] = files.map(file => calculatePackageDeps(file)); diff --git a/build/linux/rpm/dep-lists.js b/build/linux/rpm/dep-lists.js deleted file mode 100644 index 2f742daf2f8..00000000000 --- a/build/linux/rpm/dep-lists.js +++ /dev/null @@ -1,321 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.referenceGeneratedDepsByArch = exports.additionalDeps = void 0; -// Based on https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/rpm/additional_deps -// Additional dependencies not in the rpm find-requires output. -exports.additionalDeps = [ - 'ca-certificates', // Make sure users have SSL certificates. - 'libgtk-3.so.0()(64bit)', - 'libnss3.so(NSS_3.22)(64bit)', - 'libssl3.so(NSS_3.28)(64bit)', - 'rpmlib(FileDigests) <= 4.6.0-1', - 'libvulkan.so.1()(64bit)', - 'libcurl.so.4()(64bit)', - 'xdg-utils' // OS integration -]; -exports.referenceGeneratedDepsByArch = { - 'x86_64': [ - 'ca-certificates', - 'ld-linux-x86-64.so.2()(64bit)', - 'ld-linux-x86-64.so.2(GLIBC_2.2.5)(64bit)', - 'ld-linux-x86-64.so.2(GLIBC_2.3)(64bit)', - 'libX11.so.6()(64bit)', - 'libXcomposite.so.1()(64bit)', - 'libXdamage.so.1()(64bit)', - 'libXext.so.6()(64bit)', - 'libXfixes.so.3()(64bit)', - 'libXrandr.so.2()(64bit)', - 'libasound.so.2()(64bit)', - 'libasound.so.2(ALSA_0.9)(64bit)', - 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', - 'libatk-1.0.so.0()(64bit)', - 'libatk-bridge-2.0.so.0()(64bit)', - 'libatspi.so.0()(64bit)', - 'libc.so.6()(64bit)', - 'libc.so.6(GLIBC_2.10)(64bit)', - 'libc.so.6(GLIBC_2.11)(64bit)', - 'libc.so.6(GLIBC_2.12)(64bit)', - 'libc.so.6(GLIBC_2.14)(64bit)', - 'libc.so.6(GLIBC_2.15)(64bit)', - 'libc.so.6(GLIBC_2.16)(64bit)', - 'libc.so.6(GLIBC_2.17)(64bit)', - 'libc.so.6(GLIBC_2.18)(64bit)', - 'libc.so.6(GLIBC_2.2.5)(64bit)', - 'libc.so.6(GLIBC_2.25)(64bit)', - 'libc.so.6(GLIBC_2.27)(64bit)', - 'libc.so.6(GLIBC_2.28)(64bit)', - 'libc.so.6(GLIBC_2.3)(64bit)', - 'libc.so.6(GLIBC_2.3.2)(64bit)', - 'libc.so.6(GLIBC_2.3.3)(64bit)', - 'libc.so.6(GLIBC_2.3.4)(64bit)', - 'libc.so.6(GLIBC_2.4)(64bit)', - 'libc.so.6(GLIBC_2.6)(64bit)', - 'libc.so.6(GLIBC_2.7)(64bit)', - 'libc.so.6(GLIBC_2.8)(64bit)', - 'libc.so.6(GLIBC_2.9)(64bit)', - 'libcairo.so.2()(64bit)', - 'libcurl.so.4()(64bit)', - 'libdbus-1.so.3()(64bit)', - 'libdbus-1.so.3(LIBDBUS_1_3)(64bit)', - 'libdl.so.2()(64bit)', - 'libdl.so.2(GLIBC_2.2.5)(64bit)', - 'libexpat.so.1()(64bit)', - 'libgbm.so.1()(64bit)', - 'libgcc_s.so.1()(64bit)', - 'libgcc_s.so.1(GCC_3.0)(64bit)', - 'libgcc_s.so.1(GCC_3.3)(64bit)', - 'libgcc_s.so.1(GCC_4.0.0)(64bit)', - 'libgcc_s.so.1(GCC_4.2.0)(64bit)', - 'libgio-2.0.so.0()(64bit)', - 'libglib-2.0.so.0()(64bit)', - 'libgobject-2.0.so.0()(64bit)', - 'libgtk-3.so.0()(64bit)', - 'libm.so.6()(64bit)', - 'libm.so.6(GLIBC_2.2.5)(64bit)', - 'libnspr4.so()(64bit)', - 'libnss3.so()(64bit)', - 'libnss3.so(NSS_3.11)(64bit)', - 'libnss3.so(NSS_3.12)(64bit)', - 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.2)(64bit)', - 'libnss3.so(NSS_3.22)(64bit)', - 'libnss3.so(NSS_3.3)(64bit)', - 'libnss3.so(NSS_3.30)(64bit)', - 'libnss3.so(NSS_3.4)(64bit)', - 'libnss3.so(NSS_3.5)(64bit)', - 'libnss3.so(NSS_3.6)(64bit)', - 'libnss3.so(NSS_3.9.2)(64bit)', - 'libnssutil3.so()(64bit)', - 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', - 'libpango-1.0.so.0()(64bit)', - 'libpthread.so.0()(64bit)', - 'libpthread.so.0(GLIBC_2.12)(64bit)', - 'libpthread.so.0(GLIBC_2.2.5)(64bit)', - 'libpthread.so.0(GLIBC_2.3.2)(64bit)', - 'libpthread.so.0(GLIBC_2.3.3)(64bit)', - 'libpthread.so.0(GLIBC_2.3.4)(64bit)', - 'librt.so.1()(64bit)', - 'librt.so.1(GLIBC_2.2.5)(64bit)', - 'libsmime3.so()(64bit)', - 'libsmime3.so(NSS_3.10)(64bit)', - 'libsmime3.so(NSS_3.2)(64bit)', - 'libssl3.so(NSS_3.28)(64bit)', - 'libudev.so.1()(64bit)', - 'libudev.so.1(LIBUDEV_183)(64bit)', - 'libutil.so.1()(64bit)', - 'libutil.so.1(GLIBC_2.2.5)(64bit)', - 'libxcb.so.1()(64bit)', - 'libxkbcommon.so.0()(64bit)', - 'libxkbcommon.so.0(V_0.5.0)(64bit)', - 'libxkbfile.so.1()(64bit)', - 'rpmlib(FileDigests) <= 4.6.0-1', - 'rtld(GNU_HASH)', - 'xdg-utils' - ], - 'armv7hl': [ - 'ca-certificates', - 'ld-linux-armhf.so.3', - 'ld-linux-armhf.so.3(GLIBC_2.4)', - 'libX11.so.6', - 'libXcomposite.so.1', - 'libXdamage.so.1', - 'libXext.so.6', - 'libXfixes.so.3', - 'libXrandr.so.2', - 'libasound.so.2', - 'libasound.so.2(ALSA_0.9)', - 'libasound.so.2(ALSA_0.9.0rc4)', - 'libatk-1.0.so.0', - 'libatk-bridge-2.0.so.0', - 'libatspi.so.0', - 'libc.so.6', - 'libc.so.6(GLIBC_2.10)', - 'libc.so.6(GLIBC_2.11)', - 'libc.so.6(GLIBC_2.12)', - 'libc.so.6(GLIBC_2.14)', - 'libc.so.6(GLIBC_2.15)', - 'libc.so.6(GLIBC_2.16)', - 'libc.so.6(GLIBC_2.17)', - 'libc.so.6(GLIBC_2.18)', - 'libc.so.6(GLIBC_2.25)', - 'libc.so.6(GLIBC_2.27)', - 'libc.so.6(GLIBC_2.28)', - 'libc.so.6(GLIBC_2.4)', - 'libc.so.6(GLIBC_2.6)', - 'libc.so.6(GLIBC_2.7)', - 'libc.so.6(GLIBC_2.8)', - 'libc.so.6(GLIBC_2.9)', - 'libcairo.so.2', - 'libcurl.so.4()(64bit)', - 'libdbus-1.so.3', - 'libdbus-1.so.3(LIBDBUS_1_3)', - 'libdl.so.2', - 'libdl.so.2(GLIBC_2.4)', - 'libexpat.so.1', - 'libgbm.so.1', - 'libgcc_s.so.1', - 'libgcc_s.so.1(GCC_3.0)', - 'libgcc_s.so.1(GCC_3.5)', - 'libgcc_s.so.1(GCC_4.3.0)', - 'libgio-2.0.so.0', - 'libglib-2.0.so.0', - 'libgobject-2.0.so.0', - 'libgtk-3.so.0', - 'libgtk-3.so.0()(64bit)', - 'libm.so.6', - 'libm.so.6(GLIBC_2.4)', - 'libnspr4.so', - 'libnss3.so', - 'libnss3.so(NSS_3.11)', - 'libnss3.so(NSS_3.12)', - 'libnss3.so(NSS_3.12.1)', - 'libnss3.so(NSS_3.2)', - 'libnss3.so(NSS_3.22)', - 'libnss3.so(NSS_3.22)(64bit)', - 'libnss3.so(NSS_3.3)', - 'libnss3.so(NSS_3.30)', - 'libnss3.so(NSS_3.4)', - 'libnss3.so(NSS_3.5)', - 'libnss3.so(NSS_3.6)', - 'libnss3.so(NSS_3.9.2)', - 'libnssutil3.so', - 'libnssutil3.so(NSSUTIL_3.12.3)', - 'libpango-1.0.so.0', - 'libpthread.so.0', - 'libpthread.so.0(GLIBC_2.12)', - 'libpthread.so.0(GLIBC_2.4)', - 'librt.so.1', - 'librt.so.1(GLIBC_2.4)', - 'libsmime3.so', - 'libsmime3.so(NSS_3.10)', - 'libsmime3.so(NSS_3.2)', - 'libssl3.so(NSS_3.28)(64bit)', - 'libstdc++.so.6', - 'libstdc++.so.6(CXXABI_1.3)', - 'libstdc++.so.6(CXXABI_1.3.5)', - 'libstdc++.so.6(CXXABI_1.3.8)', - 'libstdc++.so.6(CXXABI_1.3.9)', - 'libstdc++.so.6(CXXABI_ARM_1.3.3)', - 'libstdc++.so.6(GLIBCXX_3.4)', - 'libstdc++.so.6(GLIBCXX_3.4.11)', - 'libstdc++.so.6(GLIBCXX_3.4.14)', - 'libstdc++.so.6(GLIBCXX_3.4.15)', - 'libstdc++.so.6(GLIBCXX_3.4.18)', - 'libstdc++.so.6(GLIBCXX_3.4.19)', - 'libstdc++.so.6(GLIBCXX_3.4.20)', - 'libstdc++.so.6(GLIBCXX_3.4.21)', - 'libstdc++.so.6(GLIBCXX_3.4.22)', - 'libstdc++.so.6(GLIBCXX_3.4.26)', - 'libstdc++.so.6(GLIBCXX_3.4.5)', - 'libstdc++.so.6(GLIBCXX_3.4.9)', - 'libudev.so.1', - 'libudev.so.1(LIBUDEV_183)', - 'libutil.so.1', - 'libutil.so.1(GLIBC_2.4)', - 'libxcb.so.1', - 'libxkbcommon.so.0', - 'libxkbcommon.so.0(V_0.5.0)', - 'libxkbfile.so.1', - 'rpmlib(FileDigests) <= 4.6.0-1', - 'rtld(GNU_HASH)', - 'xdg-utils' - ], - 'aarch64': [ - 'ca-certificates', - 'ld-linux-aarch64.so.1()(64bit)', - 'ld-linux-aarch64.so.1(GLIBC_2.17)(64bit)', - 'libX11.so.6()(64bit)', - 'libXcomposite.so.1()(64bit)', - 'libXdamage.so.1()(64bit)', - 'libXext.so.6()(64bit)', - 'libXfixes.so.3()(64bit)', - 'libXrandr.so.2()(64bit)', - 'libasound.so.2()(64bit)', - 'libasound.so.2(ALSA_0.9)(64bit)', - 'libasound.so.2(ALSA_0.9.0rc4)(64bit)', - 'libatk-1.0.so.0()(64bit)', - 'libatk-bridge-2.0.so.0()(64bit)', - 'libatspi.so.0()(64bit)', - 'libc.so.6()(64bit)', - 'libc.so.6(GLIBC_2.17)(64bit)', - 'libc.so.6(GLIBC_2.18)(64bit)', - 'libc.so.6(GLIBC_2.25)(64bit)', - 'libc.so.6(GLIBC_2.27)(64bit)', - 'libc.so.6(GLIBC_2.28)(64bit)', - 'libcairo.so.2()(64bit)', - 'libcurl.so.4()(64bit)', - 'libdbus-1.so.3()(64bit)', - 'libdbus-1.so.3(LIBDBUS_1_3)(64bit)', - 'libdl.so.2()(64bit)', - 'libdl.so.2(GLIBC_2.17)(64bit)', - 'libexpat.so.1()(64bit)', - 'libgbm.so.1()(64bit)', - 'libgcc_s.so.1()(64bit)', - 'libgcc_s.so.1(GCC_3.0)(64bit)', - 'libgcc_s.so.1(GCC_3.3)(64bit)', - 'libgcc_s.so.1(GCC_4.0.0)(64bit)', - 'libgcc_s.so.1(GCC_4.2.0)(64bit)', - 'libgcc_s.so.1(GCC_4.5.0)(64bit)', - 'libgio-2.0.so.0()(64bit)', - 'libglib-2.0.so.0()(64bit)', - 'libgobject-2.0.so.0()(64bit)', - 'libgtk-3.so.0()(64bit)', - 'libm.so.6()(64bit)', - 'libm.so.6(GLIBC_2.17)(64bit)', - 'libnspr4.so()(64bit)', - 'libnss3.so()(64bit)', - 'libnss3.so(NSS_3.11)(64bit)', - 'libnss3.so(NSS_3.12)(64bit)', - 'libnss3.so(NSS_3.12.1)(64bit)', - 'libnss3.so(NSS_3.2)(64bit)', - 'libnss3.so(NSS_3.22)(64bit)', - 'libnss3.so(NSS_3.3)(64bit)', - 'libnss3.so(NSS_3.30)(64bit)', - 'libnss3.so(NSS_3.4)(64bit)', - 'libnss3.so(NSS_3.5)(64bit)', - 'libnss3.so(NSS_3.6)(64bit)', - 'libnss3.so(NSS_3.9.2)(64bit)', - 'libnssutil3.so()(64bit)', - 'libnssutil3.so(NSSUTIL_3.12.3)(64bit)', - 'libpango-1.0.so.0()(64bit)', - 'libpthread.so.0()(64bit)', - 'libpthread.so.0(GLIBC_2.17)(64bit)', - 'libsmime3.so()(64bit)', - 'libsmime3.so(NSS_3.10)(64bit)', - 'libsmime3.so(NSS_3.2)(64bit)', - 'libssl3.so(NSS_3.28)(64bit)', - 'libstdc++.so.6()(64bit)', - 'libstdc++.so.6(CXXABI_1.3)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.5)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.8)(64bit)', - 'libstdc++.so.6(CXXABI_1.3.9)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.11)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.14)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.15)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.18)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.19)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.20)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.21)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.22)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.26)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.5)(64bit)', - 'libstdc++.so.6(GLIBCXX_3.4.9)(64bit)', - 'libudev.so.1()(64bit)', - 'libudev.so.1(LIBUDEV_183)(64bit)', - 'libutil.so.1()(64bit)', - 'libutil.so.1(GLIBC_2.17)(64bit)', - 'libxcb.so.1()(64bit)', - 'libxkbcommon.so.0()(64bit)', - 'libxkbcommon.so.0(V_0.5.0)(64bit)', - 'libxkbfile.so.1()(64bit)', - 'rpmlib(FileDigests) <= 4.6.0-1', - 'rtld(GNU_HASH)', - 'xdg-utils' - ] -}; -//# sourceMappingURL=dep-lists.js.map \ No newline at end of file diff --git a/build/linux/rpm/dep-lists.ts b/build/linux/rpm/dep-lists.ts index 90b97bed301..783923f34d9 100644 --- a/build/linux/rpm/dep-lists.ts +++ b/build/linux/rpm/dep-lists.ts @@ -256,7 +256,6 @@ export const referenceGeneratedDepsByArch = { 'libgcc_s.so.1()(64bit)', 'libgcc_s.so.1(GCC_3.0)(64bit)', 'libgcc_s.so.1(GCC_3.3)(64bit)', - 'libgcc_s.so.1(GCC_4.0.0)(64bit)', 'libgcc_s.so.1(GCC_4.2.0)(64bit)', 'libgcc_s.so.1(GCC_4.5.0)(64bit)', 'libgio-2.0.so.0()(64bit)', diff --git a/build/linux/rpm/types.js b/build/linux/rpm/types.js deleted file mode 100644 index a20b9c2fe02..00000000000 --- a/build/linux/rpm/types.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.isRpmArchString = isRpmArchString; -function isRpmArchString(s) { - return ['x86_64', 'armv7hl', 'aarch64'].includes(s); -} -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/build/npm/dirs.js b/build/npm/dirs.js deleted file mode 100644 index 6e25b856a3c..00000000000 --- a/build/npm/dirs.js +++ /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. - *--------------------------------------------------------------------------------------------*/ - -const fs = require('fs'); - -// Complete list of directories where npm should be executed to install node modules -const dirs = [ - '', - 'build', - 'extensions', - 'extensions/configuration-editing', - 'extensions/css-language-features', - 'extensions/css-language-features/server', - 'extensions/debug-auto-launch', - 'extensions/debug-server-ready', - 'extensions/emmet', - 'extensions/extension-editing', - 'extensions/git', - 'extensions/git-base', - 'extensions/github', - 'extensions/github-authentication', - 'extensions/grunt', - 'extensions/gulp', - 'extensions/html-language-features', - 'extensions/html-language-features/server', - 'extensions/ipynb', - 'extensions/jake', - 'extensions/json-language-features', - 'extensions/json-language-features/server', - 'extensions/markdown-language-features', - 'extensions/markdown-math', - 'extensions/media-preview', - 'extensions/merge-conflict', - 'extensions/mermaid-chat-features', - 'extensions/microsoft-authentication', - 'extensions/notebook-renderers', - 'extensions/npm', - 'extensions/open-remote-ssh', - 'extensions/php-language-features', - 'extensions/references-view', - 'extensions/search-result', - 'extensions/simple-browser', - 'extensions/tunnel-forwarding', - 'extensions/terminal-suggest', - 'extensions/typescript-language-features', - 'extensions/vscode-api-tests', - 'extensions/vscode-colorize-tests', - 'extensions/vscode-colorize-perf-tests', - 'extensions/vscode-test-resolver', - 'remote', - 'remote/web', - 'test/automation', - 'test/integration/browser', - 'test/monaco', - 'test/smoke', - 'test/mcp', - '.vscode/extensions/vscode-selfhost-import-aid', - '.vscode/extensions/vscode-selfhost-test-provider', -]; - -if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) { - dirs.push('.build/distro/npm'); - dirs.push('.build/distro/npm/remote'); - dirs.push('.build/distro/npm/remote/web'); -} - -exports.dirs = dirs; diff --git a/build/npm/dirs.ts b/build/npm/dirs.ts new file mode 100644 index 00000000000..189fad0aeee --- /dev/null +++ b/build/npm/dirs.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { existsSync } from 'fs'; + +/** + * Complete list of directories where npm should be executed to install node modules + */ +export const dirs = [ + '', + 'build', + 'build/vite', + 'extensions', + 'extensions/configuration-editing', + 'extensions/css-language-features', + 'extensions/css-language-features/server', + 'extensions/debug-auto-launch', + 'extensions/debug-server-ready', + 'extensions/emmet', + 'extensions/extension-editing', + 'extensions/git', + 'extensions/git-base', + 'extensions/github', + 'extensions/github-authentication', + 'extensions/grunt', + 'extensions/gulp', + 'extensions/html-language-features', + 'extensions/html-language-features/server', + 'extensions/ipynb', + 'extensions/jake', + 'extensions/json-language-features', + 'extensions/json-language-features/server', + 'extensions/markdown-language-features', + 'extensions/markdown-math', + 'extensions/media-preview', + 'extensions/merge-conflict', + 'extensions/mermaid-chat-features', + 'extensions/microsoft-authentication', + 'extensions/notebook-renderers', + 'extensions/npm', + 'extensions/open-remote-ssh', + 'extensions/php-language-features', + 'extensions/references-view', + 'extensions/search-result', + 'extensions/simple-browser', + 'extensions/tunnel-forwarding', + 'extensions/terminal-suggest', + 'extensions/typescript-language-features', + 'extensions/vscode-api-tests', + 'extensions/vscode-colorize-tests', + 'extensions/vscode-colorize-perf-tests', + 'extensions/vscode-test-resolver', + 'remote', + 'remote/web', + 'test/automation', + 'test/integration/browser', + 'test/monaco', + 'test/smoke', + 'test/mcp', + '.vscode/extensions/vscode-selfhost-import-aid', + '.vscode/extensions/vscode-selfhost-test-provider', +]; + +if (existsSync(`${import.meta.dirname}/../../.build/distro/npm`)) { + dirs.push('.build/distro/npm'); + dirs.push('.build/distro/npm/remote'); + dirs.push('.build/distro/npm/remote/web'); +} diff --git a/build/npm/gyp/custom-headers/v8-source-location.patch b/build/npm/gyp/custom-headers/v8-source-location.patch new file mode 100644 index 00000000000..545eb9a118b --- /dev/null +++ b/build/npm/gyp/custom-headers/v8-source-location.patch @@ -0,0 +1,94 @@ +--- v8-source-location.h 2025-10-28 05:57:35 ++++ v8-source-location.h 2025-11-07 03:10:02 +@@ -6,12 +6,21 @@ + #define INCLUDE_SOURCE_LOCATION_H_ + + #include +-#include + #include + + #include "v8config.h" // NOLINT(build/include_directory) + ++#if defined(__has_builtin) ++#define V8_SUPPORTS_SOURCE_LOCATION \ ++ (__has_builtin(__builtin_FUNCTION) && __has_builtin(__builtin_FILE) && \ ++ __has_builtin(__builtin_LINE)) // NOLINT ++#elif defined(V8_CC_GNU) && __GNUC__ >= 7 + #define V8_SUPPORTS_SOURCE_LOCATION 1 ++#elif defined(V8_CC_INTEL) && __ICC >= 1800 ++#define V8_SUPPORTS_SOURCE_LOCATION 1 ++#else ++#define V8_SUPPORTS_SOURCE_LOCATION 0 ++#endif + + namespace v8 { + +@@ -25,10 +34,15 @@ + * Construct source location information corresponding to the location of the + * call site. + */ ++#if V8_SUPPORTS_SOURCE_LOCATION + static constexpr SourceLocation Current( +- const std::source_location& loc = std::source_location::current()) { +- return SourceLocation(loc); ++ const char* function = __builtin_FUNCTION(), ++ const char* file = __builtin_FILE(), size_t line = __builtin_LINE()) { ++ return SourceLocation(function, file, line); + } ++#else ++ static constexpr SourceLocation Current() { return SourceLocation(); } ++#endif // V8_SUPPORTS_SOURCE_LOCATION + #ifdef DEBUG + static constexpr SourceLocation CurrentIfDebug( + const std::source_location& loc = std::source_location::current()) { +@@ -49,21 +63,21 @@ + * + * \returns the function name as cstring. + */ +- constexpr const char* Function() const { return loc_.function_name(); } ++ constexpr const char* Function() const { return function_; } + + /** + * Returns the name of the current source file represented by this object. + * + * \returns the file name as cstring. + */ +- constexpr const char* FileName() const { return loc_.file_name(); } ++ constexpr const char* FileName() const { return file_; } + + /** + * Returns the line number represented by this object. + * + * \returns the line number. + */ +- constexpr size_t Line() const { return loc_.line(); } ++ constexpr size_t Line() const { return line_; } + + /** + * Returns a human-readable string representing this object. +@@ -71,18 +85,19 @@ + * \returns a human-readable string representing source location information. + */ + std::string ToString() const { +- if (loc_.line() == 0) { ++ if (!file_) { + return {}; + } +- return std::string(loc_.function_name()) + "@" + loc_.file_name() + ":" + +- std::to_string(loc_.line()); ++ return std::string(function_) + "@" + file_ + ":" + std::to_string(line_); + } + + private: +- constexpr explicit SourceLocation(const std::source_location& loc) +- : loc_(loc) {} ++ constexpr SourceLocation(const char* function, const char* file, size_t line) ++ : function_(function), file_(file), line_(line) {} + +- std::source_location loc_; ++ const char* function_ = nullptr; ++ const char* file_ = nullptr; ++ size_t line_ = 0u; + }; + + } // namespace v8 diff --git a/build/npm/gyp/package-lock.json b/build/npm/gyp/package-lock.json index 08b6ae29b01..a4ef0b2fada 100644 --- a/build/npm/gyp/package-lock.json +++ b/build/npm/gyp/package-lock.json @@ -138,9 +138,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -352,9 +352,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -682,9 +682,9 @@ "license": "ISC" }, "node_modules/minizlib": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", - "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, "license": "MIT", "dependencies": { @@ -694,22 +694,6 @@ "node": ">= 18" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1085,17 +1069,16 @@ } }, "node_modules/tar": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", - "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", + "minizlib": "^3.1.0", "yallist": "^5.0.0" }, "engines": { diff --git a/build/npm/jsconfig.json b/build/npm/jsconfig.json deleted file mode 100644 index fa767b17d0f..00000000000 --- a/build/npm/jsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es2024", - "lib": [ - "ES2024" - ], - "module": "node16", - "checkJs": true, - "noEmit": true - } -} diff --git a/build/npm/mixin-telemetry-docs.mjs b/build/npm/mixin-telemetry-docs.mjs deleted file mode 100644 index fe8a6aec446..00000000000 --- a/build/npm/mixin-telemetry-docs.mjs +++ /dev/null @@ -1,34 +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 { 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/mixin-telemetry-docs.ts b/build/npm/mixin-telemetry-docs.ts new file mode 100644 index 00000000000..be33793431a --- /dev/null +++ b/build/npm/mixin-telemetry-docs.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { execSync } from 'child_process'; +import { join, resolve } from 'path'; +import { existsSync, rmSync } from 'fs'; + +const rootPath = resolve(import.meta.dirname, '..', '..'); +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 as Error).message); + process.exit(1); +} diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js deleted file mode 100644 index fa8da7d08c6..00000000000 --- a/build/npm/postinstall.js +++ /dev/null @@ -1,189 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const fs = require('fs'); -const path = require('path'); -const os = require('os'); -const cp = require('child_process'); -const { dirs } = require('./dirs'); -const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; -const root = path.dirname(path.dirname(__dirname)); - -function log(dir, message) { - if (process.stdout.isTTY) { - console.log(`\x1b[34m[${dir}]\x1b[0m`, message); - } else { - console.log(`[${dir}]`, message); - } -} - -function run(command, args, opts) { - log(opts.cwd || '.', '$ ' + command + ' ' + args.join(' ')); - - const result = cp.spawnSync(command, args, opts); - - if (result.error) { - console.error(`ERR Failed to spawn process: ${result.error}`); - process.exit(1); - } else if (result.status !== 0) { - console.error(`ERR Process exited with code: ${result.status}`); - process.exit(result.status); - } -} - -/** - * @param {string} dir - * @param {*} [opts] - */ -function npmInstall(dir, opts) { - opts = { - env: { ...process.env }, - ...(opts ?? {}), - cwd: dir, - stdio: 'inherit', - shell: true - }; - - const command = process.env['npm_command'] || 'install'; - - if (process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'] && /^(.build\/distro\/npm\/)?remote$/.test(dir)) { - const userinfo = os.userInfo(); - log(dir, `Installing dependencies inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); - - opts.cwd = root; - 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`, - '-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...'); - run(npm, command.split(' '), opts); - } - removeParcelWatcherPrebuild(dir); -} - -function setNpmrcConfig(dir, env) { - const npmrcPath = path.join(root, dir, '.npmrc'); - const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n'); - - for (const line of lines) { - const trimmedLine = line.trim(); - if (trimmedLine && !trimmedLine.startsWith('#')) { - const [key, value] = trimmedLine.split('='); - env[`npm_config_${key}`] = value.replace(/^"(.*)"$/, '$1'); - } - } - - // Use our bundled node-gyp version - env['npm_config_node_gyp'] = - process.platform === 'win32' - ? path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd') - : path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp'); - - // Force node-gyp to use process.config on macOS - // which defines clang variable as expected. Otherwise we - // run into compilation errors due to incorrect compiler - // configuration. - // NOTE: This means the process.config should contain - // the correct clang variable. So keep the version check - // in preinstall sync with this logic. - // Change was first introduced in https://github.com/nodejs/node/commit/6e0a2bb54c5bbeff0e9e33e1a0c683ed980a8a0f - if ((dir === 'remote' || dir === 'build') && process.platform === 'darwin') { - env['npm_config_force_process_config'] = 'true'; - } else { - delete env['npm_config_force_process_config']; - } - - if (dir === 'build') { - env['npm_config_target'] = process.versions.node; - env['npm_config_arch'] = process.arch; - } -} - -function removeParcelWatcherPrebuild(dir) { - const parcelModuleFolder = path.join(root, dir, 'node_modules', '@parcel'); - if (!fs.existsSync(parcelModuleFolder)) { - return; - } - - const parcelModules = fs.readdirSync(parcelModuleFolder); - for (const moduleName of parcelModules) { - if (moduleName.startsWith('watcher-')) { - const modulePath = path.join(parcelModuleFolder, moduleName); - fs.rmSync(modulePath, { recursive: true, force: true }); - log(dir, `Removed @parcel/watcher prebuilt module ${modulePath}`); - } - } -} - -for (let dir of dirs) { - - if (dir === '') { - removeParcelWatcherPrebuild(dir); - continue; // already executed in root - } - - let opts; - - if (dir === 'build') { - opts = { - env: { - ...process.env - }, - } - if (process.env['CC']) { opts.env['CC'] = 'gcc'; } - if (process.env['CXX']) { opts.env['CXX'] = 'g++'; } - if (process.env['CXXFLAGS']) { opts.env['CXXFLAGS'] = ''; } - if (process.env['LDFLAGS']) { opts.env['LDFLAGS'] = ''; } - - setNpmrcConfig('build', opts.env); - npmInstall('build', opts); - continue; - } - - if (/^(.build\/distro\/npm\/)?remote$/.test(dir)) { - // node modules used by vscode server - opts = { - env: { - ...process.env - }, - } - if (process.env['VSCODE_REMOTE_CC']) { - opts.env['CC'] = process.env['VSCODE_REMOTE_CC']; - } else { - delete opts.env['CC']; - } - if (process.env['VSCODE_REMOTE_CXX']) { - opts.env['CXX'] = process.env['VSCODE_REMOTE_CXX']; - } else { - delete opts.env['CXX']; - } - if (process.env['CXXFLAGS']) { delete opts.env['CXXFLAGS']; } - if (process.env['CFLAGS']) { delete opts.env['CFLAGS']; } - if (process.env['LDFLAGS']) { delete opts.env['LDFLAGS']; } - if (process.env['VSCODE_REMOTE_CXXFLAGS']) { opts.env['CXXFLAGS'] = process.env['VSCODE_REMOTE_CXXFLAGS']; } - if (process.env['VSCODE_REMOTE_LDFLAGS']) { opts.env['LDFLAGS'] = process.env['VSCODE_REMOTE_LDFLAGS']; } - if (process.env['VSCODE_REMOTE_NODE_GYP']) { opts.env['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } - - setNpmrcConfig('remote', opts.env); - npmInstall(dir, opts); - continue; - } - - npmInstall(dir, opts); -} - -cp.execSync('git config pull.rebase merges'); -cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs'); diff --git a/build/npm/postinstall.ts b/build/npm/postinstall.ts new file mode 100644 index 00000000000..b6a934f74b3 --- /dev/null +++ b/build/npm/postinstall.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import path from 'path'; +import * as os from 'os'; +import * as child_process from 'child_process'; +import { dirs } from './dirs.ts'; + +const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; +const root = path.dirname(path.dirname(import.meta.dirname)); +const rootNpmrcConfigKeys = getNpmrcConfigKeys(path.join(root, '.npmrc')); + +function log(dir: string, message: string) { + if (process.stdout.isTTY) { + console.log(`\x1b[34m[${dir}]\x1b[0m`, message); + } else { + console.log(`[${dir}]`, message); + } +} + +function run(command: string, args: string[], opts: child_process.SpawnSyncOptions) { + log(opts.cwd as string || '.', '$ ' + command + ' ' + args.join(' ')); + + const result = child_process.spawnSync(command, args, opts); + + if (result.error) { + console.error(`ERR Failed to spawn process: ${result.error}`); + process.exit(1); + } else if (result.status !== 0) { + console.error(`ERR Process exited with code: ${result.status}`); + process.exit(result.status); + } +} + +function npmInstall(dir: string, opts?: child_process.SpawnSyncOptions) { + opts = { + env: { ...process.env }, + ...(opts ?? {}), + cwd: dir, + stdio: 'inherit', + shell: true + }; + + const command = process.env['npm_command'] || 'install'; + + if (process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME'] && /^(.build\/distro\/npm\/)?remote$/.test(dir)) { + const userinfo = os.userInfo(); + log(dir, `Installing dependencies inside container ${process.env['VSCODE_REMOTE_DEPENDENCIES_CONTAINER_NAME']}...`); + + opts.cwd = root; + 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`, + '-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...'); + run(npm, command.split(' '), opts); + } + removeParcelWatcherPrebuild(dir); +} + +function setNpmrcConfig(dir: string, env: NodeJS.ProcessEnv) { + const npmrcPath = path.join(root, dir, '.npmrc'); + const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n'); + + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const [key, value] = trimmedLine.split('='); + env[`npm_config_${key}`] = value.replace(/^"(.*)"$/, '$1'); + } + } + + // Use our bundled node-gyp version + env['npm_config_node_gyp'] = + process.platform === 'win32' + ? path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd') + : path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp'); + + // Force node-gyp to use process.config on macOS + // which defines clang variable as expected. Otherwise we + // run into compilation errors due to incorrect compiler + // configuration. + // NOTE: This means the process.config should contain + // the correct clang variable. So keep the version check + // in preinstall sync with this logic. + // Change was first introduced in https://github.com/nodejs/node/commit/6e0a2bb54c5bbeff0e9e33e1a0c683ed980a8a0f + if ((dir === 'remote' || dir === 'build') && process.platform === 'darwin') { + env['npm_config_force_process_config'] = 'true'; + } else { + delete env['npm_config_force_process_config']; + } + + if (dir === 'build') { + env['npm_config_target'] = process.versions.node; + env['npm_config_arch'] = process.arch; + } +} + +function removeParcelWatcherPrebuild(dir: string) { + const parcelModuleFolder = path.join(root, dir, 'node_modules', '@parcel'); + if (!fs.existsSync(parcelModuleFolder)) { + return; + } + + const parcelModules = fs.readdirSync(parcelModuleFolder); + for (const moduleName of parcelModules) { + if (moduleName.startsWith('watcher-')) { + const modulePath = path.join(parcelModuleFolder, moduleName); + fs.rmSync(modulePath, { recursive: true, force: true }); + log(dir, `Removed @parcel/watcher prebuilt module ${modulePath}`); + } + } +} + +function getNpmrcConfigKeys(npmrcPath: string): string[] { + if (!fs.existsSync(npmrcPath)) { + return []; + } + const lines = fs.readFileSync(npmrcPath, 'utf8').split('\n'); + const keys: string[] = []; + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine && !trimmedLine.startsWith('#')) { + const eqIndex = trimmedLine.indexOf('='); + if (eqIndex > 0) { + keys.push(trimmedLine.substring(0, eqIndex).trim()); + } + } + } + return keys; +} + +function clearInheritedNpmrcConfig(dir: string, env: NodeJS.ProcessEnv): void { + const dirNpmrcPath = path.join(root, dir, '.npmrc'); + if (fs.existsSync(dirNpmrcPath)) { + return; + } + + for (const key of rootNpmrcConfigKeys) { + const envKey = `npm_config_${key.replace(/-/g, '_')}`; + delete env[envKey]; + } +} + +for (const dir of dirs) { + + if (dir === '') { + removeParcelWatcherPrebuild(dir); + continue; // already executed in root + } + + let opts: child_process.SpawnSyncOptions | undefined; + + if (dir === 'build') { + opts = { + env: { + ...process.env + }, + }; + if (process.env['CC']) { opts.env!['CC'] = 'gcc'; } + if (process.env['CXX']) { opts.env!['CXX'] = 'g++'; } + if (process.env['CXXFLAGS']) { opts.env!['CXXFLAGS'] = ''; } + if (process.env['LDFLAGS']) { opts.env!['LDFLAGS'] = ''; } + + setNpmrcConfig('build', opts.env!); + npmInstall('build', opts); + continue; + } + + if (/^(.build\/distro\/npm\/)?remote$/.test(dir)) { + // node modules used by vscode server + opts = { + env: { + ...process.env + }, + }; + if (process.env['VSCODE_REMOTE_CC']) { + opts.env!['CC'] = process.env['VSCODE_REMOTE_CC']; + } else { + delete opts.env!['CC']; + } + if (process.env['VSCODE_REMOTE_CXX']) { + opts.env!['CXX'] = process.env['VSCODE_REMOTE_CXX']; + } else { + delete opts.env!['CXX']; + } + if (process.env['CXXFLAGS']) { delete opts.env!['CXXFLAGS']; } + if (process.env['CFLAGS']) { delete opts.env!['CFLAGS']; } + if (process.env['LDFLAGS']) { delete opts.env!['LDFLAGS']; } + if (process.env['VSCODE_REMOTE_CXXFLAGS']) { opts.env!['CXXFLAGS'] = process.env['VSCODE_REMOTE_CXXFLAGS']; } + if (process.env['VSCODE_REMOTE_LDFLAGS']) { opts.env!['LDFLAGS'] = process.env['VSCODE_REMOTE_LDFLAGS']; } + if (process.env['VSCODE_REMOTE_NODE_GYP']) { opts.env!['npm_config_node_gyp'] = process.env['VSCODE_REMOTE_NODE_GYP']; } + + setNpmrcConfig('remote', opts.env!); + npmInstall(dir, opts); + continue; + } + + // For directories that don't define their own .npmrc, clear inherited config + const env = { ...process.env }; + clearInheritedNpmrcConfig(dir, env); + npmInstall(dir, { env }); +} + +child_process.execSync('git config pull.rebase merges'); +child_process.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs'); diff --git a/build/npm/preinstall.js b/build/npm/preinstall.js deleted file mode 100644 index e4b47859576..00000000000 --- a/build/npm/preinstall.js +++ /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. - *--------------------------------------------------------------------------------------------*/ - -const nodeVersion = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.node); -const majorNodeVersion = parseInt(nodeVersion[1]); -const minorNodeVersion = parseInt(nodeVersion[2]); -const patchNodeVersion = parseInt(nodeVersion[3]); - -if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { - if (majorNodeVersion < 22 || (majorNodeVersion === 22 && minorNodeVersion < 15) || (majorNodeVersion === 22 && minorNodeVersion === 15 && patchNodeVersion < 1)) { - console.error('\x1b[1;31m*** Please use Node.js v22.15.1 or later for development.\x1b[0;0m'); - throw new Error(); - } -} - -if (process.env['npm_execpath'].includes('yarn')) { - console.error('\x1b[1;31m*** Seems like you are using `yarn` which is not supported in this repo any more, please use `npm i` instead. ***\x1b[0;0m'); - throw new Error(); -} - -const path = require('path'); -const fs = require('fs'); -const cp = require('child_process'); -const os = require('os'); - -if (process.platform === 'win32') { - if (!hasSupportedVisualStudioVersion()) { - console.error('\x1b[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites.\x1b[0;0m'); - console.error('\x1b[1;31m*** If you have Visual Studio installed in a custom location, you can specify it via the environment variable:\x1b[0;0m'); - console.error('\x1b[1;31m*** set vs2022_install= (or vs2019_install for older versions)\x1b[0;0m'); - throw new Error(); - } - installHeaders(); -} - -if (process.arch !== os.arch()) { - console.error(`\x1b[1;31m*** ARCHITECTURE MISMATCH: The node.js process is ${process.arch}, but your OS architecture is ${os.arch()}. ***\x1b[0;0m`); - console.error(`\x1b[1;31m*** This can greatly increase the build time of vs code. ***\x1b[0;0m`); -} - -function hasSupportedVisualStudioVersion() { - const fs = require('fs'); - const path = require('path'); - // Translated over from - // https://source.chromium.org/chromium/chromium/src/+/master:build/vs_toolchain.py;l=140-175 - const supportedVersions = ['2022', '2019']; - - const availableVersions = []; - for (const version of supportedVersions) { - // Check environment variable first (explicit override) - let vsPath = process.env[`vs${version}_install`]; - if (vsPath && fs.existsSync(vsPath)) { - availableVersions.push(version); - break; - } - - // Check default installation paths - const programFiles86Path = process.env['ProgramFiles(x86)']; - const programFiles64Path = process.env['ProgramFiles']; - - const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools', 'IntPreview']; - if (programFiles64Path) { - vsPath = `${programFiles64Path}/Microsoft Visual Studio/${version}`; - if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { - availableVersions.push(version); - break; - } - } - - if (programFiles86Path) { - vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; - if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath, vsType)))) { - availableVersions.push(version); - break; - } - } - } - - return availableVersions.length; -} - -function installHeaders() { - cp.execSync(`npm.cmd ${process.env['npm_command'] || 'ci'}`, { - env: process.env, - cwd: path.join(__dirname, 'gyp'), - stdio: 'inherit' - }); - - // The node gyp package got installed using the above npm command using the gyp/package.json - // file checked into our repository. So from that point it is save to construct the path - // to that executable - const node_gyp = path.join(__dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd'); - const result = cp.execFileSync(node_gyp, ['list'], { encoding: 'utf8', shell: true }); - const versions = new Set(result.split(/\n/g).filter(line => !line.startsWith('gyp info')).map(value => value)); - - const local = getHeaderInfo(path.join(__dirname, '..', '..', '.npmrc')); - const remote = getHeaderInfo(path.join(__dirname, '..', '..', 'remote', '.npmrc')); - - if (local !== undefined && !versions.has(local.target)) { - // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target], { shell: true }); - } - - if (remote !== undefined && !versions.has(remote.target)) { - // Both disturl and target come from a file checked into our repository - cp.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target], { shell: true }); - } -} - -/** - * @param {string} rcFile - * @returns {{ disturl: string; target: string } | undefined} - */ -function getHeaderInfo(rcFile) { - const lines = fs.readFileSync(rcFile, 'utf8').split(/\r\n?/g); - let disturl, target; - for (const line of lines) { - let match = line.match(/\s*disturl=*\"(.*)\"\s*$/); - if (match !== null && match.length >= 1) { - disturl = match[1]; - } - match = line.match(/\s*target=*\"(.*)\"\s*$/); - if (match !== null && match.length >= 1) { - target = match[1]; - } - } - return disturl !== undefined && target !== undefined - ? { disturl, target } - : undefined; -} diff --git a/build/npm/preinstall.ts b/build/npm/preinstall.ts new file mode 100644 index 00000000000..3476fcabb50 --- /dev/null +++ b/build/npm/preinstall.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import path from 'path'; +import * as fs from 'fs'; +import * as child_process from 'child_process'; +import * as os from 'os'; + +if (!process.env['VSCODE_SKIP_NODE_VERSION_CHECK']) { + // Get the running Node.js version + const nodeVersion = /^(\d+)\.(\d+)\.(\d+)/.exec(process.versions.node); + const majorNodeVersion = parseInt(nodeVersion![1]); + const minorNodeVersion = parseInt(nodeVersion![2]); + const patchNodeVersion = parseInt(nodeVersion![3]); + + // Get the required Node.js version from .nvmrc + const nvmrcPath = path.join(import.meta.dirname, '..', '..', '.nvmrc'); + const requiredVersion = fs.readFileSync(nvmrcPath, 'utf8').trim(); + const requiredVersionMatch = /^(\d+)\.(\d+)\.(\d+)/.exec(requiredVersion); + + if (!requiredVersionMatch) { + console.error('\x1b[1;31m*** Unable to parse required Node.js version from .nvmrc\x1b[0;0m'); + throw new Error(); + } + + const requiredMajor = parseInt(requiredVersionMatch[1]); + const requiredMinor = parseInt(requiredVersionMatch[2]); + const requiredPatch = parseInt(requiredVersionMatch[3]); + + if (majorNodeVersion < requiredMajor || + (majorNodeVersion === requiredMajor && minorNodeVersion < requiredMinor) || + (majorNodeVersion === requiredMajor && minorNodeVersion === requiredMinor && patchNodeVersion < requiredPatch)) { + console.error(`\x1b[1;31m*** Please use Node.js v${requiredVersion} or later for development. Currently using v${process.versions.node}.\x1b[0;0m`); + throw new Error(); + } +} + +if (process.env.npm_execpath?.includes('yarn')) { + console.error('\x1b[1;31m*** Seems like you are using `yarn` which is not supported in this repo any more, please use `npm i` instead. ***\x1b[0;0m'); + throw new Error(); +} + +if (process.platform === 'win32') { + if (!hasSupportedVisualStudioVersion()) { + console.error('\x1b[1;31m*** Invalid C/C++ Compiler Toolchain. Please check https://github.com/microsoft/vscode/wiki/How-to-Contribute#prerequisites.\x1b[0;0m'); + console.error('\x1b[1;31m*** If you have Visual Studio installed in a custom location, you can specify it via the environment variable:\x1b[0;0m'); + console.error('\x1b[1;31m*** set vs2022_install= (or vs2019_install for older versions)\x1b[0;0m'); + throw new Error(); + } +} + +installHeaders(); + +if (process.arch !== os.arch()) { + console.error(`\x1b[1;31m*** ARCHITECTURE MISMATCH: The node.js process is ${process.arch}, but your OS architecture is ${os.arch()}. ***\x1b[0;0m`); + console.error(`\x1b[1;31m*** This can greatly increase the build time of vs code. ***\x1b[0;0m`); +} + +function hasSupportedVisualStudioVersion() { + // Translated over from + // https://source.chromium.org/chromium/chromium/src/+/master:build/vs_toolchain.py;l=140-175 + const supportedVersions = ['2022', '2019']; + + const availableVersions = []; + for (const version of supportedVersions) { + // Check environment variable first (explicit override) + let vsPath = process.env[`vs${version}_install`]; + if (vsPath && fs.existsSync(vsPath)) { + availableVersions.push(version); + break; + } + + // Check default installation paths + const programFiles86Path = process.env['ProgramFiles(x86)']; + const programFiles64Path = process.env['ProgramFiles']; + + const vsTypes = ['Enterprise', 'Professional', 'Community', 'Preview', 'BuildTools', 'IntPreview']; + if (programFiles64Path) { + vsPath = `${programFiles64Path}/Microsoft Visual Studio/${version}`; + if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath!, vsType)))) { + availableVersions.push(version); + break; + } + } + + if (programFiles86Path) { + vsPath = `${programFiles86Path}/Microsoft Visual Studio/${version}`; + if (vsTypes.some(vsType => fs.existsSync(path.join(vsPath!, vsType)))) { + availableVersions.push(version); + break; + } + } + } + + return availableVersions.length; +} + +function installHeaders() { + const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + child_process.execSync(`${npm} ${process.env.npm_command || 'ci'}`, { + env: process.env, + cwd: path.join(import.meta.dirname, 'gyp'), + stdio: 'inherit' + }); + + // The node gyp package got installed using the above npm command using the gyp/package.json + // file checked into our repository. So from that point it is safe to construct the path + // to that executable + const node_gyp = process.platform === 'win32' + ? path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp.cmd') + : path.join(import.meta.dirname, 'gyp', 'node_modules', '.bin', 'node-gyp'); + + const local = getHeaderInfo(path.join(import.meta.dirname, '..', '..', '.npmrc')); + const remote = getHeaderInfo(path.join(import.meta.dirname, '..', '..', 'remote', '.npmrc')); + + if (local !== undefined) { + // Both disturl and target come from a file checked into our repository + child_process.execFileSync(node_gyp, ['install', '--dist-url', local.disturl, local.target], { shell: true }); + } + + if (remote !== undefined) { + // Both disturl and target come from a file checked into our repository + child_process.execFileSync(node_gyp, ['install', '--dist-url', remote.disturl, remote.target], { shell: true }); + } + + // On Linux, apply a patch to the downloaded headers + // Remove dependency on std::source_location to avoid bumping the required GCC version to 11+ + // Refs https://chromium-review.googlesource.com/c/v8/v8/+/6879784 + if (process.platform === 'linux') { + const homedir = os.homedir(); + const cachePath = process.env.XDG_CACHE_HOME || path.join(homedir, '.cache'); + const nodeGypCache = path.join(cachePath, 'node-gyp'); + const localHeaderPath = path.join(nodeGypCache, local!.target, 'include', 'node'); + if (fs.existsSync(localHeaderPath)) { + console.log('Applying v8-source-location.patch to', localHeaderPath); + try { + child_process.execFileSync('patch', ['-p0', '-i', path.join(import.meta.dirname, 'gyp', 'custom-headers', 'v8-source-location.patch')], { + cwd: localHeaderPath + }); + } catch (error) { + throw new Error(`Error applying v8-source-location.patch: ${(error as Error).message}`); + } + } + } +} + +function getHeaderInfo(rcFile: string): { disturl: string; target: string } | undefined { + const lines = fs.readFileSync(rcFile, 'utf8').split(/\r\n|\n/g); + let disturl: string | undefined; + let target: string | undefined; + for (const line of lines) { + let match = line.match(/\s*disturl=*\"(.*)\"\s*$/); + if (match !== null && match.length >= 1) { + disturl = match[1]; + } + match = line.match(/\s*target=*\"(.*)\"\s*$/); + if (match !== null && match.length >= 1) { + target = match[1]; + } + } + return disturl !== undefined && target !== undefined + ? { disturl, target } + : undefined; +} diff --git a/build/npm/update-all-grammars.mjs b/build/npm/update-all-grammars.mjs deleted file mode 100644 index 7e303a655f7..00000000000 --- a/build/npm/update-all-grammars.mjs +++ /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 { spawn as _spawn } from 'child_process'; -import { readdirSync, readFileSync } from 'fs'; -import { join } from 'path'; -import url from 'url'; - -async function spawn(cmd, args, opts) { - return new Promise((c, e) => { - const child = _spawn(cmd, args, { shell: true, stdio: 'inherit', env: process.env, ...opts }); - child.on('close', code => code === 0 ? c() : e(`Returned ${code}`)); - }); -} - -async function main() { - await spawn('npm', ['ci'], { cwd: 'extensions' }); - - for (const extension of readdirSync('extensions')) { - try { - const packageJSON = JSON.parse(readFileSync(join('extensions', extension, 'package.json')).toString()); - if (!(packageJSON && packageJSON.scripts && packageJSON.scripts['update-grammar'])) { - continue; - } - } catch { - continue; - } - - await spawn(`npm`, ['run', 'update-grammar'], { cwd: `extensions/${extension}` }); - } - - // run integration tests - - if (process.platform === 'win32') { - _spawn('.\\scripts\\test-integration.bat', [], { env: process.env, stdio: 'inherit' }); - } else { - _spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); - } -} - -if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { - main().catch(err => { - console.error(err); - process.exit(1); - }); -} diff --git a/build/npm/update-all-grammars.ts b/build/npm/update-all-grammars.ts new file mode 100644 index 00000000000..b085967f0de --- /dev/null +++ b/build/npm/update-all-grammars.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn as _spawn } from 'child_process'; +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +async function spawn(cmd: string, args: string[], opts?: Parameters[2]) { + return new Promise((c, e) => { + const child = _spawn(cmd, args, { shell: true, stdio: 'inherit', env: process.env, ...opts }); + child.on('close', code => code === 0 ? c() : e(`Returned ${code}`)); + }); +} + +async function main() { + await spawn('npm', ['ci'], { cwd: 'extensions' }); + + for (const extension of readdirSync('extensions')) { + try { + const packageJSON = JSON.parse(readFileSync(join('extensions', extension, 'package.json')).toString()); + if (!(packageJSON && packageJSON.scripts && packageJSON.scripts['update-grammar'])) { + continue; + } + } catch { + continue; + } + + await spawn(`npm`, ['run', 'update-grammar'], { cwd: `extensions/${extension}` }); + } + + // run integration tests + + if (process.platform === 'win32') { + _spawn('.\\scripts\\test-integration.bat', [], { shell: true, env: process.env, stdio: 'inherit' }); + } else { + _spawn('/bin/bash', ['./scripts/test-integration.sh'], { env: process.env, stdio: 'inherit' }); + } +} + +if (import.meta.main) { + try { + await main(); + } catch (err) { + console.error(err); + process.exit(1); + } +} diff --git a/build/npm/update-distro.mjs b/build/npm/update-distro.mjs deleted file mode 100644 index 655d9f2c243..00000000000 --- a/build/npm/update-distro.mjs +++ /dev/null @@ -1,18 +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 { execSync } from 'child_process'; -import { join, resolve } from 'path'; -import { readFileSync, writeFileSync } from 'fs'; -import { fileURLToPath } from 'url'; - -const rootPath = resolve(fileURLToPath(import.meta.url), '..', '..', '..', '..'); -const vscodePath = join(rootPath, 'vscode'); -const distroPath = join(rootPath, 'vscode-distro'); -const commit = execSync('git rev-parse HEAD', { cwd: distroPath, encoding: 'utf8' }).trim(); -const packageJsonPath = join(vscodePath, 'package.json'); -const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - -packageJson.distro = commit; -writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); diff --git a/build/npm/update-distro.ts b/build/npm/update-distro.ts new file mode 100644 index 00000000000..3c58af6197e --- /dev/null +++ b/build/npm/update-distro.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { readFileSync, writeFileSync } from 'fs'; + +const rootPath = resolve(import.meta.dirname, '..', '..', '..'); +const vscodePath = join(rootPath, 'vscode'); +const distroPath = join(rootPath, 'vscode-distro'); +const commit = execSync('git rev-parse HEAD', { cwd: distroPath, encoding: 'utf8' }).trim(); +const packageJsonPath = join(vscodePath, 'package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + +packageJson.distro = commit; +writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); diff --git a/build/npm/update-localization-extension.js b/build/npm/update-localization-extension.js deleted file mode 100644 index 6274323f747..00000000000 --- a/build/npm/update-localization-extension.js +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -let i18n = require("../lib/i18n"); - -let fs = require("fs"); -let path = require("path"); - -let gulp = require('gulp'); -let vfs = require("vinyl-fs"); -let rimraf = require('rimraf'); -let minimist = require('minimist'); - -function update(options) { - let idOrPath = options._; - if (!idOrPath) { - throw new Error('Argument must be the location of the localization extension.'); - } - let location = options.location; - if (location !== undefined && !fs.existsSync(location)) { - throw new Error(`${location} doesn't exist.`); - } - let externalExtensionsLocation = options.externalExtensionsLocation; - if (externalExtensionsLocation !== undefined && !fs.existsSync(externalExtensionsLocation)) { - throw new Error(`${externalExtensionsLocation} doesn't exist.`); - } - let locExtFolder = idOrPath; - if (/^\w{2,3}(-\w+)?$/.test(idOrPath)) { - locExtFolder = path.join('..', 'vscode-loc', 'i18n', `vscode-language-pack-${idOrPath}`); - } - let locExtStat = fs.statSync(locExtFolder); - if (!locExtStat || !locExtStat.isDirectory) { - throw new Error('No directory found at ' + idOrPath); - } - let packageJSON = JSON.parse(fs.readFileSync(path.join(locExtFolder, 'package.json')).toString()); - let contributes = packageJSON['contributes']; - if (!contributes) { - throw new Error('The extension must define a "localizations" contribution in the "package.json"'); - } - let localizations = contributes['localizations']; - if (!localizations) { - throw new Error('The extension must define a "localizations" contribution of type array in the "package.json"'); - } - - localizations.forEach(function (localization) { - if (!localization.languageId || !localization.languageName || !localization.localizedLanguageName) { - throw new Error('Each localization contribution must define "languageId", "languageName" and "localizedLanguageName" properties.'); - } - let languageId = localization.languageId; - let translationDataFolder = path.join(locExtFolder, 'translations'); - - switch (languageId) { - case 'zh-cn': - languageId = 'zh-Hans'; - break; - case 'zh-tw': - languageId = 'zh-Hant'; - break; - case 'pt-br': - languageId = 'pt-BR'; - break; - } - - if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { - console.log('Clearing \'' + translationDataFolder + '\'...'); - rimraf.sync(translationDataFolder); - } - - console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); - let translationPaths = []; - gulp.src([ - path.join(location, '**', languageId, '*.xlf'), - ...i18n.EXTERNAL_EXTENSIONS.map(extensionId => path.join(externalExtensionsLocation, extensionId, languageId, '*-new.xlf')) - ], { silent: false }) - .pipe(i18n.prepareI18nPackFiles(translationPaths)) - .on('error', (error) => { - console.log(`Error occurred while importing translations:`); - translationPaths = undefined; - if (Array.isArray(error)) { - error.forEach(console.log); - } else if (error) { - console.log(error); - } else { - console.log('Unknown error'); - } - }) - .pipe(vfs.dest(translationDataFolder)) - .on('end', function () { - if (translationPaths !== undefined) { - localization.translations = []; - for (let tp of translationPaths) { - localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}` }); - } - fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t') + '\n'); - } - }); - }); -} -if (path.basename(process.argv[1]) === 'update-localization-extension.js') { - var options = minimist(process.argv.slice(2), { - string: ['location', 'externalExtensionsLocation'] - }); - update(options); -} diff --git a/build/npm/update-localization-extension.ts b/build/npm/update-localization-extension.ts new file mode 100644 index 00000000000..cb7981b9388 --- /dev/null +++ b/build/npm/update-localization-extension.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as i18n from '../lib/i18n.ts'; +import fs from 'fs'; +import path from 'path'; +import gulp from 'gulp'; +import vfs from 'vinyl-fs'; +import rimraf from 'rimraf'; +import minimist from 'minimist'; + +interface Options { + _: string[]; + location?: string; + externalExtensionsLocation?: string; +} + +interface PackageJson { + contributes?: { + localizations?: Localization[]; + }; +} + +interface Localization { + languageId: string; + languageName: string; + localizedLanguageName: string; + translations?: Array<{ id: string; path: string }>; +} + +interface TranslationPath { + id: string; + resourceName: string; +} + +function update(options: Options) { + const idOrPath = options._[0]; + if (!idOrPath) { + throw new Error('Argument must be the location of the localization extension.'); + } + const location = options.location; + if (location !== undefined && !fs.existsSync(location)) { + throw new Error(`${location} doesn't exist.`); + } + const externalExtensionsLocation = options.externalExtensionsLocation; + if (externalExtensionsLocation !== undefined && !fs.existsSync(externalExtensionsLocation)) { + throw new Error(`${externalExtensionsLocation} doesn't exist.`); + } + let locExtFolder: string = idOrPath; + if (/^\w{2,3}(-\w+)?$/.test(idOrPath)) { + locExtFolder = path.join('..', 'vscode-loc', 'i18n', `vscode-language-pack-${idOrPath}`); + } + const locExtStat = fs.statSync(locExtFolder); + if (!locExtStat || !locExtStat.isDirectory) { + throw new Error('No directory found at ' + idOrPath); + } + const packageJSON = JSON.parse(fs.readFileSync(path.join(locExtFolder, 'package.json')).toString()) as PackageJson; + const contributes = packageJSON['contributes']; + if (!contributes) { + throw new Error('The extension must define a "localizations" contribution in the "package.json"'); + } + const localizations = contributes['localizations']; + if (!localizations) { + throw new Error('The extension must define a "localizations" contribution of type array in the "package.json"'); + } + + localizations.forEach(function (localization) { + if (!localization.languageId || !localization.languageName || !localization.localizedLanguageName) { + throw new Error('Each localization contribution must define "languageId", "languageName" and "localizedLanguageName" properties.'); + } + let languageId = localization.languageId; + const translationDataFolder = path.join(locExtFolder, 'translations'); + + switch (languageId) { + case 'zh-cn': + languageId = 'zh-Hans'; + break; + case 'zh-tw': + languageId = 'zh-Hant'; + break; + case 'pt-br': + languageId = 'pt-BR'; + break; + } + + if (fs.existsSync(translationDataFolder) && fs.existsSync(path.join(translationDataFolder, 'main.i18n.json'))) { + console.log('Clearing \'' + translationDataFolder + '\'...'); + rimraf.sync(translationDataFolder); + } + + console.log(`Importing translations for ${languageId} form '${location}' to '${translationDataFolder}' ...`); + let translationPaths: TranslationPath[] | undefined = []; + gulp.src([ + path.join(location!, '**', languageId, '*.xlf'), + ...i18n.EXTERNAL_EXTENSIONS.map((extensionId: string) => path.join(externalExtensionsLocation!, extensionId, languageId, '*-new.xlf')) + ], { silent: false }) + .pipe(i18n.prepareI18nPackFiles(translationPaths)) + .on('error', (error: unknown) => { + console.log(`Error occurred while importing translations:`); + translationPaths = undefined; + if (Array.isArray(error)) { + error.forEach(console.log); + } else if (error) { + console.log(error); + } else { + console.log('Unknown error'); + } + }) + .pipe(vfs.dest(translationDataFolder)) + .on('end', function () { + if (translationPaths !== undefined) { + localization.translations = []; + for (const tp of translationPaths) { + localization.translations.push({ id: tp.id, path: `./translations/${tp.resourceName}` }); + } + fs.writeFileSync(path.join(locExtFolder, 'package.json'), JSON.stringify(packageJSON, null, '\t') + '\n'); + } + }); + }); +} +if (path.basename(process.argv[1]) === 'update-localization-extension.js') { + const options = minimist(process.argv.slice(2), { + string: ['location', 'externalExtensionsLocation'] + }) as Options; + update(options); +} diff --git a/build/package-lock.json b/build/package-lock.json index 29af501c705..255effe830e 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -53,12 +53,13 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "esbuild": "0.25.5", + "dmg-builder": "^26.5.0", + "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", - "jws": "^4.0.0", + "jws": "^4.0.1", "mime": "^1.4.1", "source-map": "0.6.1", "ternary-stream": "^3.0.0", @@ -66,7 +67,8 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0" + "yauzl": "^2.10.0", + "zx": "^8.8.5" }, "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", @@ -488,11 +490,54 @@ "node": ">=6.9.0" } }, + "node_modules/@develar/schema-utils": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", + "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.0", + "ajv-keywords": "^3.4.1" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@develar/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@develar/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@electron/asar": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.10.tgz", - "integrity": "sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", + "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", "dev": true, + "license": "MIT", "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", @@ -514,6 +559,136 @@ "node": ">= 6" } }, + "node_modules/@electron/fuses": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", + "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "fs-extra": "^9.0.1", + "minimist": "^1.2.5" + }, + "bin": { + "electron-fuses": "dist/bin.js" + } + }, + "node_modules/@electron/fuses/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/@electron/fuses/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/@electron/fuses/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/@electron/fuses/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/@electron/fuses/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/fuses/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/@electron/fuses/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/@electron/fuses/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/@electron/fuses/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/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -535,6 +710,60 @@ "global-agent": "^3.0.0" } }, + "node_modules/@electron/notarize": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", + "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.1", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@electron/notarize/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/notarize/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/@electron/notarize/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/@electron/osx-sign": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-2.0.0.tgz", @@ -583,84 +812,390 @@ "node": ">=10" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], + "node_modules/@electron/rebuild": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.1.tgz", + "integrity": "sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "got": "^11.7.0", + "graceful-fs": "^4.2.11", + "node-abi": "^4.2.0", + "node-api-version": "^0.2.1", + "node-gyp": "^11.2.0", + "ora": "^5.1.0", + "read-binary-file-arch": "^1.0.6", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/cli.js" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], + "node_modules/@electron/rebuild/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", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=18" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/rebuild/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", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": ">=18" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], + "node_modules/@electron/rebuild/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=10" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], + "node_modules/@electron/rebuild/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", - "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@electron/rebuild/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/@electron/rebuild/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/rebuild/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/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/@electron/rebuild/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@electron/rebuild/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@electron/rebuild/node_modules/node-abi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", + "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.6.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/rebuild/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/rebuild/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/@electron/rebuild/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@electron/universal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", + "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/asar": "^3.3.1", + "@malept/cross-spawn-promise": "^2.0.0", + "debug": "^4.3.1", + "dir-compare": "^4.2.0", + "fs-extra": "^11.1.1", + "minimatch": "^9.0.3", + "plist": "^3.1.0" + }, + "engines": { + "node": ">=16.4" + } + }, + "node_modules/@electron/universal/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@electron/universal/node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@electron/universal/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/@electron/universal/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@electron/universal/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/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, "os": [ "darwin" ], @@ -669,9 +1204,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -686,9 +1221,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -703,9 +1238,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -720,9 +1255,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -737,9 +1272,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -754,9 +1289,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -771,9 +1306,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -788,9 +1323,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -805,9 +1340,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -822,9 +1357,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -839,9 +1374,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -856,9 +1391,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -873,9 +1408,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -890,9 +1425,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -907,9 +1442,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -924,9 +1459,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -940,10 +1475,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -958,9 +1510,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -975,9 +1527,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -992,9 +1544,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -1074,12 +1626,25 @@ "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", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, - "funding": [ + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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", + "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", + "dev": true, + "funding": [ { "type": "individual", "url": "https://github.com/sponsors/malept" @@ -1096,6 +1661,61 @@ "node": ">= 12.13.0" } }, + "node_modules/@malept/flatpak-bundler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", + "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "fs-extra": "^9.0.0", + "lodash": "^4.17.15", + "tmp-promise": "^3.0.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@malept/flatpak-bundler/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/@malept/flatpak-bundler/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/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1134,6 +1754,67 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/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/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@secretlint/config-creator": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-10.2.2.tgz", @@ -1566,10 +2247,11 @@ "dev": true }, "node_modules/@types/fs-extra": { - "version": "9.0.12", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.12.tgz", - "integrity": "sha512-I+bsBr67CurCGnSenZZ7v94gd3tc3+Aj2taxMT4yu4ABLuOgOjeFxX3dokG24ztSRg5tnT00sL8BszO7gSMoIw==", + "version": "9.0.13", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", + "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -1804,6 +2486,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/plist": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", + "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*", + "xmlbuilder": ">=11.0.1" + } + }, "node_modules/@types/pump": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/pump/-/pump-1.0.1.tgz", @@ -1874,6 +2568,14 @@ "integrity": "sha512-Z4TYuEKn9+RbNVk1Ll2SS4x1JeLHecolIbM/a8gveaHsW0Hr+RQMraZACwTO2VD7JvepgA6UO1A1VrbktQrIbQ==", "dev": true }, + "node_modules/@types/verror": { + "version": "1.10.11", + "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", + "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/@types/vinyl": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", @@ -1930,9 +2632,9 @@ "license": "MIT" }, "node_modules/@vscode/ripgrep": { - "version": "1.15.14", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.14.tgz", - "integrity": "sha512-/G1UJPYlm+trBWQ6cMO3sv6b8D1+G16WaJH1/DSqw32JOVlzgZbLkDxRyzIpTpv30AcYGMkCf5tUqGlW6HbDWw==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz", + "integrity": "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2188,15 +2890,15 @@ "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==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -2212,11 +2914,11 @@ } }, "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==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -2287,15 +2989,29 @@ "node": ">=10.0.0" } }, + "node_modules/7zip-bin": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", + "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, "engines": { "node": ">= 14" } @@ -2317,6 +3033,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, "node_modules/ansi-colors": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", @@ -2402,6 +3128,211 @@ "node": ">= 8" } }, + "node_modules/app-builder-bin": { + "version": "5.0.0-alpha.12", + "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", + "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/app-builder-lib": { + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.5.0.tgz", + "integrity": "sha512-iRRiJhM0uFMauDeIuv8ESHZSn+LESbdDEuHi7rKdeETjrvBObecXnWJx1f3vs3KtoGcd3hCk1zURKypyvZOtFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@develar/schema-utils": "~2.6.5", + "@electron/asar": "3.4.1", + "@electron/fuses": "^1.8.0", + "@electron/notarize": "2.5.0", + "@electron/osx-sign": "1.3.3", + "@electron/rebuild": "4.0.1", + "@electron/universal": "2.0.3", + "@malept/flatpak-bundler": "^0.4.0", + "@types/fs-extra": "9.0.13", + "async-exit-hook": "^2.0.1", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chromium-pickle-js": "^0.2.0", + "ci-info": "4.3.1", + "debug": "^4.3.4", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", + "ejs": "^3.1.8", + "electron-publish": "26.4.1", + "fs-extra": "^10.1.0", + "hosted-git-info": "^4.1.0", + "isbinaryfile": "^5.0.0", + "jiti": "^2.4.2", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "lazy-val": "^1.0.5", + "minimatch": "^10.0.3", + "plist": "3.1.0", + "resedit": "^1.7.0", + "semver": "~7.7.3", + "tar": "7.5.3", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0", + "which": "^5.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "dmg-builder": "26.5.0", + "electron-builder-squirrel-windows": "26.5.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/osx-sign": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", + "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "compare-version": "^0.1.2", + "debug": "^4.3.4", + "fs-extra": "^10.0.0", + "isbinaryfile": "^4.0.8", + "minimist": "^1.2.6", + "plist": "^3.0.5" + }, + "bin": { + "electron-osx-flat": "bin/electron-osx-flat.js", + "electron-osx-sign": "bin/electron-osx-sign.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/app-builder-lib/node_modules/@electron/osx-sign/node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/app-builder-lib/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/app-builder-lib/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/app-builder-lib/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/app-builder-lib/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/app-builder-lib/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2437,7 +3368,18 @@ "node": ">=0.10.0" } }, - "node_modules/assign-symbols": { + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", @@ -2471,6 +3413,16 @@ "node": ">= 0.10" } }, + "node_modules/async-exit-hook": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", + "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -2478,6 +3430,16 @@ "dev": true, "license": "MIT" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, "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", @@ -2545,7 +3507,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, - "optional": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -2614,7 +3575,6 @@ "url": "https://feross.org/support" } ], - "optional": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2635,6 +3595,186 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builder-util": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", + "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.6", + "7zip-bin": "~5.2.0", + "app-builder-bin": "5.0.0-alpha.12", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "js-yaml": "^4.1.0", + "sanitize-filename": "^1.6.3", + "source-map-support": "^0.5.19", + "stat-mode": "^1.0.0", + "temp-file": "^3.4.0", + "tiny-async-pool": "1.3.0" + } + }, + "node_modules/builder-util-runtime": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", + "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/builder-util/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/builder-util/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/builder-util/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/builder-util/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/builder-util/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/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/builder-util/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/builder-util/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/builder-util/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/builder-util/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/builder-util/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/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -2644,6 +3784,127 @@ "node": ">=0.10.0" } }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/cacache/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/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/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/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -2782,16 +4043,230 @@ "dev": true, "optional": true }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "devOptional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/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", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/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", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cli-truncate/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", + "optional": true + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/node_modules/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/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "devOptional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg= sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", @@ -2898,6 +4373,16 @@ "node": ">=18" } }, + "node_modules/compare-version": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", + "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2910,6 +4395,17 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "devOptional": true }, + "node_modules/crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer": "^5.1.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3008,6 +4504,29 @@ "node": ">=4.0.0" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -3054,7 +4573,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", "dev": true, - "optional": true, "engines": { "node": ">=8" } @@ -3072,10 +4590,138 @@ "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", "dev": true, "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " + "minimatch": "^3.0.5", + "p-limit": "^3.1.0 " + } + }, + "node_modules/dmg-builder": { + "version": "26.5.0", + "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.5.0.tgz", + "integrity": "sha512-AyOCzpS1TCxDkSWxAzpfw5l7jBX4C8jKCucmT/6y6/24H5VKSHpjcVJD0W8o5BrFi+skC7Z7+F4aNyHmvn4AAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "app-builder-lib": "26.5.0", + "builder-util": "26.4.1", + "fs-extra": "^10.1.0", + "iconv-lite": "^0.6.2", + "js-yaml": "^4.1.0" + }, + "optionalDependencies": { + "dmg-license": "^1.0.11" + } + }, + "node_modules/dmg-builder/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/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dmg-builder/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/dmg-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/dmg-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/dmg-license": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", + "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "@types/plist": "^3.0.1", + "@types/verror": "^1.10.3", + "ajv": "^6.10.0", + "crc": "^3.8.0", + "iconv-corefoundation": "^1.1.7", + "plist": "^3.0.4", + "smart-buffer": "^4.0.2", + "verror": "^1.10.0" + }, + "bin": { + "dmg-license": "bin/dmg-license.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dmg-license/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/dmg-license/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -3131,6 +4777,35 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3191,6 +4866,166 @@ "url": "https://bevry.me/fund" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-publish": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.4.1.tgz", + "integrity": "sha512-nByal9K5Ar3BNJUfCSglXltpKUhJqpwivNpKVHnkwxTET9LKl+NxoojpGF1dSXVFcoBKVm+OhsVa28ZsoshEPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/fs-extra": "^9.0.11", + "builder-util": "26.4.1", + "builder-util-runtime": "9.5.1", + "chalk": "^4.1.2", + "form-data": "^4.0.0", + "fs-extra": "^10.1.0", + "lazy-val": "^1.0.5", + "mime": "^2.5.2" + } + }, + "node_modules/electron-publish/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/electron-publish/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/electron-publish/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/electron-publish/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/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-publish/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/electron-publish/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/electron-publish/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/electron-publish/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/electron-publish/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/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3198,6 +5033,17 @@ "dev": true, "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3241,6 +5087,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3298,9 +5151,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3311,31 +5164,42 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/escape-string-regexp": { @@ -3380,6 +5244,13 @@ "node": ">=6" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -3413,6 +5284,17 @@ "@types/yauzl": "^2.9.1" } }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "optional": true + }, "node_modules/fancy-log": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", @@ -3517,6 +5399,57 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3617,6 +5550,19 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3646,6 +5592,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4032,6 +5988,45 @@ "node": ">= 14" } }, + "node_modules/iconv-corefoundation": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", + "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "cli-truncate": "^2.1.0", + "node-addon-api": "^1.6.3" + }, + "engines": { + "node": "^8.11.2 || >=10" + } + }, + "node_modules/iconv-corefoundation/node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4050,8 +6045,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/ignore": { "version": "7.0.5", @@ -4063,6 +6057,16 @@ "node": ">= 4" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/index-to-position": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", @@ -4100,6 +6104,16 @@ "dev": true, "optional": true }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -4170,6 +6184,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4191,6 +6215,19 @@ "node": ">=0.10.0" } }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -4215,6 +6252,19 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "devOptional": true }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4264,6 +6314,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4272,9 +6357,9 @@ "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==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4312,10 +6397,11 @@ "optional": true }, "node_modules/json5": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", - "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -4355,23 +6441,25 @@ } }, "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "dev": true, + "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -4391,24 +6479,25 @@ } }, "node_modules/jwa": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", - "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "dev": true, "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -4433,6 +6522,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/lazy-val": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", + "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -4453,10 +6549,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.mergewith": { "version": "4.6.2", @@ -4471,6 +6568,99 @@ "dev": true, "license": "MIT" }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/log-symbols/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/lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -4492,6 +6682,29 @@ "node": ">=10" } }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -4625,6 +6838,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -4640,27 +6863,165 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, - "license": "ISC", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" } }, "node_modules/mkdirp-classic": { @@ -4690,6 +7051,16 @@ "dev": true, "optional": true }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abi": { "version": "3.30.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.30.0.tgz", @@ -4732,6 +7103,54 @@ "dev": true, "optional": true }, + "node_modules/node-api-version": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", + "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + } + }, + "node_modules/node-api-version/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -4744,6 +7163,45 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/node-sarif-builder": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.2.0.tgz", @@ -4796,6 +7254,22 @@ "node": ">= 10.0.0" } }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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", @@ -4918,6 +7392,22 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/open": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", @@ -4935,6 +7425,129 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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/ora/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/ora/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/ora/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/ora/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/ora/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/ora/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/ora/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/p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -5120,6 +7733,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pe-library": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", + "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -5240,6 +7868,16 @@ "integrity": "sha1-LuTyPCVgkT4IwHzlzN1t498sWvg= sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==", "dev": true }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5255,6 +7893,20 @@ "node": ">=0.4.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5272,6 +7924,16 @@ "once": "^1.3.1" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -5283,9 +7945,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -5368,9 +8030,9 @@ "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -5392,6 +8054,19 @@ "node": ">=0.8" } }, + "node_modules/read-binary-file-arch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", + "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "bin": { + "read-binary-file-arch": "cli.js" + } + }, "node_modules/read-pkg": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", @@ -5476,7 +8151,17 @@ "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "devOptional": true, "engines": { - "node": ">= 0.10" + "node": ">= 0.10" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/require-from-string": { @@ -5489,6 +8174,24 @@ "node": ">=0.10.0" } }, + "node_modules/resedit": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", + "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pe-library": "^0.4.1" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jet2jet" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -5507,6 +8210,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5566,6 +8300,23 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "devOptional": true }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sanitize-filename": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", + "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", + "dev": true, + "license": "WTFPL OR ISC", + "dependencies": { + "truncate-utf8-bytes": "^1.0.0" + } + }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -5859,6 +8610,47 @@ "dev": true, "license": "MIT" }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5868,6 +8660,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -5911,6 +8714,29 @@ "dev": true, "optional": true }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/stat-mode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", + "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -6227,6 +9053,24 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.3.tgz", + "integrity": "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -6258,6 +9102,75 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-file": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", + "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-exit-hook": "^2.0.1", + "fs-extra": "^10.0.0" + } + }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/temp-file/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/temp-file/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/terminal-link": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-4.0.0.tgz", @@ -6344,6 +9257,56 @@ "node": ">=0.10.0" } }, + "node_modules/tiny-async-pool": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", + "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.5.0" + } + }, + "node_modules/tiny-async-pool/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tmp": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", @@ -6354,6 +9317,16 @@ "node": ">=14.14" } }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tmp": "^0.2.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6449,6 +9422,16 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/truncate-utf8-bytes": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", + "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "dev": true, + "license": "WTFPL", + "dependencies": { + "utf8-byte-length": "^1.0.1" + } + }, "node_modules/tslib": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", @@ -6537,6 +9520,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", @@ -6552,12 +9561,29 @@ "node": ">= 4.0.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/url-join": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "dev": true }, + "node_modules/utf8-byte-length": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", + "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -6584,6 +9610,22 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/version-range": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", @@ -6736,6 +9778,16 @@ "node": ">= 10.0.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6937,12 +9989,51 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -6964,6 +10055,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zx": { + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } } } } diff --git a/build/package.json b/build/package.json index 0948204b038..e45161dc2c3 100644 --- a/build/package.json +++ b/build/package.json @@ -47,12 +47,13 @@ "ansi-colors": "^3.2.3", "byline": "^5.0.0", "debug": "^4.3.2", - "esbuild": "0.25.5", + "dmg-builder": "^26.5.0", + "esbuild": "0.27.2", "extract-zip": "^2.0.1", "gulp-merge-json": "^2.1.1", "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", - "jws": "^4.0.0", + "jws": "^4.0.1", "mime": "^1.4.1", "source-map": "0.6.1", "ternary-stream": "^3.0.0", @@ -60,17 +61,16 @@ "tree-sitter": "^0.22.4", "vscode-universal-bundler": "^0.1.3", "workerpool": "^6.4.0", - "yauzl": "^2.10.0" + "yauzl": "^2.10.0", + "zx": "^8.8.5" }, - "type": "commonjs", + "type": "module", "scripts": { - "copy-policy-dto": "node lib/policies/copyPolicyDto.js", - "prebuild-ts": "npm run copy-policy-dto", - "build-ts": "cd .. && npx tsgo --project build/tsconfig.build.json", - "compile": "npm run build-ts", - "watch": "npm run build-ts -- --watch", - "npmCheckJs": "npm run build-ts -- --noEmit", - "test": "npx mocha --ui tdd 'lib/**/*.test.js'" + "copy-policy-dto": "node lib/policies/copyPolicyDto.ts", + "pretypecheck": "npm run copy-policy-dto", + "typecheck": "cd .. && npx tsgo --project build/tsconfig.json", + "watch": "npm run typecheck -- --watch", + "test": "mocha --ui tdd 'lib/**/*.test.ts'" }, "optionalDependencies": { "tree-sitter-typescript": "^0.23.2", diff --git a/build/setup-npm-registry.js b/build/setup-npm-registry.js deleted file mode 100644 index 07bcf2296fa..00000000000 --- a/build/setup-npm-registry.js +++ /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. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -'use strict'; - -const fs = require('fs').promises; -const path = require('path'); - -/** - * @param {string} dir - * - * @returns {AsyncGenerator} - */ -async function* getPackageLockFiles(dir) { - const files = await fs.readdir(dir); - - for (const file of files) { - const fullPath = path.join(dir, file); - const stat = await fs.stat(fullPath); - - if (stat.isDirectory()) { - yield* getPackageLockFiles(fullPath); - } else if (file === 'package-lock.json') { - yield fullPath; - } - } -} - -/** - * @param {string} url - * @param {string} file - */ -async function setup(url, file) { - let contents = await fs.readFile(file, 'utf8'); - contents = contents.replace(/https:\/\/registry\.[^.]+\.org\//g, url); - await fs.writeFile(file, contents); -} - -/** - * @param {string} url - * @param {string} dir - */ -async function main(url, dir) { - const root = dir ?? process.cwd(); - - for await (const file of getPackageLockFiles(root)) { - console.log(`Enabling custom NPM registry: ${path.relative(root, file)}`); - await setup(url, file); - } -} - -main(process.argv[2], process.argv[3]); diff --git a/build/setup-npm-registry.ts b/build/setup-npm-registry.ts new file mode 100644 index 00000000000..670c3e339db --- /dev/null +++ b/build/setup-npm-registry.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { promises as fs } from 'fs'; +import path from 'path'; + +/** + * Recursively find all package-lock.json files in a directory + */ +async function* getPackageLockFiles(dir: string): AsyncGenerator { + const files = await fs.readdir(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = await fs.stat(fullPath); + + if (stat.isDirectory()) { + yield* getPackageLockFiles(fullPath); + } else if (file === 'package-lock.json') { + yield fullPath; + } + } +} + +/** + * Replace the registry URL in a package-lock.json file + */ +async function setup(url: string, file: string): Promise { + let contents = await fs.readFile(file, 'utf8'); + contents = contents.replace(/https:\/\/registry\.[^.]+\.org\//g, url); + await fs.writeFile(file, contents); +} + +/** + * Main function to set up custom NPM registry + */ +async function main(url: string, dir?: string): Promise { + const root = dir ?? process.cwd(); + + for await (const file of getPackageLockFiles(root)) { + console.log(`Enabling custom NPM registry: ${path.relative(root, file)}`); + await setup(url, file); + } +} + +main(process.argv[2], process.argv[3]); diff --git a/build/stylelint.js b/build/stylelint.js deleted file mode 100644 index c2f0e4482a2..00000000000 --- a/build/stylelint.js +++ /dev/null @@ -1,77 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check - -const es = require('event-stream'); -const vfs = require('vinyl-fs'); -const { stylelintFilter } = require('./filters'); -const { getVariableNameValidator } = require('./lib/stylelint/validateVariableNames'); - -module.exports = gulpstylelint; - -/** - * use regex on lines - * - * @param {function(string, boolean):void} reporter - */ -function gulpstylelint(reporter) { - const variableValidator = getVariableNameValidator(); - let errorCount = 0; - const monacoWorkbenchPattern = /\.monaco-workbench/; - const restrictedPathPattern = /^src[\/\\]vs[\/\\](base|platform|editor)[\/\\]/; - const layerCheckerDisablePattern = /\/\*\s*stylelint-disable\s+layer-checker\s*\*\//; - - return es.through(function (file) { - /** @type {string[]} */ - const lines = file.__lines || file.contents.toString('utf8').split(/\r\n|\r|\n/); - file.__lines = lines; - - const isRestrictedPath = restrictedPathPattern.test(file.relative); - - // Check if layer-checker is disabled for the entire file - const isLayerCheckerDisabled = lines.some(line => layerCheckerDisablePattern.test(line)); - - lines.forEach((line, i) => { - variableValidator(line, unknownVariable => { - reporter(file.relative + '(' + (i + 1) + ',1): Unknown variable: ' + unknownVariable, true); - errorCount++; - }); - - if (isRestrictedPath && !isLayerCheckerDisabled && monacoWorkbenchPattern.test(line)) { - reporter(file.relative + '(' + (i + 1) + ',1): The class .monaco-workbench cannot be used in files under src/vs/{base,platform,editor} because only src/vs/workbench applies it', true); - errorCount++; - } - }); - - this.emit('data', file); - }, function () { - if (errorCount > 0) { - reporter('All valid variable names are in `build/lib/stylelint/vscode-known-variables.json`\nTo update that file, run `./scripts/test-documentation.sh|bat.`', false); - } - this.emit('end'); - } - ); -} - -function stylelint() { - return vfs - .src(stylelintFilter, { base: '.', follow: true, allowEmpty: true }) - .pipe(gulpstylelint((message, isError) => { - if (isError) { - console.error(message); - } else { - console.info(message); - } - })) - .pipe(es.through(function () { /* noop, important for the stream to end */ })); -} - -if (require.main === module) { - stylelint().on('error', (err) => { - console.error(); - console.error(err); - process.exit(1); - }); -} diff --git a/build/stylelint.ts b/build/stylelint.ts new file mode 100644 index 00000000000..037fe110615 --- /dev/null +++ b/build/stylelint.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import es from 'event-stream'; +import vfs from 'vinyl-fs'; +import { stylelintFilter } from './filters.ts'; +import { getVariableNameValidator } from './lib/stylelint/validateVariableNames.ts'; + +interface FileWithLines { + __lines?: string[]; + relative: string; + contents: Buffer; +} + +type Reporter = (message: string, isError: boolean) => void; + +/** + * Stylelint gulpfile task + */ +export default function gulpstylelint(reporter: Reporter): NodeJS.ReadWriteStream { + const variableValidator = getVariableNameValidator(); + let errorCount = 0; + const monacoWorkbenchPattern = /\.monaco-workbench/; + const restrictedPathPattern = /^src[\/\\]vs[\/\\](base|platform|editor)[\/\\]/; + const layerCheckerDisablePattern = /\/\*\s*stylelint-disable\s+layer-checker\s*\*\//; + + return es.through(function (this, file: FileWithLines) { + const lines = file.__lines || file.contents.toString('utf8').split(/\r\n|\r|\n/); + file.__lines = lines; + + const isRestrictedPath = restrictedPathPattern.test(file.relative); + + // Check if layer-checker is disabled for the entire file + const isLayerCheckerDisabled = lines.some(line => layerCheckerDisablePattern.test(line)); + + lines.forEach((line, i) => { + variableValidator(line, (unknownVariable: string) => { + reporter(file.relative + '(' + (i + 1) + ',1): Unknown variable: ' + unknownVariable, true); + errorCount++; + }); + + if (isRestrictedPath && !isLayerCheckerDisabled && monacoWorkbenchPattern.test(line)) { + reporter(file.relative + '(' + (i + 1) + ',1): The class .monaco-workbench cannot be used in files under src/vs/{base,platform,editor} because only src/vs/workbench applies it', true); + errorCount++; + } + }); + + this.emit('data', file); + }, function () { + if (errorCount > 0) { + reporter('All valid variable names are in `build/lib/stylelint/vscode-known-variables.json`\nTo update that file, run `./scripts/test-documentation.sh|bat.`', false); + } + this.emit('end'); + }); +} + +function stylelint(): NodeJS.ReadWriteStream { + return vfs + .src(Array.from(stylelintFilter), { base: '.', follow: true, allowEmpty: true }) + .pipe(gulpstylelint((message, isError) => { + if (isError) { + console.error(message); + } else { + console.info(message); + } + })) + .pipe(es.through(function () { /* noop, important for the stream to end */ })); +} + +if (import.meta.main) { + stylelint().on('error', (err: Error) => { + console.error(); + console.error(err); + process.exit(1); + }); +} diff --git a/build/tsconfig.build.json b/build/tsconfig.build.json deleted file mode 100644 index dc3305690bc..00000000000 --- a/build/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "allowJs": false, - "checkJs": false, - "noEmit": false, - "skipLibCheck": true - }, - "include": [ - "**/*.ts" - ] -} diff --git a/build/tsconfig.json b/build/tsconfig.json index ab72dda392a..383d5342c04 100644 --- a/build/tsconfig.json +++ b/build/tsconfig.json @@ -5,28 +5,22 @@ "ES2024" ], "module": "nodenext", - "alwaysStrict": true, - "removeComments": false, - "preserveConstEnums": true, - "sourceMap": true, + "noEmit": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, "resolveJsonModule": true, - // enable JavaScript type checking for the language service - // use the tsconfig.build.json for compiling which disable JavaScript - // type checking so that JavaScript file are not transpiled - "allowJs": true, + "skipLibCheck": true, "strict": true, "exactOptionalPropertyTypes": false, "useUnknownInCatchVariables": false, "noUnusedLocals": true, - "noUnusedParameters": true, - "newLine": "lf", - "noEmit": true + "noUnusedParameters": true }, - "include": [ - "**/*.ts", - "**/*.js" - ], "exclude": [ - "node_modules/**" + "node_modules/**", + "monaco-editor-playground/**", + "builtin/**", + "vite/**" ] } diff --git a/build/vite/index-workbench.ts b/build/vite/index-workbench.ts new file mode 100644 index 00000000000..e237f661f5d --- /dev/null +++ b/build/vite/index-workbench.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import '../../src/vs/code/browser/workbench/workbench'; +import './setup-dev'; + diff --git a/build/vite/index.html b/build/vite/index.html new file mode 100644 index 00000000000..c3a0e36b8ef --- /dev/null +++ b/build/vite/index.html @@ -0,0 +1,9 @@ + + + + +
+

Use the Playground Launch Config for a better dev experience

+ + + diff --git a/build/vite/index.ts b/build/vite/index.ts new file mode 100644 index 00000000000..b852612bc66 --- /dev/null +++ b/build/vite/index.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/* eslint-disable local/code-no-standalone-editor */ + +export * from '../../src/vs/editor/editor.main'; +import './style.css'; +import * as monaco from '../../src/vs/editor/editor.main'; + +globalThis.monaco = monaco; +const root = document.getElementById('sampleContent'); +if (root) { + const d = monaco.editor.createDiffEditor(root); + + d.setModel({ + modified: monaco.editor.createModel(`hello world`), + original: monaco.editor.createModel(`hello monaco`), + }); +} diff --git a/build/vite/package-lock.json b/build/vite/package-lock.json new file mode 100644 index 00000000000..97fb9dc76aa --- /dev/null +++ b/build/vite/package-lock.json @@ -0,0 +1,1054 @@ +{ + "name": "@vscode/sample-source", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vscode/sample-source", + "version": "0.0.0", + "devDependencies": { + "@vscode/rollup-plugin-esm-url": "^1.0.1-0", + "vite": "^7.1.11" + } + }, + "../lib": { + "name": "monaco-editor-core", + "version": "0.0.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "postcss-copy": "^7.1.0", + "postcss-copy-assets": "^0.3.1", + "rollup": "^4.35.0", + "rollup-plugin-esbuild": "^6.2.1", + "rollup-plugin-lib-style": "^2.3.2", + "rollup-plugin-postcss": "^4.0.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz", + "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz", + "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz", + "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz", + "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz", + "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz", + "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz", + "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz", + "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz", + "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz", + "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz", + "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz", + "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz", + "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz", + "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz", + "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz", + "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz", + "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz", + "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz", + "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz", + "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/rollup-plugin-esm-url": { + "version": "1.0.1-0", + "resolved": "https://registry.npmjs.org/@vscode/rollup-plugin-esm-url/-/rollup-plugin-esm-url-1.0.1-0.tgz", + "integrity": "sha512-5k9c2ZK6xTTa2MGa0uD+f/a1cZV8SCQm9ZhorQDyBRvobIzOTg57LjOl3di9Z6zawPh7nDgbWp6g+GnSSpdCrg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "rollup": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.49.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz", + "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.49.0", + "@rollup/rollup-android-arm64": "4.49.0", + "@rollup/rollup-darwin-arm64": "4.49.0", + "@rollup/rollup-darwin-x64": "4.49.0", + "@rollup/rollup-freebsd-arm64": "4.49.0", + "@rollup/rollup-freebsd-x64": "4.49.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", + "@rollup/rollup-linux-arm-musleabihf": "4.49.0", + "@rollup/rollup-linux-arm64-gnu": "4.49.0", + "@rollup/rollup-linux-arm64-musl": "4.49.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", + "@rollup/rollup-linux-ppc64-gnu": "4.49.0", + "@rollup/rollup-linux-riscv64-gnu": "4.49.0", + "@rollup/rollup-linux-riscv64-musl": "4.49.0", + "@rollup/rollup-linux-s390x-gnu": "4.49.0", + "@rollup/rollup-linux-x64-gnu": "4.49.0", + "@rollup/rollup-linux-x64-musl": "4.49.0", + "@rollup/rollup-win32-arm64-msvc": "4.49.0", + "@rollup/rollup-win32-ia32-msvc": "4.49.0", + "@rollup/rollup-win32-x64-msvc": "4.49.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.1.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", + "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/build/vite/package.json b/build/vite/package.json new file mode 100644 index 00000000000..f65f359145e --- /dev/null +++ b/build/vite/package.json @@ -0,0 +1,15 @@ +{ + "name": "@vscode/sample-source", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@vscode/rollup-plugin-esm-url": "^1.0.1-0", + "vite": "^7.1.11" + } +} diff --git a/build/vite/setup-dev.ts b/build/vite/setup-dev.ts new file mode 100644 index 00000000000..8f1f997599c --- /dev/null +++ b/build/vite/setup-dev.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +import { enableHotReload } from '../../src/vs/base/common/hotReload.ts'; +import { getSingletonServiceDescriptors, InstantiationType, registerSingleton } from '../../src/vs/platform/instantiation/common/extensions.ts'; +import { IWebWorkerService } from '../../src/vs/platform/webWorker/browser/webWorkerService.ts'; +// eslint-disable-next-line local/code-no-standalone-editor +import { StandaloneWebWorkerService } from '../../src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts'; + +enableHotReload(); + +registerSingleton(IWebWorkerService, StandaloneWebWorkerService, InstantiationType.Eager); +const descriptors = getSingletonServiceDescriptors(); + +// Patch push to ignore future IWebWorkerService registrations. +// This is hot-reload dev only, so it is fine. +const originalPush = descriptors.push; +descriptors.push = function (item: any) { + if (item[0] === IWebWorkerService) { + return this.length; + } + return originalPush.call(this, item); +}; + +globalThis._VSCODE_DISABLE_CSS_IMPORT_MAP = true; +globalThis._VSCODE_USE_RELATIVE_IMPORTS = true; diff --git a/build/vite/style.css b/build/vite/style.css new file mode 100644 index 00000000000..b9573061e51 --- /dev/null +++ b/build/vite/style.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +#sampleContent { + height: 400px; + border: 1px solid black; +} diff --git a/build/vite/tsconfig.json b/build/vite/tsconfig.json new file mode 100644 index 00000000000..c2f24a64098 --- /dev/null +++ b/build/vite/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "checkJs": true, + "module": "preserve", + "noEmit": true, + "strict": true, + "experimentalDecorators": true, + "types": ["vite/client"] + } +} diff --git a/build/vite/vite.config.ts b/build/vite/vite.config.ts new file mode 100644 index 00000000000..1ddb4f53adf --- /dev/null +++ b/build/vite/vite.config.ts @@ -0,0 +1,199 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createLogger, defineConfig, Plugin } from 'vite'; +import path, { join } from 'path'; +import { rollupEsmUrlPlugin } from '@vscode/rollup-plugin-esm-url'; +import { statSync } from 'fs'; +import { pathToFileURL } from 'url'; + +function injectBuiltinExtensionsPlugin(): Plugin { + let builtinExtensionsCache: unknown[] | null = null; + + function replaceAllOccurrences(str: string, search: string, replace: string): string { + return str.split(search).join(replace); + } + + async function loadBuiltinExtensions() { + if (!builtinExtensionsCache) { + builtinExtensionsCache = await getScannedBuiltinExtensions(path.resolve(__dirname, '../../')); + console.log(`Found ${builtinExtensionsCache!.length} built-in extensions.`); + } + return builtinExtensionsCache; + } + + function asJSON(value: unknown): string { + return escapeHtmlByReplacingCharacters(JSON.stringify(value)); + } + + function escapeHtmlByReplacingCharacters(str: string) { + if (typeof str !== 'string') { + return ''; + } + + const escapeCharacter = (match: string) => { + switch (match) { + case '&': return '&'; + case '<': return '<'; + case '>': return '>'; + case '"': return '"'; + case '\'': return '''; + case '`': return '`'; + default: return match; + } + }; + + return str.replace(/[&<>"'`]/g, escapeCharacter); + } + + const prebuiltExtensionsLocation = '.build/builtInExtensions'; + async function getScannedBuiltinExtensions(vsCodeDevLocation: string) { + // use the build utility as to not duplicate the code + const extensionsUtil = await import(pathToFileURL(path.join(vsCodeDevLocation, 'build', 'lib', 'extensions.ts')).toString()); + const localExtensions = extensionsUtil.scanBuiltinExtensions(path.join(vsCodeDevLocation, 'extensions')); + const prebuiltExtensions = extensionsUtil.scanBuiltinExtensions(path.join(vsCodeDevLocation, prebuiltExtensionsLocation)); + for (const ext of localExtensions) { + let browserMain = ext.packageJSON.browser; + if (browserMain) { + if (!browserMain.endsWith('.js')) { + browserMain = browserMain + '.js'; + } + const browserMainLocation = path.join(vsCodeDevLocation, 'extensions', ext.extensionPath, browserMain); + if (!fileExists(browserMainLocation)) { + console.log(`${browserMainLocation} not found. Make sure all extensions are compiled (use 'yarn watch-web').`); + } + } + } + return localExtensions.concat(prebuiltExtensions); + } + + function fileExists(path: string): boolean { + try { + return statSync(path).isFile(); + } catch (err) { + return false; + } + } + + return { + name: 'inject-builtin-extensions', + transformIndexHtml: { + order: 'pre', + async handler(html) { + const search = '{{WORKBENCH_BUILTIN_EXTENSIONS}}'; + if (html.indexOf(search) === -1) { + return html; + } + + const extensions = await loadBuiltinExtensions(); + const h = replaceAllOccurrences(html, search, asJSON(extensions)); + return h; + } + } + }; +} + +function createHotClassSupport(): Plugin { + return { + name: 'createHotClassSupport', + transform: { + order: 'pre', + handler: (code, id) => { + if (id.endsWith('.ts')) { + let needsHMRAccept = false; + const hasCreateHotClass = code.includes('createHotClass'); + const hasDomWidget = code.includes('DomWidget'); + + if (!hasCreateHotClass && !hasDomWidget) { + return undefined; + } + + if (hasCreateHotClass) { + needsHMRAccept = true; + } + + if (hasDomWidget) { + const matches = code.matchAll(/class\s+([a-zA-Z0-9_]+)\s+extends\s+DomWidget/g); + /// @ts-ignore + for (const match of matches) { + const className = match[1]; + code = code + `\n${className}.registerWidgetHotReplacement(${JSON.stringify(id + '#' + className)});`; + needsHMRAccept = true; + } + } + + if (needsHMRAccept) { + code = code + `\n +if (import.meta.hot) { + import.meta.hot.accept(); +}`; + } + return code; + } + return undefined; + }, + } + }; +} + +const logger = createLogger(); +const loggerWarn = logger.warn; + +logger.warn = (msg, options) => { + // amdX and the baseUrl code cannot be analyzed by vite. + // However, they are not needed, so it is okay to silence the warning. + if (msg.indexOf('vs/amdX.ts') !== -1) { + return; + } + if (msg.indexOf('await import(new URL(`vs/workbench/workbench.desktop.main.js`, baseUrl).href)') !== -1) { + return; + } + if (msg.indexOf('const result2 = await import(workbenchUrl);') !== -1) { + return; + } + + // See https://github.com/microsoft/vscode/issues/278153 + if (msg.indexOf('marked.esm.js.map') !== -1 || msg.indexOf('purify.es.mjs.map') !== -1) { + return; + } + + loggerWarn(msg, options); +}; + +export default defineConfig({ + plugins: [ + rollupEsmUrlPlugin({}), + injectBuiltinExtensionsPlugin(), + createHotClassSupport() + ], + customLogger: logger, + esbuild: { + tsconfigRaw: { + compilerOptions: { + experimentalDecorators: true, + } + } + }, + root: '../..', // To support /out/... paths + build: { + rollupOptions: { + input: { + //index: path.resolve(__dirname, 'index.html'), + workbench: path.resolve(__dirname, 'workbench-vite.html'), + } + } + }, + server: { + cors: true, + port: 5199, + origin: 'http://localhost:5199', + fs: { + allow: [ + // To allow loading from sources, not needed when loading monaco-editor from npm package + join(import.meta.dirname, '../../../') + ] + } + } +}); diff --git a/build/vite/workbench-electron.ts b/build/vite/workbench-electron.ts new file mode 100644 index 00000000000..c50d57d1c15 --- /dev/null +++ b/build/vite/workbench-electron.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import '../../src/vs/code/electron-browser/workbench/workbench'; +import './setup-dev'; + diff --git a/build/vite/workbench-vite-electron.html b/build/vite/workbench-vite-electron.html new file mode 100644 index 00000000000..87019c6c01a --- /dev/null +++ b/build/vite/workbench-vite-electron.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/build/vite/workbench-vite.html b/build/vite/workbench-vite.html new file mode 100644 index 00000000000..99ed4e75415 --- /dev/null +++ b/build/vite/workbench-vite.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/build/win32/Cargo.lock b/build/win32/Cargo.lock index e91718ee79a..d35c41e4098 100644 --- a/build/win32/Cargo.lock +++ b/build/win32/Cargo.lock @@ -129,7 +129,7 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "inno_updater" -version = "0.16.0" +version = "0.18.2" dependencies = [ "byteorder", "crc", @@ -546,4 +546,4 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags 2.9.1", -] \ No newline at end of file +] diff --git a/build/win32/Cargo.toml b/build/win32/Cargo.toml index 37d78fc177c..40e1a7a60fd 100644 --- a/build/win32/Cargo.toml +++ b/build/win32/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "inno_updater" -version = "0.16.0" +version = "0.18.2" authors = ["Microsoft "] build = "build.rs" diff --git a/build/win32/code.iss b/build/win32/code.iss index a67faad1726..101dc2d3548 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -67,22 +67,26 @@ Name: "hungarian"; MessagesFile: "{#RepoDir}\build\win32\i18n\Default.hu.isl,{#R Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl,{#RepoDir}\build\win32\i18n\messages.tr.isl" {#LocalizedLanguageFile("trk")} [InstallDelete] -Type: filesandordirs; Name: "{app}\resources\app\out"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\plugins"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\extensions"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules"; Check: IsNotBackgroundUpdate -Type: filesandordirs; Name: "{app}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate -Type: files; Name: "{app}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\out"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\plugins"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\extensions"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules"; Check: IsNotBackgroundUpdate +Type: filesandordirs; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar.unpacked"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\node_modules.asar"; Check: IsNotBackgroundUpdate +Type: files; Name: "{app}\{#VersionedResourcesFolder}\resources\app\Credits_45.0.2454.85.html"; Check: IsNotBackgroundUpdate [UninstallDelete] Type: filesandordirs; Name: "{app}\_" +Type: filesandordirs; Name: "{app}\bin" +Type: files; Name: "{app}\old_*" +Type: files; Name: "{app}\new_*" +Type: files; Name: "{app}\updating_version" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not (IsWindows11OrLater and QualityIsInsiders) +Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#NameShort}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#NameShort}}"; GroupDescription: "{cm:Other}" Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{cm:Other}"; Check: WizardSilent @@ -91,14 +95,18 @@ Name: "runcode"; Description: "{cm:RunAfter,{#NameShort}}"; GroupDescription: "{ Name: "{app}"; AfterInstall: DisableAppDirInheritance [Files] -Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\appx,\appx\*,\resources\app\product.json"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs -Source: "tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion -Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: ignoreversion +Source: "*"; Excludes: "\CodeSignSummary*.md,\tools,\tools\*,\policies,\policies\*,\appx,\appx\*,\resources\app\product.json,\{#ExeBasename}.exe,\{#ExeBasename}.VisualElementsManifest.xml,\bin,\bin\*"; DestDir: "{code:GetDestDir}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#ExeBasename}.exe"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetExeBasename}"; Flags: ignoreversion +Source: "{#ExeBasename}.VisualElementsManifest.xml"; DestDir: "{code:GetDestDir}"; DestName: "{code:GetVisualElementsManifest}"; Flags: ignoreversion +Source: "tools\*"; DestDir: "{app}\{#VersionedResourcesFolder}\tools"; Flags: ignoreversion +Source: "policies\*"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\policies"; Flags: ignoreversion skipifsourcedoesntexist +Source: "bin\{#TunnelApplicationName}.exe"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirTunnelApplicationFilename}"; Flags: ignoreversion skipifsourcedoesntexist +Source: "bin\{#ApplicationName}.cmd"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationCmdFilename}"; Flags: ignoreversion +Source: "bin\{#ApplicationName}"; DestDir: "{code:GetDestDir}\bin"; DestName: "{code:GetBinDirApplicationFilename}"; Flags: ignoreversion +Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\resources\app"; Flags: ignoreversion #ifdef AppxPackageName -#if "user" == InstallTarget -Source: "appx\{#AppxPackage}"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -Source: "appx\{#AppxPackageDll}"; DestDir: "{app}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -#endif +Source: "appx\{#AppxPackage}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; BeforeInstall: RemoveAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater +Source: "appx\{#AppxPackageDll}"; DestDir: "{code:GetDestDir}\{#VersionedResourcesFolder}\appx"; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater #endif [Icons] @@ -121,7 +129,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -129,7 +137,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -137,7 +145,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -145,7 +153,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -153,7 +161,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -161,7 +169,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWith Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -169,7 +177,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWit Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -177,7 +185,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -185,7 +193,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -193,7 +201,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\bower.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\bower.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -201,14 +209,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -216,7 +224,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -224,7 +232,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -232,7 +240,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -240,7 +248,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -248,7 +256,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -256,7 +264,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -264,7 +272,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -272,7 +280,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -280,7 +288,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenW Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -288,7 +296,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -296,7 +304,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -304,7 +312,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -312,14 +320,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -327,7 +335,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -335,7 +343,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -343,7 +351,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -351,7 +359,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\css.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\css.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -359,7 +367,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -367,7 +375,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\csharp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -375,7 +383,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -383,7 +391,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -391,7 +399,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -399,7 +407,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -407,7 +415,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -415,7 +423,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -423,7 +431,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -431,7 +439,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWit Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -439,7 +447,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -447,7 +455,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -455,7 +463,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -463,7 +471,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -471,7 +479,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -479,7 +487,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -487,7 +495,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -495,7 +503,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -503,7 +511,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -512,7 +520,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitattributes\OpenWi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -521,7 +529,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitconfig\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -530,7 +538,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gitignore\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -538,7 +546,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\go.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\go.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -546,7 +554,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -554,7 +562,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -562,7 +570,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\c.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -570,7 +578,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -578,7 +586,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -586,14 +594,14 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -601,7 +609,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -609,7 +617,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -617,7 +625,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -625,7 +633,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\cpp.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -633,7 +641,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -641,7 +649,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -649,7 +657,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\jade.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\jade.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -657,7 +665,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -665,7 +673,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\java.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -673,7 +681,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -681,7 +689,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -689,7 +697,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -697,7 +705,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -705,7 +713,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -713,7 +721,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\json.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\json.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -721,7 +729,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -729,7 +737,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\less.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\less.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -737,7 +745,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -745,7 +753,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -753,7 +761,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -761,7 +769,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -769,7 +777,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -777,7 +785,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -785,7 +793,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -793,7 +801,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -801,7 +809,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -809,7 +817,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -817,7 +825,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -825,7 +833,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -833,7 +841,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -841,7 +849,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\markdown.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -849,7 +857,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -857,7 +865,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -865,7 +873,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\javascript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -874,7 +882,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.npmignore\OpenWithPr Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -882,7 +890,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\php.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\php.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -890,7 +898,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -898,7 +906,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -906,7 +914,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -914,7 +922,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -922,7 +930,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -930,7 +938,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -938,7 +946,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -946,7 +954,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -954,7 +962,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProg Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -962,7 +970,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithP Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -970,7 +978,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -978,7 +986,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -986,7 +994,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -994,7 +1002,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\powershell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1002,7 +1010,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1010,7 +1018,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\python.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1018,7 +1026,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1026,7 +1034,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\ruby.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1034,7 +1042,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1042,7 +1050,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithPro Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1050,7 +1058,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1058,7 +1066,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1066,7 +1074,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1074,7 +1082,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1082,7 +1090,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sass.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1090,7 +1098,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1098,7 +1106,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1106,7 +1114,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\sql.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\sql.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1114,7 +1122,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1122,7 +1130,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1130,7 +1138,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1138,7 +1146,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\typescript.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\typescript.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1146,7 +1154,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1154,7 +1162,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\react.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1162,7 +1170,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1170,7 +1178,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1178,7 +1186,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\vue.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\vue.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1186,7 +1194,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1194,7 +1202,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1202,7 +1210,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1210,7 +1218,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1218,7 +1226,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgid Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\html.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1226,7 +1234,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\xml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1234,7 +1242,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1242,7 +1250,7 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\yaml.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles @@ -1250,33 +1258,33 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\shell.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#NameLong}}"; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico" +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#VersionedResourcesFolder}\resources\app\resources\win32\default.ico" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#ExeBasename}.exe""" Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Applications\{#ExeBasename}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1""" -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater and QualityIsInsiders -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) -Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not (IsWindows11OrLater and QualityIsInsiders) +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithCodeContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#ExeBasename}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater ; Environment #if "user" == InstallTarget @@ -1304,6 +1312,11 @@ begin Result := not IsBackgroundUpdate(); end; +function IsVersionedUpdate(): Boolean; +begin + Result := '{#VersionedResourcesFolder}' <> ''; +end; + // Don't allow installing conflicting architectures function InitializeSetup(): Boolean; var @@ -1357,7 +1370,7 @@ var TaskKilled: Integer; begin Log('Stopping all tunnel services (at ' + ExpandConstant('"{app}\bin\{#TunnelApplicationName}.exe"') + ')'); - ShellExec('', 'powershell.exe', '-Command "Get-WmiObject Win32_Process | Where-Object { $_.ExecutablePath -eq ' + ExpandConstant('''{app}\bin\{#TunnelApplicationName}.exe''') + ' } | Select @{Name=''Id''; Expression={$_.ProcessId}} | Stop-Process -Force"', '', SW_HIDE, ewWaitUntilTerminated, TaskKilled) + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command "Get-WmiObject Win32_Process | Where-Object { $_.ExecutablePath -eq ' + ExpandConstant('''{app}\bin\{#TunnelApplicationName}.exe''') + ' } | Select @{Name=''Id''; Expression={$_.ProcessId}} | Stop-Process -Force"', '', SW_HIDE, ewWaitUntilTerminated, TaskKilled) WaitCounter := 10; while (WaitCounter > 0) and CheckForMutexes('{#TunnelMutex}') do @@ -1424,6 +1437,13 @@ begin Result := FileExists(ExpandConstant('{param:update}')) end; +// Check if VS Code created a session-end flag file to indicate OS is shutting down +// This prevents calling inno_updater.exe during system shutdown +function SessionEndFileExists(): Boolean; +begin + Result := FileExists(ExpandConstant('{param:sessionend}')) +end; + function ShouldRunAfterUpdate(): Boolean; begin if IsBackgroundUpdate() then @@ -1447,12 +1467,52 @@ end; function GetDestDir(Value: string): string; begin - if IsBackgroundUpdate() then + if IsBackgroundUpdate() and not IsVersionedUpdate() then Result := ExpandConstant('{app}\_') else Result := ExpandConstant('{app}'); end; +function GetVisualElementsManifest(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#ExeBasename}.VisualElementsManifest.xml') + else + Result := ExpandConstant('{#ExeBasename}.VisualElementsManifest.xml'); +end; + +function GetExeBasename(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#ExeBasename}.exe') + else + Result := ExpandConstant('{#ExeBasename}.exe'); +end; + +function GetBinDirTunnelApplicationFilename(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#TunnelApplicationName}.exe') + else + Result := ExpandConstant('{#TunnelApplicationName}.exe'); +end; + +function GetBinDirApplicationFilename(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#ApplicationName}') + else + Result := ExpandConstant('{#ApplicationName}'); +end; + +function GetBinDirApplicationCmdFilename(Value: string): string; +begin + if IsBackgroundUpdate() and IsVersionedUpdate() then + Result := ExpandConstant('new_{#ApplicationName}.cmd') + else + Result := ExpandConstant('{#ApplicationName}.cmd'); +end; + function BoolToStr(Value: Boolean): String; begin if Value then @@ -1499,9 +1559,13 @@ procedure AddAppxPackage(); var AddAppxPackageResultCode: Integer; begin - if not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin + if not SessionEndFileExists() and not AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), AddAppxPackageResultCode) then begin Log('Installing appx ' + AppxPackageFullname + ' ...'); - ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); +#if "user" == InstallTarget + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); +#else + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Add-AppxPackage -Stage ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''' -ExternalLocation ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx') + '''; Add-AppxProvisionedPackage -Online -SkipLicense -PackagePath ''' + ExpandConstant('{app}\{#VersionedResourcesFolder}\appx\{#AppxPackage}') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); +#endif Log('Add-AppxPackage complete.'); end; end; @@ -1511,16 +1575,20 @@ var RemoveAppxPackageResultCode: Integer; begin // Remove the old context menu package - // Following condition can be removed after two versions. - if QualityIsInsiders() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin + // Following condition can be removed in v1.111. + if QualityIsInsiders() and not SessionEndFileExists() and AppxPackageInstalled('Microsoft.VSCodeInsiders', RemoveAppxPackageResultCode) then begin Log('Deleting old appx ' + AppxPackageFullname + ' installation...'); ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_{#Arch}.appx')); DeleteFile(ExpandConstant('{app}\appx\code_insiders_explorer_command.dll')); end; - if AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin + if not SessionEndFileExists() and AppxPackageInstalled(ExpandConstant('{#AppxPackageName}'), RemoveAppxPackageResultCode) then begin Log('Removing current ' + AppxPackageFullname + ' appx installation...'); +#if "user" == InstallTarget ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('Remove-AppxPackage -Package ''' + AppxPackageFullname + ''''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); +#else + ShellExec('', 'powershell.exe', '-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command ' + AddQuotes('$packages = Get-AppxPackage ''' + ExpandConstant('{#AppxPackageName}') + '''; foreach ($package in $packages) { Remove-AppxProvisionedPackage -PackageName $package.PackageFullName -Online }; foreach ($package in $packages) { Remove-AppxPackage -Package $package.PackageFullName -AllUsers }'), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); +#endif Log('Remove-AppxPackage for current appx installation complete.'); end; end; @@ -1534,8 +1602,8 @@ begin if CurStep = ssPostInstall then begin #ifdef AppxPackageName - // Remove the old context menu registry keys for insiders - if QualityIsInsiders() and WizardIsTaskSelected('addcontextmenufiles') then begin + // Remove the old context menu registry keys + if IsWindows11OrLater() and WizardIsTaskSelected('addcontextmenufiles') then begin RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\*\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\shell\{#RegValueName}'); RegDeleteKeyIncludingSubkeys({#EnvironmentRootKey}, 'Software\Classes\directory\background\shell\{#RegValueName}'); @@ -1545,6 +1613,7 @@ begin if IsBackgroundUpdate() then begin + SaveStringToFile(ExpandConstant('{app}\updating_version'), '{#Commit}', False); CreateMutex('{#AppMutex}-ready'); Log('Checking whether application is still running...'); @@ -1554,9 +1623,28 @@ begin end; Log('Application appears not to be running.'); - StopTunnelServiceIfNeeded(); - - Exec(ExpandConstant('{app}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + if not SessionEndFileExists() then begin + StopTunnelServiceIfNeeded(); + Log('Invoking inno_updater for background update'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"{app}\{#ExeBasename}.exe" ' + BoolToStr(LockFileExists()) + ' "{cm:UpdatingVisualStudioCode}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + DeleteFile(ExpandConstant('{app}\updating_version')); + Log('inno_updater completed successfully'); + #if "system" == InstallTarget + if IsVersionedUpdate() then begin + Log('Invoking inno_updater to remove previous installation folder'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Log('inno_updater completed gc successfully'); + end; + #endif + end else begin + Log('Skipping inno_updater.exe call because OS session is ending'); + end; + end else begin + if IsVersionedUpdate() then begin + Log('Invoking inno_updater to remove previous installation folder'); + Exec(ExpandConstant('{app}\{#VersionedResourcesFolder}\tools\inno_updater.exe'), ExpandConstant('"--gc" "{app}\{#ExeBasename}.exe" "{#VersionedResourcesFolder}"'), '', SW_SHOW, ewWaitUntilTerminated, UpdateResultCode); + Log('inno_updater completed gc successfully'); + end; end; if ShouldRestartTunnelService then @@ -1626,9 +1714,7 @@ begin exit; end; #ifdef AppxPackageName - #if "user" == InstallTarget - RemoveAppxPackage(); - #endif + RemoveAppxPackage(); #endif if not RegQueryStringValue({#EnvironmentRootKey}, '{#EnvironmentKey}', 'Path', Path) then begin diff --git a/build/win32/explorer-dll-fetcher.js b/build/win32/explorer-dll-fetcher.js deleted file mode 100644 index 1b160974324..00000000000 --- a/build/win32/explorer-dll-fetcher.js +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.downloadExplorerDll = downloadExplorerDll; -const fs_1 = __importDefault(require("fs")); -const debug_1 = __importDefault(require("debug")); -const path_1 = __importDefault(require("path")); -const get_1 = require("@electron/get"); -const product_json_1 = __importDefault(require("../../product.json")); -const product = product_json_1.default; -const d = (0, debug_1.default)('explorer-dll-fetcher'); -async function downloadExplorerDll(outDir, quality = 'stable', targetArch = 'x64') { - const fileNamePrefix = quality === 'insider' ? 'code_insider' : 'code'; - const fileName = `${fileNamePrefix}_explorer_command_${targetArch}.dll`; - if (!await fs_1.default.existsSync(outDir)) { - await fs_1.default.mkdirSync(outDir, { recursive: true }); - } - // Read and parse checksums file - const checksumsFilePath = path_1.default.join(path_1.default.dirname(__dirname), 'checksums', 'explorer-dll.txt'); - const checksumsContent = fs_1.default.readFileSync(checksumsFilePath, 'utf8'); - const checksums = {}; - checksumsContent.split('\n').forEach(line => { - const trimmedLine = line.trim(); - if (trimmedLine) { - const [checksum, filename] = trimmedLine.split(/\s+/); - if (checksum && filename) { - checksums[filename] = checksum; - } - } - }); - d(`downloading ${fileName}`); - const artifact = await (0, get_1.downloadArtifact)({ - isGeneric: true, - version: 'v4.0.0-350164', - artifactName: fileName, - checksums, - mirrorOptions: { - mirror: 'https://github.com/microsoft/vscode-explorer-command/releases/download/', - customDir: 'v4.0.0-350164', - customFilename: fileName - } - }); - d(`moving ${artifact} to ${outDir}`); - await fs_1.default.copyFileSync(artifact, path_1.default.join(outDir, fileName)); -} -async function main(outputDir) { - const arch = process.env['VSCODE_ARCH']; - if (!outputDir) { - throw new Error('Required build env not set'); - } - await downloadExplorerDll(outputDir, product.quality, arch); -} -if (require.main === module) { - main(process.argv[2]).catch(err => { - console.error(err); - process.exit(1); - }); -} -//# sourceMappingURL=explorer-dll-fetcher.js.map \ No newline at end of file diff --git a/build/win32/explorer-dll-fetcher.ts b/build/win32/explorer-dll-fetcher.ts index 33e21b4e4a8..33603ef9517 100644 --- a/build/win32/explorer-dll-fetcher.ts +++ b/build/win32/explorer-dll-fetcher.ts @@ -2,14 +2,11 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - -'use strict'; - import fs from 'fs'; import debug from 'debug'; import path from 'path'; import { downloadArtifact } from '@electron/get'; -import productJson from '../../product.json'; +import productJson from '../../product.json' with { type: 'json' }; interface ProductConfiguration { quality?: string; @@ -29,7 +26,7 @@ export async function downloadExplorerDll(outDir: string, quality: string = 'sta } // Read and parse checksums file - const checksumsFilePath = path.join(path.dirname(__dirname), 'checksums', 'explorer-dll.txt'); + const checksumsFilePath = path.join(path.dirname(import.meta.dirname), 'checksums', 'explorer-dll.txt'); const checksumsContent = fs.readFileSync(checksumsFilePath, 'utf8'); const checksums: Record = {}; @@ -46,12 +43,12 @@ export async function downloadExplorerDll(outDir: string, quality: string = 'sta d(`downloading ${fileName}`); const artifact = await downloadArtifact({ isGeneric: true, - version: 'v4.0.0-350164', + version: 'v8.0.0-398351', artifactName: fileName, checksums, mirrorOptions: { mirror: 'https://github.com/microsoft/vscode-explorer-command/releases/download/', - customDir: 'v4.0.0-350164', + customDir: 'v8.0.0-398351', customFilename: fileName } }); @@ -70,7 +67,7 @@ async function main(outputDir?: string): Promise { await downloadExplorerDll(outputDir, product.quality, arch); } -if (require.main === module) { +if (import.meta.main) { main(process.argv[2]).catch(err => { console.error(err); process.exit(1); diff --git a/build/win32/inno_updater.exe b/build/win32/inno_updater.exe index 14ae7b2dd63..c3c4a0cd2bc 100644 Binary files a/build/win32/inno_updater.exe and b/build/win32/inno_updater.exe differ diff --git a/cglicenses.json b/cglicenses.json index 93e5297c9a8..8ee75c0fb34 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -70,7 +70,7 @@ }, { // Reason: The license cannot be found by the tool due to access controls on the repository - "name": "tas-client-umd", + "name": "tas-client", "fullLicenseText": [ "MIT License", "Copyright (c) 2020 - present Microsoft Corporation", diff --git a/cgmanifest.json b/cgmanifest.json index eba1487acbb..967a9abcebe 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "54008792bf952b599e1a7416663711f6a07c8ce3" + "commitHash": "9722f06a26ed9376ee395a4de8c472268d88a4fb" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "138.0.7204.251" + "version": "142.0.7444.265" }, { "component": { @@ -516,12 +516,12 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "caa20e28dc1f21a97f7b2a7134973fd6435b65f0", - "tag": "22.20.0" + "commitHash": "6ac4ab19ad02803f03b54501193397563e99988e", + "tag": "22.21.1" } }, "isOnlyProductionDependency": true, - "version": "22.20.0" + "version": "22.21.1" }, { "component": { @@ -529,13 +529,13 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "dfc60f0f4246a542a45601f93572eca77bdff2f9", - "tag": "37.7.0" + "commitHash": "9453e8bfe10fcec0440515dbc36ffe5941256d44", + "tag": "39.3.0" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "37.7.0" + "version": "39.3.0" }, { "component": { diff --git a/cli/ThirdPartyNotices.txt b/cli/ThirdPartyNotices.txt index 5ca10211210..07427e9894c 100644 --- a/cli/ThirdPartyNotices.txt +++ b/cli/ThirdPartyNotices.txt @@ -944,7 +944,7 @@ chrono 0.4.38 - MIT OR Apache-2.0 https://github.com/chronotope/chrono Rust-chrono is dual-licensed under The MIT License [1] and -Apache 2.0 License [2]. Copyright (c) 2014--2025, Kang Seonghoon and +Apache 2.0 License [2]. Copyright (c) 2014--2026, Kang Seonghoon and contributors. Nota Bene: This is same as the Rust Project's own license. @@ -2239,7 +2239,7 @@ DEALINGS IN THE SOFTWARE. flate2 1.0.30 - MIT OR Apache-2.0 https://github.com/rust-lang/flate2-rs -Copyright (c) 2014-2025 Alex Crichton +Copyright (c) 2014-2026 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -2952,7 +2952,7 @@ getrandom 0.1.16 - MIT OR Apache-2.0 getrandom 0.2.15 - MIT OR Apache-2.0 https://github.com/rust-random/getrandom -Copyright (c) 2018-2025 The rust-random Project Developers +Copyright (c) 2018-2026 The rust-random Project Developers Copyright (c) 2014 The Rust Project Developers Permission is hereby granted, free of charge, to any @@ -3332,7 +3332,7 @@ DEALINGS IN THE SOFTWARE. httparse 1.8.0 - MIT/Apache-2.0 https://github.com/seanmonstar/httparse -Copyright (c) 2015-2025 Sean McArthur +Copyright (c) 2015-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -3386,7 +3386,7 @@ https://github.com/hyperium/hyper The MIT License (MIT) -Copyright (c) 2014-2025 Sean McArthur +Copyright (c) 2014-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -4422,7 +4422,7 @@ DEALINGS IN THE SOFTWARE. --------------------------------------------------------- keyring 2.3.3 - MIT OR Apache-2.0 -https://github.com/hwchen/keyring-rs +https://github.com/open-source-cooperative/keyring-rs Copyright (c) 2016 keyring Developers @@ -5259,7 +5259,7 @@ DEALINGS IN THE SOFTWARE. num_cpus 1.16.0 - MIT OR Apache-2.0 https://github.com/seanmonstar/num_cpus -Copyright (c) 2015-2025 Sean McArthur +Copyright (c) 2015-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -5451,7 +5451,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --------------------------------------------------------- openssl-probe 0.1.5 - MIT/Apache-2.0 -https://github.com/alexcrichton/openssl-probe +https://github.com/rustls/openssl-probe Copyright (c) 2014 Alex Crichton @@ -7227,10 +7227,9 @@ DEALINGS IN THE SOFTWARE. rand_core 0.5.1 - MIT OR Apache-2.0 rand_core 0.6.4 - MIT OR Apache-2.0 -https://github.com/rust-random/rand +https://github.com/rust-random/rand_core -Copyright 2018 Developers of the Rand project -Copyright (c) 2014 The Rust Project Developers +Copyright (c) 2018-2026 The Rand Project Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -7449,7 +7448,7 @@ DEALINGS IN THE SOFTWARE. reqwest 0.11.27 - MIT OR Apache-2.0 https://github.com/seanmonstar/reqwest -Copyright (c) 2016-2025 Sean McArthur +Copyright (c) 2016-2026 Sean McArthur Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -8591,7 +8590,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) -[`ascon‑hash`]: ./ascon-hash +[`ascon‑hash256`]: ./ascon-hash256 [`bash‑hash`]: ./bash-hash [`belt‑hash`]: ./belt-hash [`blake2`]: ./blake2 @@ -8687,7 +8686,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (crates) -[`ascon‑hash`]: ./ascon-hash +[`ascon‑hash256`]: ./ascon-hash256 [`bash‑hash`]: ./bash-hash [`belt‑hash`]: ./belt-hash [`blake2`]: ./blake2 @@ -9963,7 +9962,7 @@ https://github.com/seanmonstar/try-lock The MIT License (MIT) -Copyright (c) 2018-2025 Sean McArthur +Copyright (c) 2018-2026 Sean McArthur Copyright (c) 2016 Alex Crichton Permission is hereby granted, free of charge, to any person obtaining a copy @@ -11516,33 +11515,7 @@ ICU 1.8.1 to ICU 57.1 © 1995-2016 International Business Machines Corporation a zbus 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- @@ -11550,39 +11523,13 @@ DEALINGS IN THE SOFTWARE. zbus_macros 3.15.2 - MIT https://github.com/z-galaxy/zbus/ -The MIT License (MIT) - -Copyright (c) 2024 Zeeshan Ali Khan & zbus contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. +LICENSE-MIT --------------------------------------------------------- --------------------------------------------------------- zbus_names 2.6.1 - MIT -https://github.com/dbus2/zbus/ +https://github.com/z-galaxy/zbus/ LICENSE-MIT --------------------------------------------------------- @@ -11878,7 +11825,7 @@ licences; see files named LICENSE.*.txt for details. --------------------------------------------------------- zvariant 3.15.2 - MIT -https://github.com/dbus2/zbus/ +https://github.com/z-galaxy/zbus/ LICENSE-MIT --------------------------------------------------------- @@ -11886,7 +11833,7 @@ LICENSE-MIT --------------------------------------------------------- zvariant_derive 3.15.2 - MIT -https://github.com/dbus2/zbus/ +https://github.com/z-galaxy/zbus/ LICENSE-MIT --------------------------------------------------------- @@ -11894,7 +11841,7 @@ LICENSE-MIT --------------------------------------------------------- zvariant_utils 1.0.1 - MIT -https://github.com/dbus2/zbus/ +https://github.com/z-galaxy/zbus/ LICENSE-MIT --------------------------------------------------------- \ No newline at end of file diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index 52c5af6d7d4..6301bdd3104 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -686,6 +686,10 @@ pub struct BaseServerArgs { /// Set the root path for extensions. #[clap(long)] pub extensions_dir: Option, + + /// Reconnection grace time in seconds. Defaults to 10800 (3 hours). + #[clap(long)] + pub reconnection_grace_time: Option, } impl BaseServerArgs { @@ -700,6 +704,10 @@ impl BaseServerArgs { if let Some(d) = &self.extensions_dir { csa.extensions_dir = Some(d.clone()); } + + if let Some(t) = self.reconnection_grace_time { + csa.reconnection_grace_time = Some(t); + } } } diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index cf00bc42835..bbabadcf90a 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -74,6 +74,8 @@ pub struct CodeServerArgs { pub connection_token: Option, pub connection_token_file: Option, pub without_connection_token: bool, + // reconnection + pub reconnection_grace_time: Option, } impl CodeServerArgs { @@ -120,6 +122,9 @@ impl CodeServerArgs { if let Some(i) = self.log { args.push(format!("--log={i}")); } + if let Some(t) = self.reconnection_grace_time { + args.push(format!("--reconnection-grace-time={t}")); + } for extension in &self.install_extensions { args.push(format!("--install-extension={extension}")); diff --git a/cli/src/update_service.rs b/cli/src/update_service.rs index 90339148188..55f1dadccdf 100644 --- a/cli/src/update_service.rs +++ b/cli/src/update_service.rs @@ -56,8 +56,15 @@ fn quality_download_segment(quality: options::Quality) -> &'static str { } } -fn get_update_endpoint() -> Result<&'static str, CodeError> { - VSCODE_CLI_UPDATE_ENDPOINT.ok_or_else(|| CodeError::UpdatesNotConfigured("no service url")) +fn get_update_endpoint() -> Result { + if let Ok(url) = std::env::var("VSCODE_CLI_UPDATE_URL") { + if !url.is_empty() { + return Ok(url); + } + } + VSCODE_CLI_UPDATE_ENDPOINT + .map(|s| s.to_string()) + .ok_or_else(|| CodeError::UpdatesNotConfigured("no service url")) } impl UpdateService { @@ -78,7 +85,7 @@ impl UpdateService { .ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?; let download_url = format!( "{}/api/versions/{}/{}/{}", - update_endpoint, + &update_endpoint, version, download_segment, quality_download_segment(quality), @@ -119,7 +126,7 @@ impl UpdateService { .ok_or_else(|| CodeError::UnsupportedPlatform(platform.to_string()))?; let download_url = format!( "{}/api/latest/{}/{}", - update_endpoint, + &update_endpoint, download_segment, quality_download_segment(quality), ); @@ -156,7 +163,7 @@ impl UpdateService { let download_url = format!( "{}/commit:{}/{}/{}", - update_endpoint, + &update_endpoint, release.commit, download_segment, quality_download_segment(release.quality), diff --git a/eslint.config.js b/eslint.config.js index 3bbf69daa38..86b3c700196 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,7 +8,7 @@ import path from 'path'; import tseslint from 'typescript-eslint'; import stylisticTs from '@stylistic/eslint-plugin-ts'; -import * as pluginLocal from './.eslint-plugin-local/index.js'; +import * as pluginLocal from './.eslint-plugin-local/index.ts'; import pluginJsdoc from 'eslint-plugin-jsdoc'; import pluginHeader from 'eslint-plugin-header'; @@ -75,7 +75,7 @@ export default tseslint.config( 'context' ], // non-complete list of globals that are easy to access unintentionally 'no-var': 'warn', - 'semi': 'off', + 'semi': 'warn', 'local/code-translation-remind': 'warn', 'local/code-no-native-private': 'warn', 'local/code-parameter-properties-must-have-explicit-accessibility': 'warn', @@ -89,7 +89,9 @@ export default tseslint.config( '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-localized-model-description': 'warn', 'local/code-policy-localization-key-match': 'warn', + 'local/code-no-localization-template-literals': 'error', 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-layering': [ 'warn', @@ -131,7 +133,7 @@ export default tseslint.config( // TS { files: [ - '**/*.ts', + '**/*.{ts,tsx,mts,cts}', ], languageOptions: { parser: tseslint.parser, @@ -143,6 +145,8 @@ export default tseslint.config( 'jsdoc': pluginJsdoc, }, rules: { + // Disable built-in semi rules in favor of stylistic + 'semi': 'off', '@stylistic/ts/semi': 'warn', '@stylistic/ts/member-delimiter-style': 'warn', 'local/code-no-unused-expressions': [ @@ -181,7 +185,8 @@ export default tseslint.config( // Disallow 'in' operator except in type predicates { files: [ - '**/*.ts' + '**/*.ts', + '.eslint-plugin-local/**/*.ts', // Explicitly include files under dot directories ], ignores: [ 'src/bootstrap-node.ts', @@ -190,13 +195,7 @@ export default tseslint.config( 'extensions/debug-auto-launch/src/extension.ts', 'extensions/emmet/src/updateImageSize.ts', 'extensions/emmet/src/util.ts', - 'extensions/git/src/blame.ts', - 'extensions/github/src/links.ts', 'extensions/github-authentication/src/node/fetch.ts', - 'extensions/ipynb/src/deserializers.ts', - 'extensions/ipynb/src/notebookImagePaste.ts', - 'extensions/ipynb/src/serializers.ts', - 'extensions/notebook-renderers/src/index.ts', 'extensions/terminal-suggest/src/fig/figInterface.ts', 'extensions/terminal-suggest/src/fig/fig-autocomplete-shared/mixins.ts', 'extensions/terminal-suggest/src/fig/fig-autocomplete-shared/specMetadata.ts', @@ -210,7 +209,6 @@ export default tseslint.config( 'src/vs/base/browser/dom.ts', 'src/vs/base/browser/markdownRenderer.ts', 'src/vs/base/browser/touch.ts', - 'src/vs/base/browser/webWorkerFactory.ts', 'src/vs/base/common/async.ts', 'src/vs/base/common/desktopEnvironmentInfo.ts', 'src/vs/base/common/objects.ts', @@ -232,28 +230,18 @@ export default tseslint.config( 'src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts', 'src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts', 'src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts', - 'src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts', 'src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts', 'src/vs/platform/configuration/common/configuration.ts', 'src/vs/platform/configuration/common/configurationModels.ts', 'src/vs/platform/contextkey/browser/contextKeyService.ts', 'src/vs/platform/contextkey/test/common/scanner.test.ts', 'src/vs/platform/dataChannel/browser/forwardingTelemetryService.ts', - 'src/vs/platform/externalTerminal/node/externalTerminalService.ts', 'src/vs/platform/hover/browser/hoverService.ts', 'src/vs/platform/hover/browser/hoverWidget.ts', 'src/vs/platform/instantiation/common/instantiationService.ts', 'src/vs/platform/mcp/common/mcpManagementCli.ts', - 'src/vs/platform/terminal/common/capabilities/commandDetection/terminalCommand.ts', - 'src/vs/platform/terminal/common/capabilities/commandDetectionCapability.ts', - 'src/vs/platform/terminal/common/terminalProfiles.ts', - 'src/vs/platform/terminal/node/ptyService.ts', - 'src/vs/platform/terminal/node/terminalProcess.ts', - 'src/vs/platform/terminal/node/terminalProfiles.ts', - 'src/vs/platform/terminal/test/node/terminalEnvironment.test.ts', 'src/vs/workbench/api/browser/mainThreadChatSessions.ts', 'src/vs/workbench/api/browser/mainThreadDebugService.ts', - 'src/vs/workbench/api/browser/mainThreadTerminalService.ts', 'src/vs/workbench/api/browser/mainThreadTesting.ts', 'src/vs/workbench/api/common/extHost.api.impl.ts', 'src/vs/workbench/api/common/extHostChatAgents2.ts', @@ -262,8 +250,6 @@ export default tseslint.config( 'src/vs/workbench/api/common/extHostNotebookKernels.ts', 'src/vs/workbench/api/common/extHostQuickOpen.ts', 'src/vs/workbench/api/common/extHostRequireInterceptor.ts', - 'src/vs/workbench/api/common/extHostTelemetry.ts', - 'src/vs/workbench/api/common/extHostTerminalService.ts', 'src/vs/workbench/api/common/extHostTypeConverters.ts', 'src/vs/workbench/api/common/extHostTypes.ts', 'src/vs/workbench/api/node/loopbackServer.ts', @@ -276,41 +262,22 @@ export default tseslint.config( 'src/vs/workbench/browser/workbench.ts', 'src/vs/workbench/common/notifications.ts', 'src/vs/workbench/contrib/accessibility/browser/accessibleView.ts', - 'src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts', - 'src/vs/workbench/contrib/chat/browser/chat.ts', - 'src/vs/workbench/contrib/chat/browser/chatAttachmentResolveService.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatElicitationContentPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatTreeContentPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts', + 'src/vs/workbench/contrib/chat/browser/attachments/chatAttachmentResolveService.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatAttachmentsContentPart.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatConfirmationWidget.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatElicitationContentPart.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatReferencesContentPart.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatTreeContentPart.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/abstractToolConfirmationSubPart.ts', 'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts', 'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSessionStorage.ts', - 'src/vs/workbench/contrib/chat/browser/chatEditorInput.ts', - 'src/vs/workbench/contrib/chat/browser/chatFollowups.ts', - 'src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts', - 'src/vs/workbench/contrib/chat/browser/chatInputPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatListRenderer.ts', - 'src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/common.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/localChatSessionsProvider.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts', - 'src/vs/workbench/contrib/chat/browser/chatWidget.ts', - 'src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts', - 'src/vs/workbench/contrib/chat/common/annotations.ts', - 'src/vs/workbench/contrib/chat/common/chat.ts', - 'src/vs/workbench/contrib/chat/common/chatAgents.ts', - 'src/vs/workbench/contrib/chat/common/chatModel.ts', - 'src/vs/workbench/contrib/chat/common/chatService.ts', - 'src/vs/workbench/contrib/chat/common/chatServiceImpl.ts', - 'src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts', - 'src/vs/workbench/contrib/chat/test/common/chatModel.test.ts', + 'src/vs/workbench/contrib/chat/browser/widget/chatContentParts/chatInlineAnchorWidget.ts', + 'src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts', + 'src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts', + 'src/vs/workbench/contrib/chat/common/model/chatModel.ts', 'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts', 'src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts', - 'src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts', - 'src/vs/workbench/contrib/debug/browser/breakpointsView.ts', + 'src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts', 'src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts', 'src/vs/workbench/contrib/debug/browser/variablesView.ts', 'src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts', @@ -332,62 +299,19 @@ export default tseslint.config( 'src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts', 'src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts', 'src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts', - 'src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookBreakpoints.ts', - 'src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts', 'src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts', 'src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts', 'src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts', - 'src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts', - 'src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts', 'src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts', 'src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts', - 'src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts', - 'src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts', 'src/vs/workbench/contrib/output/browser/outputView.ts', 'src/vs/workbench/contrib/preferences/browser/settingsTree.ts', 'src/vs/workbench/contrib/remoteTunnel/electron-browser/remoteTunnel.contribution.ts', 'src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts', 'src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts', 'src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts', - 'src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts', - 'src/vs/workbench/contrib/terminal/browser/remotePty.ts', - 'src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalActions.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalGroup.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalIcon.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalInstance.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalMenus.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalView.ts', - 'src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts', - 'src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts', - 'src/vs/workbench/contrib/terminal/common/terminalExtensionPoints.ts', - 'src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts', - 'src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts', - 'src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts', - 'src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts', - 'src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts', - 'src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts', - 'src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts', 'src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts', - 'src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts', 'src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/monitoring/outputMonitor.ts', - 'src/vs/workbench/contrib/terminalContrib/commandGuide/browser/terminal.commandGuide.contribution.ts', - 'src/vs/workbench/contrib/terminalContrib/history/browser/terminalRunRecentQuickPick.ts', - 'src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkQuickpick.ts', - 'src/vs/workbench/contrib/terminalContrib/links/test/browser/linkTestUtils.ts', - 'src/vs/workbench/contrib/terminalContrib/quickFix/browser/quickFixAddon.ts', - 'src/vs/workbench/contrib/terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.ts', - 'src/vs/workbench/contrib/terminalContrib/sendSignal/browser/terminal.sendSignal.contribution.ts', - 'src/vs/workbench/contrib/terminalContrib/stickyScroll/browser/terminalStickyScrollOverlay.ts', - 'src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts', 'src/vs/workbench/contrib/testing/browser/explorerProjections/listProjection.ts', 'src/vs/workbench/contrib/testing/browser/explorerProjections/treeProjection.ts', 'src/vs/workbench/contrib/testing/browser/testCoverageBars.ts', @@ -406,8 +330,6 @@ export default tseslint.config( 'src/vs/workbench/services/preferences/common/preferencesValidation.ts', 'src/vs/workbench/services/remote/common/tunnelModel.ts', 'src/vs/workbench/services/search/common/textSearchManager.ts', - 'src/vs/workbench/services/telemetry/test/browser/commonProperties.test.ts', - 'src/vs/workbench/services/telemetry/test/node/commonProperties.test.ts', 'src/vs/workbench/test/browser/workbenchTestServices.ts', 'test/automation/src/playwrightDriver.ts', '.eslint-plugin-local/**/*', @@ -419,62 +341,55 @@ export default tseslint.config( 'local/code-no-in-operator': 'warn', } }, - // vscode TS: strict no explicit `any` + // Strict no explicit `any` { files: [ + // Extensions + 'extensions/git/src/**/*.ts', + 'extensions/git-base/src/**/*.ts', + 'extensions/github/src/**/*.ts', + // vscode 'src/**/*.ts', ], ignores: [ + // Extensions + 'extensions/git/src/commands.ts', + 'extensions/git/src/decorators.ts', + 'extensions/git/src/git.ts', + 'extensions/git/src/util.ts', + 'extensions/git-base/src/decorators.ts', + 'extensions/github/src/util.ts', + // vscode d.ts 'src/vs/amdX.ts', 'src/vs/monaco.d.ts', 'src/vscode-dts/**', // Base 'src/vs/base/browser/dom.ts', 'src/vs/base/browser/mouseEvent.ts', - 'src/vs/base/browser/pixelRatio.ts', - 'src/vs/base/browser/trustedTypes.ts', - 'src/vs/base/browser/webWorkerFactory.ts', - 'src/vs/base/node/osDisplayProtocolInfo.ts', - 'src/vs/base/node/osReleaseInfo.ts', 'src/vs/base/node/processes.ts', 'src/vs/base/common/arrays.ts', 'src/vs/base/common/async.ts', - 'src/vs/base/common/cancellation.ts', - 'src/vs/base/common/collections.ts', 'src/vs/base/common/console.ts', - 'src/vs/base/common/controlFlow.ts', 'src/vs/base/common/decorators.ts', - 'src/vs/base/common/equals.ts', 'src/vs/base/common/errorMessage.ts', 'src/vs/base/common/errors.ts', 'src/vs/base/common/event.ts', 'src/vs/base/common/hotReload.ts', 'src/vs/base/common/hotReloadHelpers.ts', - 'src/vs/base/common/iterator.ts', 'src/vs/base/common/json.ts', 'src/vs/base/common/jsonSchema.ts', 'src/vs/base/common/lifecycle.ts', - 'src/vs/base/common/linkedList.ts', 'src/vs/base/common/map.ts', 'src/vs/base/common/marshalling.ts', - 'src/vs/base/common/network.ts', - 'src/vs/base/common/oauth.ts', 'src/vs/base/common/objects.ts', 'src/vs/base/common/performance.ts', 'src/vs/base/common/platform.ts', 'src/vs/base/common/processes.ts', - 'src/vs/base/common/resourceTree.ts', - 'src/vs/base/common/skipList.ts', - 'src/vs/base/common/strings.ts', - 'src/vs/base/common/ternarySearchTree.ts', 'src/vs/base/common/types.ts', 'src/vs/base/common/uriIpc.ts', 'src/vs/base/common/verifier.ts', 'src/vs/base/common/observableInternal/base.ts', 'src/vs/base/common/observableInternal/changeTracker.ts', - 'src/vs/base/common/observableInternal/debugLocation.ts', - 'src/vs/base/common/observableInternal/debugName.ts', - 'src/vs/base/common/observableInternal/map.ts', 'src/vs/base/common/observableInternal/set.ts', 'src/vs/base/common/observableInternal/transaction.ts', 'src/vs/base/common/worker/webWorkerBootstrap.ts', @@ -492,15 +407,6 @@ export default tseslint.config( 'src/vs/base/browser/ui/list/rowCache.ts', 'src/vs/base/browser/ui/sash/sash.ts', 'src/vs/base/browser/ui/table/tableWidget.ts', - 'src/vs/base/browser/ui/tree/abstractTree.ts', - 'src/vs/base/browser/ui/tree/asyncDataTree.ts', - 'src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts', - 'src/vs/base/browser/ui/tree/dataTree.ts', - 'src/vs/base/browser/ui/tree/indexTree.ts', - 'src/vs/base/browser/ui/tree/indexTreeModel.ts', - 'src/vs/base/browser/ui/tree/objectTree.ts', - 'src/vs/base/browser/ui/tree/objectTreeModel.ts', - 'src/vs/base/browser/ui/tree/tree.ts', 'src/vs/base/parts/ipc/common/ipc.net.ts', 'src/vs/base/parts/ipc/common/ipc.ts', 'src/vs/base/parts/ipc/electron-main/ipcMain.ts', @@ -524,46 +430,15 @@ export default tseslint.config( 'src/vs/base/common/observableInternal/logging/debugger/rpc.ts', 'src/vs/base/test/browser/ui/grid/util.ts', // Platform - 'src/vs/platform/accessibility/browser/accessibleView.ts', - 'src/vs/platform/accessibility/common/accessibility.ts', - 'src/vs/platform/action/common/action.ts', - 'src/vs/platform/actions/common/actions.ts', - 'src/vs/platform/assignment/common/assignment.ts', - 'src/vs/platform/browserElements/electron-main/nativeBrowserElementsMainService.ts', 'src/vs/platform/commands/common/commands.ts', - 'src/vs/platform/configuration/common/configuration.ts', - 'src/vs/platform/configuration/common/configurationModels.ts', - 'src/vs/platform/configuration/common/configurationRegistry.ts', - 'src/vs/platform/configuration/common/configurationService.ts', - 'src/vs/platform/configuration/common/configurations.ts', 'src/vs/platform/contextkey/browser/contextKeyService.ts', 'src/vs/platform/contextkey/common/contextkey.ts', 'src/vs/platform/contextview/browser/contextView.ts', - 'src/vs/platform/contextview/browser/contextViewService.ts', 'src/vs/platform/debug/common/extensionHostDebugIpc.ts', 'src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts', 'src/vs/platform/diagnostics/common/diagnostics.ts', - 'src/vs/platform/diagnostics/node/diagnosticsService.ts', 'src/vs/platform/download/common/downloadIpc.ts', - 'src/vs/platform/extensionManagement/common/abstractExtensionManagementService.ts', - 'src/vs/platform/extensionManagement/common/allowedExtensionsService.ts', - 'src/vs/platform/extensionManagement/common/extensionGalleryManifestServiceIpc.ts', - 'src/vs/platform/extensionManagement/common/extensionGalleryService.ts', - 'src/vs/platform/extensionManagement/common/extensionManagement.ts', - 'src/vs/platform/extensionManagement/common/extensionManagementIpc.ts', - 'src/vs/platform/extensionManagement/common/extensionManagementUtil.ts', - 'src/vs/platform/extensionManagement/common/extensionNls.ts', - 'src/vs/platform/extensionManagement/common/extensionStorage.ts', - 'src/vs/platform/extensionManagement/common/extensionTipsService.ts', - 'src/vs/platform/extensionManagement/common/extensionsProfileScannerService.ts', - 'src/vs/platform/extensionManagement/common/implicitActivationEvents.ts', - 'src/vs/platform/extensionManagement/node/extensionManagementService.ts', - 'src/vs/platform/extensionRecommendations/common/extensionRecommendationsIpc.ts', - 'src/vs/platform/extensions/common/extensionHostStarter.ts', - 'src/vs/platform/extensions/common/extensionValidator.ts', 'src/vs/platform/extensions/common/extensions.ts', - 'src/vs/platform/extensions/electron-main/extensionHostStarter.ts', - 'src/vs/platform/externalTerminal/node/externalTerminalService.ts', 'src/vs/platform/instantiation/common/descriptors.ts', 'src/vs/platform/instantiation/common/extensions.ts', 'src/vs/platform/instantiation/common/instantiation.ts', @@ -573,7 +448,6 @@ export default tseslint.config( 'src/vs/platform/keybinding/common/keybindingResolver.ts', 'src/vs/platform/keybinding/common/keybindingsRegistry.ts', 'src/vs/platform/keybinding/common/resolvedKeybindingItem.ts', - 'src/vs/platform/keyboardLayout/common/keyboardConfig.ts', 'src/vs/platform/languagePacks/node/languagePacks.ts', 'src/vs/platform/list/browser/listService.ts', 'src/vs/platform/log/browser/log.ts', @@ -583,15 +457,12 @@ export default tseslint.config( 'src/vs/platform/observable/common/wrapInHotClass.ts', 'src/vs/platform/observable/common/wrapInReloadableClass.ts', 'src/vs/platform/policy/common/policyIpc.ts', - 'src/vs/platform/policy/node/nativePolicyService.ts', 'src/vs/platform/profiling/common/profilingTelemetrySpec.ts', 'src/vs/platform/quickinput/browser/quickInputActions.ts', 'src/vs/platform/quickinput/common/quickInput.ts', 'src/vs/platform/registry/common/platform.ts', 'src/vs/platform/remote/browser/browserSocketFactory.ts', 'src/vs/platform/remote/browser/remoteAuthorityResolverService.ts', - 'src/vs/platform/remote/common/electronRemoteResources.ts', - 'src/vs/platform/remote/common/managedSocket.ts', 'src/vs/platform/remote/common/remoteAgentConnection.ts', 'src/vs/platform/remote/common/remoteAuthorityResolver.ts', 'src/vs/platform/remote/electron-browser/electronRemoteResourceLoader.ts', @@ -601,24 +472,10 @@ export default tseslint.config( 'src/vs/platform/request/common/requestIpc.ts', 'src/vs/platform/request/electron-utility/requestService.ts', 'src/vs/platform/request/node/proxy.ts', - 'src/vs/platform/telemetry/browser/1dsAppender.ts', 'src/vs/platform/telemetry/browser/errorTelemetry.ts', - 'src/vs/platform/telemetry/common/1dsAppender.ts', 'src/vs/platform/telemetry/common/errorTelemetry.ts', - 'src/vs/platform/telemetry/common/gdprTypings.ts', 'src/vs/platform/telemetry/common/remoteTelemetryChannel.ts', - 'src/vs/platform/telemetry/common/telemetry.ts', - 'src/vs/platform/telemetry/common/telemetryIpc.ts', - 'src/vs/platform/telemetry/common/telemetryLogAppender.ts', - 'src/vs/platform/telemetry/common/telemetryService.ts', - 'src/vs/platform/telemetry/common/telemetryUtils.ts', - 'src/vs/platform/telemetry/node/1dsAppender.ts', 'src/vs/platform/telemetry/node/errorTelemetry.ts', - 'src/vs/platform/terminal/common/terminal.ts', - 'src/vs/platform/terminal/node/ptyHostService.ts', - 'src/vs/platform/terminal/node/ptyService.ts', - 'src/vs/platform/terminal/node/terminalProcess.ts', - 'src/vs/platform/terminal/node/windowsShellHelper.ts', 'src/vs/platform/theme/common/iconRegistry.ts', 'src/vs/platform/theme/common/tokenClassificationRegistry.ts', 'src/vs/platform/update/common/updateIpc.ts', @@ -631,26 +488,14 @@ export default tseslint.config( 'src/vs/platform/userDataSync/common/extensionsSync.ts', 'src/vs/platform/userDataSync/common/globalStateMerge.ts', 'src/vs/platform/userDataSync/common/globalStateSync.ts', - 'src/vs/platform/userDataSync/common/ignoredExtensions.ts', 'src/vs/platform/userDataSync/common/settingsMerge.ts', 'src/vs/platform/userDataSync/common/settingsSync.ts', - 'src/vs/platform/userDataSync/common/userDataAutoSyncService.ts', 'src/vs/platform/userDataSync/common/userDataSync.ts', - 'src/vs/platform/userDataSync/common/userDataSyncAccount.ts', - 'src/vs/platform/userDataSync/common/userDataSyncEnablementService.ts', 'src/vs/platform/userDataSync/common/userDataSyncIpc.ts', - 'src/vs/platform/userDataSync/common/userDataSyncLocalStoreService.ts', - 'src/vs/platform/userDataSync/common/userDataSyncMachines.ts', - 'src/vs/platform/userDataSync/common/userDataSyncResourceProvider.ts', - 'src/vs/platform/userDataSync/common/userDataSyncService.ts', 'src/vs/platform/userDataSync/common/userDataSyncServiceIpc.ts', - 'src/vs/platform/userDataSync/common/userDataSyncStoreService.ts', - 'src/vs/platform/webContentExtractor/electron-main/cdpAccessibilityDomain.ts', 'src/vs/platform/webview/common/webviewManagerService.ts', - 'src/vs/platform/configuration/test/common/testConfigurationService.ts', 'src/vs/platform/instantiation/test/common/instantiationServiceMock.ts', 'src/vs/platform/keybinding/test/common/mockKeybindingService.ts', - 'src/vs/platform/userDataSync/test/common/userDataSyncClient.ts', // Editor 'src/vs/editor/standalone/browser/standaloneEditor.ts', 'src/vs/editor/standalone/browser/standaloneLanguages.ts', @@ -661,16 +506,11 @@ export default tseslint.config( 'src/vs/editor/contrib/codeAction/browser/codeAction.ts', 'src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts', 'src/vs/editor/contrib/codeAction/common/types.ts', - 'src/vs/editor/contrib/codelens/browser/codelens.ts', - 'src/vs/editor/contrib/codelens/browser/codelensController.ts', 'src/vs/editor/contrib/colorPicker/browser/colorDetector.ts', 'src/vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution.ts', 'src/vs/editor/contrib/dropOrPasteInto/browser/dropIntoEditorContribution.ts', 'src/vs/editor/contrib/find/browser/findController.ts', 'src/vs/editor/contrib/find/browser/findModel.ts', - 'src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts', - 'src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts', - 'src/vs/editor/contrib/folding/browser/folding.ts', 'src/vs/editor/contrib/gotoSymbol/browser/goToCommands.ts', 'src/vs/editor/contrib/gotoSymbol/browser/symbolNavigation.ts', 'src/vs/editor/contrib/hover/browser/hoverActions.ts', @@ -694,19 +534,14 @@ export default tseslint.config( 'src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts', // Workbench 'src/vs/workbench/api/browser/mainThreadChatSessions.ts', - 'src/vs/workbench/api/browser/mainThreadTerminalService.ts', - 'src/vs/workbench/api/common/configurationExtensionPoint.ts', 'src/vs/workbench/api/common/extHost.api.impl.ts', 'src/vs/workbench/api/common/extHost.protocol.ts', 'src/vs/workbench/api/common/extHostChatSessions.ts', 'src/vs/workbench/api/common/extHostCodeInsets.ts', 'src/vs/workbench/api/common/extHostCommands.ts', - 'src/vs/workbench/api/common/extHostConfiguration.ts', 'src/vs/workbench/api/common/extHostConsoleForwarder.ts', 'src/vs/workbench/api/common/extHostDataChannels.ts', 'src/vs/workbench/api/common/extHostDebugService.ts', - 'src/vs/workbench/api/common/extHostDiagnostics.ts', - 'src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts', 'src/vs/workbench/api/common/extHostExtensionActivator.ts', 'src/vs/workbench/api/common/extHostExtensionService.ts', 'src/vs/workbench/api/common/extHostFileSystemConsumer.ts', @@ -718,7 +553,6 @@ export default tseslint.config( 'src/vs/workbench/api/common/extHostMessageService.ts', 'src/vs/workbench/api/common/extHostNotebookDocument.ts', 'src/vs/workbench/api/common/extHostNotebookDocumentSaveParticipant.ts', - 'src/vs/workbench/api/common/extHostQuickOpen.ts', 'src/vs/workbench/api/common/extHostRequireInterceptor.ts', 'src/vs/workbench/api/common/extHostRpcService.ts', 'src/vs/workbench/api/common/extHostSCM.ts', @@ -726,7 +560,6 @@ export default tseslint.config( 'src/vs/workbench/api/common/extHostStatusBar.ts', 'src/vs/workbench/api/common/extHostStoragePaths.ts', 'src/vs/workbench/api/common/extHostTelemetry.ts', - 'src/vs/workbench/api/common/extHostTerminalService.ts', 'src/vs/workbench/api/common/extHostTesting.ts', 'src/vs/workbench/api/common/extHostTextEditor.ts', 'src/vs/workbench/api/common/extHostTimeline.ts', @@ -757,48 +590,13 @@ export default tseslint.config( 'src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts', 'src/vs/workbench/contrib/accessibilitySignals/browser/commands.ts', 'src/vs/workbench/contrib/authentication/browser/actions/manageTrustedMcpServersForAccountAction.ts', - 'src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts', 'src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts', - 'src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts', 'src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts', 'src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts', - 'src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts', - 'src/vs/workbench/contrib/callHierarchy/common/callHierarchy.ts', - 'src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts', - 'src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts', - 'src/vs/workbench/contrib/chat/browser/actions/chatToolActions.ts', - 'src/vs/workbench/contrib/chat/browser/chatAttachmentWidgets.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts', - 'src/vs/workbench/contrib/chat/browser/chatContentParts/chatMultiDiffContentPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts', - 'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingEditorActions.ts', - 'src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts', - 'src/vs/workbench/contrib/chat/browser/chatInputPart.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions.contribution.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/common.ts', - 'src/vs/workbench/contrib/chat/browser/chatSessions/view/sessionsTreeRenderer.ts', - 'src/vs/workbench/contrib/chat/browser/chatWidget.ts', - 'src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts', - 'src/vs/workbench/contrib/chat/common/chatAgents.ts', - 'src/vs/workbench/contrib/chat/common/chatModel.ts', - 'src/vs/workbench/contrib/chat/common/chatModes.ts', - 'src/vs/workbench/contrib/chat/common/chatService.ts', - 'src/vs/workbench/contrib/chat/common/chatServiceImpl.ts', - 'src/vs/workbench/contrib/chat/common/chatSessionsService.ts', - 'src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts', - 'src/vs/workbench/contrib/chat/common/languageModelToolsService.ts', - 'src/vs/workbench/contrib/chat/common/languageModels.ts', - 'src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts', - 'src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts', - 'src/vs/workbench/contrib/chat/test/common/languageModels.ts', - 'src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts', - 'src/vs/workbench/contrib/chat/test/common/mockPromptsService.ts', 'src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts', 'src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts', 'src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts', 'src/vs/workbench/contrib/commands/common/commands.contribution.ts', - 'src/vs/workbench/contrib/comments/browser/commentNode.ts', - 'src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts', 'src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts', 'src/vs/workbench/contrib/comments/browser/commentsView.ts', 'src/vs/workbench/contrib/comments/browser/reactionsAction.ts', @@ -823,7 +621,6 @@ export default tseslint.config( 'src/vs/workbench/contrib/debug/common/debugger.ts', 'src/vs/workbench/contrib/debug/common/replModel.ts', 'src/vs/workbench/contrib/debug/test/common/mockDebug.ts', - 'src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts', 'src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts', 'src/vs/workbench/contrib/editTelemetry/browser/helpers/documentWithAnnotatedEdits.ts', 'src/vs/workbench/contrib/editTelemetry/browser/helpers/utils.ts', @@ -837,18 +634,12 @@ export default tseslint.config( 'src/vs/workbench/contrib/extensions/browser/extensionsViews.ts', 'src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts', 'src/vs/workbench/contrib/extensions/common/extensions.ts', - 'src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts', 'src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts', - 'src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts', 'src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts', 'src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts', 'src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts', - 'src/vs/workbench/contrib/issue/browser/issueReporterModel.ts', - 'src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts', 'src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts', - 'src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts', 'src/vs/workbench/contrib/markers/browser/markers.contribution.ts', - 'src/vs/workbench/contrib/markers/browser/markersTable.ts', 'src/vs/workbench/contrib/markers/browser/markersView.ts', 'src/vs/workbench/contrib/mergeEditor/browser/commands/commands.ts', 'src/vs/workbench/contrib/mergeEditor/browser/utils.ts', @@ -857,7 +648,6 @@ export default tseslint.config( 'src/vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard.ts', 'src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts', 'src/vs/workbench/contrib/notebook/browser/contrib/layout/layoutActions.ts', - 'src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts', 'src/vs/workbench/contrib/notebook/browser/contrib/profile/notebookProfile.ts', 'src/vs/workbench/contrib/notebook/browser/contrib/troubleshoot/layout.ts', 'src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts', @@ -868,29 +658,23 @@ export default tseslint.config( 'src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts', 'src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts', 'src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts', - 'src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts', 'src/vs/workbench/contrib/notebook/browser/outputEditor/notebookOutputEditor.ts', 'src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts', 'src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts', 'src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts', 'src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts', 'src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts', - 'src/vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions.ts', 'src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts', 'src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts', 'src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts', 'src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts', - 'src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts', - 'src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts', 'src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts', 'src/vs/workbench/contrib/notebook/common/model/notebookMetadataTextModel.ts', 'src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts', 'src/vs/workbench/contrib/notebook/common/notebookCommon.ts', 'src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts', - 'src/vs/workbench/contrib/notebook/common/notebookRange.ts', 'src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts', 'src/vs/workbench/contrib/performance/electron-browser/startupProfiler.ts', - 'src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts', 'src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts', 'src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts', 'src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts', @@ -919,15 +703,12 @@ export default tseslint.config( 'src/vs/workbench/contrib/search/browser/searchTreeModel/searchTreeCommon.ts', 'src/vs/workbench/contrib/search/browser/searchTreeModel/textSearchHeading.ts', 'src/vs/workbench/contrib/search/browser/searchView.ts', - 'src/vs/workbench/contrib/search/browser/searchWidget.ts', - 'src/vs/workbench/contrib/search/common/cacheState.ts', 'src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts', 'src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts', 'src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts', 'src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts', 'src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts', 'src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts', - 'src/vs/workbench/contrib/snippets/browser/snippetsFile.ts', 'src/vs/workbench/contrib/snippets/browser/snippetsService.ts', 'src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts', 'src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts', @@ -939,120 +720,61 @@ export default tseslint.config( 'src/vs/workbench/contrib/tasks/common/taskConfiguration.ts', 'src/vs/workbench/contrib/tasks/common/taskSystem.ts', 'src/vs/workbench/contrib/tasks/common/tasks.ts', - 'src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts', - 'src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts', - 'src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts', - 'src/vs/workbench/contrib/terminal/common/basePty.ts', - 'src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts', - 'src/vs/workbench/contrib/terminal/common/terminal.ts', - 'src/vs/workbench/contrib/terminalContrib/links/browser/links.ts', - 'src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts', - 'src/vs/workbench/contrib/testing/browser/testExplorerActions.ts', - 'src/vs/workbench/contrib/testing/browser/testingExplorerView.ts', 'src/vs/workbench/contrib/testing/common/storedValue.ts', - 'src/vs/workbench/contrib/testing/common/testItemCollection.ts', 'src/vs/workbench/contrib/testing/test/browser/testObjectTree.ts', - 'src/vs/workbench/contrib/timeline/browser/timelinePane.ts', 'src/vs/workbench/contrib/typeHierarchy/browser/typeHierarchy.contribution.ts', 'src/vs/workbench/contrib/typeHierarchy/common/typeHierarchy.ts', - 'src/vs/workbench/contrib/update/browser/update.ts', - 'src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts', 'src/vs/workbench/contrib/webview/browser/overlayWebview.ts', 'src/vs/workbench/contrib/webview/browser/webview.ts', 'src/vs/workbench/contrib/webview/browser/webviewElement.ts', 'src/vs/workbench/contrib/webviewPanel/browser/webviewEditor.ts', 'src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts', 'src/vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService.ts', - 'src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts', - 'src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts', - 'src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedAccessibleView.ts', 'src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedService.ts', - 'src/vs/workbench/contrib/welcomeViews/common/newFile.contribution.ts', 'src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts', - 'src/vs/workbench/services/accounts/common/defaultAccount.ts', - 'src/vs/workbench/services/assignment/common/assignmentFilters.ts', 'src/vs/workbench/services/authentication/common/authentication.ts', 'src/vs/workbench/services/authentication/test/browser/authenticationQueryServiceMocks.ts', 'src/vs/workbench/services/commands/common/commandService.ts', - 'src/vs/workbench/services/configuration/browser/configuration.ts', - 'src/vs/workbench/services/configuration/browser/configurationService.ts', - 'src/vs/workbench/services/configuration/common/configurationModels.ts', - 'src/vs/workbench/services/configuration/common/jsonEditingService.ts', - 'src/vs/workbench/services/configuration/test/common/testServices.ts', - 'src/vs/workbench/services/configurationResolver/browser/baseConfigurationResolverService.ts', 'src/vs/workbench/services/configurationResolver/common/configurationResolver.ts', 'src/vs/workbench/services/configurationResolver/common/configurationResolverExpression.ts', - 'src/vs/workbench/services/configurationResolver/common/variableResolver.ts', - 'src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts', - 'src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts', - 'src/vs/workbench/services/extensions/common/extHostCustomers.ts', 'src/vs/workbench/services/extensions/common/extensionHostManager.ts', - 'src/vs/workbench/services/extensions/common/extensionHostProtocol.ts', - 'src/vs/workbench/services/extensions/common/extensionHostProxy.ts', 'src/vs/workbench/services/extensions/common/extensionsRegistry.ts', 'src/vs/workbench/services/extensions/common/lazyPromise.ts', 'src/vs/workbench/services/extensions/common/polyfillNestedWorker.protocol.ts', - 'src/vs/workbench/services/extensions/common/proxyIdentifier.ts', 'src/vs/workbench/services/extensions/common/rpcProtocol.ts', - 'src/vs/workbench/services/extensions/electron-browser/cachedExtensionScanner.ts', - 'src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts', 'src/vs/workbench/services/extensions/worker/polyfillNestedWorker.ts', 'src/vs/workbench/services/keybinding/browser/keybindingService.ts', 'src/vs/workbench/services/keybinding/browser/keyboardLayoutService.ts', 'src/vs/workbench/services/keybinding/common/keybindingEditing.ts', - 'src/vs/workbench/services/keybinding/common/keybindingIO.ts', 'src/vs/workbench/services/keybinding/common/keymapInfo.ts', 'src/vs/workbench/services/language/common/languageService.ts', - 'src/vs/workbench/services/languageDetection/browser/languageDetectionWorker.protocol.ts', 'src/vs/workbench/services/outline/browser/outline.ts', 'src/vs/workbench/services/outline/browser/outlineService.ts', 'src/vs/workbench/services/preferences/common/preferences.ts', 'src/vs/workbench/services/preferences/common/preferencesModels.ts', 'src/vs/workbench/services/preferences/common/preferencesValidation.ts', 'src/vs/workbench/services/remote/common/tunnelModel.ts', - 'src/vs/workbench/services/search/common/localFileSearchWorkerTypes.ts', 'src/vs/workbench/services/search/common/replace.ts', 'src/vs/workbench/services/search/common/search.ts', 'src/vs/workbench/services/search/common/searchExtConversionTypes.ts', 'src/vs/workbench/services/search/common/searchExtTypes.ts', - 'src/vs/workbench/services/search/common/searchService.ts', 'src/vs/workbench/services/search/node/fileSearch.ts', 'src/vs/workbench/services/search/node/rawSearchService.ts', 'src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts', - 'src/vs/workbench/services/search/worker/localFileSearch.ts', - 'src/vs/workbench/services/terminal/common/embedderTerminalService.ts', - 'src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateTokenizationWorker.worker.ts', - 'src/vs/workbench/services/textMate/browser/backgroundTokenization/worker/textMateWorkerHost.ts', - 'src/vs/workbench/services/textMate/browser/textMateTokenizationFeatureImpl.ts', 'src/vs/workbench/services/textMate/common/TMGrammarFactory.ts', 'src/vs/workbench/services/themes/browser/fileIconThemeData.ts', 'src/vs/workbench/services/themes/browser/productIconThemeData.ts', - 'src/vs/workbench/services/themes/browser/workbenchThemeService.ts', 'src/vs/workbench/services/themes/common/colorThemeData.ts', 'src/vs/workbench/services/themes/common/plistParser.ts', 'src/vs/workbench/services/themes/common/themeExtensionPoints.ts', 'src/vs/workbench/services/themes/common/workbenchThemeService.ts', - 'src/vs/workbench/services/userActivity/common/userActivityRegistry.ts', - 'src/vs/workbench/services/userData/browser/userDataInit.ts', - 'src/vs/workbench/services/userDataProfile/browser/userDataProfileInit.ts', - 'src/vs/workbench/services/userDataSync/browser/userDataSyncInit.ts', - 'src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts', - 'src/vs/workbench/services/userDataSync/common/userDataSync.ts', 'src/vs/workbench/test/browser/workbenchTestServices.ts', 'src/vs/workbench/test/common/workbenchTestServices.ts', 'src/vs/workbench/test/electron-browser/workbenchTestServices.ts', - 'src/vs/workbench/workbench.web.main.internal.ts', - 'src/vs/workbench/workbench.web.main.ts', // Server 'src/vs/server/node/remoteAgentEnvironmentImpl.ts', 'src/vs/server/node/remoteExtensionHostAgentServer.ts', 'src/vs/server/node/remoteExtensionsScanner.ts', - 'src/vs/server/node/remoteTerminalChannel.ts', // Tests '**/*.test.ts', '**/*.integrationTest.ts' @@ -1067,7 +789,7 @@ export default tseslint.config( '@typescript-eslint/no-explicit-any': [ 'warn', { - 'fixToUnknown': true + 'fixToUnknown': false } ] } @@ -1177,6 +899,7 @@ export default tseslint.config( ], 'verbs': [ 'accept', + 'archive', 'change', 'close', 'collapse', @@ -1722,6 +1445,7 @@ export default tseslint.config( '@vscode/vscode-languagedetection', '@vscode/ripgrep', '@vscode/iconv-lite-umd', + '@vscode/native-watchdog', '@vscode/policy-watcher', '@vscodium/policy-watcher', '@vscode/proxy-agent', @@ -1741,7 +1465,6 @@ export default tseslint.config( 'minimist', 'node:module', 'native-keymap', - 'native-watchdog', 'net', 'node-pty', 'os', @@ -1750,7 +1473,7 @@ export default tseslint.config( 'readline', 'stream', 'string_decoder', - 'tas-client-umd', + 'tas-client', 'tls', 'undici', 'undici-types', @@ -1839,7 +1562,7 @@ export default tseslint.config( 'vs/base/~', 'vs/base/parts/*/~', 'vs/platform/*/~', - 'tas-client-umd', // node module allowed even in /common/ + 'tas-client', // node module allowed even in /common/ '@microsoft/1ds-core-js', // node module allowed even in /common/ '@microsoft/1ds-post-js', // node module allowed even in /common/ '@xterm/headless' // node module allowed even in /common/ @@ -1957,7 +1680,7 @@ export default tseslint.config( 'when': 'test', 'pattern': 'vs/workbench/contrib/*/~' }, // TODO@layers - 'tas-client-umd', // node module allowed even in /common/ + 'tas-client', // node module allowed even in /common/ 'vscode-textmate', // node module allowed even in /common/ '@vscode/vscode-languagedetection', // node module allowed even in /common/ '@vscode/tree-sitter-wasm', // type import @@ -2137,7 +1860,7 @@ export default tseslint.config( 'vs/workbench/api/~', 'vs/workbench/services/*/~', 'vs/workbench/contrib/*/~', - 'vs/workbench/workbench.common.main.js' + 'vs/workbench/workbench.web.main.js' ] }, { @@ -2164,7 +1887,7 @@ export default tseslint.config( ] }, { - 'target': 'src/vs/{loader.d.ts,monaco.d.ts,nls.ts,nls.messages.ts}', + 'target': 'src/vs/{monaco.d.ts,nls.ts}', 'restrictions': [] }, { @@ -2214,6 +1937,13 @@ export default tseslint.config( '*' // node modules ] }, + { + 'target': 'test/sanity/**', + 'restrictions': [ + 'test/sanity/**', + '*' // node modules + ] + }, { 'target': 'test/automation/**', 'restrictions': [ @@ -2391,4 +2121,21 @@ export default tseslint.config( '@typescript-eslint/consistent-generic-constructors': ['warn', 'constructor'], } }, + // Allow querySelector/querySelectorAll in test files - it's acceptable for test assertions + { + files: [ + 'src/**/test/**/*.ts', + 'extensions/**/test/**/*.ts', + ], + rules: { + 'no-restricted-syntax': [ + 'warn', + // Keep the Intl helper restriction even in tests + { + 'selector': `NewExpression[callee.object.name='Intl']`, + 'message': 'Use safeIntl helper instead for safe and lazy use of potentially expensive Intl methods.' + }, + ], + } + }, ); diff --git a/extensions/configuration-editing/tsconfig.json b/extensions/configuration-editing/tsconfig.json index 7106538eb99..054dd8e9256 100644 --- a/extensions/configuration-editing/tsconfig.json +++ b/extensions/configuration-editing/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/cpp/language-configuration.json b/extensions/cpp/language-configuration.json index cb1fb733b99..a4468a758f9 100644 --- a/extensions/cpp/language-configuration.json +++ b/extensions/cpp/language-configuration.json @@ -93,8 +93,8 @@ "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", "folding": { "markers": { - "start": "^\\s*#pragma\\s+region\\b", - "end": "^\\s*#pragma\\s+endregion\\b" + "start": "^\\s*#\\s*pragma\\s+region\\b", + "end": "^\\s*#\\s*pragma\\s+endregion\\b" } }, "indentationRules": { diff --git a/extensions/csharp/cgmanifest.json b/extensions/csharp/cgmanifest.json index de6d5f6d89c..6eb3de2f572 100644 --- a/extensions/csharp/cgmanifest.json +++ b/extensions/csharp/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/csharp-tmLanguage", "repositoryUrl": "https://github.com/dotnet/csharp-tmLanguage", - "commitHash": "c32388ec18690abefb37cbaffa687a338c87d016" + "commitHash": "2e6860d87d4019b0b793b1e21e9e5c82185a01aa" } }, "license": "MIT", diff --git a/extensions/csharp/syntaxes/csharp.tmLanguage.json b/extensions/csharp/syntaxes/csharp.tmLanguage.json index 007fb719459..89b08d5c5b2 100644 --- a/extensions/csharp/syntaxes/csharp.tmLanguage.json +++ b/extensions/csharp/syntaxes/csharp.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/csharp-tmLanguage/commit/c32388ec18690abefb37cbaffa687a338c87d016", + "version": "https://github.com/dotnet/csharp-tmLanguage/commit/2e6860d87d4019b0b793b1e21e9e5c82185a01aa", "name": "C#", "scopeName": "source.cs", "patterns": [ @@ -118,9 +118,15 @@ { "include": "#type-declarations" }, + { + "include": "#constructor-declaration" + }, { "include": "#property-declaration" }, + { + "include": "#fixed-size-buffer-declaration" + }, { "include": "#field-declaration" }, @@ -133,9 +139,6 @@ { "include": "#variable-initializer" }, - { - "include": "#constructor-declaration" - }, { "include": "#destructor-declaration" }, @@ -334,6 +337,12 @@ { "include": "#is-expression" }, + { + "include": "#boolean-literal" + }, + { + "include": "#null-literal" + }, { "include": "#anonymous-method-expression" }, @@ -477,7 +486,7 @@ ] }, "attribute-section": { - "begin": "(\\[)(assembly|module|field|event|method|param|property|return|type)?(\\:)?", + "begin": "(\\[)(assembly|module|field|event|method|param|property|return|typevar|type)?(\\:)?", "beginCaptures": { "1": { "name": "punctuation.squarebracket.open.cs" @@ -1025,6 +1034,9 @@ }, "end": "(?=\\{|where|;)", "patterns": [ + { + "include": "#base-class-constructor-call" + }, { "include": "#type" }, @@ -1036,6 +1048,33 @@ } ] }, + "base-class-constructor-call": { + "begin": "(?x)\n(?:\n (@?[_[:alpha:]][_[:alnum:]]*)\\s*(\\.) # qualified name part\n)*\n(@?[_[:alpha:]][_[:alnum:]]*)\\s* # type name\n(\n <\n (?\n [^<>()]|\n \\((?:[^<>()]|<[^<>()]*>|\\([^<>()]*\\))*\\)|\n <\\g*>\n )*\n >\\s*\n)? # optional type arguments\n(?=\\() # followed by argument list", + "beginCaptures": { + "1": { + "name": "entity.name.type.cs" + }, + "2": { + "name": "punctuation.accessor.cs" + }, + "3": { + "name": "entity.name.type.cs" + }, + "4": { + "patterns": [ + { + "include": "#type-arguments" + } + ] + } + }, + "end": "(?<=\\))", + "patterns": [ + { + "include": "#argument-list" + } + ] + }, "generic-constraints": { "begin": "(where)\\s+(@?[_[:alpha:]][_[:alnum:]]*)\\s*(:)", "beginCaptures": { @@ -1096,6 +1135,33 @@ } ] }, + "fixed-size-buffer-declaration": { + "begin": "(?x)\n\\b(fixed)\\b\\s+\n(?\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* # Are there any more names being dotted into?\n )\n)\\s+\n(\\g)\\s* # buffer name\n(?=\\[)", + "beginCaptures": { + "1": { + "name": "storage.modifier.fixed.cs" + }, + "2": { + "patterns": [ + { + "include": "#type" + } + ] + }, + "6": { + "name": "entity.name.variable.field.cs" + } + }, + "end": "(?=;)", + "patterns": [ + { + "include": "#bracketed-argument-list" + }, + { + "include": "#comment" + } + ] + }, "field-declaration": { "begin": "(?x)\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)\\s+\n(\\g)\\s* # first field name\n(?!=>|==)(?=,|;|=|$)", "beginCaptures": { @@ -1302,10 +1368,13 @@ "match": "\\b(private|protected|internal)\\b" }, { - "begin": "\\b(get)\\b\\s*(?=\\{|;|=>|//|/\\*|$)", + "begin": "(?:\\b(readonly)\\s+)?\\b(get)\\b\\s*(?=\\{|;|=>|//|/\\*|$)", "beginCaptures": { "1": { - "name": "storage.type.accessor.$1.cs" + "name": "storage.modifier.readonly.cs" + }, + "2": { + "name": "storage.type.accessor.get.cs" } }, "end": "(?<=\\}|;)|(?=\\})", @@ -1511,17 +1580,14 @@ ] }, "constructor-declaration": { - "begin": "(?=@?[_[:alpha:]][_[:alnum:]]*\\s*\\()", + "begin": "(@?[_[:alpha:]][_[:alnum:]]*)\\s*(?=\\(|$)", + "beginCaptures": { + "1": { + "name": "entity.name.function.cs" + } + }, "end": "(?<=\\})|(?=;)", "patterns": [ - { - "match": "(@?[_[:alpha:]][_[:alnum:]]*)\\b", - "captures": { - "1": { - "name": "entity.name.function.cs" - } - } - }, { "begin": "(:)", "beginCaptures": { @@ -2661,6 +2727,15 @@ }, { "include": "#local-variable-declaration" + }, + { + "include": "#local-tuple-var-deconstruction" + }, + { + "include": "#tuple-deconstruction-assignment" + }, + { + "include": "#expression" } ] }, @@ -3045,11 +3120,14 @@ }, { "include": "#local-tuple-var-deconstruction" + }, + { + "include": "#local-tuple-declaration-deconstruction" } ] }, "local-variable-declaration": { - "begin": "(?x)\n(?:\n (?:(\\bref)\\s+(?:(\\breadonly)\\s+)?)?(\\bvar\\b)| # ref local\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*[?*]\\s*)? # nullable or pointer suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", + "begin": "(?x)\n(?:\n (?:(\\bref)\\s+(?:(\\breadonly)\\s+)?)?(\\bvar\\b)| # ref local\n (?\n (?:\n (?:ref\\s+(?:readonly\\s+)?)? # ref local\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\*\\s*)* # pointer suffix?\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n )\n)\\s+\n(\\g)\\s*\n(?!=>)\n(?=,|;|=|\\))", "beginCaptures": { "1": { "name": "storage.modifier.ref.cs" @@ -3193,6 +3271,18 @@ } ] }, + "local-tuple-declaration-deconstruction": { + "match": "(?x) # e.g. (int x, var y) = GetPoint();\n(?\\((?:[^\\(\\)]|\\g)+\\))\\s*\n(?!=>|==)(?==)", + "captures": { + "1": { + "patterns": [ + { + "include": "#tuple-declaration-deconstruction-element-list" + } + ] + } + } + }, "tuple-deconstruction-assignment": { "match": "(?x)\n(?\\s*\\((?:[^\\(\\)]|\\g)+\\))\\s*\n(?!=>|==)(?==)", "captures": { @@ -4355,7 +4445,7 @@ } }, "array-creation-expression": { - "begin": "(?x)\n\\b(new|stackalloc)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(?=\\[)", + "begin": "(?x)\n\\b(new|stackalloc)\\b\\s*\n(?\n (?:\n (?:\n (?:(?@?[_[:alpha:]][_[:alnum:]]*)\\s*\\:\\:\\s*)? # alias-qualification\n (? # identifier + type arguments (if any)\n \\g\\s*\n (?\\s*<(?:[^<>]|\\g)+>\\s*)?\n )\n (?:\\s*\\.\\s*\\g)* | # Are there any more names being dotted into?\n (?\\s*\\((?:[^\\(\\)]|\\g)+\\))\n )\n (?:\\s*\\*\\s*)* # pointer suffix?\n (?:\\s*\\?\\s*)? # nullable suffix?\n (?:\\s* # array suffix?\n \\[\n (?:\\s*,\\s*)* # commata for multi-dimensional arrays\n \\]\n \\s*\n (?:\\?)? # arrays can be nullable reference types\n \\s*\n )*\n )\n)?\\s*\n(?=\\[)", "beginCaptures": { "1": { "name": "keyword.operator.expression.$1.cs" @@ -5204,7 +5294,7 @@ "end": "(?<=$)", "patterns": [ { - "include": "#comment" + "include": "#preprocessor-comment" }, { "include": "#preprocessor-define-or-undef" @@ -5238,6 +5328,32 @@ }, { "include": "#preprocessor-pragma-checksum" + }, + { + "include": "#preprocessor-app-directive" + } + ] + }, + "preprocessor-comment": { + "patterns": [ + { + "name": "comment.line.double-slash.cs", + "match": "(//).*(?=$)", + "captures": { + "1": { + "name": "punctuation.definition.comment.cs" + } + } + }, + { + "name": "comment.block.cs", + "begin": "/\\*", + "end": "\\*/", + "captures": { + "0": { + "name": "punctuation.definition.comment.cs" + } + } } ] }, @@ -5268,7 +5384,7 @@ "end": "(?=$)", "patterns": [ { - "include": "#comment" + "include": "#preprocessor-comment" }, { "include": "#preprocessor-expression" @@ -5447,6 +5563,129 @@ } } }, + "preprocessor-app-directive": { + "begin": "\\s*(:)\\s*", + "beginCaptures": { + "1": { + "name": "punctuation.separator.colon.cs" + } + }, + "end": "(?=$)", + "patterns": [ + { + "include": "#preprocessor-app-directive-package" + }, + { + "include": "#preprocessor-app-directive-property" + }, + { + "include": "#preprocessor-app-directive-project" + }, + { + "include": "#preprocessor-app-directive-sdk" + }, + { + "include": "#preprocessor-app-directive-generic" + } + ] + }, + "preprocessor-app-directive-package": { + "match": "\\b(package)\\b\\s*([_[:alpha:]][_.[:alnum:]]*)?(@)?(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.package.cs" + }, + "2": { + "patterns": [ + { + "include": "#preprocessor-app-directive-package-name" + } + ] + }, + "3": { + "name": "punctuation.separator.at.cs" + }, + "4": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, + "preprocessor-app-directive-property": { + "match": "\\b(property)\\b\\s*([_[:alpha:]][_[:alnum:]]*)?(=)?(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.property.cs" + }, + "2": { + "name": "entity.name.variable.preprocessor.symbol.cs" + }, + "3": { + "name": "punctuation.separator.equals.cs" + }, + "4": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, + "preprocessor-app-directive-project": { + "match": "\\b(project)\\b\\s*(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.project.cs" + }, + "2": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, + "preprocessor-app-directive-sdk": { + "match": "\\b(sdk)\\b\\s*([_[:alpha:]][_.[:alnum:]]*)?(@)?(.*)?\\s*", + "captures": { + "1": { + "name": "keyword.preprocessor.sdk.cs" + }, + "2": { + "patterns": [ + { + "include": "#preprocessor-app-directive-package-name" + } + ] + }, + "3": { + "name": "punctuation.separator.at.cs" + }, + "4": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, + "preprocessor-app-directive-package-name": { + "patterns": [ + { + "match": "(\\.)([_[:alpha:]][_[:alnum:]]*)", + "captures": { + "1": { + "name": "punctuation.dot.cs" + }, + "2": { + "name": "entity.name.variable.preprocessor.symbol.cs" + } + } + }, + { + "name": "entity.name.variable.preprocessor.symbol.cs", + "match": "[_[:alpha:]][_[:alnum:]]*" + } + ] + }, + "preprocessor-app-directive-generic": { + "match": "\\b(.*)?\\s*", + "captures": { + "1": { + "name": "string.unquoted.preprocessor.message.cs" + } + } + }, "preprocessor-expression": { "patterns": [ { diff --git a/extensions/css-language-features/client/tsconfig.json b/extensions/css-language-features/client/tsconfig.json index 51303a368a2..af9ff253d79 100644 --- a/extensions/css-language-features/client/tsconfig.json +++ b/extensions/css-language-features/client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "webworker" diff --git a/extensions/css-language-features/package-lock.json b/extensions/css-language-features/package-lock.json index 5b64dc53327..42656ea4bae 100644 --- a/extensions/css-language-features/package-lock.json +++ b/extensions/css-language-features/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "vscode-languageclient": "^10.0.0-next.17", + "vscode-languageclient": "^10.0.0-next.18", "vscode-uri": "^3.1.0" }, "devDependencies": { @@ -85,35 +85,35 @@ "license": "MIT" }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.17", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.17.tgz", - "integrity": "sha512-hSnWKNS8MqMih/HlT7eABuzsvifa9qtGbL8oGH90K9jangtJXx6FKSFIjyWz0Yt8NRz1bGJ7rNM5t8B5+NCSDQ==", + "version": "10.0.0-next.18", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.18.tgz", + "integrity": "sha512-Dpcr0VEEf4SuMW17TFCuKovhvbCx6/tHTnmFyLW1KTJCdVmNG08hXVAmw8Z/izec7TQlzEvzw5PvRfYGzdtr5Q==", "license": "MIT", "dependencies": { "minimatch": "^10.0.3", "semver": "^7.7.1", - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 4c46a1391c9..ab57bc0c9b2 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -994,7 +994,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^10.0.0-next.17", + "vscode-languageclient": "^10.0.0-next.18", "vscode-uri": "^3.1.0" }, "devDependencies": { diff --git a/extensions/css-language-features/server/package-lock.json b/extensions/css-language-features/server/package-lock.json index 91d89604285..60165842552 100644 --- a/extensions/css-language-features/server/package-lock.json +++ b/extensions/css-language-features/server/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.8", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-css-languageservice": "^6.3.9", + "vscode-languageserver": "^10.0.0-next.15", "vscode-uri": "^3.1.0" }, "devDependencies": { @@ -52,9 +52,9 @@ "license": "MIT" }, "node_modules/vscode-css-languageservice": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.8.tgz", - "integrity": "sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.9.tgz", + "integrity": "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -64,33 +64,33 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { - "version": "10.0.0-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.14.tgz", - "integrity": "sha512-1TqBDfRLlAIPs6MR5ISI8z7sWlvGL3oHGm9GAHLNOmBZ2+9pmw0yR9vB44/SYuU4bSizxU24tXDFW+rw9jek4A==", + "version": "10.0.0-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.15.tgz", + "integrity": "sha512-vs+bwci/lM83ZhrR9t8DcZ2AgS2CKx4i6Yw86teKKkqlzlrYWTixuBd9w6H/UP9s8EGBvii0jnbjQd6wsKJ0ig==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index a13b2d0e55d..1d6d0cd2cbc 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -11,8 +11,8 @@ "browser": "./dist/browser/cssServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.8", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-css-languageservice": "^6.3.9", + "vscode-languageserver": "^10.0.0-next.15", "vscode-uri": "^3.1.0" }, "devDependencies": { diff --git a/extensions/css-language-features/server/tsconfig.json b/extensions/css-language-features/server/tsconfig.json index 0b49ec72b8f..97428f411f9 100644 --- a/extensions/css-language-features/server/tsconfig.json +++ b/extensions/css-language-features/server/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "ES2024", diff --git a/extensions/debug-auto-launch/tsconfig.json b/extensions/debug-auto-launch/tsconfig.json index 22c47de77db..a2cbe0e9ea3 100644 --- a/extensions/debug-auto-launch/tsconfig.json +++ b/extensions/debug-auto-launch/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/debug-server-ready/tsconfig.json b/extensions/debug-server-ready/tsconfig.json index 21e3648ffed..36674d29b49 100644 --- a/extensions/debug-server-ready/tsconfig.json +++ b/extensions/debug-server-ready/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/dotenv/cgmanifest.json b/extensions/dotenv/cgmanifest.json index 0d7c5c8dc98..637e505549b 100644 --- a/extensions/dotenv/cgmanifest.json +++ b/extensions/dotenv/cgmanifest.json @@ -37,4 +37,4 @@ } ], "version": 1 -} +} \ No newline at end of file diff --git a/extensions/emmet/tsconfig.json b/extensions/emmet/tsconfig.json index a6353d515d3..212e16da77a 100644 --- a/extensions/emmet/tsconfig.json +++ b/extensions/emmet/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/extension-editing/src/extensionLinter.ts b/extensions/extension-editing/src/extensionLinter.ts index 187100b563f..5c73304b4d8 100644 --- a/extensions/extension-editing/src/extensionLinter.ts +++ b/extensions/extension-editing/src/extensionLinter.ts @@ -33,7 +33,7 @@ const dataUrlsNotValid = l10n.t("Data URLs are not a valid image source."); const relativeUrlRequiresHttpsRepository = l10n.t("Relative image URLs require a repository with HTTPS protocol to be specified in the package.json."); const relativeBadgeUrlRequiresHttpsRepository = l10n.t("Relative badge URLs require a repository with HTTPS protocol to be specified in this package.json."); const apiProposalNotListed = l10n.t("This proposal cannot be used because for this extension the product defines a fixed set of API proposals. You can test your extension but before publishing you MUST reach out to the VS Code team."); -const bumpEngineForImplicitActivationEvents = l10n.t("This activation event can be removed for extensions targeting engine version ^1.75.0 as VS Code will generate these automatically from your package.json contribution declarations."); + const starActivation = l10n.t("Using '*' activation is usually a bad idea as it impacts performance."); const parsingErrorHeader = l10n.t("Error parsing the when-clause:"); @@ -162,13 +162,12 @@ export class ExtensionLinter { if (activationEventsNode?.type === 'array' && activationEventsNode.children) { for (const activationEventNode of activationEventsNode.children) { const activationEvent = getNodeValue(activationEventNode); - const isImplicitActivationSupported = info.engineVersion && info.engineVersion?.majorBase >= 1 && info.engineVersion?.minorBase >= 75; + const isImplicitActivationSupported = info.engineVersion && (info.engineVersion.majorBase > 1 || (info.engineVersion.majorBase === 1 && info.engineVersion.minorBase >= 75)); // Redundant Implicit Activation - if (info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) { + if (isImplicitActivationSupported && info.implicitActivationEvents?.has(activationEvent) && redundantImplicitActivationEventPrefixes.some((prefix) => activationEvent.startsWith(prefix))) { const start = document.positionAt(activationEventNode.offset); const end = document.positionAt(activationEventNode.offset + activationEventNode.length); - const message = isImplicitActivationSupported ? redundantImplicitActivationEvent : bumpEngineForImplicitActivationEvents; - diagnostics.push(new Diagnostic(new Range(start, end), message, isImplicitActivationSupported ? DiagnosticSeverity.Warning : DiagnosticSeverity.Information)); + diagnostics.push(new Diagnostic(new Range(start, end), redundantImplicitActivationEvent, DiagnosticSeverity.Warning)); } // Reserved Implicit Activation diff --git a/extensions/extension-editing/tsconfig.json b/extensions/extension-editing/tsconfig.json index 796a159a61c..e723410bedf 100644 --- a/extensions/extension-editing/tsconfig.json +++ b/extensions/extension-editing/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/git-base/src/api/api1.ts b/extensions/git-base/src/api/api1.ts index 049951c62e8..19038bc1eec 100644 --- a/extensions/git-base/src/api/api1.ts +++ b/extensions/git-base/src/api/api1.ts @@ -14,8 +14,7 @@ export class ApiImpl implements API { constructor(private _model: Model) { } pickRemoteSource(options: PickRemoteSourceOptions): Promise { - // eslint-disable-next-line local/code-no-any-casts - return pickRemoteSource(this._model, options as any); + return pickRemoteSource(this._model, options); } getRemoteSourceActions(url: string): Promise { @@ -31,12 +30,11 @@ export function registerAPICommands(extension: GitBaseExtensionImpl): Disposable const disposables: Disposable[] = []; disposables.push(commands.registerCommand('git-base.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => { - if (!extension.model) { + if (!extension.model || !opts) { return; } - // eslint-disable-next-line local/code-no-any-casts - return pickRemoteSource(extension.model, opts as any); + return pickRemoteSource(extension.model, opts); })); return Disposable.from(...disposables); diff --git a/extensions/git-base/src/remoteSource.ts b/extensions/git-base/src/remoteSource.ts index eb86b27367a..9c6f1b02fa4 100644 --- a/extensions/git-base/src/remoteSource.ts +++ b/extensions/git-base/src/remoteSource.ts @@ -123,6 +123,7 @@ export async function getRemoteSourceActions(model: Model, url: string): Promise return remoteSourceActions; } +export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch?: false | undefined }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions & { branch: true }): Promise; export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise { diff --git a/extensions/git-base/tsconfig.json b/extensions/git-base/tsconfig.json index 796a159a61c..e723410bedf 100644 --- a/extensions/git-base/tsconfig.json +++ b/extensions/git-base/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/git/extension.webpack.config.js b/extensions/git/extension.webpack.config.js index 15cf273015b..34f801e2eca 100644 --- a/extensions/git/extension.webpack.config.js +++ b/extensions/git/extension.webpack.config.js @@ -13,3 +13,5 @@ export default withDefaults({ ['git-editor-main']: './src/git-editor-main.ts' } }); + +export const StripOutSourceMaps = ['dist/askpass-main.js']; diff --git a/extensions/git/package.json b/extensions/git/package.json index 79e40433e56..fd5b557f6dd 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -24,8 +24,8 @@ "contribSourceControlTitleMenu", "contribViewsWelcome", "editSessionIdentityProvider", + "findFiles2", "quickDiffProvider", - "quickInputButtonLocation", "quickPickSortByLabel", "scmActionButton", "scmArtifactProvider", @@ -39,7 +39,8 @@ "tabInputMultiDiff", "tabInputTextMerge", "textEditorDiffInformation", - "timeline" + "timeline", + "workspaceTrust" ], "categories": [ "Other" @@ -321,6 +322,13 @@ "icon": "$(discard)", "enablement": "!operationInProgress" }, + { + "command": "git.delete", + "title": "%command.delete%", + "category": "Git", + "icon": "$(trash)", + "enablement": "!operationInProgress" + }, { "command": "git.commit", "title": "%command.commit%", @@ -448,13 +456,12 @@ { "command": "git.commitMessageAccept", "title": "%command.commitMessageAccept%", - "icon": "$(check)", "category": "Git" }, { "command": "git.commitMessageDiscard", "title": "%command.commitMessageDiscard%", - "icon": "$(discard)", + "icon": "$(close)", "category": "Git" }, { @@ -572,12 +579,6 @@ "category": "Git", "enablement": "!operationInProgress" }, - { - "command": "git.createWorktreeWithDefaults", - "title": "Create Worktree With Defaults", - "category": "Git", - "enablement": "!operationInProgress" - }, { "command": "git.deleteWorktree", "title": "%command.deleteWorktree%", @@ -585,8 +586,8 @@ "enablement": "!operationInProgress" }, { - "command": "git.deleteWorktreeFromPalette", - "title": "%command.deleteWorktreeFromPalette%", + "command": "git.deleteWorktree2", + "title": "%command.deleteWorktree2%", "category": "Git", "enablement": "!operationInProgress" }, @@ -1089,6 +1090,61 @@ "title": "%command.createFrom%", "category": "Git", "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.stashView", + "title": "%command.stashView2%", + "icon": "$(diff-multiple)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.stashApply", + "title": "%command.stashApplyEditor%", + "icon": "$(git-stash-apply)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.stashPop", + "title": "%command.stashPopEditor%", + "icon": "$(git-stash-pop)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.stashDrop", + "title": "%command.stashDropEditor%", + "icon": "$(trash)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.createWorktree", + "title": "%command.createWorktree%", + "icon": "$(plus)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.openWorktree", + "title": "%command.openWorktree2%", + "icon": "$(folder-opened)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.openWorktreeInNewWindow", + "title": "%command.openWorktreeInNewWindow2%", + "icon": "$(folder-opened)", + "category": "Git", + "enablement": "!operationInProgress" + }, + { + "command": "git.repositories.deleteWorktree", + "title": "%command.deleteRef%", + "category": "Git", + "enablement": "!operationInProgress" } ], "continueEditSession": [ @@ -1249,6 +1305,10 @@ "command": "git.rename", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file && scmActiveResourceRepository" }, + { + "command": "git.delete", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && resourceScheme == file" + }, { "command": "git.commit", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1437,10 +1497,6 @@ "command": "git.createWorktree", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, - { - "command": "git.deleteWorktree", - "when": "false" - }, { "command": "git.openWorktree", "when": "false" @@ -1450,9 +1506,13 @@ "when": "false" }, { - "command": "git.deleteWorktreeFromPalette", + "command": "git.deleteWorktree", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.deleteWorktree2", + "when": "false" + }, { "command": "git.deleteRemoteTag", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -1627,19 +1687,19 @@ }, { "command": "git.stashView", - "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.viewChanges", - "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.viewStagedChanges", - "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing" }, { "command": "git.viewUntrackedChanges", - "when": "config.git.enabled && !git.missing && config.multiDiffEditor.experimental.enabled && config.git.untrackedChanges == separate" + "when": "config.git.enabled && !git.missing && config.git.untrackedChanges == separate" }, { "command": "git.viewCommit", @@ -1752,6 +1812,38 @@ { "command": "git.repositories.createFrom", "when": "false" + }, + { + "command": "git.repositories.stashView", + "when": "false" + }, + { + "command": "git.repositories.stashApply", + "when": "false" + }, + { + "command": "git.repositories.stashPop", + "when": "false" + }, + { + "command": "git.repositories.stashDrop", + "when": "false" + }, + { + "command": "git.repositories.createWorktree", + "when": "false" + }, + { + "command": "git.repositories.openWorktree", + "when": "false" + }, + { + "command": "git.repositories.openWorktreeInNewWindow", + "when": "false" + }, + { + "command": "git.repositories.deleteWorktree", + "when": "false" } ], "scm/title": [ @@ -1937,7 +2029,7 @@ "when": "scmProvider == git && scmProviderContext == worktree" }, { - "command": "git.deleteWorktree", + "command": "git.deleteWorktree2", "group": "2_worktree@1", "when": "scmProvider == git && scmProviderContext == worktree" } @@ -1952,23 +2044,59 @@ "command": "git.repositories.createTag", "group": "inline@1", "when": "scmProvider == git && scmArtifactGroup == tags" + }, + { + "submenu": "git.repositories.stash", + "group": "inline@1", + "when": "scmProvider == git && scmArtifactGroup == stashes" + }, + { + "command": "git.repositories.createWorktree", + "group": "inline@1", + "when": "scmProvider == git && scmArtifactGroup == worktrees" } ], "scm/artifact/context": [ { "command": "git.repositories.checkout", "group": "inline@1", - "when": "scmProvider == git" + "when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)" + }, + { + "command": "git.repositories.stashApply", + "alt": "git.repositories.stashPop", + "group": "inline@1", + "when": "scmProvider == git && scmArtifactGroupId == stashes" + }, + { + "command": "git.repositories.stashView", + "group": "1_view@1", + "when": "scmProvider == git && scmArtifactGroupId == stashes" + }, + { + "command": "git.repositories.stashApply", + "group": "2_apply@1", + "when": "scmProvider == git && scmArtifactGroupId == stashes" + }, + { + "command": "git.repositories.stashPop", + "group": "2_apply@2", + "when": "scmProvider == git && scmArtifactGroupId == stashes" + }, + { + "command": "git.repositories.stashDrop", + "group": "3_drop@3", + "when": "scmProvider == git && scmArtifactGroupId == stashes" }, { "command": "git.repositories.checkout", "group": "1_checkout@1", - "when": "scmProvider == git" + "when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)" }, { "command": "git.repositories.checkoutDetached", "group": "1_checkout@2", - "when": "scmProvider == git" + "when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)" }, { "command": "git.repositories.merge", @@ -1998,7 +2126,27 @@ { "command": "git.repositories.compareRef", "group": "4_compare@1", - "when": "scmProvider == git" + "when": "scmProvider == git && (scmArtifactGroupId == branches || scmArtifactGroupId == tags)" + }, + { + "command": "git.repositories.openWorktreeInNewWindow", + "group": "inline@1", + "when": "scmProvider == git && scmArtifactGroupId == worktrees" + }, + { + "command": "git.repositories.openWorktree", + "group": "1_open@1", + "when": "scmProvider == git && scmArtifactGroupId == worktrees" + }, + { + "command": "git.repositories.openWorktreeInNewWindow", + "group": "1_open@2", + "when": "scmProvider == git && scmArtifactGroupId == worktrees" + }, + { + "command": "git.repositories.deleteWorktree", + "group": "2_modify@1", + "when": "scmProvider == git && scmArtifactGroupId == worktrees" } ], "scm/resourceGroup/context": [ @@ -2024,12 +2172,12 @@ }, { "command": "git.viewStagedChanges", - "when": "scmProvider == git && scmResourceGroup == index && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == index", "group": "inline@1" }, { "command": "git.viewChanges", - "when": "scmProvider == git && scmResourceGroup == workingTree && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == workingTree", "group": "inline@1" }, { @@ -2084,7 +2232,7 @@ }, { "command": "git.viewUntrackedChanges", - "when": "scmProvider == git && scmResourceGroup == untracked && config.multiDiffEditor.experimental.enabled", + "when": "scmProvider == git && scmResourceGroup == untracked", "group": "inline@1" }, { @@ -2509,16 +2657,6 @@ "group": "navigation@2", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && !isInDiffEditor && !isMergeEditor && resourceScheme == file && scmActiveResourceHasChanges" }, - { - "command": "git.commitMessageAccept", - "group": "navigation", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" - }, - { - "command": "git.commitMessageDiscard", - "group": "navigation", - "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" - }, { "command": "git.stashApplyEditor", "alt": "git.stashPopEditor", @@ -2591,7 +2729,17 @@ { "command": "git.openMergeEditor", "group": "navigation@-10", - "when": "config.git.enabled && !git.missing && !isInDiffEditor && !isMergeEditor && git.activeResourceHasMergeConflicts" + "when": "config.git.enabled && !git.missing && !isInDiffEditor && !isMergeEditor && resource in git.mergeChanges && git.activeResourceHasMergeConflicts" + }, + { + "command": "git.commitMessageAccept", + "group": "navigation", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" + }, + { + "command": "git.commitMessageDiscard", + "group": "secondary", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0 && editorLangId == git-commit" } ], "multiDiffEditor/resource/title": [ @@ -2643,7 +2791,7 @@ { "command": "git.timeline.viewCommit", "group": "inline", - "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" }, { "command": "git.timeline.openDiff", @@ -2653,7 +2801,7 @@ { "command": "git.timeline.viewCommit", "group": "1_actions@2", - "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection && config.multiDiffEditor.experimental.enabled" + "when": "config.git.enabled && !git.missing && timelineItem =~ /git:file:commit\\b/ && !listMultiSelection" }, { "command": "git.timeline.compareWithSelected", @@ -2918,22 +3066,40 @@ }, { "command": "git.stashView", - "when": "config.multiDiffEditor.experimental.enabled", "group": "5_preview@1" } ], + "git.repositories.stash": [ + { + "command": "git.stash", + "group": "1_stash@1" + }, + { + "command": "git.stashStaged", + "when": "gitVersion2.35", + "group": "2_stash@1" + }, + { + "command": "git.stashIncludeUntracked", + "group": "2_stash@2" + } + ], "git.tags": [ { "command": "git.createTag", - "group": "tags@1" + "group": "1_tags@1" }, { "command": "git.deleteTag", - "group": "tags@2" + "group": "1_tags@2" }, { "command": "git.deleteRemoteTag", - "group": "tags@3" + "group": "1_tags@3" + }, + { + "command": "git.pushTags", + "group": "2_tags@1" } ], "git.worktrees": [ @@ -2954,7 +3120,7 @@ }, { "when": "scmProviderContext == worktree", - "command": "git.deleteWorktree", + "command": "git.deleteWorktree2", "group": "worktrees@2" } ] @@ -2991,6 +3157,11 @@ { "id": "git.worktrees", "label": "%submenu.worktrees%" + }, + { + "id": "git.repositories.stash", + "label": "%submenu.stash%", + "icon": "$(plus)" } ], "configuration": { @@ -3144,6 +3315,11 @@ "description": "%config.confirmSync%", "default": true }, + "git.confirmCommittedDelete": { + "type": "boolean", + "description": "%config.confirmCommittedDelete%", + "default": true + }, "git.countBadge": { "type": "string", "enum": [ @@ -3409,9 +3585,21 @@ "git.detectWorktreesLimit": { "type": "number", "scope": "resource", - "default": 10, + "default": 50, "description": "%config.detectWorktreesLimit%" }, + "git.worktreeIncludeFiles": { + "type": "array", + "items": { + "type": "string" + }, + "default": [], + "markdownDescription": "%config.worktreeIncludeFiles%", + "scope": "resource", + "tags": [ + "experimental" + ] + }, "git.alwaysShowStagedChangesResourceGroup": { "type": "boolean", "scope": "resource", @@ -3730,6 +3918,11 @@ "default": "${subject}, ${authorName} (${authorDateAgo})", "markdownDescription": "%config.blameEditorDecoration.template%" }, + "git.blame.editorDecoration.disableHover": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.blameEditorDecoration.disableHover%" + }, "git.blame.statusBarItem.enabled": { "type": "boolean", "default": true, @@ -3740,6 +3933,11 @@ "default": "${authorName} (${authorDateAgo})", "markdownDescription": "%config.blameStatusBarItem.template%" }, + "git.blame.ignoreWhitespace": { + "type": "boolean", + "default": false, + "markdownDescription": "%config.blameIgnoreWhitespace%" + }, "git.commitShortHashLength": { "type": "number", "default": 7, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 4d037b40220..94a1f61a516 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -11,7 +11,9 @@ "command.close": "Close Repository", "command.closeOtherRepositories": "Close Other Repositories", "command.openWorktree": "Open Worktree in Current Window", + "command.openWorktree2": "Open", "command.openWorktreeInNewWindow": "Open Worktree in New Window", + "command.openWorktreeInNewWindow2": "Open in New Window", "command.refresh": "Refresh", "command.compareWithWorkspace": "Compare with Workspace", "command.openChange": "Open Changes", @@ -34,6 +36,7 @@ "command.unstageChange": "Unstage Change", "command.unstageSelectedRanges": "Unstage Selected Ranges", "command.rename": "Rename", + "command.delete": "Delete", "command.clean": "Discard Changes", "command.cleanAll": "Discard All Changes", "command.cleanAllTracked": "Discard All Tracked Changes", @@ -60,8 +63,8 @@ "command.commitAllNoVerify": "Commit All (No Verify)", "command.commitAllSignedNoVerify": "Commit All (Signed Off, No Verify)", "command.commitAllAmendNoVerify": "Commit All (Amend, No Verify)", - "command.commitMessageAccept": "Accept Commit Message", - "command.commitMessageDiscard": "Discard Commit Message", + "command.commitMessageAccept": "Commit", + "command.commitMessageDiscard": "Cancel", "command.restoreCommitTemplate": "Restore Commit Template", "command.undoCommit": "Undo Last Commit", "command.checkout": "Checkout to...", @@ -83,8 +86,8 @@ "command.deleteTag": "Delete Tag...", "command.migrateWorktreeChanges": "Migrate Worktree Changes...", "command.createWorktree": "Create Worktree...", - "command.deleteWorktree": "Delete Worktree", - "command.deleteWorktreeFromPalette": "Delete Worktree...", + "command.deleteWorktree": "Delete Worktree...", + "command.deleteWorktree2": "Delete Worktree", "command.deleteRemoteTag": "Delete Remote Tag...", "command.fetch": "Fetch", "command.fetchPrune": "Fetch (Prune)", @@ -124,6 +127,7 @@ "command.stashDropAll": "Drop All Stashes...", "command.stashDropEditor": "Drop Stash", "command.stashView": "View Stash...", + "command.stashView2": "View Stash", "command.timelineOpenDiff": "Open Changes", "command.timelineCopyCommitId": "Copy Commit ID", "command.timelineCopyCommitMessage": "Copy Commit Message", @@ -164,6 +168,7 @@ "config.autofetch": "When set to true, commits will automatically be fetched from the default remote of the current Git repository. Setting to `all` will fetch from all remotes.", "config.autofetchPeriod": "Duration in seconds between each automatic git fetch, when `#git.autofetch#` is enabled.", "config.confirmSync": "Confirm before synchronizing Git repositories.", + "config.confirmCommittedDelete": "Confirm before deleting committed files with Git.", "config.countBadge": "Controls the Git count badge.", "config.countBadge.all": "Count all changes.", "config.countBadge.tracked": "Count only tracked changes.", @@ -233,6 +238,7 @@ "config.detectSubmodulesLimit": "Controls the limit of Git submodules detected.", "config.detectWorktrees": "Controls whether to automatically detect Git worktrees.", "config.detectWorktreesLimit": "Controls the limit of Git worktrees detected.", + "config.worktreeIncludeFiles": "Configure [glob patterns](https://aka.ms/vscode-glob-patterns) for files and folders that are included when creating a new worktree. Only files and folders that match the patterns and are listed in `.gitignore` will be copied to the newly created worktree.", "config.alwaysShowStagedChangesResourceGroup": "Always show the Staged Changes resource group.", "config.alwaysSignOff": "Controls the signoff flag for all commits.", "config.ignoreSubmodules": "Ignore modifications to submodules in the file tree.", @@ -300,8 +306,10 @@ "config.similarityThreshold": "Controls the threshold of the similarity index (the amount of additions/deletions compared to the file's size) for changes in a pair of added/deleted files to be considered a rename. **Note:** Requires Git version `2.18.0` or later.", "config.blameEditorDecoration.enabled": "Controls whether to show blame information in the editor using editor decorations.", "config.blameEditorDecoration.template": "Template for the blame information editor decoration. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.blameEditorDecoration.disableHover": "Controls whether to disable the blame information editor decoration hover.", "config.blameStatusBarItem.enabled": "Controls whether to show blame information in the status bar.", "config.blameStatusBarItem.template": "Template for the blame information status bar item. Supported variables:\n\n* `hash`: Commit hash\n\n* `hashShort`: First N characters of the commit hash according to `#git.commitShortHashLength#`\n\n* `subject`: First line of the commit message\n\n* `authorName`: Author name\n\n* `authorEmail`: Author email\n\n* `authorDate`: Author date\n\n* `authorDateAgo`: Time difference between now and the author date\n\n", + "config.blameIgnoreWhitespace": "Controls whether to ignore whitespace changes when computing blame information.", "config.commitShortHashLength": "Controls the length of the commit short hash.", "config.diagnosticsCommitHook.enabled": "Controls whether to check for unresolved diagnostics before committing.", "config.diagnosticsCommitHook.sources": "Controls the list of sources (**Item**) and the minimum severity (**Value**) to be considered before committing. **Note:** To ignore diagnostics from a particular source, add the source to the list and set the minimum severity to `none`.", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index 818dfc536e3..4932b07d5d4 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -7,7 +7,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions, SourceControlHistoryItemDetailsProvider, GitErrorCodes, CloneOptions, CommitShortStat, DiffChange, Worktree, RepositoryKind, RepositoryAccessDetails } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; import { combinedDisposable, filterEvent, mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -52,6 +52,7 @@ export class ApiRepositoryState implements RepositoryState { get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; } get remotes(): Remote[] { return [...this.#repository.remotes]; } get submodules(): Submodule[] { return [...this.#repository.submodules]; } + get worktrees(): Worktree[] { return this.#repository.worktrees; } get rebaseCommit(): Commit | undefined { return this.#repository.rebaseCommit; } get mergeChanges(): Change[] { return this.#repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); } @@ -77,6 +78,7 @@ export class ApiRepository implements Repository { readonly rootUri: Uri; readonly inputBox: InputBox; + readonly kind: RepositoryKind; readonly state: RepositoryState; readonly ui: RepositoryUIState; @@ -86,6 +88,7 @@ export class ApiRepository implements Repository { constructor(repository: BaseRepository) { this.#repository = repository; + this.kind = this.#repository.kind; this.rootUri = Uri.file(this.#repository.root); this.inputBox = new ApiInputBox(this.#repository.inputBox); this.state = new ApiRepositoryState(this.#repository); @@ -97,8 +100,11 @@ export class ApiRepository implements Repository { filterEvent(this.#repository.onDidRunOperation, e => e.operation.kind === OperationKind.Checkout || e.operation.kind === OperationKind.CheckoutTracking), () => null); } - apply(patch: string, reverse?: boolean): Promise { - return this.#repository.apply(patch, reverse); + apply(patch: string, reverse?: boolean): Promise; + apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise; + apply(patch: string, reverseOrOptions?: boolean | { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise { + const options = typeof reverseOrOptions === 'boolean' ? { reverse: reverseOrOptions } : reverseOrOptions; + return this.#repository.apply(patch, options); } getConfigs(): Promise<{ key: string; value: string }[]> { @@ -163,6 +169,10 @@ export class ApiRepository implements Repository { return this.#repository.diffWithHEAD(path); } + diffWithHEADShortStats(path?: string): Promise { + return this.#repository.diffWithHEADShortStats(path); + } + diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffWith(ref: string, path?: string): Promise { @@ -175,6 +185,10 @@ export class ApiRepository implements Repository { return this.#repository.diffIndexWithHEAD(path); } + diffIndexWithHEADShortStats(path?: string): Promise { + return this.#repository.diffIndexWithHEADShortStats(path); + } + diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffIndexWith(ref: string, path?: string): Promise { @@ -191,6 +205,14 @@ export class ApiRepository implements Repository { return this.#repository.diffBetween(ref1, ref2, path); } + diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise { + return this.#repository.diffBetweenPatch(ref1, ref2, path); + } + + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise { + return this.#repository.diffBetweenWithStats(ref1, ref2, path); + } + hashObject(data: string): Promise { return this.#repository.hashObject(data); } @@ -299,6 +321,10 @@ export class ApiRepository implements Repository { return this.#repository.mergeAbort(); } + createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise { + return this.#repository.createStash(options?.message, options?.includeUntracked, options?.staged); + } + applyStash(index?: number): Promise { return this.#repository.applyStash(index); } @@ -310,6 +336,18 @@ export class ApiRepository implements Repository { dropStash(index?: number): Promise { return this.#repository.dropStash(index); } + + createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise { + return this.#repository.createWorktree(options); + } + + deleteWorktree(path: string, options?: { force?: boolean }): Promise { + return this.#repository.deleteWorktree(path, options); + } + + migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise { + return this.#repository.migrateChanges(sourceRepositoryPath, options); + } } export class ApiGit implements Git { @@ -365,6 +403,10 @@ export class ApiImpl implements API { return this.#model.repositories.map(r => new ApiRepository(r)); } + get recentRepositories(): Iterable { + return this.#model.repositoryCache.recentRepositories; + } + toGitUri(uri: Uri, ref: string): Uri { return toGitUri(uri, ref); } @@ -525,6 +567,7 @@ export function registerAPICommands(extension: GitExtensionImpl): Disposable { refs: state.refs.map(ref), remotes: state.remotes, submodules: state.submodules, + worktrees: state.worktrees, rebaseCommit: state.rebaseCommit, mergeChanges: state.mergeChanges.map(change), indexChanges: state.indexChanges.map(change), diff --git a/extensions/git/src/api/extension.ts b/extensions/git/src/api/extension.ts index 3bbb717e23f..a716fa00dae 100644 --- a/extensions/git/src/api/extension.ts +++ b/extensions/git/src/api/extension.ts @@ -9,13 +9,13 @@ import { ApiRepository, ApiImpl } from './api1'; import { Event, EventEmitter } from 'vscode'; import { CloneManager } from '../cloneManager'; -function deprecated(original: any, context: ClassMemberDecoratorContext) { - if (context.kind !== 'method') { +function deprecated(original: unknown, context: ClassMemberDecoratorContext) { + if (typeof original !== 'function' || context.kind !== 'method') { throw new Error('not supported'); } const key = context.name.toString(); - return function (this: any, ...args: any[]): any { + return function (this: unknown, ...args: unknown[]) { console.warn(`Git extension API method '${key}' is deprecated.`); return original.apply(this, args); }; diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index c59c2f90658..287dd4399bf 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -76,6 +76,14 @@ export interface Remote { readonly isReadOnly: boolean; } +export interface Worktree { + readonly name: string; + readonly path: string; + readonly ref: string; + readonly main: boolean; + readonly detached: boolean; +} + export const enum Status { INDEX_MODIFIED, INDEX_ADDED, @@ -113,11 +121,19 @@ export interface Change { readonly status: Status; } +export interface DiffChange extends Change { + readonly insertions: number; + readonly deletions: number; +} + +export type RepositoryKind = 'repository' | 'submodule' | 'worktree'; + export interface RepositoryState { readonly HEAD: Branch | undefined; readonly refs: Ref[]; readonly remotes: Remote[]; readonly submodules: Submodule[]; + readonly worktrees: Worktree[]; readonly rebaseCommit: Commit | undefined; readonly mergeChanges: Change[]; @@ -133,6 +149,11 @@ export interface RepositoryUIState { readonly onDidChange: Event; } +export interface RepositoryAccessDetails { + readonly rootUri: Uri; + readonly lastAccessTime: number; +} + /** * Log options. */ @@ -156,6 +177,11 @@ export interface CommitOptions { all?: boolean | 'tracked'; amend?: boolean; signoff?: boolean; + /** + * true - sign the commit + * false - do not sign the commit + * undefined - use the repository/global git config + */ signCommit?: boolean; empty?: boolean; noVerify?: boolean; @@ -200,7 +226,7 @@ export interface RefQuery { readonly contains?: string; readonly count?: number; readonly pattern?: string | string[]; - readonly sort?: 'alphabetically' | 'committerdate'; + readonly sort?: 'alphabetically' | 'committerdate' | 'creatordate'; } export interface BranchQuery extends RefQuery { @@ -213,6 +239,7 @@ export interface Repository { readonly inputBox: InputBox; readonly state: RepositoryState; readonly ui: RepositoryUIState; + readonly kind: RepositoryKind; readonly onDidCommit: Event; readonly onDidCheckout: Event; @@ -234,18 +261,23 @@ export interface Repository { clean(paths: string[]): Promise; apply(patch: string, reverse?: boolean): Promise; + apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean; }): Promise; diff(cached?: boolean): Promise; diffWithHEAD(): Promise; diffWithHEAD(path: string): Promise; + diffWithHEADShortStats(path?: string): Promise; diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffIndexWithHEAD(): Promise; diffIndexWithHEAD(path: string): Promise; + diffIndexWithHEADShortStats(path?: string): Promise; diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffBlobs(object1: string, object2: string): Promise; diffBetween(ref1: string, ref2: string): Promise; diffBetween(ref1: string, ref2: string, path: string): Promise; + diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise; + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise; hashObject(data: string): Promise; @@ -262,7 +294,7 @@ export interface Repository { getMergeBase(ref1: string, ref2: string): Promise; - tag(name: string, upstream: string): Promise; + tag(name: string, message: string, ref?: string | undefined): Promise; deleteTag(name: string): Promise; status(): Promise; @@ -284,9 +316,15 @@ export interface Repository { merge(ref: string): Promise; mergeAbort(): Promise; + createStash(options?: { message?: string; includeUntracked?: boolean; staged?: boolean }): Promise; applyStash(index?: number): Promise; popStash(index?: number): Promise; dropStash(index?: number): Promise; + + createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise; + deleteWorktree(path: string, options?: { force?: boolean }): Promise; + + migrateChanges(sourceRepositoryPath: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise; } export interface RemoteSource { @@ -372,6 +410,7 @@ export interface API { readonly onDidPublish: Event; readonly git: Git; readonly repositories: Repository[]; + readonly recentRepositories: Iterable; readonly onDidOpenRepository: Event; readonly onDidCloseRepository: Event; diff --git a/extensions/git/src/artifactProvider.ts b/extensions/git/src/artifactProvider.ts index a935fda047c..f63899efa3e 100644 --- a/extensions/git/src/artifactProvider.ts +++ b/extensions/git/src/artifactProvider.ts @@ -3,24 +3,66 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable } from 'vscode'; -import { dispose, fromNow, IDisposable } from './util'; +import { LogOutputChannel, SourceControlArtifactProvider, SourceControlArtifactGroup, SourceControlArtifact, Event, EventEmitter, ThemeIcon, l10n, workspace, Uri, Disposable, Command } from 'vscode'; +import { coalesce, dispose, filterEvent, IDisposable, isCopilotWorktree } from './util'; import { Repository } from './repository'; -import { Ref, RefType } from './api/git'; +import { Ref, RefType, Worktree } from './api/git'; +import { OperationKind } from './operation'; -function getArtifactDescription(ref: Ref, shortCommitLength: number): string { - const segments: string[] = []; - if (ref.commitDetails?.commitDate) { - segments.push(fromNow(ref.commitDetails.commitDate)); +/** + * Sorts refs like a directory tree: refs with more path segments (directories) appear first + * and are sorted alphabetically, while refs at the same level (files) maintain insertion order. + * Refs without '/' maintain their insertion order and appear after refs with '/'. + */ +function sortRefByName(refA: Ref, refB: Ref): number { + const nameA = refA.name ?? ''; + const nameB = refB.name ?? ''; + + const lastSlashA = nameA.lastIndexOf('/'); + const lastSlashB = nameB.lastIndexOf('/'); + + // Neither ref has a slash, maintain insertion order + if (lastSlashA === -1 && lastSlashB === -1) { + return 0; } - if (ref.commit) { - segments.push(ref.commit.substring(0, shortCommitLength)); + + // Ref with a slash comes first + if (lastSlashA !== -1 && lastSlashB === -1) { + return -1; + } else if (lastSlashA === -1 && lastSlashB !== -1) { + return 1; + } + + // Both have slashes + // Get directory segments + const segmentsA = nameA.substring(0, lastSlashA).split('/'); + const segmentsB = nameB.substring(0, lastSlashB).split('/'); + + // Compare directory segments + for (let index = 0; index < Math.min(segmentsA.length, segmentsB.length); index++) { + const result = segmentsA[index].localeCompare(segmentsB[index]); + if (result !== 0) { + return result; + } } - if (ref.commitDetails?.message) { - segments.push(ref.commitDetails.message.split('\n')[0]); + + // Directory with more segments comes first + if (segmentsA.length !== segmentsB.length) { + return segmentsB.length - segmentsA.length; } - return segments.join(' \u2022 '); + // Insertion order + return 0; +} + +function sortByWorktreeTypeAndNameAsc(a: Worktree, b: Worktree): number { + if (a.main && !b.main) { + return -1; + } else if (!a.main && b.main) { + return 1; + } else { + return a.name.localeCompare(b.name); + } } export class GitArtifactProvider implements SourceControlArtifactProvider, IDisposable { @@ -35,8 +77,10 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp private readonly logger: LogOutputChannel ) { this._groups = [ - { id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch') }, - { id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag') } + { id: 'branches', name: l10n.t('Branches'), icon: new ThemeIcon('git-branch'), supportsFolders: true }, + { id: 'stashes', name: l10n.t('Stashes'), icon: new ThemeIcon('git-stash'), supportsFolders: false }, + { id: 'tags', name: l10n.t('Tags'), icon: new ThemeIcon('tag'), supportsFolders: true }, + { id: 'worktrees', name: l10n.t('Worktrees'), icon: new ThemeIcon('worktree'), supportsFolders: false } ]; this._disposables.push(this._onDidChangeArtifacts); @@ -52,6 +96,17 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp this._onDidChangeArtifacts.fire(Array.from(groups)); })); + + const onDidRunWriteOperation = filterEvent( + repository.onDidRunOperation, e => !e.operation.readOnly); + + this._disposables.push(onDidRunWriteOperation(result => { + if (result.operation.kind === OperationKind.Stash) { + this._onDidChangeArtifacts.fire(['stashes']); + } else if (result.operation.kind === OperationKind.Worktree) { + this._onDidChangeArtifacts.fire(['worktrees']); + } + })); } provideArtifactGroups(): SourceControlArtifactGroup[] { @@ -65,27 +120,66 @@ export class GitArtifactProvider implements SourceControlArtifactProvider, IDisp try { if (group === 'branches') { const refs = await this.repository - .getRefs({ pattern: 'refs/heads', includeCommitDetails: true }); + .getRefs({ pattern: 'refs/heads', includeCommitDetails: true, sort: 'creatordate' }); - return refs.map(r => ({ + return refs.sort(sortRefByName).map(r => ({ id: `refs/heads/${r.name}`, name: r.name ?? r.commit ?? '', - description: getArtifactDescription(r, shortCommitLength), + description: coalesce([ + r.commit?.substring(0, shortCommitLength), + r.commitDetails?.message.split('\n')[0] + ]).join(' \u2022 '), icon: this.repository.HEAD?.type === RefType.Head && r.name === this.repository.HEAD?.name ? new ThemeIcon('target') - : new ThemeIcon('git-branch') + : new ThemeIcon('git-branch'), + timestamp: r.commitDetails?.commitDate?.getTime() })); } else if (group === 'tags') { const refs = await this.repository - .getRefs({ pattern: 'refs/tags', includeCommitDetails: true }); + .getRefs({ pattern: 'refs/tags', includeCommitDetails: true, sort: 'creatordate' }); - return refs.map(r => ({ + return refs.sort(sortRefByName).map(r => ({ id: `refs/tags/${r.name}`, name: r.name ?? r.commit ?? '', - description: getArtifactDescription(r, shortCommitLength), + description: coalesce([ + r.commit?.substring(0, shortCommitLength), + r.commitDetails?.message.split('\n')[0] + ]).join(' \u2022 '), icon: this.repository.HEAD?.type === RefType.Tag && r.name === this.repository.HEAD?.name ? new ThemeIcon('target') - : new ThemeIcon('tag') + : new ThemeIcon('tag'), + timestamp: r.commitDetails?.commitDate?.getTime() + })); + } else if (group === 'stashes') { + const stashes = await this.repository.getStashes(); + + return stashes.map(s => ({ + id: `stash@{${s.index}}`, + name: s.description, + description: s.branchName, + icon: new ThemeIcon('git-stash'), + timestamp: s.commitDate?.getTime(), + command: { + title: l10n.t('View Stash'), + command: 'git.repositories.stashView' + } satisfies Command + })); + } else if (group === 'worktrees') { + const worktrees = await this.repository.getWorktreeDetails(); + + return worktrees.sort(sortByWorktreeTypeAndNameAsc).map(w => ({ + id: w.path, + name: w.name, + description: coalesce([ + w.detached ? l10n.t('detached') : w.ref.substring(11), + w.commitDetails?.hash.substring(0, shortCommitLength), + w.commitDetails?.message.split('\n')[0] + ]).join(' \u2022 '), + icon: w.main + ? new ThemeIcon('repo') + : isCopilotWorktree(w.path) + ? new ThemeIcon('chat-sparkle') + : new ThemeIcon('worktree') })); } } catch (err) { diff --git a/extensions/git/src/askpass-main.ts b/extensions/git/src/askpass-main.ts index cb93adf2821..21402fbaf34 100644 --- a/extensions/git/src/askpass-main.ts +++ b/extensions/git/src/askpass-main.ts @@ -6,7 +6,7 @@ import * as fs from 'fs'; import { IPCClient } from './ipc/ipcClient'; -function fatal(err: any): void { +function fatal(err: unknown): void { console.error('Missing or invalid credentials.'); console.error(err); process.exit(1); diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index d9c852031e3..1cb1890e242 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -5,10 +5,10 @@ import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n, LogOutputChannel } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable, extractFilePathFromArgs } from './util'; -import * as path from 'path'; import { IIPCHandler, IIPCServer } from './ipc/ipcServer'; import { CredentialsProvider, Credentials } from './api/git'; import { ITerminalEnvironmentProvider } from './terminal'; +import { AskpassPaths } from './askpassManager'; export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { @@ -20,23 +20,30 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider { readonly featureDescription = 'git auth provider'; - constructor(private ipc: IIPCServer | undefined, private readonly logger: LogOutputChannel) { + constructor( + private ipc: IIPCServer | undefined, + private readonly logger: LogOutputChannel, + askpassPaths: AskpassPaths + ) { if (ipc) { this.disposable = ipc.registerHandler('askpass', this); } + const askpassScript = this.ipc ? askpassPaths.askpass : askpassPaths.askpassEmpty; + const sshAskpassScript = this.ipc ? askpassPaths.sshAskpass : askpassPaths.sshAskpassEmpty; + this.env = { // GIT_ASKPASS - GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'), + GIT_ASKPASS: askpassScript, // VSCODE_GIT_ASKPASS VSCODE_GIT_ASKPASS_NODE: process.execPath, VSCODE_GIT_ASKPASS_EXTRA_ARGS: '', - VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js') + VSCODE_GIT_ASKPASS_MAIN: askpassPaths.askpassMain }; this.sshEnv = { // SSH_ASKPASS - SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'), + SSH_ASKPASS: sshAskpassScript, SSH_ASKPASS_REQUIRE: 'force' }; } diff --git a/extensions/git/src/askpassManager.ts b/extensions/git/src/askpassManager.ts new file mode 100644 index 00000000000..9b610346420 --- /dev/null +++ b/extensions/git/src/askpassManager.ts @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as cp from 'child_process'; +import { env, LogOutputChannel } from 'vscode'; + +/** + * Manages content-addressed copies of askpass scripts in a user-controlled folder. + * + * This solves the problem on Windows user/system setups where environment variables + * like GIT_ASKPASS point to scripts inside the VS Code installation directory, which + * changes on each update. By copying the scripts to a content-addressed location in + * user storage, the paths remain stable across updates (as long as the script contents + * don't change). + * + * This feature is only enabled on Windows user and system setups (not archive or portable) + * because those are the only configurations where the installation path changes on each update. + * + * Security considerations: + * - Scripts are placed in user-controlled storage (not TEMP to avoid TOCTOU attacks) + * - On Windows, ACLs are set to allow only the current user to modify the files + */ + +/** + * Checks if the current VS Code installation is a Windows user or system setup. + * Returns false for archive, portable, or non-Windows installations. + */ +function isWindowsUserOrSystemSetup(): boolean { + if (process.platform !== 'win32') { + return false; + } + + try { + const productJsonPath = path.join(env.appRoot, 'product.json'); + const productJson = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); + const target = productJson.target as string | undefined; + + // Target is 'user' or 'system' for Inno Setup installations. + // Archive and portable builds don't have a target property. + return target === 'user' || target === 'system'; + } catch { + // If we can't read product.json, assume not applicable + return false; + } +} + +interface SourceAskpassPaths { + askpass: string; + askpassMain: string; + sshAskpass: string; + askpassEmpty: string; + sshAskpassEmpty: string; +} + +/** + * Computes a SHA-256 hash of the combined contents of all askpass-related files. + * This hash is used to create content-addressed directories. + */ +function computeContentHash(sourcePaths: SourceAskpassPaths): string { + const hash = crypto.createHash('sha256'); + + // Hash all source files in a deterministic order + const files = [ + sourcePaths.askpass, + sourcePaths.askpassMain, + sourcePaths.sshAskpass, + sourcePaths.askpassEmpty, + sourcePaths.sshAskpassEmpty, + ]; + + for (const file of files) { + const content = fs.readFileSync(file); + hash.update(content); + // Include filename in hash to ensure different files with same content produce different hash + hash.update(path.basename(file)); + } + + return hash.digest('hex').substring(0, 16); +} + +/** + * Sets restrictive file permissions on Windows using icacls. + * Grants full control only to the current user and removes inherited permissions. + */ +async function setWindowsPermissions(filePath: string, logger: LogOutputChannel): Promise { + const username = process.env['USERNAME']; + if (!username) { + logger.warn(`[askpassManager] Cannot set Windows permissions: USERNAME not set`); + return; + } + + return new Promise((resolve) => { + // icacls /inheritance:r /grant:r ":F" + // /inheritance:r - Remove all inherited permissions + // /grant:r - Replace (not add) permissions, giving Full control to user + const args = [filePath, '/inheritance:r', '/grant:r', `${username}:F`]; + + cp.execFile('icacls', args, (error, _stdout, stderr) => { + if (error) { + logger.warn(`[askpassManager] Failed to set permissions on ${filePath}: ${error.message}`); + if (stderr) { + logger.warn(`[askpassManager] icacls stderr: ${stderr}`); + } + } else { + logger.trace(`[askpassManager] Set permissions on ${filePath}`); + } + resolve(); + }); + }); +} + +/** + * Copies a file to the destination, creating parent directories as needed. + * Sets restrictive permissions on the copied file. + */ +async function copyFileSecure( + source: string, + dest: string, + logger: LogOutputChannel +): Promise { + const content = await fs.promises.readFile(source); + await fs.promises.writeFile(dest, content); + await setWindowsPermissions(dest, logger); +} + +/** + * Updates the modification time of a directory to mark it as recently used. + */ +async function updateDirectoryMtime(dirPath: string, logger: LogOutputChannel): Promise { + try { + const now = new Date(); + await fs.promises.utimes(dirPath, now, now); + logger.trace(`[askpassManager] Updated mtime for ${dirPath}`); + } catch (err) { + logger.warn(`[askpassManager] Failed to update mtime for ${dirPath}: ${err}`); + } +} + +/** + * Garbage collects old content-addressed askpass directories that haven't been used in 7 days. + * This prevents accumulation of old versions when VS Code updates. + */ +async function garbageCollectOldDirectories( + askpassBaseDir: string, + currentHash: string, + logger: LogOutputChannel +): Promise { + try { + // Check if the askpass base directory exists + try { + await fs.promises.access(askpassBaseDir); + } catch { + // Directory doesn't exist, nothing to clean + return; + } + + const entries = await fs.promises.readdir(askpassBaseDir); + const sevenDaysAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); + + for (const entry of entries) { + // Skip the current content-addressed directory + if (entry === currentHash) { + continue; + } + + const entryPath = path.join(askpassBaseDir, entry); + + try { + const stat = await fs.promises.stat(entryPath); + + // Only process directories + if (!stat.isDirectory()) { + continue; + } + + // Check if the directory hasn't been used in 7 days + if (stat.mtime.getTime() < sevenDaysAgo) { + logger.info(`[askpassManager] Removing old askpass directory: ${entryPath} (last used: ${stat.mtime.toISOString()})`); + + // Remove the directory and all its contents + await fs.promises.rm(entryPath, { recursive: true, force: true }); + } else { + logger.trace(`[askpassManager] Keeping askpass directory: ${entryPath} (last used: ${stat.mtime.toISOString()})`); + } + } catch (err) { + logger.warn(`[askpassManager] Failed to process/remove directory ${entryPath}: ${err}`); + } + } + } catch (err) { + logger.warn(`[askpassManager] Failed to garbage collect old directories: ${err}`); + } +} + +export interface AskpassPaths { + readonly askpass: string; + readonly askpassMain: string; + readonly sshAskpass: string; + readonly askpassEmpty: string; + readonly sshAskpassEmpty: string; +} + +/** + * Ensures that content-addressed copies of askpass scripts exist in user storage. + * Returns the paths to the content-addressed copies. + * + * @param sourceDir The directory containing the original askpass scripts (__dirname) + * @param storageDir The user-controlled storage directory (context.storageUri.fsPath) + * @param logger Logger for diagnostic output + */ +export async function ensureAskpassScripts( + sourceDir: string, + storageDir: string, + logger: LogOutputChannel +): Promise { + const sourcePaths: SourceAskpassPaths = { + askpass: path.join(sourceDir, 'askpass.sh'), + askpassMain: path.join(sourceDir, 'askpass-main.js'), + sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'), + }; + + // Compute content hash + const contentHash = computeContentHash(sourcePaths); + logger.trace(`[askpassManager] Content hash: ${contentHash}`); + + // Create content-addressed directory + const askpassBaseDir = path.join(storageDir, 'askpass'); + const askpassDir = path.join(askpassBaseDir, contentHash); + + const destPaths: AskpassPaths = { + askpass: path.join(askpassDir, 'askpass.sh'), + askpassMain: path.join(askpassDir, 'askpass-main.js'), + sshAskpass: path.join(askpassDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(askpassDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(askpassDir, 'ssh-askpass-empty.sh'), + }; + + // Check if already exists (fast path for subsequent activations) + try { + const stat = await fs.promises.stat(destPaths.askpass); + if (stat.isFile()) { + logger.trace(`[askpassManager] Using existing content-addressed askpass at ${askpassDir}`); + + // Update mtime to mark this directory as recently used + await updateDirectoryMtime(askpassDir, logger); + + return destPaths; + } + } catch { + // Directory doesn't exist, create it + } + + logger.info(`[askpassManager] Creating content-addressed askpass scripts at ${askpassDir}`); + + // Create directory and set Windows ACLs + await fs.promises.mkdir(askpassDir, { recursive: true }); + await setWindowsPermissions(askpassDir, logger); + + // Copy all files + await Promise.all([ + copyFileSecure(sourcePaths.askpass, destPaths.askpass, logger), + copyFileSecure(sourcePaths.askpassMain, destPaths.askpassMain, logger), + copyFileSecure(sourcePaths.sshAskpass, destPaths.sshAskpass, logger), + copyFileSecure(sourcePaths.askpassEmpty, destPaths.askpassEmpty, logger), + copyFileSecure(sourcePaths.sshAskpassEmpty, destPaths.sshAskpassEmpty, logger), + ]); + + logger.info(`[askpassManager] Successfully created content-addressed askpass scripts`); + + // Update mtime to mark this directory as recently used + await updateDirectoryMtime(askpassDir, logger); + + // Garbage collect old directories + await garbageCollectOldDirectories(askpassBaseDir, contentHash, logger); + + return destPaths; +} + +/** + * Returns the askpass script paths. Uses content-addressed copies + * on Windows user/system setups (to keep paths stable across updates), + * otherwise returns paths relative to the source directory. + */ +export async function getAskpassPaths( + sourceDir: string, + storagePath: string | undefined, + logger: LogOutputChannel +): Promise { + // Try content-addressed paths on Windows user/system setups + if (storagePath && isWindowsUserOrSystemSetup()) { + try { + return await ensureAskpassScripts(sourceDir, storagePath, logger); + } catch (err) { + logger.error(`[askpassManager] Failed to create content-addressed askpass scripts: ${err}`); + } + } + + // Fallback to source directory paths (for development or non-Windows setups) + return { + askpass: path.join(sourceDir, 'askpass.sh'), + askpassMain: path.join(sourceDir, 'askpass-main.js'), + sshAskpass: path.join(sourceDir, 'ssh-askpass.sh'), + askpassEmpty: path.join(sourceDir, 'askpass-empty.sh'), + sshAskpassEmpty: path.join(sourceDir, 'ssh-askpass-empty.sh'), + }; +} diff --git a/extensions/git/src/blame.ts b/extensions/git/src/blame.ts index f5814370251..c6f121a01b3 100644 --- a/extensions/git/src/blame.ts +++ b/extensions/git/src/blame.ts @@ -15,7 +15,7 @@ import { getWorkingTreeAndIndexDiffInformation, getWorkingTreeDiffInformation } import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { AvatarQuery, AvatarQueryCommit } from './api/git'; import { LRUCache } from './cache'; -import { AVATAR_SIZE, getHistoryItemHover, getHistoryItemHoverCommitHashCommands, processHistoryItemRemoteHoverCommands } from './historyProvider'; +import { AVATAR_SIZE, getCommitHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover'; function lineRangesContainLine(changes: readonly TextEditorChange[], lineNumber: number): boolean { return changes.some(c => c.modified.startLineNumber <= lineNumber && lineNumber < c.modified.endLineNumberExclusive); @@ -126,6 +126,10 @@ interface LineBlameInformation { class GitBlameInformationCache { private readonly _cache = new Map>(); + clear(): void { + this._cache.clear(); + } + delete(repository: Repository): boolean { return this._cache.delete(repository); } @@ -196,7 +200,9 @@ export class GitBlameController { } satisfies BlameInformationTemplateTokens; return template.replace(/\$\{(.+?)\}/g, (_, token) => { - return token in templateTokens ? templateTokens[token as keyof BlameInformationTemplateTokens] : `\${${token}}`; + return templateTokens.hasOwnProperty(token) + ? templateTokens[token as keyof BlameInformationTemplateTokens] + : `\${${token}}`; }); } @@ -249,8 +255,8 @@ export class GitBlameController { // Commands const commands: Command[][] = [ - getHistoryItemHoverCommitHashCommands(documentUri, hash), - processHistoryItemRemoteHoverCommands(remoteHoverCommands, hash) + getHoverCommitHashCommands(documentUri, hash), + processHoverRemoteCommands(remoteHoverCommands, hash) ]; commands.push([{ @@ -260,16 +266,24 @@ export class GitBlameController { arguments: ['git.blame'] }] satisfies Command[]); - return getHistoryItemHover(commitAvatar, authorName, authorEmail, authorDate, message, commitInformation?.shortStat, commands); + return getCommitHover(commitAvatar, authorName, authorEmail, authorDate, message, commitInformation?.shortStat, commands); } private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { if (e && + !e.affectsConfiguration('git.blame.ignoreWhitespace') && !e.affectsConfiguration('git.blame.editorDecoration.enabled') && !e.affectsConfiguration('git.blame.statusBarItem.enabled')) { return; } + // Clear cache when ignoreWhitespace setting changes + if (e && e.affectsConfiguration('git.blame.ignoreWhitespace')) { + this._repositoryBlameCache.clear(); + this._updateTextEditorBlameInformation(window.activeTextEditor); + return; + } + const config = workspace.getConfiguration('git'); const editorDecorationEnabled = config.get('blame.editorDecoration.enabled') === true; const statusBarItemEnabled = config.get('blame.statusBarItem.enabled') === true; @@ -576,7 +590,8 @@ class GitBlameEditorDecoration implements HoverProvider { private _onDidChangeConfiguration(e?: ConfigurationChangeEvent): void { if (e && !e.affectsConfiguration('git.commitShortHashLength') && - !e.affectsConfiguration('git.blame.editorDecoration.template')) { + !e.affectsConfiguration('git.blame.editorDecoration.template') && + !e.affectsConfiguration('git.blame.editorDecoration.disableHover')) { return; } @@ -640,7 +655,9 @@ class GitBlameEditorDecoration implements HoverProvider { private _registerHoverProvider(): void { this._hoverDisposable?.dispose(); - if (window.activeTextEditor && isResourceSchemeSupported(window.activeTextEditor.document.uri)) { + const config = workspace.getConfiguration('git'); + const disableHover = config.get('blame.editorDecoration.disableHover', false); + if (!disableHover && window.activeTextEditor && isResourceSchemeSupported(window.activeTextEditor.document.uri)) { this._hoverDisposable = languages.registerHoverProvider({ pattern: window.activeTextEditor.document.uri.fsPath }, this); diff --git a/extensions/git/src/cache.ts b/extensions/git/src/cache.ts index df0c0df5561..ad2db75edc8 100644 --- a/extensions/git/src/cache.ts +++ b/extensions/git/src/cache.ts @@ -132,7 +132,7 @@ class LinkedMap implements Map { return item.value; } - forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: unknown): void { const state = this._state; let current = this._head; while (current) { diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 37f8732ee34..b3e05e0016b 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -9,12 +9,12 @@ import { Command, commands, Disposable, MessageOptions, Position, QuickPickItem, import TelemetryReporter from '@vscode/extension-telemetry'; import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator'; import { ForcePushMode, GitErrorCodes, RefType, Status, CommitOptions, RemoteSourcePublisher, Remote, Branch, Ref } from './api/git'; -import { Git, Stash, Worktree } from './git'; +import { Git, GitError, Stash, Worktree } from './git'; import { Model } from './model'; import { GitResourceGroup, Repository, Resource, ResourceGroupType } from './repository'; import { DiffEditorSelectionHunkToolbarContext, LineChange, applyLineChanges, getIndexDiffInformation, getModifiedRange, getWorkingTreeDiffInformation, intersectDiffWithRange, invertLineChange, toLineChanges, toLineRanges, compareLineChanges } from './staging'; import { fromGitUri, toGitUri, isGitUri, toMergeUris, toMultiFileDiffEditorUris } from './uri'; -import { DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util'; +import { coalesce, DiagnosticSeverityConfig, dispose, fromNow, getHistoryItemDisplayName, getStashDescription, grep, isDefined, isDescendant, isLinuxSnap, isRemote, isWindows, pathEquals, relativePath, subject, toDiagnosticSeverity, truncate } from './util'; import { GitTimelineItem } from './timelineProvider'; import { ApiRepository } from './api/api1'; import { getRemoteSourceActions, pickRemoteSource } from './remoteSource'; @@ -58,19 +58,6 @@ class RefItemSeparator implements QuickPickItem { constructor(private readonly refType: RefType) { } } -class WorktreeItem implements QuickPickItem { - - get label(): string { - return `$(list-tree) ${this.worktree.name}`; - } - - get description(): string { - return this.worktree.path; - } - - constructor(readonly worktree: Worktree) { } -} - class RefItem implements QuickPickItem { get label(): string { @@ -240,25 +227,46 @@ class RemoteTagDeleteItem extends RefItem { } } +class WorktreeItem implements QuickPickItem { + + get label(): string { + return `$(list-tree) ${this.worktree.name}`; + } + + get description(): string | undefined { + return this.worktree.path; + } + + constructor(readonly worktree: Worktree) { } +} + class WorktreeDeleteItem extends WorktreeItem { + override get description(): string | undefined { + if (!this.worktree.commitDetails) { + return undefined; + } + + return coalesce([ + this.worktree.detached ? l10n.t('detached') : this.worktree.ref.substring(11), + this.worktree.commitDetails.hash.substring(0, this.shortCommitLength), + this.worktree.commitDetails.message.split('\n')[0] + ]).join(' \u2022 '); + } + + get detail(): string { + return this.worktree.path; + } + + constructor(worktree: Worktree, private readonly shortCommitLength: number) { + super(worktree); + } + async run(mainRepository: Repository): Promise { if (!this.worktree.path) { return; } - try { - await mainRepository.deleteWorktree(this.worktree.path); - } catch (err) { - if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) { - const forceDelete = l10n.t('Force Delete'); - const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?'); - const choice = await window.showWarningMessage(message, { modal: true }, forceDelete); - - if (choice === forceDelete) { - await mainRepository.deleteWorktree(this.worktree.path, { force: true }); - } - } - } + await mainRepository.deleteWorktree(this.worktree.path); } } @@ -345,7 +353,7 @@ class RepositoryItem implements QuickPickItem { class StashItem implements QuickPickItem { get label(): string { return `#${this.stash.index}: ${this.stash.description}`; } - get description(): string | undefined { return this.stash.branchName; } + get description(): string | undefined { return getStashDescription(this.stash); } constructor(readonly stash: Stash) { } } @@ -365,8 +373,8 @@ interface ScmCommand { const Commands: ScmCommand[] = []; function command(commandId: string, options: ScmCommandOptions = {}): Function { - return (value: any, context: ClassMethodDecoratorContext) => { - if (context.kind !== 'method') { + return (value: unknown, context: ClassMethodDecoratorContext) => { + if (typeof value !== 'function' || context.kind !== 'method') { throw new Error('not supported'); } const key = context.name.toString(); @@ -774,8 +782,6 @@ export class CommandCenter { private disposables: Disposable[]; private commandErrors = new CommandErrorOutputTextDocumentContentProvider(); - private static readonly WORKTREE_ROOT_KEY = 'worktreeRoot'; - constructor( private git: Git, private model: Model, @@ -1149,7 +1155,7 @@ export class CommandCenter { path = result[0].fsPath; } - await this.model.openRepository(path, true); + await this.model.openRepository(path, true, true); } @command('git.reopenClosedRepositories', { repository: false }) @@ -1185,7 +1191,7 @@ export class CommandCenter { } for (const repository of closedRepositories) { - await this.model.openRepository(repository, true); + await this.model.openRepository(repository, true, true); } } @@ -1399,6 +1405,41 @@ export class CommandCenter { await commands.executeCommand('vscode.open', Uri.file(path.join(repository.root, to)), { viewColumn: ViewColumn.Active }); } + @command('git.delete') + async delete(uri: Uri | undefined): Promise { + const activeDocument = window.activeTextEditor?.document; + uri = uri ?? activeDocument?.uri; + if (!uri) { + return; + } + + const repository = this.model.getRepository(uri); + if (!repository) { + return; + } + + const allChangedResources = [ + ...repository.workingTreeGroup.resourceStates, + ...repository.indexGroup.resourceStates, + ...repository.mergeGroup.resourceStates, + ...repository.untrackedGroup.resourceStates + ]; + + // Check if file has uncommitted changes + const uriString = uri.toString(); + if (allChangedResources.some(o => pathEquals(o.resourceUri.toString(), uriString))) { + window.showInformationMessage(l10n.t('Git: Delete can only be performed on committed files without uncommitted changes.')); + return; + } + + await repository.rm([uri]); + + // Close the active editor if it's not dirty + if (activeDocument && !activeDocument.isDirty && pathEquals(activeDocument.uri.toString(), uriString)) { + await commands.executeCommand('workbench.action.closeActiveEditor'); + } + } + @command('git.stage') async stage(...resourceStates: SourceControlResourceState[]): Promise { this.logger.debug(`[CommandCenter][stage] git.stage ${resourceStates.length} `); @@ -2265,7 +2306,6 @@ export class CommandCenter { } let enableSmartCommit = config.get('enableSmartCommit') === true; - const enableCommitSigning = config.get('enableCommitSigning') === true; let noStagedChanges = repository.indexGroup.resourceStates.length === 0; let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; @@ -2339,8 +2379,9 @@ export class CommandCenter { } } - // enable signing of commits if configured - opts.signCommit = enableCommitSigning; + // Enable signing of commits if the setting is enabled. If the setting is not enabled, + // we set the option to undefined so that we let git use the repository/global config. + opts.signCommit = config.get('enableCommitSigning') === true ? true : undefined; if (config.get('alwaysSignOff')) { opts.signoff = true; @@ -2431,7 +2472,7 @@ export class CommandCenter { let pick: string | undefined = commitToNewBranch; if (branchProtectionPrompt === 'alwaysPrompt') { - const message = l10n.t('You are trying to commit to a protected branch and you might not have permission to push your commits to the remote.\n\nHow would you like to proceed?'); + const message = l10n.t('You are trying to commit to a protected branch. How would you like to proceed?'); const commit = l10n.t('Commit Anyway'); pick = await window.showWarningMessage(message, { modal: true }, commitToNewBranch, commit); @@ -3192,7 +3233,7 @@ export class CommandCenter { } try { - const changes = await repository.diffBetween2(ref1.id, ref2.id); + const changes = await repository.diffBetweenWithStats(ref1.id, ref2.id); if (changes.length === 0) { window.showInformationMessage(l10n.t('There are no changes between "{0}" and "{1}".', ref1.displayId ?? ref1.id, ref2.displayId ?? ref2.id)); @@ -3398,89 +3439,35 @@ export class CommandCenter { @command('git.migrateWorktreeChanges', { repository: true, repositoryFilter: ['repository', 'submodule'] }) async migrateWorktreeChanges(repository: Repository): Promise { - const worktreePicks = async (): Promise => { - const worktrees = await repository.getWorktrees(); - return worktrees.length === 0 - ? [{ label: l10n.t('$(info) This repository has no worktrees.') }] - : worktrees.map(worktree => new WorktreeItem(worktree)); - }; + let worktreeRepository: Repository | undefined; - const placeHolder = l10n.t('Select a worktree to migrate changes from'); - const choice = await this.pickRef(worktreePicks(), placeHolder); - - if (!choice || !(choice instanceof WorktreeItem)) { - return; - } - - const worktreeRepository = this.model.getRepository(choice.worktree.path); - if (!worktreeRepository) { - return; - } - - if (worktreeRepository.indexGroup.resourceStates.length === 0 && - worktreeRepository.workingTreeGroup.resourceStates.length === 0 && - worktreeRepository.untrackedGroup.resourceStates.length === 0) { - await window.showInformationMessage(l10n.t('There are no changes in the selected worktree to migrate.')); - return; - } - - const worktreeChangedFilePaths = [ - ...worktreeRepository.indexGroup.resourceStates, - ...worktreeRepository.workingTreeGroup.resourceStates, - ...worktreeRepository.untrackedGroup.resourceStates - ].map(resource => path.relative(worktreeRepository.root, resource.resourceUri.fsPath)); - - const targetChangedFilePaths = [ - ...repository.workingTreeGroup.resourceStates, - ...repository.untrackedGroup.resourceStates - ].map(resource => path.relative(repository.root, resource.resourceUri.fsPath)); - - // Detect overlapping unstaged files in worktree stash and target repository - const conflicts = worktreeChangedFilePaths.filter(path => targetChangedFilePaths.includes(path)); + const worktrees = await repository.getWorktrees(); + if (worktrees.length === 1) { + worktreeRepository = this.model.getRepository(worktrees[0].path); + } else { + const worktreePicks = async (): Promise => { + return worktrees.length === 0 + ? [{ label: l10n.t('$(info) This repository has no worktrees.') }] + : worktrees.map(worktree => new WorktreeItem(worktree)); + }; - // Check for 'LocalChangesOverwritten' error - if (conflicts.length > 0) { - const maxFilesShown = 5; - const filesToShow = conflicts.slice(0, maxFilesShown); - const remainingCount = conflicts.length - maxFilesShown; + const placeHolder = l10n.t('Select a worktree to migrate changes from'); + const choice = await this.pickRef(worktreePicks(), placeHolder); - const fileList = filesToShow.join('\n ') + - (remainingCount > 0 ? l10n.t('\n and {0} more file{1}...', remainingCount, remainingCount > 1 ? 's' : '') : ''); + if (!choice || !(choice instanceof WorktreeItem)) { + return; + } - const message = l10n.t('Your local changes to the following files would be overwritten by merge:\n {0}\n\nPlease stage, commit, or stash your changes in the repository before migrating changes.', fileList); - await window.showErrorMessage(message, { modal: true }); - return; + worktreeRepository = this.model.getRepository(choice.worktree.path); } - const message = l10n.t('Proceed with migrating changes to the current repository?'); - const detail = l10n.t('This will apply the worktree\'s changes to this repository and discard changes in the worktree.\nThis is IRREVERSIBLE!'); - const proceed = l10n.t('Proceed'); - const pick = await window.showWarningMessage(message, { modal: true, detail }, proceed); - if (pick !== proceed) { + if (!worktreeRepository || worktreeRepository.kind !== 'worktree') { return; } - await worktreeRepository.createStash(undefined, true); - const stashes = await worktreeRepository.getStashes(); - - try { - await repository.applyStash(stashes[0].index); - worktreeRepository.dropStash(stashes[0].index); - } catch (err) { - if (err.gitErrorCode !== GitErrorCodes.StashConflict) { - await worktreeRepository.popStash(); - throw err; - } - repository.isWorktreeMigrating = true; - - const message = l10n.t('There are merge conflicts from migrating changes. Please resolve them before committing.'); - const show = l10n.t('Show Changes'); - const choice = await window.showWarningMessage(message, show); - if (choice === show) { - await commands.executeCommand('workbench.view.scm'); - } - worktreeRepository.dropStash(stashes[0].index); - } + await repository.migrateChanges(worktreeRepository.root, { + confirmation: true, deleteFromSource: true, untracked: true + }); } @command('git.openWorktreeMergeEditor') @@ -3499,121 +3486,51 @@ export class CommandCenter { }); } - @command('git.createWorktreeWithDefaults', { repository: true, repositoryFilter: ['repository'] }) - async createWorktreeWithDefaults( - repository: Repository, - commitish: string = 'HEAD' - ): Promise { - const config = workspace.getConfiguration('git'); - const branchPrefix = config.get('branchPrefix', ''); - - // Generate branch name if not provided - let branch = await this.generateRandomBranchName(repository, '-'); - if (!branch) { - // Fallback to timestamp-based name if random generation fails - const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); - branch = `${branchPrefix}worktree-${timestamp}`; - } - - // Ensure branch name starts with prefix if configured - if (branchPrefix && !branch.startsWith(branchPrefix)) { - branch = branchPrefix + branch; + @command('git.createWorktree', { repository: true, repositoryFilter: ['repository', 'submodule'] }) + async createWorktree(repository?: Repository): Promise { + if (!repository) { + return; } - // Create worktree name from branch name - const worktreeName = branch.startsWith(branchPrefix) - ? branch.substring(branchPrefix.length).replace(/\//g, '-') - : branch.replace(/\//g, '-'); - - // Determine default worktree path - const defaultWorktreeRoot = this.globalState.get(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`); - const defaultWorktreePath = defaultWorktreeRoot - ? path.join(defaultWorktreeRoot, worktreeName) - : path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName); - - // Check if worktree already exists at this path - const existingWorktree = repository.worktrees.find(worktree => - pathEquals(path.normalize(worktree.path), path.normalize(defaultWorktreePath)) - ); + await this._createWorktree(repository); + } - if (existingWorktree) { - // Generate unique path by appending a number - let counter = 1; - let uniquePath = `${defaultWorktreePath}-${counter}`; - while (repository.worktrees.some(wt => pathEquals(path.normalize(wt.path), path.normalize(uniquePath)))) { - counter++; - uniquePath = `${defaultWorktreePath}-${counter}`; - } - const finalWorktreePath = uniquePath; + async _createWorktree(repository: Repository): Promise { + const config = workspace.getConfiguration('git'); + const branchPrefix = config.get('branchPrefix')!; - try { - await repository.addWorktree({ path: finalWorktreePath, branch, commitish }); + // Get commitish and branch for the new worktree + const worktreeDetails = await this.getWorktreeCommitishAndBranch(repository); + if (!worktreeDetails) { + return; + } - // Update worktree root in global state - const worktreeRoot = path.dirname(finalWorktreePath); - if (worktreeRoot !== defaultWorktreeRoot) { - this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot); - } + const { commitish, branch } = worktreeDetails; + const worktreeName = ((branch ?? commitish).startsWith(branchPrefix) + ? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-') + : (branch ?? commitish).replace(/\//g, '-')); - return finalWorktreePath; - } catch (err) { - // Return undefined on failure - return undefined; - } + // Get path for the new worktree + const worktreePath = await this.getWorktreePath(repository, worktreeName); + if (!worktreePath) { + return; } try { - await repository.addWorktree({ path: defaultWorktreePath, branch, commitish }); - - // Update worktree root in global state - const worktreeRoot = path.dirname(defaultWorktreePath); - if (worktreeRoot !== defaultWorktreeRoot) { - this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot); - } - - return defaultWorktreePath; + await repository.createWorktree({ path: worktreePath, branch, commitish: commitish }); } catch (err) { - // Return undefined on failure - return undefined; - } - } - - @command('git.createWorktree') - async createWorktree(repository: any): Promise { - repository = this.model.getRepository(repository); - - if (!repository) { - // Single repository/submodule/worktree - if (this.model.repositories.length === 1) { - repository = this.model.repositories[0]; - } - } - - if (!repository) { - // Single repository/submodule - const repositories = this.model.repositories - .filter(r => r.kind === 'repository' || r.kind === 'submodule'); - - if (repositories.length === 1) { - repository = repositories[0]; + if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) { + await this.handleWorktreeAlreadyExists(err); + } else if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) { + await this.handleWorktreeBranchAlreadyUsed(err); + } else { + throw err; } } - - if (!repository) { - // Multiple repositories/submodules - repository = await this.model.pickRepository(['repository', 'submodule']); - } - - if (!repository) { - return; - } - - await this._createWorktree(repository); } - private async _createWorktree(repository: Repository): Promise { - const config = workspace.getConfiguration('git'); - const branchPrefix = config.get('branchPrefix')!; + private async getWorktreeCommitishAndBranch(repository: Repository): Promise<{ commitish: string; branch: string | undefined } | undefined> { + const config = workspace.getConfiguration('git', Uri.file(repository.root)); const showRefDetails = config.get('showReferenceDetails') === true; const createBranch = new CreateBranchItem(); @@ -3632,23 +3549,21 @@ export class CommandCenter { const choice = await this.pickRef(getBranchPicks(), placeHolder); if (!choice) { - return; + return undefined; } - let branch: string | undefined = undefined; - let commitish: string; - if (choice === createBranch) { - branch = await this.promptForBranchName(repository); - + // Create new branch + const branch = await this.promptForBranchName(repository); if (!branch) { - return; + return undefined; } - commitish = 'HEAD'; + return { commitish: 'HEAD', branch }; } else { + // Existing reference if (!(choice instanceof RefItem) || !choice.refName) { - return; + return undefined; } if (choice.refName === repository.HEAD?.name) { @@ -3657,15 +3572,14 @@ export class CommandCenter { const pick = await window.showWarningMessage(message, { modal: true }, createBranch); if (pick === createBranch) { - branch = await this.promptForBranchName(repository); - + const branch = await this.promptForBranchName(repository); if (!branch) { - return; + return undefined; } - commitish = 'HEAD'; + return { commitish: 'HEAD', branch }; } else { - return; + return undefined; } } else { // Check whether the selected branch is checked out in an existing worktree @@ -3675,17 +3589,14 @@ export class CommandCenter { await this.handleWorktreeConflict(worktree.path, message); return; } - commitish = choice.refName; + return { commitish: choice.refName, branch: undefined }; } } + } - const worktreeName = ((branch ?? commitish).startsWith(branchPrefix) - ? (branch ?? commitish).substring(branchPrefix.length).replace(/\//g, '-') - : (branch ?? commitish).replace(/\//g, '-')); - - // If user selects folder button, they manually select the worktree path through folder picker + private async getWorktreePath(repository: Repository, worktreeName: string): Promise { const getWorktreePath = async (): Promise => { - const worktreeRoot = this.globalState.get(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`); + const worktreeRoot = this.globalState.get(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`); const defaultUri = worktreeRoot ? Uri.file(worktreeRoot) : Uri.file(path.dirname(repository.root)); const uris = await window.showOpenDialog({ @@ -3721,10 +3632,12 @@ export class CommandCenter { }; // Default worktree path is based on the last worktree location or a worktree folder for the repository - const defaultWorktreeRoot = this.globalState.get(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`); + const defaultWorktreeRoot = this.globalState.get(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${repository.root}`); const defaultWorktreePath = defaultWorktreeRoot ? path.join(defaultWorktreeRoot, worktreeName) - : path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName); + : repository.kind === 'worktree' + ? path.join(path.dirname(repository.root), worktreeName) + : path.join(path.dirname(repository.root), `${path.basename(repository.root)}.worktrees`, worktreeName); const disposables: Disposable[] = []; const inputBox = window.createInputBox(); @@ -3760,33 +3673,11 @@ export class CommandCenter { dispose(disposables); - if (!worktreePath) { - return; - } - - try { - await repository.addWorktree({ path: worktreePath, branch, commitish: commitish }); - - // Update worktree root in global state - const worktreeRoot = path.dirname(worktreePath); - if (worktreeRoot !== defaultWorktreeRoot) { - this.globalState.update(`${CommandCenter.WORKTREE_ROOT_KEY}:${repository.root}`, worktreeRoot); - } - } catch (err) { - if (err.gitErrorCode === GitErrorCodes.WorktreeAlreadyExists) { - await this.handleWorktreeAlreadyExists(err); - } else if (err.gitErrorCode === GitErrorCodes.WorktreeBranchAlreadyUsed) { - await this.handleWorktreeBranchAlreadyUsed(err); - } else { - throw err; - } - - return; - } + return worktreePath; } - private async handleWorktreeBranchAlreadyUsed(err: any): Promise { - const match = err.stderr.match(/fatal: '([^']+)' is already used by worktree at '([^']+)'/); + private async handleWorktreeBranchAlreadyUsed(err: GitError): Promise { + const match = err.stderr?.match(/fatal: '([^']+)' is already used by worktree at '([^']+)'/); if (!match) { return; @@ -3797,8 +3688,8 @@ export class CommandCenter { await this.handleWorktreeConflict(path, message); } - private async handleWorktreeAlreadyExists(err: any): Promise { - const match = err.stderr.match(/fatal: '([^']+)'/); + private async handleWorktreeAlreadyExists(err: GitError): Promise { + const match = err.stderr?.match(/fatal: '([^']+)'/); if (!match) { return; @@ -3810,7 +3701,7 @@ export class CommandCenter { } private async handleWorktreeConflict(path: string, message: string): Promise { - await this.model.openRepository(path, true); + await this.model.openRepository(path, true, true); const worktreeRepository = this.model.getRepository(path); @@ -3830,48 +3721,16 @@ export class CommandCenter { return; } - @command('git.deleteWorktree', { repository: true, repositoryFilter: ['worktree'] }) - async deleteWorktree(repository: Repository): Promise { - if (!repository.dotGit.commonPath) { - return; - } - - const mainRepository = this.model.getRepository(path.dirname(repository.dotGit.commonPath)); - if (!mainRepository) { - await window.showErrorMessage(l10n.t('You cannot delete the worktree you are currently in. Please switch to the main repository first.'), { modal: true }); - return; - } - - // Dispose worktree repository - this.model.disposeRepository(repository); - - try { - await mainRepository.deleteWorktree(repository.root); - } catch (err) { - if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) { - const forceDelete = l10n.t('Force Delete'); - const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?'); - const choice = await window.showWarningMessage(message, { modal: true }, forceDelete); - if (choice === forceDelete) { - await mainRepository.deleteWorktree(repository.root, { force: true }); - } else { - await this.model.openRepository(repository.root); - } - - return; - } - - throw err; - } - } - - @command('git.deleteWorktreeFromPalette', { repository: true, repositoryFilter: ['repository', 'submodule'] }) + @command('git.deleteWorktree', { repository: true, repositoryFilter: ['repository', 'submodule'] }) async deleteWorktreeFromPalette(repository: Repository): Promise { + const config = workspace.getConfiguration('git', Uri.file(repository.root)); + const commitShortHashLength = config.get('commitShortHashLength') ?? 7; + const worktreePicks = async (): Promise => { - const worktrees = await repository.getWorktrees(); + const worktrees = await repository.getWorktreeDetails(); return worktrees.length === 0 ? [{ label: l10n.t('$(info) This repository has no worktrees.') }] - : worktrees.map(worktree => new WorktreeDeleteItem(worktree)); + : worktrees.map(worktree => new WorktreeDeleteItem(worktree, commitShortHashLength)); }; const placeHolder = l10n.t('Select a worktree to delete'); @@ -3882,6 +3741,21 @@ export class CommandCenter { } } + @command('git.deleteWorktree2', { repository: true, repositoryFilter: ['worktree'] }) + async deleteWorktree(repository: Repository): Promise { + if (!repository.dotGit.commonPath) { + return; + } + + const mainRepository = this.model.getRepository(path.dirname(repository.dotGit.commonPath)); + if (!mainRepository) { + await window.showErrorMessage(l10n.t('You cannot delete the worktree you are currently in. Please switch to the main repository first.'), { modal: true }); + return; + } + + await mainRepository.deleteWorktree(repository.root); + } + @command('git.openWorktree', { repository: true }) async openWorktreeInCurrentWindow(repository: Repository): Promise { if (!repository) { @@ -4734,7 +4608,7 @@ export class CommandCenter { return; } - await this._stashDrop(repository, stash); + await this._stashDrop(repository, stash.index, stash.description); } @command('git.stashDropAll', { repository: true }) @@ -4767,15 +4641,15 @@ export class CommandCenter { return; } - if (await this._stashDrop(result.repository, result.stash)) { + if (await this._stashDrop(result.repository, result.stash.index, result.stash.description)) { await commands.executeCommand('workbench.action.closeActiveEditor'); } } - async _stashDrop(repository: Repository, stash: Stash): Promise { + async _stashDrop(repository: Repository, index: number, description: string): Promise { const yes = l10n.t('Yes'); const result = await window.showWarningMessage( - l10n.t('Are you sure you want to drop the stash: {0}?', stash.description), + l10n.t('Are you sure you want to drop the stash: {0}?', description), { modal: true }, yes ); @@ -4783,7 +4657,7 @@ export class CommandCenter { return false; } - await repository.dropStash(stash.index); + await repository.dropStash(index); return true; } @@ -4796,36 +4670,7 @@ export class CommandCenter { return; } - const stashChanges = await repository.showStash(stash.index); - if (!stashChanges || stashChanges.length === 0) { - return; - } - - // A stash commit can have up to 3 parents: - // 1. The first parent is the commit that was HEAD when the stash was created. - // 2. The second parent is the commit that represents the index when the stash was created. - // 3. The third parent (when present) represents the untracked files when the stash was created. - const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`; - const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined; - const stashUntrackedFiles: string[] = []; - - if (stashUntrackedFilesParentCommit) { - const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit); - stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file))); - } - - const title = `Git Stash #${stash.index}: ${stash.description}`; - const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' }); - - const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = []; - for (const change of stashChanges) { - const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath)); - const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash; - - resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef)); - } - - commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + await this._viewStash(repository, stash); } private async pickStash(repository: Repository, placeHolder: string): Promise { @@ -4870,6 +4715,39 @@ export class CommandCenter { return { repository, stash }; } + private async _viewStash(repository: Repository, stash: Stash): Promise { + const stashChanges = await repository.showStash(stash.index); + if (!stashChanges || stashChanges.length === 0) { + return; + } + + // A stash commit can have up to 3 parents: + // 1. The first parent is the commit that was HEAD when the stash was created. + // 2. The second parent is the commit that represents the index when the stash was created. + // 3. The third parent (when present) represents the untracked files when the stash was created. + const stashFirstParentCommit = stash.parents.length > 0 ? stash.parents[0] : `${stash.hash}^`; + const stashUntrackedFilesParentCommit = stash.parents.length === 3 ? stash.parents[2] : undefined; + const stashUntrackedFiles: string[] = []; + + if (stashUntrackedFilesParentCommit) { + const untrackedFiles = await repository.getObjectFiles(stashUntrackedFilesParentCommit); + stashUntrackedFiles.push(...untrackedFiles.map(f => path.join(repository.root, f.file))); + } + + const title = `Git Stash #${stash.index}: ${stash.description}`; + const multiDiffSourceUri = toGitUri(Uri.file(repository.root), `stash@{${stash.index}}`, { scheme: 'git-stash' }); + + const resources: { originalUri: Uri | undefined; modifiedUri: Uri | undefined }[] = []; + for (const change of stashChanges) { + const isChangeUntracked = !!stashUntrackedFiles.find(f => pathEquals(f, change.uri.fsPath)); + const modifiedUriRef = !isChangeUntracked ? stash.hash : stashUntrackedFilesParentCommit ?? stash.hash; + + resources.push(toMultiFileDiffEditorUris(change, stashFirstParentCommit, modifiedUriRef)); + } + + commands.executeCommand('_workbench.openMultiDiffEditor', { multiDiffSourceUri, title, resources }); + } + @command('git.timeline.openDiff', { repository: false }) async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) { const cmd = this.resolveTimelineOpenDiffCommand( @@ -4944,7 +4822,7 @@ export class CommandCenter { const commit = await repository.getCommit(item.ref); const commitParentId = commit.parents.length > 0 ? commit.parents[0] : await repository.getEmptyTree(); - const changes = await repository.diffBetween2(commitParentId, commit.hash); + const changes = await repository.diffBetweenWithStats(commitParentId, commit.hash); const resources = changes.map(c => toMultiFileDiffEditorUris(c, commitParentId, commit.hash)); const title = `${item.shortRef} - ${subject(commit.message)}`; @@ -5218,7 +5096,7 @@ export class CommandCenter { const multiDiffSourceUri = Uri.from({ scheme: 'scm-history-item', path: `${repository.root}/${historyItemParentId}..${historyItemId}` }); - const changes = await repository.diffBetween2(historyItemParentId, historyItemId); + const changes = await repository.diffBetweenWithStats(historyItemParentId, historyItemId); const resources = changes.map(c => toMultiFileDiffEditorUris(c, historyItemParentId, historyItemId)); const reveal = revealUri ? { modifiedUri: toGitUri(revealUri, historyItemId) } : undefined; @@ -5269,6 +5147,15 @@ export class CommandCenter { await this._createTag(repository); } + @command('git.repositories.createWorktree', { repository: true }) + async artifactGroupCreateWorktree(repository: Repository): Promise { + if (!repository) { + return; + } + + await this._createWorktree(repository); + } + @command('git.repositories.checkout', { repository: true }) async artifactCheckout(repository: Repository, artifact: SourceControlArtifact): Promise { if (!repository || !artifact) { @@ -5407,6 +5294,107 @@ export class CommandCenter { await repository.deleteTag(artifact.name); } + @command('git.repositories.stashView', { repository: true }) + async artifactStashView(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + // Extract stash index from artifact id + const regex = /^stash@\{(\d+)\}$/; + const match = regex.exec(artifact.id); + if (!match) { + return; + } + + const stashes = await repository.getStashes(); + const stash = stashes.find(s => s.index === parseInt(match[1])); + if (!stash) { + return; + } + + await this._viewStash(repository, stash); + } + + @command('git.repositories.stashApply', { repository: true }) + async artifactStashApply(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + // Extract stash index from artifact id (format: "stash@{index}") + const regex = /^stash@\{(\d+)\}$/; + const match = regex.exec(artifact.id); + if (!match) { + return; + } + + const stashIndex = parseInt(match[1]); + await repository.applyStash(stashIndex); + } + + @command('git.repositories.stashPop', { repository: true }) + async artifactStashPop(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + // Extract stash index from artifact id (format: "stash@{index}") + const regex = /^stash@\{(\d+)\}$/; + const match = regex.exec(artifact.id); + if (!match) { + return; + } + + const stashIndex = parseInt(match[1]); + await repository.popStash(stashIndex); + } + + @command('git.repositories.stashDrop', { repository: true }) + async artifactStashDrop(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + // Extract stash index from artifact id + const regex = /^stash@\{(\d+)\}$/; + const match = regex.exec(artifact.id); + if (!match) { + return; + } + + await this._stashDrop(repository, parseInt(match[1]), artifact.name); + } + + @command('git.repositories.openWorktree', { repository: true }) + async artifactOpenWorktree(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + const uri = Uri.file(artifact.id); + await commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true }); + } + + @command('git.repositories.openWorktreeInNewWindow', { repository: true }) + async artifactOpenWorktreeInNewWindow(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + const uri = Uri.file(artifact.id); + await commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true }); + } + + @command('git.repositories.deleteWorktree', { repository: true }) + async artifactDeleteWorktree(repository: Repository, artifact: SourceControlArtifact): Promise { + if (!repository || !artifact) { + return; + } + + await repository.deleteWorktree(artifact.id); + } + private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index ea4f031e0f9..fb895d5aff2 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -32,7 +32,7 @@ class GitIgnoreDecorationProvider implements FileDecorationProvider { private disposables: Disposable[] = []; constructor(private model: Model) { - const onDidChangeRepository = anyEvent( + const onDidChangeRepository = anyEvent( filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)), model.onDidOpenRepository, model.onDidCloseRepository @@ -257,7 +257,7 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider return []; } - const changes = await this.repository.diffBetween2(ancestor, currentHistoryItemRemoteRef.id); + const changes = await this.repository.diffBetweenWithStats(ancestor, currentHistoryItemRemoteRef.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/decorators.ts b/extensions/git/src/decorators.ts index cd1c7d72d3b..0e59a849ed2 100644 --- a/extensions/git/src/decorators.ts +++ b/extensions/git/src/decorators.ts @@ -6,8 +6,8 @@ import { done } from './util'; function decorate(decorator: (fn: Function, key: string) => Function): Function { - return function (original: any, context: ClassMethodDecoratorContext) { - if (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter') { + return function (original: unknown, context: ClassMethodDecoratorContext) { + if (typeof original === 'function' && (context.kind === 'method' || context.kind === 'getter' || context.kind === 'setter')) { return decorator(original, context.name.toString()); } throw new Error('not supported'); diff --git a/extensions/git/src/git-editor-main.ts b/extensions/git/src/git-editor-main.ts index eb4da4a40b5..80615b56e5a 100644 --- a/extensions/git/src/git-editor-main.ts +++ b/extensions/git/src/git-editor-main.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IPCClient } from './ipc/ipcClient'; -function fatal(err: any): void { +function fatal(err: unknown): void { console.error(err); process.exit(1); } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index d9f930dd398..2274087f032 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -13,7 +13,7 @@ import { EventEmitter } from 'events'; import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals, isMacintosh, isDescendant, relativePathWithNoFallback, Mutable } from './util'; import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Progress, Uri, workspace } from 'vscode'; -import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions } from './api/git'; +import { Commit as ApiCommit, Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery as ApiRefQuery, InitOptions, DiffChange, Worktree as ApiWorktree } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -29,6 +29,7 @@ export interface IDotGit { readonly path: string; readonly commonPath?: string; readonly superProjectPath?: string; + readonly isBare: boolean; } export interface IFileStatus { @@ -44,6 +45,8 @@ export interface Stash { readonly index: number; readonly description: string; readonly branchName?: string; + readonly authorDate?: Date; + readonly commitDate?: Date; } interface MutableRemote extends Remote { @@ -117,7 +120,7 @@ function findGitDarwin(onValidate: (path: string) => boolean): Promise { } // must check if XCode is installed - cp.exec('xcode-select -p', (err: any) => { + cp.exec('xcode-select -p', (err) => { if (err && err.code === 2) { // git is not installed, and launching /usr/bin/git // will prompt the user to install it @@ -370,7 +373,7 @@ function sanitizeRelativePath(path: string): string { } const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B'; -const STASH_FORMAT = '%H%n%P%n%gd%n%gs'; +const STASH_FORMAT = '%H%n%P%n%gd%n%gs%n%at%n%ct'; export interface ICloneOptions { readonly parentPath: string; @@ -573,7 +576,12 @@ export class Git { commonDotGitPath = path.normalize(commonDotGitPath); } + const raw = await fs.readFile(path.join(commonDotGitPath ?? dotGitPath, 'config'), 'utf8'); + const coreSections = GitConfigParser.parse(raw).find(s => s.name === 'core'); + const isBare = coreSections?.properties['bare'] === 'true'; + return { + isBare, path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined, superProjectPath: superProjectPath ? path.normalize(superProjectPath) : undefined @@ -676,6 +684,7 @@ export class Git { options.env = assign({}, process.env, this.env, options.env || {}, { VSCODE_GIT_COMMAND: args[0], + LANGUAGE: 'en', LC_ALL: 'en_US.UTF-8', LANG: 'en_US.UTF-8', GIT_PAGER: 'cat' @@ -865,12 +874,6 @@ export class GitStatusParser { } } -export interface Worktree { - readonly name: string; - readonly path: string; - readonly ref: string; -} - export interface Submodule { name: string; path: string; @@ -999,12 +1002,12 @@ export function parseLsFiles(raw: string): LsFilesElement[] { .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); } -const stashRegex = /([0-9a-f]{40})\n(.*)\nstash@{(\d+)}\n(WIP\s)*on([^:]+):(.*)(?:\x00)/gmi; +const stashRegex = /([0-9a-f]{40})\n(.*)\nstash@{(\d+)}\n(WIP\s)?on\s([^:]+):\s(.*)\n(\d+)\n(\d+)(?:\x00)/gmi; function parseGitStashes(raw: string): Stash[] { const result: Stash[] = []; - let match, hash, parents, index, wip, branchName, description; + let match, hash, parents, index, wip, branchName, description, authorDate, commitDate; do { match = stashRegex.exec(raw); @@ -1012,13 +1015,15 @@ function parseGitStashes(raw: string): Stash[] { break; } - [, hash, parents, index, wip, branchName, description] = match; + [, hash, parents, index, wip, branchName, description, authorDate, commitDate] = match; result.push({ hash, parents: parents.split(' '), index: parseInt(index), branchName: branchName.trim(), - description: wip ? `WIP (${description.trim()})` : description.trim() + description: wip ? `WIP (${description.trim()})` : description.trim(), + authorDate: authorDate ? new Date(Number(authorDate) * 1000) : undefined, + commitDate: commitDate ? new Date(Number(commitDate) * 1000) : undefined, }); } while (true); @@ -1086,6 +1091,90 @@ function parseGitChanges(repositoryRoot: string, raw: string): Change[] { return result; } +function parseGitChangesRaw(repositoryRoot: string, raw: string): DiffChange[] { + const changes: Change[] = []; + const numStats = new Map(); + + let index = 0; + const segments = raw.trim().split('\x00').filter(s => s); + + segmentsLoop: + while (index < segments.length) { + const segment = segments[index++]; + if (!segment) { + break; + } + + if (segment.startsWith(':')) { + // Parse --raw output + const [, , , , change] = segment.split(' '); + const filePath = segments[index++]; + const originalUri = Uri.file(path.isAbsolute(filePath) ? filePath : path.join(repositoryRoot, filePath)); + + let uri = originalUri; + let renameUri = originalUri; + let status = Status.UNTRACKED; + + switch (change[0]) { + case 'A': + status = Status.INDEX_ADDED; + break; + case 'M': + status = Status.MODIFIED; + break; + case 'D': + status = Status.DELETED; + break; + case 'R': { + if (index >= segments.length) { + break; + } + const newPath = segments[index++]; + if (!newPath) { + break; + } + + status = Status.INDEX_RENAMED; + uri = renameUri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(repositoryRoot, newPath)); + break; + } + default: + // Unknown status + break segmentsLoop; + } + + changes.push({ status, uri, originalUri, renameUri }); + } else { + // Parse --numstat output + const [insertions, deletions, filePath] = segment.split('\t'); + + let numstatPath: string; + if (filePath === '') { + // For renamed files, filePath is empty and the old/new paths + // are in the next two null-terminated segments. We skip the + // old path and use the new path for the stats key. + index++; + + const renamePath = segments[index++]; + numstatPath = path.isAbsolute(renamePath) ? renamePath : path.join(repositoryRoot, renamePath); + } else { + numstatPath = path.isAbsolute(filePath) ? filePath : path.join(repositoryRoot, filePath); + } + + numStats.set(numstatPath, { + insertions: insertions === '-' ? 0 : parseInt(insertions), + deletions: deletions === '-' ? 0 : parseInt(deletions), + }); + } + } + + return changes.map(change => ({ + ...change, + insertions: numStats.get(change.uri.fsPath)?.insertions ?? 0, + deletions: numStats.get(change.uri.fsPath)?.deletions ?? 0, + })); +} + export interface BlameInformation { readonly hash: string; readonly subject?: string; @@ -1232,6 +1321,10 @@ export interface PullOptions { readonly cancellationToken?: CancellationToken; } +export interface Worktree extends ApiWorktree { + readonly commitDetails?: ApiCommit; +} + export class Repository { private _isUsingRefTable = false; @@ -1586,11 +1679,19 @@ export class Repository { } } - async apply(patch: string, reverse?: boolean): Promise { + async apply(patch: string, options?: { reverse?: boolean; threeWay?: boolean; allowEmpty?: boolean }): Promise { const args = ['apply', patch]; - if (reverse) { - args.push('-R'); + if (options?.allowEmpty) { + args.push('--allow-empty'); + } + + if (options?.reverse) { + args.push('--reverse'); + } + + if (options?.threeWay) { + args.push('--3way'); } try { @@ -1628,6 +1729,10 @@ export class Repository { return result.stdout; } + async diffWithHEADShortStats(path?: string): Promise { + return this.diffFilesShortStat(undefined, { cached: false, path }); + } + diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffWith(ref: string, path?: string | undefined): Promise; @@ -1654,6 +1759,10 @@ export class Repository { return result.stdout; } + async diffIndexWithHEADShortStats(path?: string): Promise { + return this.diffFilesShortStat(undefined, { cached: true, path }); + } + diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffIndexWith(ref: string, path?: string | undefined): Promise; @@ -1688,8 +1797,35 @@ export class Repository { return result.stdout.trim(); } - async diffBetween2(ref1: string, ref2: string, options: { similarityThreshold?: number }): Promise { - return await this.diffFiles(`${ref1}...${ref2}`, { cached: false, similarityThreshold: options.similarityThreshold }); + async diffBetweenPatch(ref: string, options: { path?: string }): Promise { + const args = ['diff', ref, '--']; + + if (options.path) { + args.push(this.sanitizeRelativePath(options.path)); + } + + const result = await this.exec(args); + return result.stdout; + } + + async diffBetweenWithStats(ref: string, options: { path?: string; similarityThreshold?: number }): Promise { + const args = ['diff', '--raw', '--numstat', '--diff-filter=ADMR', '-z',]; + + if (options.similarityThreshold) { + args.push(`--find-renames=${options.similarityThreshold}%`); + } + + args.push(...[ref, '--']); + if (options.path) { + args.push(this.sanitizeRelativePath(options.path)); + } + + const gitResult = await this.exec(args); + if (gitResult.exitCode) { + return []; + } + + return parseGitChangesRaw(this.repositoryRoot, gitResult.stdout); } private async diffFiles(ref: string | undefined, options: { cached: boolean; similarityThreshold?: number }): Promise { @@ -1717,8 +1853,34 @@ export class Repository { return parseGitChanges(this.repositoryRoot, gitResult.stdout); } - async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise { - const args = ['diff-tree', '-r', '--name-status', '-z', '--diff-filter=ADMR']; + private async diffFilesShortStat(ref: string | undefined, options: { cached: boolean; path?: string }): Promise { + const args = ['diff', '--shortstat']; + + if (options.cached) { + args.push('--cached'); + } + + if (ref !== undefined) { + args.push(ref); + } + + args.push('--'); + + if (options.path) { + args.push(this.sanitizeRelativePath(options.path)); + } + + const result = await this.exec(args); + if (result.exitCode) { + return { files: 0, insertions: 0, deletions: 0 }; + } + + return parseGitDiffShortStat(result.stdout.trim()); + } + + + async diffTrees(treeish1: string, treeish2?: string, options?: { similarityThreshold?: number }): Promise { + const args = ['diff-tree', '-r', '--raw', '--numstat', '--diff-filter=ADMR', '-z']; if (options?.similarityThreshold) { args.push(`--find-renames=${options.similarityThreshold}%`); @@ -1737,7 +1899,7 @@ export class Repository { return []; } - return parseGitChanges(this.repositoryRoot, gitResult.stdout); + return parseGitChangesRaw(this.repositoryRoot, gitResult.stdout); } async getMergeBase(ref1: string, ref2: string, ...refs: string[]): Promise { @@ -1799,7 +1961,15 @@ export class Repository { async stage(path: string, data: Uint8Array): Promise { const relativePath = this.sanitizeRelativePath(path); const child = this.stream(['hash-object', '--stdin', '-w', '--path', relativePath], { stdio: [null, null, null] }); - child.stdin!.end(data); + + if (!child.stdin) { + throw new GitError({ + message: 'Failed to spawn git process', + exitCode: -1 + }); + } + + child.stdin.end(data); const { exitCode, stdout } = await exec(child); const hash = stdout.toString('utf8'); @@ -1903,8 +2073,12 @@ export class Repository { args.push('--signoff'); } - if (opts.signCommit) { - args.push('-S'); + if (opts.signCommit !== undefined) { + if (opts.signCommit) { + args.push('-S'); + } else { + args.push('--no-gpg-sign'); + } } if (opts.empty) { @@ -1941,11 +2115,12 @@ export class Repository { } } - private async handleCommitError(commitErr: any): Promise { - if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) { + + private async handleCommitError(commitErr: unknown): Promise { + if (commitErr instanceof GitError && /not possible because you have unmerged files/.test(commitErr.stderr || '')) { commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges; throw commitErr; - } else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) { + } else if (commitErr instanceof GitError && /Aborting commit due to empty commit message/.test(commitErr.stderr || '')) { commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage; throw commitErr; } @@ -2079,8 +2254,8 @@ export class Repository { const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p)); const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]); - const limiter = new Limiter(5); - const promises: Promise[] = []; + const limiter = new Limiter>(5); + const promises: Promise>[] = []; const args = ['clean', '-f', '-q']; for (const paths of groups) { @@ -2381,10 +2556,14 @@ export class Repository { } } - async blame2(path: string, ref?: string): Promise { + async blame2(path: string, ref?: string, ignoreWhitespace?: boolean): Promise { try { const args = ['blame', '--root', '--incremental']; + if (ignoreWhitespace) { + args.push('-w'); + } + if (ref) { args.push(ref); } @@ -2426,13 +2605,19 @@ export class Repository { } } - async popStash(index?: number): Promise { + async popStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise { const args = ['stash', 'pop']; + if (options?.reinstateStagedChanges) { + args.push('--index'); + } await this.popOrApplyStash(args, index); } - async applyStash(index?: number): Promise { + async applyStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise { const args = ['stash', 'apply']; + if (options?.reinstateStagedChanges) { + args.push('--index'); + } await this.popOrApplyStash(args, index); } @@ -2548,19 +2733,23 @@ export class Repository { if (limit !== 0 && parser.status.length > limit) { child.removeListener('close', onClose); - child.stdout!.removeListener('data', onStdoutData); + child.stdout?.removeListener('data', onStdoutData); child.kill(); c({ status: parser.status.slice(0, limit), statusLength: parser.status.length, didHitLimit: true }); } }; - child.stdout!.setEncoding('utf8'); - child.stdout!.on('data', onStdoutData); + if (child.stdout) { + child.stdout.setEncoding('utf8'); + child.stdout.on('data', onStdoutData); + } const stderrData: string[] = []; - child.stderr!.setEncoding('utf8'); - child.stderr!.on('data', raw => stderrData.push(raw as string)); + if (child.stderr) { + child.stderr.setEncoding('utf8'); + child.stderr.on('data', raw => stderrData.push(raw as string)); + } child.on('error', cpErrorHandler(e)); child.on('close', onClose); @@ -2781,19 +2970,29 @@ export class Repository { } private async getWorktreesFS(): Promise { - const config = workspace.getConfiguration('git', Uri.file(this.repositoryRoot)); - const shouldDetectWorktrees = config.get('detectWorktrees') === true; - - if (!shouldDetectWorktrees) { - this.logger.info('[Git][getWorktreesFS] Worktree detection is disabled, skipping worktree detection'); - return []; - } + const result: Worktree[] = []; + const mainRepositoryPath = this.dotGit.commonPath ?? this.dotGit.path; try { + if (!this.dotGit.isBare) { + // Add main worktree for a non-bare repository + const headPath = path.join(mainRepositoryPath, 'HEAD'); + const headContent = (await fs.readFile(headPath, 'utf8')).trim(); + + const mainRepositoryWorktreeName = path.basename(path.dirname(mainRepositoryPath)); + + result.push({ + name: mainRepositoryWorktreeName, + path: path.dirname(mainRepositoryPath), + ref: headContent.replace(/^ref: /, ''), + detached: !headContent.startsWith('ref: '), + main: true + } satisfies Worktree); + } + // List all worktree folder names - const worktreesPath = path.join(this.dotGit.commonPath ?? this.dotGit.path, 'worktrees'); + const worktreesPath = path.join(mainRepositoryPath, 'worktrees'); const dirents = await fs.readdir(worktreesPath, { withFileTypes: true }); - const result: Worktree[] = []; for (const dirent of dirents) { if (!dirent.isDirectory()) { @@ -2813,6 +3012,9 @@ export class Repository { path: gitdirContent.replace(/\/.git.*$/, ''), // Remove 'ref: ' prefix ref: headContent.replace(/^ref: /, ''), + // Detached if HEAD does not start with 'ref: ' + detached: !headContent.startsWith('ref: '), + main: false }); } catch (err) { if (/ENOENT/.test(err.message)) { @@ -2827,7 +3029,7 @@ export class Repository { } catch (err) { if (/ENOENT/.test(err.message) || /ENOTDIR/.test(err.message)) { - return []; + return result; } throw err; diff --git a/extensions/git/src/gitEditor.ts b/extensions/git/src/gitEditor.ts index 6291e5152a7..cbbea2c6d78 100644 --- a/extensions/git/src/gitEditor.ts +++ b/extensions/git/src/gitEditor.ts @@ -34,7 +34,7 @@ export class GitEditor implements IIPCHandler, ITerminalEnvironmentProvider { }; } - async handle({ commitMessagePath }: GitEditorRequest): Promise { + async handle({ commitMessagePath }: GitEditorRequest): Promise { if (commitMessagePath) { const uri = Uri.file(commitMessagePath); const doc = await workspace.openTextDocument(uri); @@ -49,6 +49,8 @@ export class GitEditor implements IIPCHandler, ITerminalEnvironmentProvider { }); }); } + + return Promise.resolve(false); } getEnv(): { [key: string]: string } { diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 5c323c6acd6..f921f5734a5 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ -import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, MarkdownString, Command, commands } from 'vscode'; +import { CancellationToken, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent, workspace, ConfigurationChangeEvent, Command, commands } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, fromNow, getCommitShortHash, subject, truncate } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose, filterEvent, subject, truncate } from './util'; import { toMultiFileDiffEditorUris } from './uri'; import { AvatarQuery, AvatarQueryCommit, Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; -import { Commit, CommitShortStat } from './git'; +import { Commit } from './git'; import { OperationKind, OperationResult } from './operation'; import { ISourceControlHistoryItemDetailsProviderRegistry, provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { throttle } from './decorators'; +import { getHistoryItemHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover'; function compareSourceControlHistoryItemRef(ref1: SourceControlHistoryItemRef, ref2: SourceControlHistoryItemRef): number { const getOrder = (ref: SourceControlHistoryItemRef): number => { @@ -304,8 +305,8 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const references = this._resolveHistoryItemRefs(commit); const commands: Command[][] = [ - getHistoryItemHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash), - processHistoryItemRemoteHoverCommands(remoteHoverCommands, commit.hash) + getHoverCommitHashCommands(Uri.file(this.repository.root), commit.hash), + processHoverRemoteCommands(remoteHoverCommands, commit.hash) ]; const tooltip = getHistoryItemHover(avatarUrl, commit.authorName, commit.authorEmail, commit.authorDate ?? commit.commitDate, messageWithLinks, commit.shortStat, commands); @@ -338,7 +339,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec const historyItemChangesUri: Uri[] = []; const historyItemChanges: SourceControlHistoryItemChange[] = []; - const changes = await this.repository.diffBetween2(historyItemParentId, historyItemId); + const changes = await this.repository.diffBetweenWithStats(historyItemParentId, historyItemId); for (const change of changes) { const historyItemUri = change.uri.with({ @@ -608,123 +609,3 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec dispose(this.disposables); } } - -export const AVATAR_SIZE = 20; - -export function getHistoryItemHoverCommitHashCommands(documentUri: Uri, hash: string): Command[] { - return [{ - title: `$(git-commit) ${getCommitShortHash(documentUri, hash)}`, - tooltip: l10n.t('Open Commit'), - command: 'git.viewCommit', - arguments: [documentUri, hash, documentUri] - }, { - title: `$(copy)`, - tooltip: l10n.t('Copy Commit Hash'), - command: 'git.copyContentToClipboard', - arguments: [hash] - }] satisfies Command[]; -} - -export function processHistoryItemRemoteHoverCommands(commands: Command[], hash: string): Command[] { - return commands.map(command => ({ - ...command, - arguments: [...command.arguments ?? [], hash] - } satisfies Command)); -} - -export function getHistoryItemHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, commands: Command[][] | undefined): MarkdownString { - const markdownString = new MarkdownString('', true); - markdownString.isTrusted = { - enabledCommands: commands?.flat().map(c => c.command) ?? [] - }; - - // Author - if (authorName) { - // Avatar - if (authorAvatar) { - markdownString.appendMarkdown('!['); - markdownString.appendText(authorName); - markdownString.appendMarkdown(']('); - markdownString.appendText(authorAvatar); - markdownString.appendMarkdown(`|width=${AVATAR_SIZE},height=${AVATAR_SIZE})`); - } else { - markdownString.appendMarkdown('$(account)'); - } - - // Email - if (authorEmail) { - markdownString.appendMarkdown(' [**'); - markdownString.appendText(authorName); - markdownString.appendMarkdown('**](mailto:'); - markdownString.appendText(authorEmail); - markdownString.appendMarkdown(')'); - } else { - markdownString.appendMarkdown(' **'); - markdownString.appendText(authorName); - markdownString.appendMarkdown('**'); - } - - // Date - if (authorDate && !isNaN(new Date(authorDate).getTime())) { - const dateString = new Date(authorDate).toLocaleString(undefined, { - year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' - }); - - markdownString.appendMarkdown(', $(history)'); - markdownString.appendText(` ${fromNow(authorDate, true, true)} (${dateString})`); - } - - markdownString.appendMarkdown('\n\n'); - } - - // Subject | Message (escape image syntax) - markdownString.appendMarkdown(`${emojify(message.replace(/!\[/g, '![').replace(/\r\n|\r|\n/g, '\n\n'))}\n\n`); - markdownString.appendMarkdown(`---\n\n`); - - // Short stats - if (shortStats) { - markdownString.appendMarkdown(`${shortStats.files === 1 ? - l10n.t('{0} file changed', shortStats.files) : - l10n.t('{0} files changed', shortStats.files)}`); - - if (shortStats.insertions) { - markdownString.appendMarkdown(`, ${shortStats.insertions === 1 ? - l10n.t('{0} insertion{1}', shortStats.insertions, '(+)') : - l10n.t('{0} insertions{1}', shortStats.insertions, '(+)')}`); - } - - if (shortStats.deletions) { - markdownString.appendMarkdown(`, ${shortStats.deletions === 1 ? - l10n.t('{0} deletion{1}', shortStats.deletions, '(-)') : - l10n.t('{0} deletions{1}', shortStats.deletions, '(-)')}`); - } - - markdownString.appendMarkdown(`\n\n---\n\n`); - } - - // References - // TODO@lszomoru - move these to core - // if (references && references.length > 0) { - // markdownString.appendMarkdown((references ?? []).map(ref => { - // console.log(ref); - // const labelIconId = ref.icon instanceof ThemeIcon ? ref.icon.id : ''; - // return ` $(${labelIconId}) ${ref.name}  `; - // }).join('  ')); - // markdownString.appendMarkdown(`\n\n---\n\n`); - // } - - // Commands - if (commands && commands.length > 0) { - for (let index = 0; index < commands.length; index++) { - if (index !== 0) { - markdownString.appendMarkdown('  |  '); - } - - const commandsMarkdown = commands[index] - .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`); - markdownString.appendMarkdown(commandsMarkdown.join(' ')); - } - } - - return markdownString; -} diff --git a/extensions/git/src/hover.ts b/extensions/git/src/hover.ts new file mode 100644 index 00000000000..7d33893a348 --- /dev/null +++ b/extensions/git/src/hover.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Command, l10n, MarkdownString, Uri } from 'vscode'; +import { fromNow, getCommitShortHash } from './util'; +import { emojify } from './emoji'; +import { CommitShortStat } from './git'; + +export const AVATAR_SIZE = 20; + +export function getCommitHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, commands: Command[][] | undefined): MarkdownString { + const markdownString = new MarkdownString('', true); + markdownString.isTrusted = { + enabledCommands: commands?.flat().map(c => c.command) ?? [] + }; + + // Author, Subject | Message (escape image syntax) + appendContent(markdownString, authorAvatar, authorName, authorEmail, authorDate, message); + + // Short stats + if (shortStats) { + appendShortStats(markdownString, shortStats); + } + + // Commands + if (commands && commands.length > 0) { + appendCommands(markdownString, commands); + } + + return markdownString; +} + +export function getHistoryItemHover(authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string, shortStats: CommitShortStat | undefined, commands: Command[][] | undefined): MarkdownString[] { + const hoverContent: MarkdownString[] = []; + + // Author, Subject | Message (escape image syntax) + const authorMarkdownString = new MarkdownString('', true); + appendContent(authorMarkdownString, authorAvatar, authorName, authorEmail, authorDate, message); + hoverContent.push(authorMarkdownString); + + // Short stats + if (shortStats) { + const shortStatsMarkdownString = new MarkdownString('', true); + shortStatsMarkdownString.supportHtml = true; + appendShortStats(shortStatsMarkdownString, shortStats); + hoverContent.push(shortStatsMarkdownString); + } + + // Commands + if (commands && commands.length > 0) { + const commandsMarkdownString = new MarkdownString('', true); + commandsMarkdownString.isTrusted = { + enabledCommands: commands?.flat().map(c => c.command) ?? [] + }; + appendCommands(commandsMarkdownString, commands); + hoverContent.push(commandsMarkdownString); + } + + return hoverContent; +} + +function appendContent(markdownString: MarkdownString, authorAvatar: string | undefined, authorName: string | undefined, authorEmail: string | undefined, authorDate: Date | number | undefined, message: string): void { + // Author + if (authorName) { + // Avatar + if (authorAvatar) { + markdownString.appendMarkdown('!['); + markdownString.appendText(authorName); + markdownString.appendMarkdown(']('); + markdownString.appendText(authorAvatar); + markdownString.appendMarkdown(`|width=${AVATAR_SIZE},height=${AVATAR_SIZE})`); + } else { + markdownString.appendMarkdown('$(account)'); + } + + // Email + if (authorEmail) { + markdownString.appendMarkdown(' [**'); + markdownString.appendText(authorName); + markdownString.appendMarkdown('**](mailto:'); + markdownString.appendText(authorEmail); + markdownString.appendMarkdown(')'); + } else { + markdownString.appendMarkdown(' **'); + markdownString.appendText(authorName); + markdownString.appendMarkdown('**'); + } + + // Date + if (authorDate && !isNaN(new Date(authorDate).getTime())) { + const dateString = new Date(authorDate).toLocaleString(undefined, { + year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' + }); + + markdownString.appendMarkdown(', $(history)'); + markdownString.appendText(` ${fromNow(authorDate, true, true)} (${dateString})`); + } + + markdownString.appendMarkdown('\n\n'); + } + + // Subject | Message (escape image syntax) + markdownString.appendMarkdown(`${emojify(message.replace(/!\[/g, '![').replace(/\r\n|\r|\n/g, '\n\n'))}`); + markdownString.appendMarkdown(`\n\n---\n\n`); +} + +function appendShortStats(markdownString: MarkdownString, shortStats: { files: number; insertions: number; deletions: number }): void { + // Short stats + markdownString.appendMarkdown(`${shortStats.files === 1 ? + l10n.t('{0} file changed', shortStats.files) : + l10n.t('{0} files changed', shortStats.files)}`); + + if (shortStats.insertions) { + markdownString.appendMarkdown(`, ${shortStats.insertions === 1 ? + l10n.t('{0} insertion{1}', shortStats.insertions, '(+)') : + l10n.t('{0} insertions{1}', shortStats.insertions, '(+)')}`); + } + + if (shortStats.deletions) { + markdownString.appendMarkdown(`, ${shortStats.deletions === 1 ? + l10n.t('{0} deletion{1}', shortStats.deletions, '(-)') : + l10n.t('{0} deletions{1}', shortStats.deletions, '(-)')}`); + } + + markdownString.appendMarkdown(`\n\n---\n\n`); +} + +function appendCommands(markdownString: MarkdownString, commands: Command[][]): void { + for (let index = 0; index < commands.length; index++) { + if (index !== 0) { + markdownString.appendMarkdown('  |  '); + } + + const commandsMarkdown = commands[index] + .map(command => `[${command.title}](command:${command.command}?${encodeURIComponent(JSON.stringify(command.arguments))} "${command.tooltip}")`); + markdownString.appendMarkdown(commandsMarkdown.join(' ')); + } +} + +export function getHoverCommitHashCommands(documentUri: Uri, hash: string): Command[] { + return [{ + title: `$(git-commit) ${getCommitShortHash(documentUri, hash)}`, + tooltip: l10n.t('Open Commit'), + command: 'git.viewCommit', + arguments: [documentUri, hash, documentUri] + }, { + title: `$(copy)`, + tooltip: l10n.t('Copy Commit Hash'), + command: 'git.copyContentToClipboard', + arguments: [hash] + }] satisfies Command[]; +} + +export function processHoverRemoteCommands(commands: Command[], hash: string): Command[] { + return commands.map(command => ({ + ...command, + arguments: [...command.arguments ?? [], hash] + } satisfies Command)); +} diff --git a/extensions/git/src/ipc/ipcClient.ts b/extensions/git/src/ipc/ipcClient.ts index f623b3f7b6f..9aab55e44a3 100644 --- a/extensions/git/src/ipc/ipcClient.ts +++ b/extensions/git/src/ipc/ipcClient.ts @@ -19,7 +19,7 @@ export class IPCClient { this.ipcHandlePath = ipcHandlePath; } - call(request: any): Promise { + call(request: unknown): Promise { const opts: http.RequestOptions = { socketPath: this.ipcHandlePath, path: `/${this.handlerName}`, diff --git a/extensions/git/src/ipc/ipcServer.ts b/extensions/git/src/ipc/ipcServer.ts index a7142fe22e1..5e56f9ceef5 100644 --- a/extensions/git/src/ipc/ipcServer.ts +++ b/extensions/git/src/ipc/ipcServer.ts @@ -25,7 +25,7 @@ function getIPCHandlePath(id: string): string { } export interface IIPCHandler { - handle(request: any): Promise; + handle(request: unknown): Promise; } export async function createIPCServer(context?: string): Promise { diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 30c8fbaacdb..b37ae9c79c5 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -28,10 +28,11 @@ import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider'; import { GitCommitInputBoxCodeActionsProvider, GitCommitInputBoxDiagnosticsManager } from './diagnostics'; import { GitBlameController } from './blame'; import { CloneManager } from './cloneManager'; +import { getAskpassPaths } from './askpassManager'; -const deactivateTasks: { (): Promise }[] = []; +const deactivateTasks: { (): Promise }[] = []; -export async function deactivate(): Promise { +export async function deactivate(): Promise { for (const task of deactivateTasks) { await task(); } @@ -71,7 +72,8 @@ async function createModel(context: ExtensionContext, logger: LogOutputChannel, logger.error(`[main] Failed to create git IPC: ${err}`); } - const askpass = new Askpass(ipcServer, logger); + const askpassPaths = await getAskpassPaths(__dirname, context.globalStorageUri.fsPath, logger); + const askpass = new Askpass(ipcServer, logger, askpassPaths); disposables.push(askpass); const gitEditor = new GitEditor(ipcServer); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index b2c536e5e07..aabfa256039 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder, ThemeIcon } from 'vscode'; +import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder, ThemeIcon, ResourceTrustRequestOptions } from 'vscode'; import TelemetryReporter from '@vscode/extension-telemetry'; import { IRepositoryResolver, Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; @@ -227,7 +227,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return Promise.resolve(); } - return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise; + return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized') as Event) as Promise; } private remoteSourcePublishers = new Set(); @@ -290,6 +290,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi this._unsafeRepositoriesManager = new UnsafeRepositoriesManager(); workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables); + workspace.onDidChangeWorkspaceTrustedFolders(this.onDidChangeWorkspaceTrustedFolders, this, this.disposables); window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables); window.onDidChangeActiveTextEditor(this.onDidChangeActiveTextEditor, this, this.disposables); workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables); @@ -457,7 +458,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi @debounce(500) private eventuallyScanPossibleGitRepositories(): void { for (const path of this.possibleGitRepositoryPaths) { - this.openRepository(path, false, true); + this.openRepository(path); } this.possibleGitRepositoryPaths.clear(); @@ -488,6 +489,27 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } + private async onDidChangeWorkspaceTrustedFolders(): Promise { + try { + const openRepositoriesToDispose: OpenRepository[] = []; + + for (const openRepository of this.openRepositories) { + const dotGitPath = openRepository.repository.dotGit.commonPath ?? openRepository.repository.dotGit.path; + const isTrusted = await workspace.isResourceTrusted(Uri.file(path.dirname(dotGitPath))); + + if (!isTrusted) { + openRepositoriesToDispose.push(openRepository); + this.logger.trace(`[Model][onDidChangeWorkspaceTrustedFolders] Repository is no longer trusted: ${openRepository.repository.root}`); + } + } + + openRepositoriesToDispose.forEach(r => r.dispose()); + } + catch (err) { + this.logger.warn(`[Model][onDidChangeWorkspaceTrustedFolders] Error: ${err}`); + } + } + private onDidChangeConfiguration(): void { const possibleRepositoryFolders = (workspace.workspaceFolders || []) .filter(folder => workspace.getConfiguration('git', folder.uri).get('enabled') === true) @@ -588,20 +610,6 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return; } - if (!workspace.isTrusted) { - // Check if the folder is a bare repo: if it has a file named HEAD && `rev-parse --show -cdup` is empty - try { - fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK); - const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']); - if (result.stderr.trim() === '' && result.stdout.trim() === '') { - this.logger.trace(`[Model][openRepository] Bare repository: ${repoPath}`); - return; - } - } catch { - // If this throw, we should be good to open the repo (e.g. HEAD doesn't exist) - } - } - try { const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath); this.logger.trace(`[Model][openRepository] Repository root for path ${repoPath} is: ${repositoryRoot}`); @@ -657,8 +665,22 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return; } - // Open repository + // Get .git path and real path const [dotGit, repositoryRootRealPath] = await Promise.all([this.git.getRepositoryDotGit(repositoryRoot), this.getRepositoryRootRealPath(repositoryRoot)]); + + // Check that the folder containing the .git folder is trusted + const dotGitPath = dotGit.commonPath ?? dotGit.path; + const result = await workspace.requestResourceTrust({ + message: l10n.t('You are opening a repository from a location that is not trusted. Do you trust the authors of the files in the repository you are opening?'), + uri: Uri.file(path.dirname(dotGitPath)), + } satisfies ResourceTrustRequestOptions); + + if (!result) { + this.logger.trace(`[Model][openRepository] Repository folder is not trusted: ${path.dirname(dotGitPath)}`); + return; + } + + // Open repository const gitRepository = this.git.open(repositoryRoot, repositoryRootRealPath, dotGit, this.logger); const repository = new Repository(gitRepository, this, this, this, this, this, this, this.globalState, this.logger, this.telemetryReporter, this._repositoryCache); @@ -835,7 +857,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi commands.executeCommand('setContext', 'operationInProgress', operationInProgress); }; - const operationEvent = anyEvent(repository.onDidRunOperation as Event, repository.onRunOperation as Event); + const operationEvent = anyEvent(repository.onDidRunOperation as Event, repository.onRunOperation as Event); const operationListener = operationEvent(() => updateOperationInProgressContext()); updateOperationInProgressContext(); @@ -877,7 +899,8 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } const repositories = this.openRepositories - .filter(r => !repositoryFilter || repositoryFilter.includes(r.repository.kind)); + .filter(r => !r.repository.isHidden && + (!repositoryFilter || repositoryFilter.includes(r.repository.kind))); if (repositories.length === 0) { throw new Error(l10n.t('There are no available repositories matching the filter')); @@ -901,11 +924,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi return pick && pick.repository; } - getRepository(sourceControl: SourceControl): Repository | undefined; - getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined; - getRepository(path: string): Repository | undefined; - getRepository(resource: Uri): Repository | undefined; - getRepository(hint: any): Repository | undefined { + getRepository(hint: SourceControl | SourceControlResourceGroup | Uri | string): Repository | undefined { const liveRepository = this.getOpenRepository(hint); return liveRepository && liveRepository.repository; } @@ -932,12 +951,7 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } - private getOpenRepository(repository: Repository): OpenRepository | undefined; - private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined; - private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined; - private getOpenRepository(path: string): OpenRepository | undefined; - private getOpenRepository(resource: Uri): OpenRepository | undefined; - private getOpenRepository(hint: any): OpenRepository | undefined { + private getOpenRepository(hint: SourceControl | SourceControlResourceGroup | Repository | Uri | string): OpenRepository | undefined { if (!hint) { return undefined; } @@ -1096,13 +1110,26 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } private async isRepositoryOutsideWorkspace(repositoryPath: string): Promise { - const workspaceFolders = (workspace.workspaceFolders || []) + // Allow opening repositories in the empty workspace + if (workspace.workspaceFolders === undefined) { + return false; + } + + const workspaceFolders = workspace.workspaceFolders .filter(folder => folder.uri.scheme === 'file'); if (workspaceFolders.length === 0) { return true; } + // The repository path may be a worktree (usually stored outside the workspace) so we have + // to check the repository path against all the worktree paths of the repositories that have + // already been opened. + const worktreePaths = this.repositories.map(r => r.worktrees.map(w => w.path)).flat(); + if (worktreePaths.some(p => pathEquals(p, repositoryPath))) { + return false; + } + // The repository path may be a canonical path or it may contain a symbolic link so we have // to match it against the workspace folders and the canonical paths of the workspace folders const workspaceFolderPaths = new Set([ @@ -1180,16 +1207,6 @@ export class Model implements IRepositoryResolver, IBranchProtectionProviderRegi } } - disposeRepository(repository: Repository): void { - const openRepository = this.getOpenRepository(repository); - if (!openRepository) { - return; - } - - this.logger.info(`[Model][disposeRepository] Repository: ${repository.root}`); - openRepository.dispose(); - } - dispose(): void { const openRepositories = [...this.openRepositories]; openRepositories.forEach(r => r.dispose()); diff --git a/extensions/git/src/operation.ts b/extensions/git/src/operation.ts index eaa91d4a047..96fffa4dc87 100644 --- a/extensions/git/src/operation.ts +++ b/extensions/git/src/operation.ts @@ -32,7 +32,6 @@ export const enum OperationKind { GetObjectDetails = 'GetObjectDetails', GetObjectFiles = 'GetObjectFiles', GetRefs = 'GetRefs', - GetWorktrees = 'GetWorktrees', GetRemoteRefs = 'GetRemoteRefs', HashObject = 'HashObject', Ignore = 'Ignore', @@ -69,8 +68,8 @@ export const enum OperationKind { export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation | CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation | - DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DeleteWorktreeOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | - GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | GetWorktreesOperation | + DeleteRefOperation | DeleteRemoteRefOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation | + GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetObjectFilesOperation | GetRefsOperation | GetRemoteRefsOperation | HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation | MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation | ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RefreshOperation | RevertFilesOperation | @@ -93,7 +92,6 @@ export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.Delete export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef }; export type DeleteRemoteRefOperation = BaseOperation & { kind: OperationKind.DeleteRemoteRef }; export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag }; -export type DeleteWorktreeOperation = BaseOperation & { kind: OperationKind.DeleteWorktree }; export type DiffOperation = BaseOperation & { kind: OperationKind.Diff }; export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch }; export type FindTrackingBranchesOperation = BaseOperation & { kind: OperationKind.FindTrackingBranches }; @@ -103,7 +101,6 @@ export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.G export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails }; export type GetObjectFilesOperation = BaseOperation & { kind: OperationKind.GetObjectFiles }; export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs }; -export type GetWorktreesOperation = BaseOperation & { kind: OperationKind.GetWorktrees }; export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs }; export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject }; export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore }; @@ -153,7 +150,6 @@ export const Operation = { DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation, DeleteRemoteRef: { kind: OperationKind.DeleteRemoteRef, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteRefOperation, DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation, - DeleteWorktree: { kind: OperationKind.DeleteWorktree, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteWorktreeOperation, Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as DiffOperation, Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation), FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation, @@ -163,7 +159,6 @@ export const Operation = { GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation, GetObjectFiles: { kind: OperationKind.GetObjectFiles, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectFilesOperation, GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation, - GetWorktrees: { kind: OperationKind.GetWorktrees, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetWorktreesOperation, GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation, HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation, Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation, @@ -191,16 +186,16 @@ export const Operation = { Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation, Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation, Status: { kind: OperationKind.Status, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StatusOperation, - Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation, + Stash: (readOnly: boolean) => ({ kind: OperationKind.Stash, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as StashOperation), SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation, Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation, Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation, - Worktree: { kind: OperationKind.Worktree, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as WorktreeOperation + Worktree: (readOnly: boolean) => ({ kind: OperationKind.Worktree, blocking: false, readOnly, remote: false, retry: false, showProgress: true } as WorktreeOperation) }; export interface OperationResult { operation: Operation; - error: any; + error: unknown; } interface IOperationManager { diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index 07c6d88e57e..7ae8527b101 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,16 +5,17 @@ import TelemetryReporter from '@vscode/extension-telemetry'; import * as fs from 'fs'; +import * as fsPromises from 'fs/promises'; import * as path from 'path'; import picomatch from 'picomatch'; -import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; +import { CancellationError, CancellationToken, CancellationTokenSource, Command, commands, Disposable, Event, EventEmitter, ExcludeSettingOptions, FileDecoration, FileType, l10n, LogLevel, LogOutputChannel, Memento, ProgressLocation, ProgressOptions, QuickDiffProvider, RelativePattern, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, TabInputNotebookDiff, TabInputTextDiff, TabInputTextMultiDiff, ThemeColor, ThemeIcon, Uri, window, workspace, WorkspaceEdit } from 'vscode'; import { ActionButton } from './actionButton'; import { ApiRepository } from './api/api1'; -import { Branch, BranchQuery, Change, CommitOptions, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status } from './api/git'; +import { Branch, BranchQuery, Change, CommitOptions, DiffChange, FetchOptions, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, RepositoryKind, Status } from './api/git'; import { AutoFetcher } from './autofetch'; import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection'; import { debounce, memoize, sequentialize, throttle } from './decorators'; -import { Repository as BaseRepository, BlameInformation, Commit, GitError, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git'; +import { Repository as BaseRepository, BlameInformation, Commit, CommitShortStat, GitError, IDotGit, LogFileOptions, LsTreeElement, PullOptions, RefQuery, Stash, Submodule, Worktree } from './git'; import { GitHistoryProvider } from './historyProvider'; import { Operation, OperationKind, OperationManager, OperationResult } from './operation'; import { CommitCommandsCenter, IPostCommitCommandsProviderRegistry } from './postCommitCommands'; @@ -22,7 +23,7 @@ import { IPushErrorHandlerRegistry } from './pushError'; import { IRemoteSourcePublisherRegistry } from './remotePublisher'; import { StatusBarCommands } from './statusbar'; import { toGitUri } from './uri'; -import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; +import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, getCommitShortHash, IDisposable, isCopilotWorktree, isDescendant, isLinuxSnap, isRemote, isWindows, Limiter, onceEvent, pathEquals, relativePath } from './util'; import { IFileWatcher, watch } from './watch'; import { ISourceControlHistoryItemDetailsProviderRegistry } from './historyItemDetailsProvider'; import { GitArtifactProvider } from './artifactProvider'; @@ -186,7 +187,7 @@ export class Resource implements SourceControlResourceState { get renameResourceUri(): Uri | undefined { return this._renameResourceUri; } get contextValue(): string | undefined { return this._repositoryKind; } - private static Icons: any = { + private static Icons = { light: { Modified: getIconUri('status-modified', 'light'), Added: getIconUri('status-added', 'light'), @@ -211,7 +212,7 @@ export class Resource implements SourceControlResourceState { } }; - private getIconPath(theme: string): Uri { + private getIconPath(theme: 'light' | 'dark'): Uri { switch (this.type) { case Status.INDEX_MODIFIED: return Resource.Icons[theme].Modified; case Status.MODIFIED: return Resource.Icons[theme].Modified; @@ -692,14 +693,11 @@ interface BranchProtectionMatcher { } export interface IRepositoryResolver { - getRepository(sourceControl: SourceControl): Repository | undefined; - getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined; - getRepository(path: string): Repository | undefined; - getRepository(resource: Uri): Repository | undefined; - getRepository(hint: any): Repository | undefined; + getRepository(hint: SourceControl | SourceControlResourceGroup | Uri | string): Repository | undefined; } export class Repository implements Disposable { + static readonly WORKTREE_ROOT_STORAGE_KEY = 'worktreeRoot'; private _onDidChangeRepository = new EventEmitter(); readonly onDidChangeRepository: Event = this._onDidChangeRepository.event; @@ -724,7 +722,9 @@ export class Repository implements Disposable { @memoize get onDidChangeOperations(): Event { - return anyEvent(this.onRunOperation as Event, this.onDidRunOperation as Event); + return anyEvent( + this.onRunOperation as Event, + this.onDidRunOperation as Event) as Event; } private _sourceControl: SourceControl; @@ -866,11 +866,11 @@ export class Repository implements Disposable { return this.repository.rootRealPath; } - get dotGit(): { path: string; commonPath?: string } { + get dotGit(): IDotGit { return this.repository.dotGit; } - get kind(): 'repository' | 'submodule' | 'worktree' { + get kind(): RepositoryKind { return this.repository.kind; } @@ -880,6 +880,9 @@ export class Repository implements Disposable { private _historyProvider: GitHistoryProvider; get historyProvider(): GitHistoryProvider { return this._historyProvider; } + private _isHidden: boolean; + get isHidden(): boolean { return this._isHidden; } + private isRepositoryHuge: false | { limit: number } = false; private didWarnAboutLimit = false; @@ -898,7 +901,7 @@ export class Repository implements Disposable { postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry, private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry, historyItemDetailProviderRegistry: ISourceControlHistoryItemDetailsProviderRegistry, - globalState: Memento, + private readonly globalState: Memento, private readonly logger: LogOutputChannel, private telemetryReporter: TelemetryReporter, private readonly repositoryCache: RepositoryCache @@ -940,17 +943,28 @@ export class Repository implements Disposable { : repository.kind === 'worktree' && repository.dotGit.commonPath ? path.dirname(repository.dotGit.commonPath) : undefined; - const parent = this.repositoryResolver.getRepository(parentRoot)?.sourceControl; + const parent = parentRoot + ? this.repositoryResolver.getRepository(parentRoot)?.sourceControl + : undefined; // Icon const icon = repository.kind === 'submodule' ? new ThemeIcon('archive') : repository.kind === 'worktree' - ? new ThemeIcon('list-tree') + ? isCopilotWorktree(repository.root) + ? new ThemeIcon('chat-sparkle') + : new ThemeIcon('worktree') : new ThemeIcon('repo'); + // Hidden + // This is a temporary solution to hide worktrees created by Copilot + // when the main repository is opened. Users can still manually open + // the worktree from the Repositories view. + this._isHidden = repository.kind === 'worktree' && + isCopilotWorktree(repository.root) && parent !== undefined; + const root = Uri.file(repository.root); - this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, parent); + this._sourceControl = scm.createSourceControl('git', 'Git', root, icon, this._isHidden, parent); this._sourceControl.contextValue = repository.kind; this._sourceControl.quickDiffProvider = this; @@ -1100,6 +1114,12 @@ export class Repository implements Disposable { return undefined; } + // Ignore path that is inside the .git directory (ex: COMMIT_EDITMSG) + if (isDescendant(this.dotGit.commonPath ?? this.dotGit.path, uri.fsPath)) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is inside .git directory: ${uri.toString()}`); + return undefined; + } + // Ignore symbolic links const stat = await workspace.fs.stat(uri); if ((stat.type & FileType.SymbolicLink) !== 0) { @@ -1113,6 +1133,12 @@ export class Repository implements Disposable { return undefined; } + // Ignore path that is inside a hidden repository + if (this.isHidden === true) { + this.logger.trace(`[Repository][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + // Ignore path that is inside a merge group if (this.mergeGroup.resourceStates.some(r => pathEquals(r.resourceUri.fsPath, uri.fsPath))) { this.logger.trace(`[Repository][provideOriginalResource] Resource is part of a merge group: ${uri.toString()}`); @@ -1126,17 +1152,10 @@ export class Repository implements Disposable { return undefined; } - const activeTabInput = window.tabGroups.activeTabGroup.activeTab?.input; - - // Ignore file that is on the right-hand side of a diff editor - if (activeTabInput instanceof TabInputTextDiff && pathEquals(activeTabInput.modified.fsPath, uri.fsPath)) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is on the right-hand side of a diff editor: ${uri.toString()}`); - return undefined; - } - - // Ignore file that is on the right -hand side of a multi-file diff editor - if (activeTabInput instanceof TabInputTextMultiDiff && activeTabInput.textDiffs.some(diff => pathEquals(diff.modified.fsPath, uri.fsPath))) { - this.logger.trace(`[Repository][provideOriginalResource] Resource is on the right-hand side of a multi-file diff editor: ${uri.toString()}`); + // Ignore path that is git ignored + const ignored = await this.checkIgnore([uri.fsPath]); + if (ignored.size > 0) { + this.logger.trace(`[Repository][provideOriginalResource] Resource is git ignored: ${uri.toString()}`); return undefined; } @@ -1207,6 +1226,10 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path)); } + diffWithHEADShortStats(path?: string): Promise { + return this.run(Operation.Diff, () => this.repository.diffWithHEADShortStats(path)); + } + diffWith(ref: string): Promise; diffWith(ref: string, path: string): Promise; diffWith(ref: string, path?: string | undefined): Promise; @@ -1221,6 +1244,10 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path)); } + diffIndexWithHEADShortStats(path?: string): Promise { + return this.run(Operation.Diff, () => this.repository.diffIndexWithHEADShortStats(path)); + } + diffIndexWith(ref: string): Promise; diffIndexWith(ref: string, path: string): Promise; diffIndexWith(ref: string, path?: string | undefined): Promise; @@ -1239,7 +1266,12 @@ export class Repository implements Disposable { return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path)); } - diffBetween2(ref1: string, ref2: string): Promise { + diffBetweenPatch(ref1: string, ref2: string, path?: string): Promise { + return this.run(Operation.Diff, () => + this.repository.diffBetweenPatch(`${ref1}...${ref2}`, { path })); + } + + diffBetweenWithStats(ref1: string, ref2: string, path?: string): Promise { if (ref1 === this._EMPTY_TREE) { // Use git diff-tree to get the // changes in the first commit @@ -1249,10 +1281,11 @@ export class Repository implements Disposable { const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); const similarityThreshold = scopedConfig.get('similarityThreshold', 50); - return this.run(Operation.Diff, () => this.repository.diffBetween2(ref1, ref2, { similarityThreshold })); + return this.run(Operation.Diff, () => + this.repository.diffBetweenWithStats(`${ref1}...${ref2}`, { path, similarityThreshold })); } - diffTrees(treeish1: string, treeish2?: string): Promise { + diffTrees(treeish1: string, treeish2?: string): Promise { const scopedConfig = workspace.getConfiguration('git', Uri.file(this.root)); const similarityThreshold = scopedConfig.get('similarityThreshold', 50); @@ -1758,7 +1791,37 @@ export class Repository implements Disposable { } async getWorktrees(): Promise { - return await this.run(Operation.GetWorktrees, () => this.repository.getWorktrees()); + return await this.run(Operation.Worktree(true), () => this.repository.getWorktrees()); + } + + async getWorktreeDetails(): Promise { + return this.run(Operation.Worktree(true), async () => { + const worktrees = await this.repository.getWorktrees(); + if (worktrees.length === 0) { + return []; + } + + // Get refs for worktrees that point to a ref + const worktreeRefs = worktrees + .filter(worktree => !worktree.detached) + .map(worktree => worktree.ref); + + // Get the commit details for worktrees that point to a ref + const refs = await this.getRefs({ pattern: worktreeRefs, includeCommitDetails: true }); + + // Get the commit details for detached worktrees + const commits = await Promise.all(worktrees + .filter(worktree => worktree.detached) + .map(worktree => this.repository.getCommit(worktree.ref))); + + return worktrees.map(worktree => { + const commitDetails = worktree.detached + ? commits.find(commit => commit.hash === worktree.ref) + : refs.find(ref => `refs/heads/${ref.name}` === worktree.ref)?.commitDetails; + + return { ...worktree, commitDetails } satisfies Worktree; + }); + }); } async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise { @@ -1789,12 +1852,175 @@ export class Repository implements Disposable { await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name)); } - async addWorktree(options: { path: string; commitish: string; branch?: string }): Promise { - await this.run(Operation.Worktree, () => this.repository.addWorktree(options)); + async createWorktree(options?: { path?: string; commitish?: string; branch?: string }): Promise { + const defaultWorktreeRoot = this.globalState.get(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${this.root}`); + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const branchPrefix = config.get('branchPrefix', ''); + + return await this.run(Operation.Worktree(false), async () => { + let worktreeName: string | undefined; + let { path: worktreePath, commitish, branch } = options || {}; + + // Create worktree path based on the branch name + if (worktreePath === undefined && branch !== undefined) { + worktreeName = branch.startsWith(branchPrefix) + ? branch.substring(branchPrefix.length).replace(/\//g, '-') + : branch.replace(/\//g, '-'); + + worktreePath = defaultWorktreeRoot + ? path.join(defaultWorktreeRoot, worktreeName) + : path.join(path.dirname(this.root), `${path.basename(this.root)}.worktrees`, worktreeName); + } + + // Ensure that the worktree path is unique + if (this.worktrees.some(worktree => pathEquals(path.normalize(worktree.path), path.normalize(worktreePath!)))) { + let counter = 0, uniqueWorktreePath: string; + do { + uniqueWorktreePath = `${worktreePath}-${++counter}`; + } while (this.worktrees.some(wt => pathEquals(path.normalize(wt.path), path.normalize(uniqueWorktreePath)))); + + worktreePath = uniqueWorktreePath; + } + + // Create the worktree + await this.repository.addWorktree({ path: worktreePath!, commitish: commitish ?? 'HEAD', branch }); + + // Update worktree root in global state + const newWorktreeRoot = path.dirname(worktreePath!); + if (defaultWorktreeRoot && !pathEquals(newWorktreeRoot, defaultWorktreeRoot)) { + this.globalState.update(`${Repository.WORKTREE_ROOT_STORAGE_KEY}:${this.root}`, newWorktreeRoot); + } + + // Copy worktree include files. We explicitly do not await this + // since we don't want to block the worktree creation on the + // copy operation. + this._copyWorktreeIncludeFiles(worktreePath!); + + return worktreePath!; + }); + } + + private async _getWorktreeIncludePaths(): Promise> { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const worktreeIncludeFiles = config.get('worktreeIncludeFiles', []); + + if (worktreeIncludeFiles.length === 0) { + return new Set(); + } + + const filePattern = worktreeIncludeFiles + .map(pattern => new RelativePattern(this.root, pattern)); + + // Get all files matching the globs (no ignore files applied) + const allFiles = await workspace.findFiles2(filePattern, { + useExcludeSettings: ExcludeSettingOptions.None, + useIgnoreFiles: { local: false, parent: false, global: false } + }); + + // Get files matching the globs with git ignore files applied + const nonIgnoredFiles = await workspace.findFiles2(filePattern, { + useExcludeSettings: ExcludeSettingOptions.None, + useIgnoreFiles: { local: true, parent: true, global: true } + }); + + // Files that are git ignored = all files - non-ignored files + const gitIgnoredFiles = new Set(allFiles.map(uri => uri.fsPath)); + for (const uri of nonIgnoredFiles) { + gitIgnoredFiles.delete(uri.fsPath); + } + + // Add the folder paths for git ignored files + const gitIgnoredPaths = new Set(gitIgnoredFiles); + + for (const filePath of gitIgnoredFiles) { + let dir = path.dirname(filePath); + while (dir !== this.root && !gitIgnoredFiles.has(dir)) { + gitIgnoredPaths.add(dir); + dir = path.dirname(dir); + } + } + + return gitIgnoredPaths; + } + + private async _copyWorktreeIncludeFiles(worktreePath: string): Promise { + const gitIgnoredPaths = await this._getWorktreeIncludePaths(); + if (gitIgnoredPaths.size === 0) { + return; + } + + try { + // Find minimal set of paths (folders and files) to copy. + // The goal is to reduce the number of copy operations + // needed. + const pathsToCopy = new Set(); + for (const filePath of gitIgnoredPaths) { + const relativePath = path.relative(this.root, filePath); + const firstSegment = relativePath.split(path.sep)[0]; + pathsToCopy.add(path.join(this.root, firstSegment)); + } + + const startTime = Date.now(); + const limiter = new Limiter(15); + const files = Array.from(pathsToCopy); + + // Copy files + const results = await Promise.allSettled(files.map(sourceFile => + limiter.queue(async () => { + const targetFile = path.join(worktreePath, relativePath(this.root, sourceFile)); + await fsPromises.mkdir(path.dirname(targetFile), { recursive: true }); + await fsPromises.cp(sourceFile, targetFile, { + filter: src => gitIgnoredPaths.has(src), + force: true, + mode: fs.constants.COPYFILE_FICLONE, + recursive: true, + verbatimSymlinks: true + }); + }) + )); + + // Log any failed operations + const failedOperations = results.filter(r => r.status === 'rejected'); + this.logger.info(`[Repository][_copyWorktreeIncludeFiles] Copied ${files.length - failedOperations.length}/${files.length} folder(s)/file(s) to worktree. [${Date.now() - startTime}ms]`); + + if (failedOperations.length > 0) { + window.showWarningMessage(l10n.t('Failed to copy {0} folder(s)/file(s) to the worktree.', failedOperations.length)); + + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy ${failedOperations.length} folder(s)/file(s) to worktree.`); + for (const error of failedOperations) { + this.logger.warn(` - ${(error as PromiseRejectedResult).reason}`); + } + } + } catch (err) { + this.logger.warn(`[Repository][_copyWorktreeIncludeFiles] Failed to copy folder(s)/file(s) to worktree: ${err}`); + } } async deleteWorktree(path: string, options?: { force?: boolean }): Promise { - await this.run(Operation.DeleteWorktree, () => this.repository.deleteWorktree(path, options)); + await this.run(Operation.Worktree(false), async () => { + const worktree = this.repositoryResolver.getRepository(path); + + const deleteWorktree = async (options?: { force?: boolean }): Promise => { + await this.repository.deleteWorktree(path, options); + worktree?.dispose(); + }; + + try { + await deleteWorktree(); + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.WorktreeContainsChanges) { + const forceDelete = l10n.t('Force Delete'); + const message = l10n.t('The worktree contains modified or untracked files. Do you want to force delete?'); + const choice = await window.showWarningMessage(message, { modal: true }, forceDelete); + if (choice === forceDelete) { + await deleteWorktree({ ...options, force: true }); + } + return; + } + + throw err; + } + }); } async deleteRemoteRef(remoteName: string, refName: string, options?: { force?: boolean }): Promise { @@ -2010,7 +2236,11 @@ export class Repository implements Disposable { } async blame2(path: string, ref?: string): Promise { - return await this.run(Operation.Blame(false), () => this.repository.blame2(path, ref)); + return await this.run(Operation.Blame(false), () => { + const config = workspace.getConfiguration('git', Uri.file(this.root)); + const ignoreWhitespace = config.get('blame.ignoreWhitespace', false); + return this.repository.blame2(path, ref, ignoreWhitespace); + }); } @throttle @@ -2157,12 +2387,12 @@ export class Repository implements Disposable { return this.run(Operation.Show, () => this.repository.detectObjectType(object)); } - async apply(patch: string, reverse?: boolean): Promise { - return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse)); + async apply(patch: string, options?: { allowEmpty?: boolean; reverse?: boolean; threeWay?: boolean }): Promise { + return await this.run(Operation.Apply, () => this.repository.apply(patch, options)); } async getStashes(): Promise { - return this.run(Operation.Stash, () => this.repository.getStashes()); + return this.run(Operation.Stash(true), () => this.repository.getStashes()); } async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise { @@ -2171,26 +2401,26 @@ export class Repository implements Disposable { ...!staged ? this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath) : [], ...includeUntracked ? this.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath) : []]; - return await this.run(Operation.Stash, async () => { + return await this.run(Operation.Stash(false), async () => { await this.repository.createStash(message, includeUntracked, staged); this.closeDiffEditors(indexResources, workingGroupResources); }); } - async popStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.popStash(index)); + async popStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise { + return await this.run(Operation.Stash(false), () => this.repository.popStash(index, options)); } async dropStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.dropStash(index)); + return await this.run(Operation.Stash(false), () => this.repository.dropStash(index)); } - async applyStash(index?: number): Promise { - return await this.run(Operation.Stash, () => this.repository.applyStash(index)); + async applyStash(index?: number, options?: { reinstateStagedChanges?: boolean }): Promise { + return await this.run(Operation.Stash(false), () => this.repository.applyStash(index, options)); } async showStash(index: number): Promise { - return await this.run(Operation.Stash, () => this.repository.showStash(index)); + return await this.run(Operation.Stash(true), () => this.repository.showStash(index)); } async getCommitTemplate(): Promise { @@ -2239,7 +2469,15 @@ export class Repository implements Disposable { // https://git-scm.com/docs/git-check-ignore#git-check-ignore--z const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] }); - child.stdin!.end(filePaths.join('\0'), 'utf8'); + + if (!child.stdin) { + return reject(new GitError({ + message: 'Failed to spawn git process', + exitCode: -1 + })); + } + + child.stdin.end(filePaths.join('\0'), 'utf8'); const onExit = (exitCode: number) => { if (exitCode === 1) { @@ -2261,12 +2499,16 @@ export class Repository implements Disposable { data += raw; }; - child.stdout!.setEncoding('utf8'); - child.stdout!.on('data', onStdoutData); + if (child.stdout) { + child.stdout.setEncoding('utf8'); + child.stdout.on('data', onStdoutData); + } let stderr: string = ''; - child.stderr!.setEncoding('utf8'); - child.stderr!.on('data', raw => stderr += raw); + if (child.stderr) { + child.stderr.setEncoding('utf8'); + child.stderr.on('data', raw => stderr += raw); + } child.on('error', reject); child.on('exit', onExit); @@ -2318,14 +2560,15 @@ export class Repository implements Disposable { private async run( operation: Operation, - runOperation: () => Promise = () => Promise.resolve(null), - getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined): Promise { + runOperation: () => Promise = () => Promise.resolve(null) as Promise, + getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined + ): Promise { if (this.state !== RepositoryState.Idle) { throw new Error('Repository not initialized'); } - let error: any = null; + let error: unknown = null; this._operations.start(operation); this._onRunOperation.fire(operation.kind); @@ -2341,7 +2584,7 @@ export class Repository implements Disposable { } catch (err) { error = err; - if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) { + if (err instanceof GitError && err.gitErrorCode === GitErrorCodes.NotAGitRepository) { this.state = RepositoryState.Disposed; } @@ -2356,7 +2599,90 @@ export class Repository implements Disposable { } } - private async retryRun(operation: Operation, runOperation: () => Promise = () => Promise.resolve(null)): Promise { + async migrateChanges(sourceRepositoryRoot: string, options?: { confirmation?: boolean; deleteFromSource?: boolean; untracked?: boolean }): Promise { + const sourceRepository = this.repositoryResolver.getRepository(sourceRepositoryRoot); + if (!sourceRepository) { + window.showWarningMessage(l10n.t('The source repository could not be found.')); + return; + } + + if (sourceRepository.indexGroup.resourceStates.length === 0 && + sourceRepository.workingTreeGroup.resourceStates.length === 0 && + sourceRepository.untrackedGroup.resourceStates.length === 0) { + await window.showInformationMessage(l10n.t('There are no changes in the selected worktree to migrate.')); + return; + } + + const sourceFilePaths = [ + ...sourceRepository.indexGroup.resourceStates, + ...sourceRepository.workingTreeGroup.resourceStates, + ...sourceRepository.untrackedGroup.resourceStates + ].map(resource => path.relative(sourceRepository.root, resource.resourceUri.fsPath)); + + const targetFilePaths = [ + ...this.workingTreeGroup.resourceStates, + ...this.untrackedGroup.resourceStates + ].map(resource => path.relative(this.root, resource.resourceUri.fsPath)); + + // Detect overlapping unstaged files in worktree stash and target repository + const conflicts = sourceFilePaths.filter(path => targetFilePaths.includes(path)); + + if (conflicts.length > 0) { + const maxFilesShown = 5; + const filesToShow = conflicts.slice(0, maxFilesShown); + const remainingCount = conflicts.length - maxFilesShown; + + const fileList = filesToShow.join('\n ') + + (remainingCount > 0 ? l10n.t('\n and {0} more file{1}...', remainingCount, remainingCount > 1 ? 's' : '') : ''); + + const message = l10n.t('Your local changes to the following files would be overwritten by merge:\n {0}\n\nPlease stage, commit, or stash your changes in the repository before migrating changes.', fileList); + await window.showErrorMessage(message, { modal: true }); + return; + } + + if (options?.confirmation) { + // Non-interactive migration, do not show confirmation dialog + const message = l10n.t('Proceed with migrating changes to the current repository?'); + const detail = l10n.t('This will apply the worktree\'s changes to this repository and discard changes in the worktree.\nThis is IRREVERSIBLE!'); + const proceed = l10n.t('Proceed'); + const pick = await window.showWarningMessage(message, { modal: true, detail }, proceed); + if (pick !== proceed) { + return; + } + } + + const stashName = `migration:${sourceRepository.HEAD?.name ?? sourceRepository.HEAD?.commit}-${this.HEAD?.name ?? this.HEAD?.commit}`; + await sourceRepository.createStash(stashName, options?.untracked); + const stashes = await sourceRepository.getStashes(); + + try { + if (options?.deleteFromSource) { + await this.popStash(stashes[0].index); + } else { + await this.applyStash(stashes[0].index); + await sourceRepository.popStash(stashes[0].index, { reinstateStagedChanges: true }); + } + } catch (err) { + if (err.gitErrorCode === GitErrorCodes.StashConflict) { + this.isWorktreeMigrating = true; + + const message = l10n.t('There are merge conflicts from migrating changes. Please resolve them before committing.'); + const show = l10n.t('Show Changes'); + const choice = await window.showWarningMessage(message, show); + if (choice === show) { + await commands.executeCommand('workbench.view.scm'); + } + + await sourceRepository.popStash(stashes[0].index, { reinstateStagedChanges: true }); + return; + } + + await sourceRepository.popStash(stashes[0].index, { reinstateStagedChanges: true }); + throw err; + } + } + + private async retryRun(operation: Operation, runOperation: () => Promise): Promise { let attempt = 0; while (true) { @@ -2696,7 +3022,7 @@ export class Repository implements Disposable { const result = await runOperation(); return result; } finally { - await this.repository.popStash(); + await this.repository.popStash(undefined, { reinstateStagedChanges: true }); } } @@ -2974,6 +3300,12 @@ export class StagedResourceQuickDiffProvider implements QuickDiffProvider { return undefined; } + // Ignore path that is inside a hidden repository + if (this._repository.isHidden === true) { + this.logger.trace(`[StagedResourceQuickDiffProvider][provideOriginalResource] Repository is hidden: ${uri.toString()}`); + return undefined; + } + // Ignore symbolic links const stat = await workspace.fs.stat(uri); if ((stat.type & FileType.SymbolicLink) !== 0) { diff --git a/extensions/git/src/repositoryCache.ts b/extensions/git/src/repositoryCache.ts index 5e3f8cbe594..9254714d760 100644 --- a/extensions/git/src/repositoryCache.ts +++ b/extensions/git/src/repositoryCache.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LogOutputChannel, Memento, workspace } from 'vscode'; +import { LogOutputChannel, Memento, Uri, workspace } from 'vscode'; import { LRUCache } from './cache'; -import { Remote } from './api/git'; +import { Remote, RepositoryAccessDetails } from './api/git'; import { isDescendant } from './util'; export interface RepositoryCacheInfo { workspacePath: string; // path of the workspace folder or workspace file + lastTouchedTime?: number; // timestamp when the repository was last touched } function isRepositoryCacheInfo(obj: unknown): obj is RepositoryCacheInfo { @@ -17,7 +18,8 @@ function isRepositoryCacheInfo(obj: unknown): obj is RepositoryCacheInfo { return false; } const rec = obj as Record; - return typeof rec.workspacePath === 'string'; + return typeof rec.workspacePath === 'string' && + (rec.lastOpenedTime === undefined || typeof rec.lastOpenedTime === 'number'); } export class RepositoryCache { @@ -38,6 +40,33 @@ export class RepositoryCache { // Outer LRU: repoUrl -> inner LRU (folderPathOrWorkspaceFile -> RepositoryCacheInfo). private readonly lru = new LRUCache>(RepositoryCache.MAX_REPO_ENTRIES); + private _recentRepositories: Map | undefined; + + get recentRepositories(): Iterable { + if (!this._recentRepositories) { + this._recentRepositories = new Map(); + + for (const [_, inner] of this.lru) { + for (const [repositoryPath, repositoryDetails] of inner) { + if (!repositoryDetails.lastTouchedTime) { + continue; + } + + // Check whether the repository exists with a more recent access time + const repositoryLastAccessTime = this._recentRepositories.get(repositoryPath); + if (repositoryLastAccessTime && repositoryDetails.lastTouchedTime <= repositoryLastAccessTime) { + continue; + } + + this._recentRepositories.set(repositoryPath, repositoryDetails.lastTouchedTime); + } + } + } + + return Array.from(this._recentRepositories.entries()).map(([rootPath, lastAccessTime]) => + ({ rootUri: Uri.file(rootPath), lastAccessTime } satisfies RepositoryAccessDetails)); + } + constructor(public readonly _globalState: Memento, private readonly _logger: LogOutputChannel) { this.load(); } @@ -70,7 +99,8 @@ export class RepositoryCache { } foldersLru.set(folderPathOrWorkspaceFile, { - workspacePath: folderPathOrWorkspaceFile + workspacePath: folderPathOrWorkspaceFile, + lastTouchedTime: Date.now() }); // touch entry this.lru.set(key, foldersLru); this.save(); @@ -190,5 +220,9 @@ export class RepositoryCache { serialized.push([repo, folders]); } void this._globalState.update(RepositoryCache.STORAGE_KEY, serialized); + + // Invalidate recent repositories map + this._recentRepositories?.clear(); + this._recentRepositories = undefined; } } diff --git a/extensions/git/src/test/askpassManager.test.ts b/extensions/git/src/test/askpassManager.test.ts new file mode 100644 index 00000000000..3a90c078873 --- /dev/null +++ b/extensions/git/src/test/askpassManager.test.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { ensureAskpassScripts } from '../askpassManager'; +import { Event, EventEmitter, LogLevel, LogOutputChannel } from 'vscode'; + +class MockLogOutputChannel implements LogOutputChannel { + logLevel: LogLevel = LogLevel.Info; + onDidChangeLogLevel: Event = new EventEmitter().event; + private logs: { level: string; message: string }[] = []; + + trace(message: string, ..._args: any[]): void { + this.logs.push({ level: 'trace', message }); + } + debug(message: string, ..._args: any[]): void { + this.logs.push({ level: 'debug', message }); + } + info(message: string, ..._args: any[]): void { + this.logs.push({ level: 'info', message }); + } + warn(message: string, ..._args: any[]): void { + this.logs.push({ level: 'warn', message }); + } + error(error: string | Error, ..._args: any[]): void { + this.logs.push({ level: 'error', message: error.toString() }); + } + + name: string = 'MockLogOutputChannel'; + append(_value: string): void { } + appendLine(_value: string): void { } + replace(_value: string): void { } + clear(): void { } + show(_column?: unknown, _preserveFocus?: unknown): void { } + hide(): void { } + dispose(): void { } + + getLogs(): { level: string; message: string }[] { + return this.logs; + } + + hasLog(level: string, messageSubstring: string): boolean { + return this.logs.some(log => log.level === level && log.message.includes(messageSubstring)); + } +} + +// Helper to set mtime on a directory +async function setDirectoryMtime(dirPath: string, mtime: Date): Promise { + await fs.promises.utimes(dirPath, mtime, mtime); +} + +suite('askpassManager', () => { + let tempDir: string; + let sourceDir: string; + + setup(async () => { + // Create a temporary directory for testing + tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'askpass-test-')); + + // Create source directory with dummy askpass files + sourceDir = path.join(tempDir, 'source'); + await fs.promises.mkdir(sourceDir, { recursive: true }); + + const askpassFiles = ['askpass.sh', 'askpass-main.js', 'ssh-askpass.sh', 'askpass-empty.sh', 'ssh-askpass-empty.sh']; + for (const file of askpassFiles) { + await fs.promises.writeFile(path.join(sourceDir, file), `#!/bin/sh\n# ${file}\n`); + } + }); + + teardown(async () => { + // Clean up temporary directory + try { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + } catch { + // Ignore errors during cleanup + } + }); + + test('garbage collection removes old directories', async function () { + const storageDir = path.join(tempDir, 'storage'); + const askpassBaseDir = path.join(storageDir, 'askpass'); + const logger = new MockLogOutputChannel(); + + // Create old directories with old mtimes (8 days ago) + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + const oldDirs = ['oldhash1', 'oldhash2']; + + for (const dirName of oldDirs) { + const dirPath = path.join(askpassBaseDir, dirName); + await fs.promises.mkdir(dirPath, { recursive: true }); + await fs.promises.writeFile(path.join(dirPath, 'test.txt'), 'old'); + await setDirectoryMtime(dirPath, oldDate); + } + + // Create a recent directory (1 day ago) + const recentDate = new Date(Date.now() - (1 * 24 * 60 * 60 * 1000)); + const recentDir = path.join(askpassBaseDir, 'recenthash'); + await fs.promises.mkdir(recentDir, { recursive: true }); + await fs.promises.writeFile(path.join(recentDir, 'test.txt'), 'recent'); + await setDirectoryMtime(recentDir, recentDate); + + // Call ensureAskpassScripts which should trigger garbage collection when creating a new directory + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that old directories were removed + for (const dirName of oldDirs) { + const dirPath = path.join(askpassBaseDir, dirName); + const exists = await fs.promises.access(dirPath).then(() => true).catch(() => false); + assert.strictEqual(exists, false, `Old directory ${dirName} should have been removed`); + } + + // Check that recent directory still exists + const recentExists = await fs.promises.access(recentDir).then(() => true).catch(() => false); + assert.strictEqual(recentExists, true, 'Recent directory should still exist'); + + // Check logs + assert.ok(logger.hasLog('info', 'Removing old askpass directory'), 'Should log removal of old directories'); + }); + + test('garbage collection skips non-directory entries', async function () { + const storageDir = path.join(tempDir, 'storage'); + const askpassBaseDir = path.join(storageDir, 'askpass'); + const logger = new MockLogOutputChannel(); + + // Create a file in the askpass directory (not a directory) + await fs.promises.mkdir(askpassBaseDir, { recursive: true }); + const filePath = path.join(askpassBaseDir, 'somefile.txt'); + await fs.promises.writeFile(filePath, 'test'); + + // Set old mtime + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + await fs.promises.utimes(filePath, oldDate, oldDate); + + // Call ensureAskpassScripts which should trigger garbage collection + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that file still exists (should not be removed) + const exists = await fs.promises.access(filePath).then(() => true).catch(() => false); + assert.strictEqual(exists, true, 'Non-directory file should not be removed'); + }); + + test('mtime is updated on existing directory', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Call ensureAskpassScripts to create the directory + const paths1 = await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Get the directory path and its initial mtime + const askpassDir = path.dirname(paths1.askpass); + const stat1 = await fs.promises.stat(askpassDir); + const mtime1 = stat1.mtime.getTime(); + + // Wait a bit to ensure time difference + await new Promise(resolve => setTimeout(resolve, 100)); + + // Call again (should update mtime) + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Check that mtime was updated + const stat2 = await fs.promises.stat(askpassDir); + const mtime2 = stat2.mtime.getTime(); + + assert.ok(mtime2 > mtime1, 'Mtime should be updated on subsequent calls'); + }); + + test('garbage collection handles empty askpass directory', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Don't create any askpass directories, just call ensureAskpassScripts + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Should complete without errors + assert.ok(true, 'Should handle empty or non-existent askpass directory gracefully'); + }); + + test('current content-addressed directory is not removed', async function () { + const storageDir = path.join(tempDir, 'storage'); + const logger = new MockLogOutputChannel(); + + // Create the current content-addressed directory + const paths = await ensureAskpassScripts(sourceDir, storageDir, logger); + const currentDir = path.dirname(paths.askpass); + + // Set its mtime to 8 days ago (would normally be removed) + const oldDate = new Date(Date.now() - (8 * 24 * 60 * 60 * 1000)); + await setDirectoryMtime(currentDir, oldDate); + + // Call again which should trigger GC + await ensureAskpassScripts(sourceDir, storageDir, logger); + + // Current directory should still exist + const exists = await fs.promises.access(currentDir).then(() => true).catch(() => false); + assert.strictEqual(exists, true, 'Current content-addressed directory should not be removed'); + }); +}); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 47000d78e91..1ccf04a423d 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -13,7 +13,7 @@ import { OperationKind, OperationResult } from './operation'; import { truncate } from './util'; import { provideSourceControlHistoryItemAvatar, provideSourceControlHistoryItemHoverCommands, provideSourceControlHistoryItemMessageLinks } from './historyItemDetailsProvider'; import { AvatarQuery, AvatarQueryCommit } from './api/git'; -import { getHistoryItemHover, getHistoryItemHoverCommitHashCommands, processHistoryItemRemoteHoverCommands } from './historyProvider'; +import { getCommitHover, getHoverCommitHashCommands, processHoverRemoteCommands } from './hover'; export class GitTimelineItem extends TimelineItem { static is(item: TimelineItem): item is GitTimelineItem { @@ -198,11 +198,11 @@ export class GitTimelineProvider implements TimelineProvider { const messageWithLinks = await provideSourceControlHistoryItemMessageLinks(this.model, repo, message) ?? message; const commands: Command[][] = [ - getHistoryItemHoverCommitHashCommands(uri, c.hash), - processHistoryItemRemoteHoverCommands(commitRemoteSourceCommands, c.hash) + getHoverCommitHashCommands(uri, c.hash), + processHoverRemoteCommands(commitRemoteSourceCommands, c.hash) ]; - item.tooltip = getHistoryItemHover(avatars?.get(c.hash), c.authorName, c.authorEmail, date, messageWithLinks, c.shortStat, commands); + item.tooltip = getCommitHover(avatars?.get(c.hash), c.authorName, c.authorEmail, date, messageWithLinks, c.shortStat, commands); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -227,7 +227,7 @@ export class GitTimelineProvider implements TimelineProvider { // TODO@eamodio: Replace with a better icon -- reflecting its status maybe? item.iconPath = new ThemeIcon('git-commit'); item.description = ''; - item.tooltip = getHistoryItemHover(undefined, you, undefined, date, Resource.getStatusText(index.type), undefined, undefined); + item.tooltip = getCommitHover(undefined, you, undefined, date, Resource.getStatusText(index.type), undefined, undefined); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { @@ -249,7 +249,7 @@ export class GitTimelineProvider implements TimelineProvider { const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working'); item.iconPath = new ThemeIcon('circle-outline'); item.description = ''; - item.tooltip = getHistoryItemHover(undefined, you, undefined, date, Resource.getStatusText(working.type), undefined, undefined); + item.tooltip = getCommitHover(undefined, you, undefined, date, Resource.getStatusText(working.type), undefined, undefined); const cmd = this.commands.resolveTimelineOpenDiffCommand(item, uri); if (cmd) { diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index fcc820c8cd4..c6ec6ece45c 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -8,6 +8,7 @@ import { dirname, normalize, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; import byline from 'byline'; +import { Stash } from './git'; export const isMacintosh = process.platform === 'darwin'; export const isWindows = process.platform === 'win32'; @@ -140,6 +141,9 @@ export function groupBy(arr: T[], fn: (el: T) => string): { [key: string]: T[ }, Object.create(null)); } +export function coalesce(array: ReadonlyArray): T[] { + return array.filter((e): e is T => !!e); +} export async function mkdirp(path: string, mode?: number): Promise { const mkdir = async () => { @@ -846,3 +850,27 @@ export function extractFilePathFromArgs(argv: string[], startIndex: number): str // leading quote and return the path as-is return path.slice(1); } + +export function getStashDescription(stash: Stash): string | undefined { + if (!stash.commitDate && !stash.branchName) { + return undefined; + } + + const descriptionSegments: string[] = []; + if (stash.commitDate) { + descriptionSegments.push(fromNow(stash.commitDate)); + } + if (stash.branchName) { + descriptionSegments.push(stash.branchName); + } + + return descriptionSegments.join(' \u2022 '); +} + +export function isCopilotWorktree(path: string): boolean { + const lastSepIndex = path.lastIndexOf(sep); + + return lastSepIndex !== -1 + ? path.substring(lastSepIndex + 1).startsWith('copilot-worktree-') + : path.startsWith('copilot-worktree-'); +} diff --git a/extensions/git/tsconfig.json b/extensions/git/tsconfig.json index eac688f81de..c9df6ca6f90 100644 --- a/extensions/git/tsconfig.json +++ b/extensions/git/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" @@ -11,9 +12,9 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts", + "../../src/vscode-dts/vscode.proposed.findFiles2.d.ts", "../../src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts", "../../src/vscode-dts/vscode.proposed.quickDiffProvider.d.ts", - "../../src/vscode-dts/vscode.proposed.quickInputButtonLocation.d.ts", "../../src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts", "../../src/vscode-dts/vscode.proposed.scmActionButton.d.ts", "../../src/vscode-dts/vscode.proposed.scmArtifactProvider.d.ts", @@ -28,6 +29,7 @@ "../../src/vscode-dts/vscode.proposed.tabInputTextMerge.d.ts", "../../src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts", "../../src/vscode-dts/vscode.proposed.timeline.d.ts", + "../../src/vscode-dts/vscode.proposed.workspaceTrust.d.ts", "../types/lib.textEncoder.d.ts" ] } diff --git a/extensions/github-authentication/media/index.html b/extensions/github-authentication/media/index.html index 385aa8991f1..3292e2a08fc 100644 --- a/extensions/github-authentication/media/index.html +++ b/extensions/github-authentication/media/index.html @@ -46,7 +46,12 @@

Launching

if (error) { document.querySelector('.error-message > .detail').textContent = error; document.querySelector('body').classList.add('error'); - } else if (redirectUri) { + } else if (!redirectUri) { + // Portable mode: authentication succeeded, no redirect needed + document.querySelector('.title').textContent = appName; + document.querySelector('.success-message > .subtitle').textContent = 'You have successfully signed in.'; + document.querySelector('.success-message > .detail').textContent = 'You can now close this window.'; + } else { // Wrap the redirect URI so that the browser remembers who triggered the redirect const wrappedRedirectUri = `https://vscode.dev/redirect?url=${encodeURIComponent(redirectUri)}`; // Set up the fallback link diff --git a/extensions/github-authentication/package-lock.json b/extensions/github-authentication/package-lock.json index b9aa790d966..93b4b14be17 100644 --- a/extensions/github-authentication/package-lock.json +++ b/extensions/github-authentication/package-lock.json @@ -193,6 +193,20 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "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-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -214,36 +228,219 @@ "node": ">=0.4.0" } }, + "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/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" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "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" + } + }, "node_modules/form-data": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", - "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "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.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", + "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" + }, + "funding": { + "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/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" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "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.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "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.44.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 91f2d1b57db..b9beaccfe2e 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -19,7 +19,8 @@ ], "enabledApiProposals": [ "authIssuers", - "authProviderSpecific" + "authProviderSpecific", + "envIsAppPortable" ], "activationEvents": [], "capabilities": { diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 76da600118a..dc9813436ac 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -354,7 +354,7 @@ class LocalServerFlow implements IFlow { path: '/login/oauth/authorize', query: searchParams.toString() }); - const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true), callbackUri.toString(true)); + const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl.toString(true), callbackUri.toString(true), env.isAppPortable); const port = await server.start(); let codeToExchange; diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 5d70ef443a1..b8646696a8a 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -301,9 +301,11 @@ export class GitHubServer implements IGitHubServer { ? 'faculty' : 'none'; } else { + this._logger.info(`Unable to resolve optional EDU details. Status: ${result.status} ${result.statusText}`); edu = 'unknown'; } } catch (e) { + this._logger.info(`Unable to resolve optional EDU details. Error: ${e}`); edu = 'unknown'; } diff --git a/extensions/github-authentication/src/node/authServer.ts b/extensions/github-authentication/src/node/authServer.ts index 0bc2768826d..45dca93e2d0 100644 --- a/extensions/github-authentication/src/node/authServer.ts +++ b/extensions/github-authentication/src/node/authServer.ts @@ -87,7 +87,7 @@ export class LoopbackAuthServer implements ILoopbackServer { return this._startingRedirect.searchParams.get('state') ?? undefined; } - constructor(serveRoot: string, startingRedirect: string, callbackUri: string) { + constructor(serveRoot: string, startingRedirect: string, callbackUri: string, isPortable: boolean) { if (!serveRoot) { throw new Error('serveRoot must be defined'); } @@ -132,7 +132,11 @@ export class LoopbackAuthServer implements ILoopbackServer { throw new Error('Nonce does not match.'); } deferred.resolve({ code, state }); - res.writeHead(302, { location: `/?redirect_uri=${encodeURIComponent(callbackUri)}${appNameQueryParam}` }); + if (isPortable) { + res.writeHead(302, { location: `/?app_name=${encodeURIComponent(env.appName)}` }); + } else { + res.writeHead(302, { location: `/?redirect_uri=${encodeURIComponent(callbackUri)}${appNameQueryParam}` }); + } res.end(); break; } diff --git a/extensions/github-authentication/src/node/fetch.ts b/extensions/github-authentication/src/node/fetch.ts index 8a20d30bd74..7bd5f929029 100644 --- a/extensions/github-authentication/src/node/fetch.ts +++ b/extensions/github-authentication/src/node/fetch.ts @@ -76,6 +76,16 @@ export function createFetch(): Fetch { }; } +function shouldNotRetry(status: number): boolean { + // Don't retry with other fetchers for these HTTP status codes: + // - 429 Too Many Requests (rate limiting) + // - 401 Unauthorized (authentication issue) + // - 403 Forbidden (authorization issue) + // - 404 Not Found (resource doesn't exist) + // These are application-level errors where retrying with a different fetcher won't help + return status === 429 || status === 401 || status === 403 || status === 404; +} + async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: string, options: FetchOptions, logService: Log): Promise<{ response: FetchResponse; updatedFetchers?: Fetcher[] }> { if (options.retryFallbacks && availableFetchers.length > 1) { let firstResult: { ok: boolean; response: FetchResponse } | { ok: false; err: any } | undefined; @@ -85,6 +95,11 @@ async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: st firstResult = result; } if (!result.ok) { + // For certain HTTP status codes, don't retry with other fetchers + // These are application-level errors, not network-level errors + if ('response' in result && shouldNotRetry(result.response.status)) { + return { response: result.response }; + } continue; } if (fetcher !== availableFetchers[0]) { @@ -110,6 +125,7 @@ async function fetchWithFallbacks(availableFetchers: readonly Fetcher[], url: st async function tryFetch(fetcher: Fetcher, url: string, options: FetchOptions, logService: Log): Promise<{ ok: boolean; response: FetchResponse } | { ok: false; err: any }> { try { + logService.debug(`FetcherService: trying fetcher ${fetcher.name} for ${url}`); const response = await fetcher.fetch(url, options); if (!response.ok) { logService.info(`FetcherService: ${fetcher.name} failed with status: ${response.status} ${response.statusText}`); diff --git a/extensions/github-authentication/src/test/node/authServer.test.ts b/extensions/github-authentication/src/test/node/authServer.test.ts index e7fdf6139bb..0ea8d0353bd 100644 --- a/extensions/github-authentication/src/test/node/authServer.test.ts +++ b/extensions/github-authentication/src/test/node/authServer.test.ts @@ -12,7 +12,7 @@ suite('LoopbackAuthServer', () => { let port: number; setup(async () => { - server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com'); + server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com', false); port = await server.start(); }); @@ -64,3 +64,35 @@ suite('LoopbackAuthServer', () => { ]); }); }); + +suite('LoopbackAuthServer (portable mode)', () => { + let server: LoopbackAuthServer; + let port: number; + + setup(async () => { + server = new LoopbackAuthServer(__dirname, 'http://localhost:8080', 'https://code.visualstudio.com', true); + port = await server.start(); + }); + + teardown(async () => { + await server.stop(); + }); + + test('should redirect to success page without redirect_uri on /callback', async () => { + server.state = 'valid-state'; + const response = await fetch( + `http://localhost:${port}/callback?code=valid-code&state=${server.state}&nonce=${server.nonce}`, + { redirect: 'manual' } + ); + assert.strictEqual(response.status, 302); + // In portable mode, should redirect to success page without redirect_uri + assert.strictEqual(response.headers.get('location'), `/?app_name=${encodeURIComponent(env.appName)}`); + await Promise.race([ + server.waitForOAuthResponse().then(result => { + assert.strictEqual(result.code, 'valid-code'); + assert.strictEqual(result.state, server.state); + }), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)) + ]); + }); +}); diff --git a/extensions/github-authentication/src/test/node/fetch.test.ts b/extensions/github-authentication/src/test/node/fetch.test.ts index 6ce569378b0..211b133e406 100644 --- a/extensions/github-authentication/src/test/node/fetch.test.ts +++ b/extensions/github-authentication/src/test/node/fetch.test.ts @@ -144,4 +144,46 @@ suite('fetching', () => { assert.strictEqual(res.status, 200); assert.deepStrictEqual(await res.text(), 'Hello, world!'); }); + + test('should not retry with other fetchers on 429 status', async () => { + // Set up server to return 429 for the first request + let requestCount = 0; + const oldListener = server.listeners('request')[0] as (req: http.IncomingMessage, res: http.ServerResponse) => void; + if (!oldListener) { + throw new Error('No request listener found on server'); + } + + server.removeAllListeners('request'); + server.on('request', (req, res) => { + requestCount++; + if (req.url === '/rate-limited') { + res.writeHead(429, { + 'Content-Type': 'text/plain', + 'X-Client-User-Agent': String(req.headers['user-agent'] ?? '').toLowerCase(), + }); + res.end('Too Many Requests'); + } else { + oldListener(req, res); + } + }); + + try { + const res = await createFetch()(`http://localhost:${port}/rate-limited`, { + logger, + retryFallbacks: true, + expectJSON: false, + }); + + // Verify only one request was made (no fallback attempts) + assert.strictEqual(requestCount, 1, 'Should only make one request for 429 status'); + assert.strictEqual(res.status, 429); + // Note: We only check that we got a response, not which fetcher was used, + // as the fetcher order may vary by configuration + assert.strictEqual(await res.text(), 'Too Many Requests'); + } finally { + // Restore original listener + server.removeAllListeners('request'); + server.on('request', oldListener); + } + }); }); diff --git a/extensions/github-authentication/tsconfig.json b/extensions/github-authentication/tsconfig.json index 1a65fe81095..faf5d5a39a3 100644 --- a/extensions/github-authentication/tsconfig.json +++ b/extensions/github-authentication/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" @@ -13,6 +14,7 @@ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts", - "../../src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts" + "../../src/vscode-dts/vscode.proposed.authProviderSpecific.d.ts", + "../../src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts" ] } diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index 8eb0f6b23f6..b4f8379e5f7 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -47,12 +47,12 @@ interface EditorLineNumberContext { export type LinkContext = vscode.Uri | EditorLineNumberContext | undefined; function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined; lineNumber: number | undefined } { - if (context instanceof vscode.Uri) { + if (context === undefined) { + return { fileUri: undefined, lineNumber: undefined }; + } else if (context instanceof vscode.Uri) { return { fileUri: context, lineNumber: undefined }; - } else if (context !== undefined && 'lineNumber' in context && 'uri' in context) { - return { fileUri: context.uri, lineNumber: context.lineNumber }; } else { - return { fileUri: undefined, lineNumber: undefined }; + return { fileUri: context.uri, lineNumber: context.lineNumber }; } } diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts index 291a3f1a6ba..bed2bb1aa6b 100644 --- a/extensions/github/src/remoteSourceProvider.ts +++ b/extensions/github/src/remoteSourceProvider.ts @@ -10,7 +10,15 @@ import { Octokit } from '@octokit/rest'; import { getRepositoryFromQuery, getRepositoryFromUrl } from './util.js'; import { getBranchLink, getVscodeDevHost } from './links.js'; -function asRemoteSource(raw: any): RemoteSource { +type RemoteSourceResponse = { + readonly full_name: string; + readonly description: string | null; + readonly stargazers_count: number; + readonly clone_url: string; + readonly ssh_url: string; +}; + +function asRemoteSource(raw: RemoteSourceResponse): RemoteSource { const protocol = workspace.getConfiguration('github').get<'https' | 'ssh'>('gitProtocol'); return { name: `$(github) ${raw.full_name}`, diff --git a/extensions/github/tsconfig.json b/extensions/github/tsconfig.json index f52e554090c..5f1ca63127b 100644 --- a/extensions/github/tsconfig.json +++ b/extensions/github/tsconfig.json @@ -1,12 +1,11 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", + "outDir": "./out", "module": "NodeNext", "moduleResolution": "NodeNext", - "outDir": "./out", "skipLibCheck": true, - "allowSyntheticDefaultImports": false, - "ignoreDeprecations": "6.0", "typeRoots": [ "./node_modules/@types" ] diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index d41f8a2672d..b697426969b 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -6,12 +6,12 @@ "git": { "name": "go-syntax", "repositoryUrl": "https://github.com/worlpaker/go-syntax", - "commitHash": "8c70c078f56d237f72574ce49cc95839c4f8a741" + "commitHash": "6e8421faf8f1445512825f63925e54a62106bcf1" } }, "license": "MIT", "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/worlpaker/go-syntax, which in turn was derived from https://github.com/jeff-hykin/better-go-syntax.", - "version": "0.8.4" + "version": "0.8.5" } ], "version": 1 diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index e83763a8eb5..72d7df0cb40 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/worlpaker/go-syntax/commit/8c70c078f56d237f72574ce49cc95839c4f8a741", + "version": "https://github.com/worlpaker/go-syntax/commit/6e8421faf8f1445512825f63925e54a62106bcf1", "name": "Go", "scopeName": "source.go", "patterns": [ @@ -2487,6 +2487,9 @@ { "include": "#struct_variables_types" }, + { + "include": "#support_functions" + }, { "include": "#type-declarations" }, diff --git a/extensions/grunt/tsconfig.json b/extensions/grunt/tsconfig.json index 22c47de77db..a2cbe0e9ea3 100644 --- a/extensions/grunt/tsconfig.json +++ b/extensions/grunt/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/gulp/tsconfig.json b/extensions/gulp/tsconfig.json index 22c47de77db..a2cbe0e9ea3 100644 --- a/extensions/gulp/tsconfig.json +++ b/extensions/gulp/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/html-language-features/client/tsconfig.json b/extensions/html-language-features/client/tsconfig.json index 051d5823fe5..b919fbffad5 100644 --- a/extensions/html-language-features/client/tsconfig.json +++ b/extensions/html-language-features/client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "webworker" diff --git a/extensions/html-language-features/package-lock.json b/extensions/html-language-features/package-lock.json index afdc70287a1..442b79121eb 100644 --- a/extensions/html-language-features/package-lock.json +++ b/extensions/html-language-features/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "vscode-languageclient": "^10.0.0-next.17", + "vscode-languageclient": "^10.0.0-next.18", "vscode-uri": "^3.1.0" }, "devDependencies": { @@ -224,35 +224,35 @@ "license": "MIT" }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.17", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.17.tgz", - "integrity": "sha512-hSnWKNS8MqMih/HlT7eABuzsvifa9qtGbL8oGH90K9jangtJXx6FKSFIjyWz0Yt8NRz1bGJ7rNM5t8B5+NCSDQ==", + "version": "10.0.0-next.18", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.18.tgz", + "integrity": "sha512-Dpcr0VEEf4SuMW17TFCuKovhvbCx6/tHTnmFyLW1KTJCdVmNG08hXVAmw8Z/izec7TQlzEvzw5PvRfYGzdtr5Q==", "license": "MIT", "dependencies": { "minimatch": "^10.0.3", "semver": "^7.7.1", - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 2aae9f4a113..4e630b72736 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -265,7 +265,7 @@ }, "dependencies": { "@vscode/extension-telemetry": "^0.9.8", - "vscode-languageclient": "^10.0.0-next.17", + "vscode-languageclient": "^10.0.0-next.18", "vscode-uri": "^3.1.0" }, "devDependencies": { diff --git a/extensions/html-language-features/server/package-lock.json b/extensions/html-language-features/server/package-lock.json index 90b03405061..dc257f8b9d7 100644 --- a/extensions/html-language-features/server/package-lock.json +++ b/extensions/html-language-features/server/package-lock.json @@ -10,9 +10,9 @@ "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.8", - "vscode-html-languageservice": "^5.5.2", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-css-languageservice": "^6.3.9", + "vscode-html-languageservice": "^5.6.1", + "vscode-languageserver": "^10.0.0-next.15", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0" }, @@ -54,9 +54,9 @@ "license": "MIT" }, "node_modules/vscode-css-languageservice": { - "version": "6.3.8", - "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.8.tgz", - "integrity": "sha512-dBk/9ullEjIMbfSYAohGpDOisOVU1x2MQHOeU12ohGJQI7+r0PCimBwaa/pWpxl/vH4f7ibrBfxIZY3anGmHKQ==", + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.9.tgz", + "integrity": "sha512-1tLWfp+TDM5ZuVWht3jmaY5y7O6aZmpeXLoHl5bv1QtRsRKt4xYGRMmdJa5Pqx/FTkgRbsna9R+Gn2xE+evVuA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -66,9 +66,9 @@ } }, "node_modules/vscode-html-languageservice": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.5.2.tgz", - "integrity": "sha512-QpaUhCjvb7U/qThOzo4V6grwsRE62Jk/vf8BRJZoABlMw3oplLB5uovrvcrLO9vYhkeMiSjyqLnCxbfHzzZqmw==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.1.tgz", + "integrity": "sha512-5Mrqy5CLfFZUgkyhNZLA1Ye5g12Cb/v6VM7SxUzZUaRKWMDz4md+y26PrfRTSU0/eQAl3XpO9m2og+GGtDMuaA==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -78,33 +78,33 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { - "version": "10.0.0-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.14.tgz", - "integrity": "sha512-1TqBDfRLlAIPs6MR5ISI8z7sWlvGL3oHGm9GAHLNOmBZ2+9pmw0yR9vB44/SYuU4bSizxU24tXDFW+rw9jek4A==", + "version": "10.0.0-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.15.tgz", + "integrity": "sha512-vs+bwci/lM83ZhrR9t8DcZ2AgS2CKx4i6Yw86teKKkqlzlrYWTixuBd9w6H/UP9s8EGBvii0jnbjQd6wsKJ0ig==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 1eb6a452937..8208d3f22e6 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -10,9 +10,9 @@ "main": "./out/node/htmlServerMain", "dependencies": { "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.8", - "vscode-html-languageservice": "^5.5.2", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-css-languageservice": "^6.3.9", + "vscode-html-languageservice": "^5.6.1", + "vscode-languageserver": "^10.0.0-next.15", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0" }, diff --git a/extensions/html-language-features/server/tsconfig.json b/extensions/html-language-features/server/tsconfig.json index 0b49ec72b8f..97428f411f9 100644 --- a/extensions/html-language-features/server/tsconfig.json +++ b/extensions/html-language-features/server/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "ES2024", diff --git a/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts index dbd3ea1a618..f0330c88440 100644 --- a/extensions/ipynb/src/common.ts +++ b/extensions/ipynb/src/common.ts @@ -65,3 +65,36 @@ export interface CellMetadata { execution_count?: number | null; } + + +type KeysOfUnionType = T extends T ? keyof T : never; +type FilterType = T extends TTest ? T : never; +type MakeOptionalAndBool = { [K in keyof T]?: boolean }; + +/** + * Type guard that checks if an object has specific keys and narrows the type accordingly. + * + * @param x - The object to check + * @param key - An object with boolean values indicating which keys to check for + * @returns true if all specified keys exist in the object, false otherwise + * + * @example + * ```typescript + * type A = { a: string }; + * type B = { b: number }; + * const obj: A | B = getObject(); + * + * if (hasKey(obj, { a: true })) { + * // obj is now narrowed to type A + * console.log(obj.a); + * } + * ``` + */ +export function hasKey(x: T, key: TKeys & MakeOptionalAndBool): x is FilterType & keyof TKeys]: unknown }> { + for (const k in key) { + if (!(k in x)) { + return false; + } + } + return true; +} diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts index 930092f6feb..596a03db468 100644 --- a/extensions/ipynb/src/deserializers.ts +++ b/extensions/ipynb/src/deserializers.ts @@ -20,8 +20,7 @@ const jupyterLanguageToMonacoLanguageMapping = new Map([ export function getPreferredLanguage(metadata?: nbformat.INotebookMetadata) { const jupyterLanguage = metadata?.language_info?.name || - // eslint-disable-next-line local/code-no-any-casts - (metadata?.kernelspec as any)?.language; + (metadata?.kernelspec as unknown as { language: string })?.language; // Default to python language only if the Python extension is installed. const defaultLanguage = @@ -151,7 +150,7 @@ function convertJupyterOutputToBuffer(mime: string, value: unknown): NotebookCel } } -function getNotebookCellMetadata(cell: nbformat.IBaseCell): { +function getNotebookCellMetadata(cell: nbformat.ICell): { [key: string]: any; } { // We put this only for VSC to display in diff view. @@ -169,7 +168,7 @@ function getNotebookCellMetadata(cell: nbformat.IBaseCell): { cellMetadata['metadata'] = JSON.parse(JSON.stringify(cell['metadata'])); } - if ('id' in cell && typeof cell.id === 'string') { + if (typeof cell.id === 'string') { cellMetadata.id = cell.id; } @@ -291,8 +290,7 @@ export function jupyterCellOutputToCellOutput(output: nbformat.IOutput): Noteboo if (fn) { result = fn(output); } else { - // eslint-disable-next-line local/code-no-any-casts - result = translateDisplayDataOutput(output as any); + result = translateDisplayDataOutput(output as unknown as nbformat.IDisplayData | nbformat.IDisplayUpdate | nbformat.IExecuteResult); } return result; } @@ -324,7 +322,7 @@ function createNotebookCellDataFromCodeCell(cell: nbformat.ICodeCell, cellLangua ? { executionOrder: cell.execution_count as number } : {}; - const vscodeCustomMetadata = cell.metadata['vscode'] as { [key: string]: any } | undefined; + const vscodeCustomMetadata = cell.metadata?.['vscode'] as { [key: string]: any } | undefined; const cellLanguageId = vscodeCustomMetadata && vscodeCustomMetadata.languageId && typeof vscodeCustomMetadata.languageId === 'string' ? vscodeCustomMetadata.languageId : cellLanguage; const cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguageId); diff --git a/extensions/ipynb/src/helper.ts b/extensions/ipynb/src/helper.ts index 40dad3b887d..6a23633f52c 100644 --- a/extensions/ipynb/src/helper.ts +++ b/extensions/ipynb/src/helper.ts @@ -6,27 +6,26 @@ import { CancellationError } from 'vscode'; export function deepClone(obj: T): T { - if (!obj || typeof obj !== 'object') { + if (obj === null || typeof obj !== 'object') { return obj; } if (obj instanceof RegExp) { // See https://github.com/microsoft/TypeScript/issues/10990 - // eslint-disable-next-line local/code-no-any-casts - return obj as any; + return obj; + } + if (Array.isArray(obj)) { + return obj.map(item => deepClone(item)) as unknown as T; } - const result: any = Array.isArray(obj) ? [] : {}; - // eslint-disable-next-line local/code-no-any-casts - Object.keys(obj).forEach((key: string) => { - // eslint-disable-next-line local/code-no-any-casts - if ((obj)[key] && typeof (obj)[key] === 'object') { - // eslint-disable-next-line local/code-no-any-casts - result[key] = deepClone((obj)[key]); + const result = {}; + for (const key of Object.keys(obj as object) as Array) { + const value = obj[key]; + if (value && typeof value === 'object') { + (result as T)[key] = deepClone(value); } else { - // eslint-disable-next-line local/code-no-any-casts - result[key] = (obj)[key]; + (result as T)[key] = value; } - }); - return result; + } + return result as T; } // from https://github.com/microsoft/vscode/blob/43ae27a30e7b5e8711bf6b218ee39872ed2b8ef6/src/vs/base/common/objects.ts#L117 diff --git a/extensions/ipynb/src/notebookImagePaste.ts b/extensions/ipynb/src/notebookImagePaste.ts index 70a24e9bf2d..97c2ee73946 100644 --- a/extensions/ipynb/src/notebookImagePaste.ts +++ b/extensions/ipynb/src/notebookImagePaste.ts @@ -274,7 +274,7 @@ function buildAttachment( const filenameWithoutExt = basename(attachment.fileName, fileExt); let tempFilename = filenameWithoutExt + fileExt; - for (let appendValue = 2; tempFilename in cellMetadata.attachments; appendValue++) { + for (let appendValue = 2; cellMetadata.attachments[tempFilename]; appendValue++) { const objEntries = Object.entries(cellMetadata.attachments[tempFilename]); if (objEntries.length) { // check that mime:b64 are present const [mime, attachmentb64] = objEntries[0]; diff --git a/extensions/ipynb/src/serializers.ts b/extensions/ipynb/src/serializers.ts index a38ae39b6c7..6647c27176f 100644 --- a/extensions/ipynb/src/serializers.ts +++ b/extensions/ipynb/src/serializers.ts @@ -5,7 +5,7 @@ import type * as nbformat from '@jupyterlab/nbformat'; import type { NotebookCell, NotebookCellData, NotebookCellOutput, NotebookData, NotebookDocument } from 'vscode'; -import { CellOutputMetadata, type CellMetadata } from './common'; +import { CellOutputMetadata, hasKey, type CellMetadata } from './common'; import { textMimeTypes, NotebookCellKindMarkup, CellOutputMimeTypes, defaultNotebookFormat } from './constants'; const textDecoder = new TextDecoder(); @@ -37,20 +37,19 @@ export function sortObjectPropertiesRecursively(obj: any): any { } if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { return ( - // eslint-disable-next-line local/code-no-any-casts Object.keys(obj) .sort() - .reduce>((sortedObj, prop) => { + .reduce>((sortedObj, prop) => { sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); return sortedObj; - }, {}) as any + }, {}) ); } return obj; } export function getCellMetadata(options: { cell: NotebookCell | NotebookCellData } | { metadata?: { [key: string]: any } }): CellMetadata { - if ('cell' in options) { + if (hasKey(options, { cell: true })) { const cell = options.cell; const metadata = { execution_count: null, @@ -58,8 +57,7 @@ export function getCellMetadata(options: { cell: NotebookCell | NotebookCellData ...(cell.metadata ?? {}) } satisfies CellMetadata; if (cell.kind === NotebookCellKindMarkup) { - // eslint-disable-next-line local/code-no-any-casts - delete (metadata as any).execution_count; + delete (metadata as Record).execution_count; } return metadata; } else { @@ -400,10 +398,8 @@ export function pruneCell(cell: nbformat.ICell): nbformat.ICell { // Remove outputs and execution_count from non code cells if (result.cell_type !== 'code') { - // eslint-disable-next-line local/code-no-any-casts - delete (result).outputs; - // eslint-disable-next-line local/code-no-any-casts - delete (result).execution_count; + delete (result as Record).outputs; + delete (result as Record).execution_count; } else { // Clean outputs from code cells result.outputs = result.outputs ? (result.outputs as nbformat.IOutput[]).map(fixupOutput) : []; @@ -472,7 +468,7 @@ export function serializeNotebookToString(data: NotebookData): string { .map(cell => createJupyterCellFromNotebookCell(cell, preferredCellLanguage)) .map(pruneCell); - const indentAmount = data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string' ? + const indentAmount = data.metadata && typeof data.metadata.indentAmount === 'string' ? data.metadata.indentAmount : ' '; diff --git a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts index c0d921cb886..42395b0a238 100644 --- a/extensions/ipynb/src/test/notebookModelStoreSync.test.ts +++ b/extensions/ipynb/src/test/notebookModelStoreSync.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { CancellationTokenSource, Disposable, EventEmitter, ExtensionContext, NotebookCellKind, NotebookDocumentChangeEvent, NotebookDocumentWillSaveEvent, NotebookEdit, NotebookRange, TextDocumentSaveReason, workspace, type CancellationToken, type NotebookCell, type NotebookDocument, type WorkspaceEdit, type WorkspaceEditMetadata } from 'vscode'; +import { CancellationTokenSource, Disposable, EventEmitter, ExtensionContext, NotebookCellKind, NotebookDocumentChangeEvent, NotebookDocumentWillSaveEvent, NotebookEdit, NotebookRange, TextDocument, TextDocumentSaveReason, workspace, type CancellationToken, type NotebookCell, type NotebookDocument, type WorkspaceEdit, type WorkspaceEditMetadata } from 'vscode'; import { activate } from '../notebookModelStoreSync'; suite(`Notebook Model Store Sync`, () => { @@ -36,9 +36,8 @@ suite(`Notebook Model Store Sync`, () => { disposables.push(onDidChangeNotebookDocument); onWillSaveNotebookDocument = new AsyncEmitter(); - sinon.stub(NotebookEdit, 'updateCellMetadata').callsFake((index, metadata) => { - // eslint-disable-next-line local/code-no-any-casts - const edit = (NotebookEdit.updateCellMetadata as any).wrappedMethod.call(NotebookEdit, index, metadata); + const stub = sinon.stub(NotebookEdit, 'updateCellMetadata').callsFake((index, metadata) => { + const edit = stub.wrappedMethod.call(NotebookEdit, index, metadata); cellMetadataUpdates.push(edit); return edit; } @@ -76,8 +75,7 @@ suite(`Notebook Model Store Sync`, () => { test('Adding cell for non Jupyter Notebook will not result in any updates', async () => { sinon.stub(notebook, 'notebookType').get(() => 'some-other-type'); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -106,8 +104,7 @@ suite(`Notebook Model Store Sync`, () => { test('Adding cell to nbformat 4.2 notebook will result in adding empty metadata', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 2 })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -138,8 +135,7 @@ suite(`Notebook Model Store Sync`, () => { test('Added cell will have a cell id if nbformat is 4.5', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -173,8 +169,7 @@ suite(`Notebook Model Store Sync`, () => { test('Do not add cell id if one already exists', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -210,8 +205,7 @@ suite(`Notebook Model Store Sync`, () => { test('Do not perform any updates if cell id and metadata exists', async () => { sinon.stub(notebook, 'metadata').get(() => ({ nbformat: 4, nbformat_minor: 5 })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -248,10 +242,9 @@ suite(`Notebook Model Store Sync`, () => { } })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'javascript' - } as any, + } as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -271,10 +264,9 @@ suite(`Notebook Model Store Sync`, () => { cellChanges: [ { cell, - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'javascript' - } as any, + } as unknown as TextDocument, metadata: undefined, outputs: undefined, executionSummary: undefined @@ -300,10 +292,9 @@ suite(`Notebook Model Store Sync`, () => { } })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'javascript' - } as any, + } as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -344,10 +335,9 @@ suite(`Notebook Model Store Sync`, () => { } })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'javascript' - } as any, + } as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -368,10 +358,9 @@ suite(`Notebook Model Store Sync`, () => { cellChanges: [ { cell, - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'javascript' - } as any, + } as unknown as TextDocument, metadata: undefined, outputs: undefined, executionSummary: undefined @@ -397,10 +386,9 @@ suite(`Notebook Model Store Sync`, () => { } })); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'powershell' - } as any, + } as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, @@ -421,10 +409,9 @@ suite(`Notebook Model Store Sync`, () => { cellChanges: [ { cell, - // eslint-disable-next-line local/code-no-any-casts document: { languageId: 'powershell' - } as any, + } as unknown as TextDocument, metadata: undefined, outputs: undefined, executionSummary: undefined @@ -456,8 +443,7 @@ suite(`Notebook Model Store Sync`, () => { }); const cell: NotebookCell = { - // eslint-disable-next-line local/code-no-any-casts - document: {} as any, + document: {} as unknown as TextDocument, executionSummary: {}, index: 0, kind: NotebookCellKind.Code, diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts index e132b6b2b1d..acc13995ff5 100644 --- a/extensions/ipynb/src/test/serializers.test.ts +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -75,6 +75,53 @@ suite(`ipynb serializer`, () => { assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedCodeCell2, expectedMarkdownCell]); }); + test('Deserialize cells without metadata field', async () => { + // Test case for issue where cells without metadata field cause "Cannot read properties of undefined" error + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [], + source: 'print(1)' + }, + { + cell_type: 'code', + outputs: [], + source: 'print(2)' + }, + { + cell_type: 'markdown', + source: '# HEAD' + } + ] as unknown as nbformat.ICell[]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + assert.ok(notebook); + assert.strictEqual(notebook.cells.length, 3); + + // First cell with execution count + const cell1 = notebook.cells[0]; + assert.strictEqual(cell1.kind, vscode.NotebookCellKind.Code); + assert.strictEqual(cell1.value, 'print(1)'); + assert.strictEqual(cell1.languageId, 'python'); + assert.ok(cell1.metadata); + assert.strictEqual(cell1.metadata.execution_count, 10); + assert.deepStrictEqual(cell1.executionSummary, { executionOrder: 10 }); + + // Second cell without execution count + const cell2 = notebook.cells[1]; + assert.strictEqual(cell2.kind, vscode.NotebookCellKind.Code); + assert.strictEqual(cell2.value, 'print(2)'); + assert.strictEqual(cell2.languageId, 'python'); + assert.ok(cell2.metadata); + assert.strictEqual(cell2.metadata.execution_count, null); + assert.deepStrictEqual(cell2.executionSummary, {}); + + // Markdown cell + const cell3 = notebook.cells[2]; + assert.strictEqual(cell3.kind, vscode.NotebookCellKind.Markup); + assert.strictEqual(cell3.value, '# HEAD'); + assert.strictEqual(cell3.languageId, 'markdown'); + }); test('Serialize', async () => { const markdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# header1', 'markdown'); diff --git a/extensions/ipynb/tsconfig.json b/extensions/ipynb/tsconfig.json index 39ab6fc882d..e95df8b0015 100644 --- a/extensions/ipynb/tsconfig.json +++ b/extensions/ipynb/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "ES2024", diff --git a/extensions/jake/tsconfig.json b/extensions/jake/tsconfig.json index 22c47de77db..a2cbe0e9ea3 100644 --- a/extensions/jake/tsconfig.json +++ b/extensions/jake/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/java/cgmanifest.json b/extensions/java/cgmanifest.json index a62f6cdd1aa..ebb3d19beb5 100644 --- a/extensions/java/cgmanifest.json +++ b/extensions/java/cgmanifest.json @@ -49,4 +49,4 @@ } ], "version": 1 -} +} \ No newline at end of file diff --git a/extensions/javascript/javascript-language-configuration.json b/extensions/javascript/javascript-language-configuration.json index f458f66187f..7ca6762946c 100644 --- a/extensions/javascript/javascript-language-configuration.json +++ b/extensions/javascript/javascript-language-configuration.json @@ -26,6 +26,10 @@ ] ], "autoClosingPairs": [ + { + "open": "${", + "close": "}" + }, { "open": "{", "close": "}" @@ -70,6 +74,14 @@ } ], "surroundingPairs": [ + [ + "${", + "}" + ], + [ + "$", + "" + ], [ "{", "}" diff --git a/extensions/json-language-features/client/src/jsonClient.ts b/extensions/json-language-features/client/src/jsonClient.ts index 6d832e6c159..eb336e8f89b 100644 --- a/extensions/json-language-features/client/src/jsonClient.ts +++ b/extensions/json-language-features/client/src/jsonClient.ts @@ -7,9 +7,9 @@ export type JSONLanguageStatus = { schemas: string[] }; import { workspace, window, languages, commands, LogOutputChannel, ExtensionContext, extensions, Uri, ColorInformation, - Diagnostic, StatusBarAlignment, TextEditor, TextDocument, FormattingOptions, CancellationToken, FoldingRange, + Diagnostic, StatusBarAlignment, TextDocument, FormattingOptions, CancellationToken, FoldingRange, ProviderResult, TextEdit, Range, Position, Disposable, CompletionItem, CompletionList, CompletionContext, Hover, MarkdownString, FoldingContext, DocumentSymbol, SymbolInformation, l10n, - RelativePattern + RelativePattern, CodeAction, CodeActionKind, CodeActionContext } from 'vscode'; import { LanguageClientOptions, RequestType, NotificationType, FormattingOptions as LSPFormattingOptions, DocumentDiagnosticReportKind, @@ -20,8 +20,9 @@ import { import { hash } from './utils/hash'; -import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem } from './languageStatus'; +import { createDocumentSymbolsLimitItem, createLanguageStatusItem, createLimitStatusItem, createSchemaLoadIssueItem, createSchemaLoadStatusItem } from './languageStatus'; import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; +import { matchesUrlPattern } from './utils/urlMatch'; namespace VSCodeContentRequest { export const type: RequestType = new RequestType('vscode/content'); @@ -42,6 +43,7 @@ namespace LanguageStatusRequest { namespace ValidateContentRequest { export const type: RequestType<{ schemaUri: string; content: string }, LSPDiagnostic[], any> = new RequestType('json/validateContent'); } + interface SortOptions extends LSPFormattingOptions { } @@ -110,6 +112,7 @@ export namespace SettingIds { export const enableKeepLines = 'json.format.keepLines'; export const enableValidation = 'json.validate.enable'; export const enableSchemaDownload = 'json.schemaDownload.enable'; + export const trustedDomains = 'json.schemaDownload.trustedDomains'; export const maxItemsComputed = 'json.maxItemsComputed'; export const editorFoldingMaximumRegions = 'editor.foldingMaximumRegions'; export const editorColorDecoratorsLimit = 'editor.colorDecoratorsLimit'; @@ -119,6 +122,17 @@ export namespace SettingIds { export const colorDecoratorsLimit = 'colorDecoratorsLimit'; } +export namespace CommandIds { + export const workbenchActionOpenSettings = 'workbench.action.openSettings'; + export const workbenchTrustManage = 'workbench.trust.manage'; + export const retryResolveSchemaCommandId = '_json.retryResolveSchema'; + export const configureTrustedDomainsCommandId = '_json.configureTrustedDomains'; + export const showAssociatedSchemaList = '_json.showAssociatedSchemaList'; + export const clearCacheCommandId = 'json.clearCache'; + export const validateCommandId = 'json.validate'; + export const sortCommandId = 'json.sort'; +} + export interface TelemetryReporter { sendTelemetryEvent(eventName: string, properties?: { [key: string]: string; @@ -143,6 +157,16 @@ export interface SchemaRequestService { clearCache?(): Promise; } +export enum SchemaRequestServiceErrors { + UntrustedWorkspaceError = 1, + UntrustedSchemaError = 2, + OpenTextDocumentAccessError = 3, + HTTPDisabledError = 4, + HTTPError = 5, + VSCodeAccessError = 6, + UntitledAccessError = 7, +} + export const languageServerDescription = l10n.t('JSON Language Server'); let resultLimit = 5000; @@ -191,6 +215,8 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP const toDispose: Disposable[] = []; let rangeFormatting: Disposable | undefined = undefined; + let settingsCache: Settings | undefined = undefined; + let schemaAssociationsCache: Promise | undefined = undefined; const documentSelector = languageParticipants.documentSelector; @@ -200,14 +226,18 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP toDispose.push(schemaResolutionErrorStatusBarItem); const fileSchemaErrors = new Map(); - let schemaDownloadEnabled = true; + let schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload); + let trustedDomains = workspace.getConfiguration().get>(SettingIds.trustedDomains, {}); let isClientReady = false; const documentSymbolsLimitStatusbarItem = createLimitStatusItem((limit: number) => createDocumentSymbolsLimitItem(documentSelector, SettingIds.maxItemsComputed, limit)); toDispose.push(documentSymbolsLimitStatusbarItem); - toDispose.push(commands.registerCommand('json.clearCache', async () => { + const schemaLoadStatusItem = createSchemaLoadStatusItem((diagnostic: Diagnostic) => createSchemaLoadIssueItem(documentSelector, schemaDownloadEnabled, diagnostic)); + toDispose.push(schemaLoadStatusItem); + + toDispose.push(commands.registerCommand(CommandIds.clearCacheCommandId, async () => { if (isClientReady && runtime.schemaRequests.clearCache) { const cachedSchemas = await runtime.schemaRequests.clearCache(); await client.sendNotification(SchemaContentChangeNotification.type, cachedSchemas); @@ -215,12 +245,12 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP window.showInformationMessage(l10n.t('JSON schema cache cleared.')); })); - toDispose.push(commands.registerCommand('json.validate', async (schemaUri: Uri, content: string) => { + toDispose.push(commands.registerCommand(CommandIds.validateCommandId, async (schemaUri: Uri, content: string) => { const diagnostics: LSPDiagnostic[] = await client.sendRequest(ValidateContentRequest.type, { schemaUri: schemaUri.toString(), content }); return diagnostics.map(client.protocol2CodeConverter.asDiagnostic); })); - toDispose.push(commands.registerCommand('json.sort', async () => { + toDispose.push(commands.registerCommand(CommandIds.sortCommandId, async () => { if (isClientReady) { const textEditor = window.activeTextEditor; @@ -239,17 +269,10 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP } })); - function filterSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] { - const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); - if (schemaErrorIndex !== -1) { - const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; - fileSchemaErrors.set(uri.toString(), schemaResolveDiagnostic.message); - if (!schemaDownloadEnabled) { - diagnostics = diagnostics.filter(d => !isSchemaResolveError(d)); - } - if (window.activeTextEditor && window.activeTextEditor.document.uri.toString() === uri.toString()) { - schemaResolutionErrorStatusBarItem.show(); - } + function handleSchemaErrorDiagnostics(uri: Uri, diagnostics: Diagnostic[]): Diagnostic[] { + schemaLoadStatusItem.update(uri, diagnostics); + if (!schemaDownloadEnabled) { + return diagnostics.filter(d => !isSchemaResolveError(d)); } return diagnostics; } @@ -270,18 +293,18 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP }, middleware: { workspace: { - didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }) + didChangeConfiguration: () => client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) }) }, provideDiagnostics: async (uriOrDoc, previousResolutId, token, next) => { const diagnostics = await next(uriOrDoc, previousResolutId, token); if (diagnostics && diagnostics.kind === DocumentDiagnosticReportKind.Full) { const uri = uriOrDoc instanceof Uri ? uriOrDoc : uriOrDoc.uri; - diagnostics.items = filterSchemaErrorDiagnostics(uri, diagnostics.items); + diagnostics.items = handleSchemaErrorDiagnostics(uri, diagnostics.items); } return diagnostics; }, handleDiagnostics: (uri: Uri, diagnostics: Diagnostic[], next: HandleDiagnosticsSignature) => { - diagnostics = filterSchemaErrorDiagnostics(uri, diagnostics); + diagnostics = handleSchemaErrorDiagnostics(uri, diagnostics); next(uri, diagnostics); }, // testing the replace / insert mode @@ -373,7 +396,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP const uri = Uri.parse(uriPath); const uriString = uri.toString(true); if (uri.scheme === 'untitled') { - throw new ResponseError(3, l10n.t('Unable to load {0}', uriString)); + throw new ResponseError(SchemaRequestServiceErrors.UntitledAccessError, l10n.t('Unable to load {0}', uriString)); } if (uri.scheme === 'vscode') { try { @@ -382,7 +405,7 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP const content = await workspace.fs.readFile(uri); return new TextDecoder().decode(content); } catch (e) { - throw new ResponseError(5, e.toString(), e); + throw new ResponseError(SchemaRequestServiceErrors.VSCodeAccessError, e.toString(), e); } } else if (uri.scheme !== 'http' && uri.scheme !== 'https') { try { @@ -390,9 +413,15 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP schemaDocuments[uriString] = true; return document.getText(); } catch (e) { - throw new ResponseError(2, e.toString(), e); + throw new ResponseError(SchemaRequestServiceErrors.OpenTextDocumentAccessError, e.toString(), e); + } + } else if (schemaDownloadEnabled) { + if (!workspace.isTrusted) { + throw new ResponseError(SchemaRequestServiceErrors.UntrustedWorkspaceError, l10n.t('Downloading schemas is disabled in untrusted workspaces')); + } + if (!await isTrusted(uri)) { + throw new ResponseError(SchemaRequestServiceErrors.UntrustedSchemaError, l10n.t('Location {0} is untrusted', uriString)); } - } else if (schemaDownloadEnabled && workspace.isTrusted) { if (runtime.telemetry && uri.authority === 'schema.management.azure.com') { /* __GDPR__ "json.schema" : { @@ -406,13 +435,10 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP try { return await runtime.schemaRequests.getContent(uriString); } catch (e) { - throw new ResponseError(4, e.toString()); + throw new ResponseError(SchemaRequestServiceErrors.HTTPError, e.toString(), e); } } else { - if (!workspace.isTrusted) { - throw new ResponseError(1, l10n.t('Downloading schemas is disabled in untrusted workspaces')); - } - throw new ResponseError(1, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); + throw new ResponseError(SchemaRequestServiceErrors.HTTPDisabledError, l10n.t('Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } }); @@ -427,19 +453,6 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP } return false; }; - const handleActiveEditorChange = (activeEditor?: TextEditor) => { - if (!activeEditor) { - return; - } - - const activeDocUri = activeEditor.document.uri.toString(); - - if (activeDocUri && fileSchemaErrors.has(activeDocUri)) { - schemaResolutionErrorStatusBarItem.show(); - } else { - schemaResolutionErrorStatusBarItem.hide(); - } - }; const handleContentClosed = (uriString: string) => { if (handleContentChange(uriString)) { delete schemaDocuments[uriString]; @@ -484,59 +497,81 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP toDispose.push(workspace.onDidChangeTextDocument(e => handleContentChange(e.document.uri.toString()))); toDispose.push(workspace.onDidCloseTextDocument(d => handleContentClosed(d.uri.toString()))); - toDispose.push(window.onDidChangeActiveTextEditor(handleActiveEditorChange)); + toDispose.push(commands.registerCommand(CommandIds.retryResolveSchemaCommandId, triggerValidation)); - const handleRetryResolveSchemaCommand = () => { - if (window.activeTextEditor) { - schemaResolutionErrorStatusBarItem.text = '$(watch)'; - const activeDocUri = window.activeTextEditor.document.uri.toString(); - client.sendRequest(ForceValidateRequest.type, activeDocUri).then((diagnostics) => { - const schemaErrorIndex = diagnostics.findIndex(isSchemaResolveError); - if (schemaErrorIndex !== -1) { - // Show schema resolution errors in status bar only; ref: #51032 - const schemaResolveDiagnostic = diagnostics[schemaErrorIndex]; - fileSchemaErrors.set(activeDocUri, schemaResolveDiagnostic.message); - } else { - schemaResolutionErrorStatusBarItem.hide(); + toDispose.push(commands.registerCommand(CommandIds.configureTrustedDomainsCommandId, configureTrustedDomains)); + + toDispose.push(languages.registerCodeActionsProvider(documentSelector, { + provideCodeActions(_document: TextDocument, _range: Range, context: CodeActionContext): CodeAction[] { + const codeActions: CodeAction[] = []; + + for (const diagnostic of context.diagnostics) { + if (typeof diagnostic.code !== 'number') { + continue; } - schemaResolutionErrorStatusBarItem.text = '$(alert)'; - }); - } - }; + switch (diagnostic.code) { + case ErrorCodes.UntrustedSchemaError: { + const title = l10n.t('Configure Trusted Domains...'); + const action = new CodeAction(title, CodeActionKind.QuickFix); + const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri; + if (schemaUri) { + action.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title }; + } else { + action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title }; + } + action.diagnostics = [diagnostic]; + action.isPreferred = true; + codeActions.push(action); + } + break; + case ErrorCodes.HTTPDisabledError: { + const title = l10n.t('Enable Schema Downloading...'); + const action = new CodeAction(title, CodeActionKind.QuickFix); + action.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title }; + action.diagnostics = [diagnostic]; + action.isPreferred = true; + codeActions.push(action); + } + break; + } + } - toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand)); + return codeActions; + } + }, { + providedCodeActionKinds: [CodeActionKind.QuickFix] + })); - client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(false)); toDispose.push(extensions.onDidChange(async _ => { - client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true)); })); - const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern( - Uri.parse(`vscode://schemas-associations/`), - '**/schemas-associations.json') - ); + const associationWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.parse(`vscode://schemas-associations/`), '**/schemas-associations.json')); toDispose.push(associationWatcher); toDispose.push(associationWatcher.onDidChange(async _e => { - client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations()); + client.sendNotification(SchemaAssociationNotification.type, await getSchemaAssociations(true)); })); // manually register / deregister format provider based on the `json.format.enable` setting avoiding issues with late registration. See #71652. updateFormatterRegistration(); toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); - updateSchemaDownloadSetting(); - toDispose.push(workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(SettingIds.enableFormatter)) { updateFormatterRegistration(); } else if (e.affectsConfiguration(SettingIds.enableSchemaDownload)) { - updateSchemaDownloadSetting(); + schemaDownloadEnabled = !!workspace.getConfiguration().get(SettingIds.enableSchemaDownload); + triggerValidation(); } else if (e.affectsConfiguration(SettingIds.editorFoldingMaximumRegions) || e.affectsConfiguration(SettingIds.editorColorDecoratorsLimit)) { - client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings() }); + client.sendNotification(DidChangeConfigurationNotification.type, { settings: getSettings(true) }); + } else if (e.affectsConfiguration(SettingIds.trustedDomains)) { + trustedDomains = workspace.getConfiguration().get>(SettingIds.trustedDomains, {}); + triggerValidation(); } })); - toDispose.push(workspace.onDidGrantWorkspaceTrust(updateSchemaDownloadSetting)); + toDispose.push(workspace.onDidGrantWorkspaceTrust(() => triggerValidation())); toDispose.push(createLanguageStatusItem(documentSelector, (uri: string) => client.sendRequest(LanguageStatusRequest.type, uri))); @@ -572,20 +607,13 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP } } - function updateSchemaDownloadSetting() { - if (!workspace.isTrusted) { - schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to download schemas in untrusted workspaces.'); - schemaResolutionErrorStatusBarItem.command = 'workbench.trust.manage'; - return; - } - schemaDownloadEnabled = workspace.getConfiguration().get(SettingIds.enableSchemaDownload) !== false; - if (schemaDownloadEnabled) { - schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Unable to resolve schema. Click to retry.'); - schemaResolutionErrorStatusBarItem.command = '_json.retryResolveSchema'; - handleRetryResolveSchemaCommand(); - } else { - schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Downloading schemas is disabled. Click to configure.'); - schemaResolutionErrorStatusBarItem.command = { command: 'workbench.action.openSettings', arguments: [SettingIds.enableSchemaDownload], title: '' }; + async function triggerValidation() { + const activeTextEditor = window.activeTextEditor; + if (activeTextEditor && languageParticipants.hasLanguage(activeTextEditor.document.languageId)) { + schemaResolutionErrorStatusBarItem.text = '$(watch)'; + schemaResolutionErrorStatusBarItem.tooltip = l10n.t('Validating...'); + const activeDocUri = activeTextEditor.document.uri.toString(); + await client.sendRequest(ForceValidateRequest.type, activeDocUri); } } @@ -612,6 +640,111 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP }); } + function getSettings(forceRefresh: boolean): Settings { + if (!settingsCache || forceRefresh) { + settingsCache = computeSettings(); + } + return settingsCache; + } + + async function getSchemaAssociations(forceRefresh: boolean): Promise { + if (!schemaAssociationsCache || forceRefresh) { + schemaAssociationsCache = computeSchemaAssociations(); + } + return schemaAssociationsCache; + } + + async function isTrusted(uri: Uri): Promise { + if (uri.scheme !== 'http' && uri.scheme !== 'https') { + return true; + } + const uriString = uri.toString(true); + + // Check against trustedDomains setting + if (matchesUrlPattern(uri, trustedDomains)) { + return true; + } + + const knownAssociations = await getSchemaAssociations(false); + for (const association of knownAssociations) { + if (association.uri === uriString) { + return true; + } + } + const settingsCache = getSettings(false); + if (settingsCache.json && settingsCache.json.schemas) { + for (const schemaSetting of settingsCache.json.schemas) { + const schemaUri = schemaSetting.url; + if (schemaUri === uriString) { + return true; + } + } + } + return false; + } + + async function configureTrustedDomains(schemaUri: string): Promise { + interface QuickPickItemWithAction { + label: string; + description?: string; + execute: () => Promise; + } + + const items: QuickPickItemWithAction[] = []; + + try { + const uri = Uri.parse(schemaUri); + const domain = `${uri.scheme}://${uri.authority}`; + + // Add "Trust domain" option + items.push({ + label: l10n.t('Trust Domain: {0}', domain), + description: l10n.t('Allow all schemas from this domain'), + execute: async () => { + const config = workspace.getConfiguration(); + const currentDomains = config.get>(SettingIds.trustedDomains, {}); + currentDomains[domain] = true; + await config.update(SettingIds.trustedDomains, currentDomains, true); + await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains); + } + }); + + // Add "Trust URI" option + items.push({ + label: l10n.t('Trust URI: {0}', schemaUri), + description: l10n.t('Allow only this specific schema'), + execute: async () => { + const config = workspace.getConfiguration(); + const currentDomains = config.get>(SettingIds.trustedDomains, {}); + currentDomains[schemaUri] = true; + await config.update(SettingIds.trustedDomains, currentDomains, true); + await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains); + } + }); + } catch (e) { + runtime.logOutputChannel.error(`Failed to parse schema URI: ${schemaUri}`); + } + + + // Always add "Configure setting" option + items.push({ + label: l10n.t('Configure Setting'), + description: l10n.t('Open settings editor'), + execute: async () => { + await commands.executeCommand(CommandIds.workbenchActionOpenSettings, SettingIds.trustedDomains); + } + }); + + const selected = await window.showQuickPick(items, { + placeHolder: l10n.t('Select how to configure trusted schema domains') + }); + + if (selected) { + await selected.execute(); + } + } + + return { dispose: async () => { await client.stop(); @@ -621,9 +754,9 @@ async function startClientWithParticipants(_context: ExtensionContext, languageP }; } -async function getSchemaAssociations(): Promise { - return getSchemaExtensionAssociations() - .concat(await getDynamicSchemaAssociations()); +async function computeSchemaAssociations(): Promise { + const extensionAssociations = getSchemaExtensionAssociations(); + return extensionAssociations.concat(await getDynamicSchemaAssociations()); } function getSchemaExtensionAssociations(): ISchemaAssociation[] { @@ -680,7 +813,9 @@ async function getDynamicSchemaAssociations(): Promise { return result; } -function getSettings(): Settings { + + +function computeSettings(): Settings { const configuration = workspace.getConfiguration(); const httpSettings = workspace.getConfiguration('http'); @@ -781,8 +916,14 @@ function updateMarkdownString(h: MarkdownString): MarkdownString { return n; } -function isSchemaResolveError(d: Diagnostic) { - return d.code === /* SchemaResolveError */ 0x300; +export namespace ErrorCodes { + export const SchemaResolveError = 0x10000; + export const UntrustedSchemaError = SchemaResolveError + SchemaRequestServiceErrors.UntrustedSchemaError; + export const HTTPDisabledError = SchemaResolveError + SchemaRequestServiceErrors.HTTPDisabledError; +} + +export function isSchemaResolveError(d: Diagnostic) { + return typeof d.code === 'number' && d.code >= ErrorCodes.SchemaResolveError; } diff --git a/extensions/json-language-features/client/src/languageStatus.ts b/extensions/json-language-features/client/src/languageStatus.ts index 1064a0b5956..a608b4be7ca 100644 --- a/extensions/json-language-features/client/src/languageStatus.ts +++ b/extensions/json-language-features/client/src/languageStatus.ts @@ -6,9 +6,9 @@ import { window, languages, Uri, Disposable, commands, QuickPickItem, extensions, workspace, Extension, WorkspaceFolder, QuickPickItemKind, - ThemeIcon, TextDocument, LanguageStatusSeverity, l10n, DocumentSelector + ThemeIcon, TextDocument, LanguageStatusSeverity, l10n, DocumentSelector, Diagnostic } from 'vscode'; -import { JSONLanguageStatus, JSONSchemaSettings } from './jsonClient'; +import { CommandIds, ErrorCodes, isSchemaResolveError, JSONLanguageStatus, JSONSchemaSettings, SettingIds } from './jsonClient'; type ShowSchemasInput = { schemas: string[]; @@ -168,7 +168,7 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta statusItem.name = l10n.t('JSON Validation Status'); statusItem.severity = LanguageStatusSeverity.Information; - const showSchemasCommand = commands.registerCommand('_json.showAssociatedSchemaList', showSchemaList); + const showSchemasCommand = commands.registerCommand(CommandIds.showAssociatedSchemaList, showSchemaList); const activeEditorListener = window.onDidChangeActiveTextEditor(() => { updateLanguageStatus(); @@ -195,7 +195,7 @@ export function createLanguageStatusItem(documentSelector: DocumentSelector, sta statusItem.detail = l10n.t('multiple JSON schemas configured'); } statusItem.command = { - command: '_json.showAssociatedSchemaList', + command: CommandIds.showAssociatedSchemaList, title: l10n.t('Show Schemas'), arguments: [{ schemas, uri: document.uri.toString() } satisfies ShowSchemasInput] }; @@ -279,3 +279,86 @@ export function createDocumentSymbolsLimitItem(documentSelector: DocumentSelecto } +export function createSchemaLoadStatusItem(newItem: (fileSchemaError: Diagnostic) => Disposable) { + let statusItem: Disposable | undefined; + const fileSchemaErrors: Map = new Map(); + + const toDispose: Disposable[] = []; + toDispose.push(window.onDidChangeActiveTextEditor(textEditor => { + statusItem?.dispose(); + statusItem = undefined; + const doc = textEditor?.document; + if (doc) { + const fileSchemaError = fileSchemaErrors.get(doc.uri.toString()); + if (fileSchemaError !== undefined) { + statusItem = newItem(fileSchemaError); + } + } + })); + toDispose.push(workspace.onDidCloseTextDocument(document => { + fileSchemaErrors.delete(document.uri.toString()); + })); + + function update(uri: Uri, diagnostics: Diagnostic[]) { + const fileSchemaError = diagnostics.find(isSchemaResolveError); + const uriString = uri.toString(); + + if (fileSchemaError === undefined) { + fileSchemaErrors.delete(uriString); + if (statusItem && uriString === window.activeTextEditor?.document.uri.toString()) { + statusItem.dispose(); + statusItem = undefined; + } + } else { + const current = fileSchemaErrors.get(uriString); + if (current?.message === fileSchemaError.message) { + return; + } + fileSchemaErrors.set(uriString, fileSchemaError); + if (uriString === window.activeTextEditor?.document.uri.toString()) { + statusItem?.dispose(); + statusItem = newItem(fileSchemaError); + } + } + } + return { + update, + dispose() { + statusItem?.dispose(); + toDispose.forEach(d => d.dispose()); + toDispose.length = 0; + statusItem = undefined; + fileSchemaErrors.clear(); + } + }; +} + + + +export function createSchemaLoadIssueItem(documentSelector: DocumentSelector, schemaDownloadEnabled: boolean | undefined, diagnostic: Diagnostic): Disposable { + const statusItem = languages.createLanguageStatusItem('json.documentSymbolsStatus', documentSelector); + statusItem.name = l10n.t('JSON Outline Status'); + statusItem.severity = LanguageStatusSeverity.Error; + statusItem.text = 'Schema download issue'; + if (!workspace.isTrusted) { + statusItem.detail = l10n.t('Workspace untrusted'); + statusItem.command = { command: CommandIds.workbenchTrustManage, title: 'Configure Trust' }; + } else if (!schemaDownloadEnabled) { + statusItem.detail = l10n.t('Download disabled'); + statusItem.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.enableSchemaDownload], title: 'Configure' }; + } else if (typeof diagnostic.code === 'number' && diagnostic.code === ErrorCodes.UntrustedSchemaError) { + statusItem.detail = l10n.t('Location untrusted'); + const schemaUri = diagnostic.relatedInformation?.[0]?.location.uri; + if (schemaUri) { + statusItem.command = { command: CommandIds.configureTrustedDomainsCommandId, arguments: [schemaUri.toString()], title: 'Configure Trusted Domains' }; + } else { + statusItem.command = { command: CommandIds.workbenchActionOpenSettings, arguments: [SettingIds.trustedDomains], title: 'Configure Trusted Domains' }; + } + } else { + statusItem.detail = l10n.t('Unable to resolve schema'); + statusItem.command = { command: CommandIds.retryResolveSchemaCommandId, title: 'Retry' }; + } + return Disposable.from(statusItem); +} + + diff --git a/extensions/json-language-features/client/src/utils/urlMatch.ts b/extensions/json-language-features/client/src/utils/urlMatch.ts new file mode 100644 index 00000000000..a870c2d0726 --- /dev/null +++ b/extensions/json-language-features/client/src/utils/urlMatch.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri } from 'vscode'; + +/** + * Check whether a URL matches the list of trusted domains or URIs. + * + * trustedDomains is an object where: + * - Keys are full domains (https://www.microsoft.com) or full URIs (https://www.test.com/schemas/mySchema.json) + * - Keys can include wildcards (https://*.microsoft.com) or glob patterns + * - Values are booleans indicating if the domain/URI is trusted (true) or blocked (false) + * + * @param url The URL to check + * @param trustedDomains Object mapping domain patterns to boolean trust values + */ +export function matchesUrlPattern(url: Uri, trustedDomains: Record): boolean { + // Check localhost + if (isLocalhostAuthority(url.authority)) { + return true; + } + + for (const [pattern, isTrusted] of Object.entries(trustedDomains)) { + if (typeof pattern !== 'string' || pattern.trim() === '') { + continue; + } + + // Wildcard matches everything + if (pattern === '*') { + return isTrusted; + } + + try { + const patternUri = Uri.parse(pattern); + + // Scheme must match + if (url.scheme !== patternUri.scheme) { + continue; + } + + // Check authority (host:port) + if (!matchesAuthority(url.authority, patternUri.authority)) { + continue; + } + + // Check path + if (!matchesPath(url.path, patternUri.path)) { + continue; + } + + return isTrusted; + } catch { + // Invalid pattern, skip + continue; + } + } + + return false; +} + +function matchesAuthority(urlAuthority: string, patternAuthority: string): boolean { + urlAuthority = urlAuthority.toLowerCase(); + patternAuthority = patternAuthority.toLowerCase(); + + if (patternAuthority === urlAuthority) { + return true; + } + // Handle wildcard subdomains (e.g., *.github.com) + if (patternAuthority.startsWith('*.')) { + const patternDomain = patternAuthority.substring(2); + // Exact match or subdomain match + return urlAuthority === patternDomain || urlAuthority.endsWith('.' + patternDomain); + } + + return false; +} + +function matchesPath(urlPath: string, patternPath: string): boolean { + // Empty pattern path or just "/" matches any path + if (!patternPath || patternPath === '/') { + return true; + } + + // Exact match + if (urlPath === patternPath) { + return true; + } + + // If pattern ends with '/', it matches any path starting with it + if (patternPath.endsWith('/')) { + return urlPath.startsWith(patternPath); + } + + // Otherwise, pattern must be a prefix + return urlPath.startsWith(patternPath + '/') || urlPath === patternPath; +} + + +const rLocalhost = /^(.+\.)?localhost(:\d+)?$/i; +const r127 = /^127\.0\.0\.1(:\d+)?$/; +const rIPv6Localhost = /^\[::1\](:\d+)?$/; + +function isLocalhostAuthority(authority: string): boolean { + return rLocalhost.test(authority) || r127.test(authority) || rIPv6Localhost.test(authority); +} diff --git a/extensions/json-language-features/client/tsconfig.json b/extensions/json-language-features/client/tsconfig.json index bc775d950e5..10c85fba3b4 100644 --- a/extensions/json-language-features/client/tsconfig.json +++ b/extensions/json-language-features/client/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "webworker" diff --git a/extensions/json-language-features/package-lock.json b/extensions/json-language-features/package-lock.json index b81fc2f026e..c7c66f40ccc 100644 --- a/extensions/json-language-features/package-lock.json +++ b/extensions/json-language-features/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", - "vscode-languageclient": "^10.0.0-next.17" + "vscode-languageclient": "^10.0.0-next.18" }, "devDependencies": { "@types/node": "22.x" @@ -229,35 +229,35 @@ "license": "MIT" }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageclient": { - "version": "10.0.0-next.17", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.17.tgz", - "integrity": "sha512-hSnWKNS8MqMih/HlT7eABuzsvifa9qtGbL8oGH90K9jangtJXx6FKSFIjyWz0Yt8NRz1bGJ7rNM5t8B5+NCSDQ==", + "version": "10.0.0-next.18", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-10.0.0-next.18.tgz", + "integrity": "sha512-Dpcr0VEEf4SuMW17TFCuKovhvbCx6/tHTnmFyLW1KTJCdVmNG08hXVAmw8Z/izec7TQlzEvzw5PvRfYGzdtr5Q==", "license": "MIT", "dependencies": { "minimatch": "^10.0.3", "semver": "^7.7.1", - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "engines": { "vscode": "^1.91.0" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/json-language-features/package.json b/extensions/json-language-features/package.json index 0fb4901a586..9529d657286 100644 --- a/extensions/json-language-features/package.json +++ b/extensions/json-language-features/package.json @@ -126,6 +126,23 @@ "tags": [ "usesOnlineServices" ] + }, + "json.schemaDownload.trustedDomains": { + "type": "object", + "default": { + "https://schemastore.azurewebsites.net/": true, + "https://raw.githubusercontent.com/": true, + "https://www.schemastore.org/": true, + "https://json.schemastore.org/": true, + "https://json-schema.org/": true + }, + "additionalProperties": { + "type": "boolean" + }, + "description": "%json.schemaDownload.trustedDomains.desc%", + "tags": [ + "usesOnlineServices" + ] } } }, @@ -171,7 +188,7 @@ "dependencies": { "@vscode/extension-telemetry": "^0.9.8", "request-light": "^0.8.0", - "vscode-languageclient": "^10.0.0-next.17" + "vscode-languageclient": "^10.0.0-next.18" }, "devDependencies": { "@types/node": "22.x" diff --git a/extensions/json-language-features/package.nls.json b/extensions/json-language-features/package.nls.json index abc07c993dc..9052d3781c9 100644 --- a/extensions/json-language-features/package.nls.json +++ b/extensions/json-language-features/package.nls.json @@ -19,6 +19,6 @@ "json.enableSchemaDownload.desc": "When enabled, JSON schemas can be fetched from http and https locations.", "json.command.clearCache": "Clear Schema Cache", "json.command.sort": "Sort Document", - "json.workspaceTrust": "The extension requires workspace trust to load schemas from http and https." - + "json.workspaceTrust": "The extension requires workspace trust to load schemas from http and https.", + "json.schemaDownload.trustedDomains.desc": "List of trusted domains for downloading JSON schemas over http(s). Use '*' to trust all domains. '*' can also be used as a wildcard in domain names." } diff --git a/extensions/json-language-features/server/package-lock.json b/extensions/json-language-features/server/package-lock.json index e0f73335f29..4761136e1bf 100644 --- a/extensions/json-language-features/server/package-lock.json +++ b/extensions/json-language-features/server/package-lock.json @@ -12,8 +12,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.6.3", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-json-languageservice": "^5.7.1", + "vscode-languageserver": "^10.0.0-next.15", "vscode-uri": "^3.1.0" }, "bin": { @@ -67,9 +67,9 @@ "license": "MIT" }, "node_modules/vscode-json-languageservice": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.6.3.tgz", - "integrity": "sha512-UDF7sJF5t7mzUzXL6dsClkvnHS4xnDL/gOMKGQiizRHmswlk/xSPGZxEvAtszWQF0ImNcJ0j9l+rHuefGzit1w==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-5.7.1.tgz", + "integrity": "sha512-sMK2F8p7St0lJCr/4IfbQRoEUDUZRR7Ud0IiSl8I/JtN+m9Gv+FJlNkSAYns2R7Ebm/PKxqUuWYOfBej/rAdBQ==", "license": "MIT", "dependencies": { "@vscode/l10n": "^0.0.18", @@ -80,33 +80,33 @@ } }, "node_modules/vscode-jsonrpc": { - "version": "9.0.0-next.9", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.9.tgz", - "integrity": "sha512-IM/RHL7ZklEUh1N2Rh4OjRL6D9MyIXq3v+zIkPLXq74hM1eW7WRLP0/cjzNu/baRFC00sFxJm95RBKsT8dXzRQ==", + "version": "9.0.0-next.10", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.10.tgz", + "integrity": "sha512-P+UOjuG/B1zkLM+bGIdmBwSkDejxtgo6EjG0pIkwnFBI0a2Mb7od36uUu8CPbECeQuh+n3zGcNwDl16DhuJ5IA==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/vscode-languageserver": { - "version": "10.0.0-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.14.tgz", - "integrity": "sha512-1TqBDfRLlAIPs6MR5ISI8z7sWlvGL3oHGm9GAHLNOmBZ2+9pmw0yR9vB44/SYuU4bSizxU24tXDFW+rw9jek4A==", + "version": "10.0.0-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.0-next.15.tgz", + "integrity": "sha512-vs+bwci/lM83ZhrR9t8DcZ2AgS2CKx4i6Yw86teKKkqlzlrYWTixuBd9w6H/UP9s8EGBvii0jnbjQd6wsKJ0ig==", "license": "MIT", "dependencies": { - "vscode-languageserver-protocol": "3.17.6-next.14" + "vscode-languageserver-protocol": "3.17.6-next.15" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "node_modules/vscode-languageserver-protocol": { - "version": "3.17.6-next.14", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.14.tgz", - "integrity": "sha512-0VD83wxN5kI9vgeaIDQnAxgrbZfKiFNIxdFY5LKe3SZdZd+LAJLMrklSrwfefS7hEzaHw6Z++VFdVJJU+gh1Zg==", + "version": "3.17.6-next.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.15.tgz", + "integrity": "sha512-aoWX1wwGCndzfrTRhGKVpKAPVy9+WYhUtZW/PJQfHODmVwhVwb4we68CgsQZRTl36t8ZqlSOO2c2TdBPW7hrCw==", "license": "MIT", "dependencies": { - "vscode-jsonrpc": "9.0.0-next.9", + "vscode-jsonrpc": "9.0.0-next.10", "vscode-languageserver-types": "3.17.6-next.6" } }, diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index 446087c31f0..6534e6f0eca 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -15,8 +15,8 @@ "@vscode/l10n": "^0.0.18", "jsonc-parser": "^3.3.1", "request-light": "^0.8.0", - "vscode-json-languageservice": "^5.6.3", - "vscode-languageserver": "^10.0.0-next.14", + "vscode-json-languageservice": "^5.7.1", + "vscode-languageserver": "^10.0.0-next.15", "vscode-uri": "^3.1.0" }, "devDependencies": { diff --git a/extensions/json-language-features/server/src/jsonServer.ts b/extensions/json-language-features/server/src/jsonServer.ts index cbe1e7d02b4..811cbcd2e91 100644 --- a/extensions/json-language-features/server/src/jsonServer.ts +++ b/extensions/json-language-features/server/src/jsonServer.ts @@ -5,7 +5,7 @@ import { Connection, - TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, + TextDocuments, InitializeParams, InitializeResult, NotificationType, RequestType, ResponseError, DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentSyncKind, TextEdit, DocumentFormattingRequest, TextDocumentIdentifier, FormattingOptions, Diagnostic, CodeAction, CodeActionKind } from 'vscode-languageserver'; @@ -36,6 +36,10 @@ namespace ForceValidateRequest { export const type: RequestType = new RequestType('json/validate'); } +namespace ForceValidateAllRequest { + export const type: RequestType = new RequestType('json/validateAll'); +} + namespace LanguageStatusRequest { export const type: RequestType = new RequestType('json/languageStatus'); } @@ -102,8 +106,8 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } return connection.sendRequest(VSCodeContentRequest.type, uri).then(responseText => { return responseText; - }, error => { - return Promise.reject(error.message); + }, (error: ResponseError) => { + return Promise.reject(error); }); }; } @@ -298,6 +302,10 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) }); // Retry schema validation on all open documents + connection.onRequest(ForceValidateAllRequest.type, async () => { + diagnosticsSupport?.requestRefresh(); + }); + connection.onRequest(ForceValidateRequest.type, async uri => { const document = documents.get(uri); if (document) { @@ -387,11 +395,11 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) connection.onDidChangeWatchedFiles((change) => { // Monitored files have changed in VSCode let hasChanges = false; - change.changes.forEach(c => { + for (const c of change.changes) { if (languageService.resetSchema(c.uri)) { hasChanges = true; } - }); + } if (hasChanges) { diagnosticsSupport?.requestRefresh(); } diff --git a/extensions/json-language-features/server/tsconfig.json b/extensions/json-language-features/server/tsconfig.json index 07433e08b62..2c01a5ed332 100644 --- a/extensions/json-language-features/server/tsconfig.json +++ b/extensions/json-language-features/server/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "sourceMap": true, "sourceRoot": "../src", diff --git a/extensions/json/build/update-grammars.js b/extensions/json/build/update-grammars.js index 2b7f76f8f90..13356a2c4c4 100644 --- a/extensions/json/build/update-grammars.js +++ b/extensions/json/build/update-grammars.js @@ -9,7 +9,7 @@ var updateGrammar = require('vscode-grammar-updater'); function adaptJSON(grammar, name, replacementScope, replaceeScope = 'json') { grammar.name = name; grammar.scopeName = `source${replacementScope}`; - const regex = new RegExp(`\.${replaceeScope}`, 'g'); + const regex = new RegExp(`\\.${replaceeScope}`, 'g'); var fixScopeNames = function (rule) { if (typeof rule.name === 'string') { rule.name = rule.name.replace(regex, replacementScope); diff --git a/extensions/julia/cgmanifest.json b/extensions/julia/cgmanifest.json index 2d59c264d57..e8c9413cb07 100644 --- a/extensions/julia/cgmanifest.json +++ b/extensions/julia/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "JuliaEditorSupport/atom-language-julia", "repositoryUrl": "https://github.com/JuliaEditorSupport/atom-language-julia", - "commitHash": "111548fbd25d083ec131d2732a4f46953ea92a65" + "commitHash": "93454227ce9a7aa92f41b157c6a74f3971b4ae14" } }, "license": "MIT", diff --git a/extensions/julia/package.json b/extensions/julia/package.json index 12d38ed31b2..741271fb231 100644 --- a/extensions/julia/package.json +++ b/extensions/julia/package.json @@ -9,7 +9,7 @@ "vscode": "0.10.x" }, "scripts": { - "update-grammar": "node ../node_modules/vscode-grammar-updater/bin JuliaEditorSupport/atom-language-julia grammars/julia_vscode.json ./syntaxes/julia.tmLanguage.json" + "update-grammar": "node ../node_modules/vscode-grammar-updater/bin JuliaEditorSupport/atom-language-julia variants/julia_vscode.json ./syntaxes/julia.tmLanguage.json" }, "categories": ["Programming Languages"], "contributes": { diff --git a/extensions/julia/syntaxes/julia.tmLanguage.json b/extensions/julia/syntaxes/julia.tmLanguage.json index 8f7298ea4ce..b222b3ba644 100644 --- a/extensions/julia/syntaxes/julia.tmLanguage.json +++ b/extensions/julia/syntaxes/julia.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/JuliaEditorSupport/atom-language-julia/blob/master/grammars/julia_vscode.json", + "This file has been converted from https://github.com/JuliaEditorSupport/atom-language-julia/blob/master/variants/julia_vscode.json", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/111548fbd25d083ec131d2732a4f46953ea92a65", + "version": "https://github.com/JuliaEditorSupport/atom-language-julia/commit/93454227ce9a7aa92f41b157c6a74f3971b4ae14", "name": "Julia", "scopeName": "source.julia", "comment": "This grammar is used by Atom (Oniguruma), GitHub (PCRE), and VSCode (Oniguruma),\nso all regexps must be compatible with both engines.\n\nSpecs:\n- https://github.com/kkos/oniguruma/blob/master/doc/RE\n- https://www.pcre.org/current/doc/html/", diff --git a/extensions/latex/cgmanifest.json b/extensions/latex/cgmanifest.json index 3dc5c4baef7..0b127e039fe 100644 --- a/extensions/latex/cgmanifest.json +++ b/extensions/latex/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "jlelong/vscode-latex-basics", "repositoryUrl": "https://github.com/jlelong/vscode-latex-basics", - "commitHash": "ca85e20304afcb5c6a28a6e0b9fc1ead8f124001" + "commitHash": "1f62731d63abfd134e03f4744fcbccadac4e0153" } }, "license": "MIT", - "version": "1.15.0", + "version": "1.16.0", "description": "The files in syntaxes/ were originally part of https://github.com/James-Yu/LaTeX-Workshop. They have been extracted in the hope that they can useful outside of the LaTeX-Workshop extension.", "licenseDetail": [ "Copyright (c) vscode-latex-basics authors", diff --git a/extensions/latex/syntaxes/LaTeX.tmLanguage.json b/extensions/latex/syntaxes/LaTeX.tmLanguage.json index 0ff855afddc..45e1ae4f7ed 100644 --- a/extensions/latex/syntaxes/LaTeX.tmLanguage.json +++ b/extensions/latex/syntaxes/LaTeX.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/jlelong/vscode-latex-basics/commit/84ce12aa6be384369ff218ac25efb27e6f34e78c", + "version": "https://github.com/jlelong/vscode-latex-basics/commit/1f62731d63abfd134e03f4744fcbccadac4e0153", "name": "LaTeX", "scopeName": "text.tex.latex", "patterns": [ @@ -209,7 +209,7 @@ "name": "meta.function.section.$3.latex", "patterns": [ { - "include": "text.tex#braces" + "include": "#braces" }, { "include": "$self" @@ -241,7 +241,7 @@ "name": "meta.function.emph.latex", "patterns": [ { - "include": "text.tex#braces" + "include": "#braces" }, { "include": "$self" @@ -272,7 +272,7 @@ "name": "meta.function.textit.latex", "patterns": [ { - "include": "text.tex#braces" + "include": "#braces" }, { "include": "$self" @@ -302,7 +302,7 @@ "name": "meta.function.textbf.latex", "patterns": [ { - "include": "text.tex#braces" + "include": "#braces" }, { "include": "$self" @@ -332,7 +332,7 @@ "name": "meta.function.texttt.latex", "patterns": [ { - "include": "text.tex#braces" + "include": "#braces" }, { "include": "$self" @@ -2480,7 +2480,7 @@ "name": "meta.function.verb.latex" }, { - "match": "((\\\\)(?:(?:py|pycon|pylab|pylabcon|sympy|sympycon)[cv]?|pyq|pycq|pyif))((?:\\[[^\\[]*?\\])?)(?:(?:([^a-zA-Z\\{])(.*?)(\\4))|(?:(\\{)(.*?)(\\})))", + "match": "((\\\\)(?:(?:py|pycon|pylab|pylabcon|sympy|sympycon)[cv]?|pyq|pycq|pyif))((?:\\[[^\\[]*?\\])?)(?:(?:([^a-zA-Z\\{\\}\\[\\](),;\\s])(.*?)(\\4))|(?:(\\{)(.*?)(\\})))", "captures": { "1": { "name": "support.function.verb.latex" @@ -3308,7 +3308,7 @@ ] }, "citation-macro": { - "begin": "((\\\\)(?:[aA]uto|foot|full|no|ref|short|[tT]ext|[pP]aren|[sS]mart)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}\\p{Number}_:.-]*\\})*)(<[^\\]<>]*>)?((?:\\[[^\\]]*\\])*)(\\{)", + "begin": "((\\\\)(?:[aA]uto|foot|full|footfull|no|ref|short|[tT]ext|[pP]aren|[sS]mart|[fFpP]vol|vol)?[cC]ite(?:al)?(?:p|s|t|author|year(?:par)?|title|url|date)?[ANP]*\\*?)((?:(?:\\([^\\)]*\\)){0,2}(?:\\[[^\\]]*\\]){0,2}\\{[\\p{Alphabetic}\\p{Number}_:.-]*\\})*)(<[^\\]<>]*>)?((?:\\[[^\\]]*\\])*)(\\{)", "captures": { "1": { "name": "keyword.control.cite.latex" @@ -3622,7 +3622,7 @@ "all-balanced-env": { "patterns": [ { - "begin": "(?:\\s*)((\\\\)begin)(\\{)((?:\\+?array|equation|(?:IEEE|sub)?eqnarray|multline|align|aligned|alignat|alignedat|flalign|flaligned|flalignat|split|gather|gathered|\\+?cases|(?:display)?math|\\+?[a-zA-Z]*matrix|[pbBvV]?NiceMatrix|[pbBvV]?NiceArray|(?:(?:arg)?(?:mini|maxi)))(?:\\*|!)?)(\\})(\\s*\\n)?", + "begin": "(?:\\s*)((\\\\)begin)(\\{)((?:\\+?array|equation|(?:IEEE|sub)?eqnarray|multline|align|aligned|alignat|alignedat|flalign|flaligned|flalignat|split|gather|gathered|(?:\\+|d|r|dr)?cases|(?:display)?math|\\+?[a-zA-Z]*matrix|[pbBvV]?NiceMatrix|[pbBvV]?NiceArray|(?:(?:arg)?(?:mini|maxi)))(?:\\*|!)?)(\\})(\\s*\\n)?", "captures": { "1": { "name": "support.function.be.latex" @@ -3921,10 +3921,51 @@ "include": "#column-specials" }, { - "include": "text.tex#braces" + "include": "#braces" + }, + { + "include": "text.tex" + } + ] + }, + "braces": { + "begin": "(?][ \\t]*", "name": "keyword.operator.lua" }, { - "match": "([a-zA-Z_][a-zA-Z0-9_\\.\\*\\[\\]\\<\\>\\,\\-]*)(? !x.open); + if (this._detailParentElements.some(x => !x.open)) { + return false; + } + + const style = window.getComputedStyle(this.element); + if (style.display === 'none' || style.visibility === 'hidden') { + return false; + } + + const bounds = this.element.getBoundingClientRect(); + if (bounds.height === 0 || bounds.width === 0) { + return false; + } + + return true; } } diff --git a/extensions/markdown-language-features/preview-src/tsconfig.json b/extensions/markdown-language-features/preview-src/tsconfig.json index 4001ffeb068..5c51c32b141 100644 --- a/extensions/markdown-language-features/preview-src/tsconfig.json +++ b/extensions/markdown-language-features/preview-src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": ".", "outDir": "./dist/", "jsx": "react", "esModuleInterop": true, diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 733fcf9e437..e2cea47e718 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -9,7 +9,7 @@ import * as vscode from 'vscode'; import { ILogger } from './logging'; import { MarkdownContributionProvider } from './markdownExtensions'; import { MarkdownPreviewConfiguration } from './preview/previewConfig'; -import { Slugifier } from './slugify'; +import { ISlugifier, SlugBuilder } from './slugify'; import { ITextDocument } from './types/textDocument'; import { WebviewResourceProvider } from './util/resources'; import { isOfScheme, Schemes } from './util/schemes'; @@ -85,13 +85,14 @@ export interface RenderOutput { } interface RenderEnv { - containingImages: Set; - currentDocument: vscode.Uri | undefined; - resourceProvider: WebviewResourceProvider | undefined; + readonly containingImages: Set; + readonly currentDocument: vscode.Uri | undefined; + readonly resourceProvider: WebviewResourceProvider | undefined; + readonly slugifier: SlugBuilder; } export interface IMdParser { - readonly slugifier: Slugifier; + readonly slugifier: ISlugifier; tokenize(document: ITextDocument): Promise; } @@ -100,14 +101,13 @@ export class MarkdownItEngine implements IMdParser { private _md?: Promise; - private _slugCount = new Map(); private readonly _tokenCache = new TokenCache(); - public readonly slugifier: Slugifier; + public readonly slugifier: ISlugifier; public constructor( private readonly _contributionProvider: MarkdownContributionProvider, - slugifier: Slugifier, + slugifier: ISlugifier, private readonly _logger: ILogger, ) { this.slugifier = slugifier; @@ -183,7 +183,6 @@ export class MarkdownItEngine implements IMdParser { ): Token[] { const cached = this._tokenCache.tryGetCached(document, config); if (cached) { - this._resetSlugCount(); return cached; } @@ -194,13 +193,13 @@ export class MarkdownItEngine implements IMdParser { } private _tokenizeString(text: string, engine: MarkdownIt) { - this._resetSlugCount(); - - return engine.parse(text, {}); - } - - private _resetSlugCount(): void { - this._slugCount = new Map(); + const env: RenderEnv = { + currentDocument: undefined, + containingImages: new Set(), + slugifier: this.slugifier.createBuilder(), + resourceProvider: undefined, + }; + return engine.parse(text, env); } public async render(input: ITextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { @@ -215,6 +214,7 @@ export class MarkdownItEngine implements IMdParser { containingImages: new Set(), currentDocument: typeof input === 'string' ? undefined : input.uri, resourceProvider, + slugifier: this.slugifier.createBuilder(), }; const html = engine.renderer.render(tokens, { @@ -313,18 +313,9 @@ export class MarkdownItEngine implements IMdParser { private _addNamedHeaders(md: MarkdownIt): void { const original = md.renderer.rules.heading_open; - md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => { + md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env: unknown, self) => { const title = this._tokenToPlainText(tokens[idx + 1]); - let slug = this.slugifier.fromHeading(title); - - if (this._slugCount.has(slug.value)) { - const count = this._slugCount.get(slug.value)!; - this._slugCount.set(slug.value, count + 1); - slug = this.slugifier.fromHeading(slug.value + '-' + (count + 1)); - } else { - this._slugCount.set(slug.value, 0); - } - + const slug = (env as RenderEnv).slugifier ? (env as RenderEnv).slugifier.add(title) : this.slugifier.fromHeading(title); tokens[idx].attrSet('id', slug.value); if (original) { diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index 1a7a859d446..19d1755e7eb 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -110,15 +110,17 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } })); - const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); - this._register(watcher.onDidChange(uri => { - if (this.isPreviewOf(uri)) { - // Only use the file system event when VS Code does not already know about the file - if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() === uri.toString())) { - this.refresh(); + if (vscode.workspace.fs.isWritableFileSystem(resource.scheme)) { + const watcher = this._register(vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(resource, '*'))); + this._register(watcher.onDidChange(uri => { + if (this.isPreviewOf(uri)) { + // Only use the file system event when VS Code does not already know about the file + if (!vscode.workspace.textDocuments.some(doc => doc.uri.toString() === uri.toString())) { + this.refresh(); + } } - } - })); + })); + } this._register(this._webviewPanel.webview.onDidReceiveMessage((e: FromWebviewMessage.Type) => { if (e.source !== this._resource.toString()) { diff --git a/extensions/markdown-language-features/src/slugify.ts b/extensions/markdown-language-features/src/slugify.ts index 0d4b1896d8c..645d9ab6ae8 100644 --- a/extensions/markdown-language-features/src/slugify.ts +++ b/extensions/markdown-language-features/src/slugify.ts @@ -3,31 +3,86 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export class Slug { +export interface ISlug { + readonly value: string; + equals(other: ISlug): boolean; +} + +export class GithubSlug implements ISlug { public constructor( public readonly value: string ) { } - public equals(other: Slug): boolean { - return this.value === other.value; + public equals(other: ISlug): boolean { + return other instanceof GithubSlug && this.value.toLowerCase() === other.value.toLowerCase(); } } -export interface Slugifier { - fromHeading(heading: string): Slug; +export interface SlugBuilder { + add(headingText: string): ISlug; } -export const githubSlugifier: Slugifier = new class implements Slugifier { - fromHeading(heading: string): Slug { - const slugifiedHeading = encodeURI( - heading.trim() - .toLowerCase() - .replace(/\s+/g, '-') // Replace whitespace with - - // allow-any-unicode-next-line - .replace(/[\]\[\!\/\'\"\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators - .replace(/^\-+/, '') // Remove leading - - .replace(/\-+$/, '') // Remove trailing - - ); - return new Slug(slugifiedHeading); +/** + * Generates unique ids for headers in the Markdown. + */ +export interface ISlugifier { + /** + * Create a new slug from the text of a markdown heading. + * + * For a heading such as `# Header`, this will be called with `Header` + */ + fromHeading(headingText: string): ISlug; + + /** + * Create a slug from a link fragment. + * + * For a link such as `[text](#header)`, this will be called with `header` + */ + fromFragment(fragmentText: string): ISlug; + + /** + * Creates a stateful object that can be used to build slugs incrementally. + * + * This should be used when getting all slugs in a document as it handles duplicate headings + */ + createBuilder(): SlugBuilder; +} + +// Copied from https://github.com/Flet/github-slugger since we can't use esm yet. +// eslint-disable-next-line no-misleading-character-class +const githubSlugReplaceRegex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g; + +/** + * A {@link ISlugifier slugifier} that approximates how GitHub's slugifier works. + */ +export const githubSlugifier: ISlugifier = new class implements ISlugifier { + fromHeading(heading: string): ISlug { + const slugifiedHeading = heading.trim() + .toLowerCase() + .replace(githubSlugReplaceRegex, '') + .replace(/\s/g, '-'); // Replace whitespace with - + + return new GithubSlug(slugifiedHeading); + } + + fromFragment(fragmentText: string): ISlug { + return new GithubSlug(fragmentText.toLowerCase()); + } + + createBuilder() { + const entries = new Map(); + return { + add: (heading: string): ISlug => { + const slug = this.fromHeading(heading); + const existingSlugEntry = entries.get(slug.value); + if (existingSlugEntry) { + ++existingSlugEntry.count; + return this.fromHeading(slug.value + '-' + existingSlugEntry.count); + } + + entries.set(slug.value, { count: 0 }); + return slug; + } + }; } }; diff --git a/extensions/markdown-language-features/src/util/file.ts b/extensions/markdown-language-features/src/util/file.ts index f99135b0200..df745296df4 100644 --- a/extensions/markdown-language-features/src/util/file.ts +++ b/extensions/markdown-language-features/src/util/file.ts @@ -19,7 +19,7 @@ export const markdownFileExtensions = Object.freeze([ 'workbook', ]); -export const markdownLanguageIds = ['markdown', 'prompt', 'instructions', 'chatmode']; +export const markdownLanguageIds = ['markdown', 'prompt', 'instructions', 'chatagent', 'skill']; export function isMarkdownFile(document: vscode.TextDocument) { return markdownLanguageIds.indexOf(document.languageId) !== -1; diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index 0e7a865e1f5..6ae3def2ed1 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/markdown-math/notebook/tsconfig.json b/extensions/markdown-math/notebook/tsconfig.json index def3077d238..27545a8dcaf 100644 --- a/extensions/markdown-math/notebook/tsconfig.json +++ b/extensions/markdown-math/notebook/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./dist/", "jsx": "react", "module": "es2020", diff --git a/extensions/markdown-math/tsconfig.json b/extensions/markdown-math/tsconfig.json index 40e645a1ed6..5a8a0a1c1b4 100644 --- a/extensions/markdown-math/tsconfig.json +++ b/extensions/markdown-math/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [], "typeRoots": [ diff --git a/extensions/media-preview/media/imagePreview.js b/extensions/media-preview/media/imagePreview.js index ab8ad542a2d..d31728e76bc 100644 --- a/extensions/media-preview/media/imagePreview.js +++ b/extensions/media-preview/media/imagePreview.js @@ -306,6 +306,8 @@ return; } + console.error('Error loading image', e); + hasLoadedImage = true; document.body.classList.add('error'); document.body.classList.remove('loading'); diff --git a/extensions/media-preview/package.json b/extensions/media-preview/package.json index 18cc50bfb3d..3f7e1c01653 100644 --- a/extensions/media-preview/package.json +++ b/extensions/media-preview/package.json @@ -155,7 +155,7 @@ "compile": "gulp compile-extension:media-preview", "watch": "npm run build-preview && gulp watch-extension:media-preview", "vscode:prepublish": "npm run build-ext", - "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:media-preview ./tsconfig.json", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:media-preview ./tsconfig.json", "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/media-preview/tsconfig.json b/extensions/media-preview/tsconfig.json index 796a159a61c..e723410bedf 100644 --- a/extensions/media-preview/tsconfig.json +++ b/extensions/media-preview/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/merge-conflict/tsconfig.json b/extensions/merge-conflict/tsconfig.json index 22c47de77db..a2cbe0e9ea3 100644 --- a/extensions/merge-conflict/tsconfig.json +++ b/extensions/merge-conflict/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/mermaid-chat-features/chat-webview-src/index-editor.ts b/extensions/mermaid-chat-features/chat-webview-src/index-editor.ts new file mode 100644 index 00000000000..86ead17b014 --- /dev/null +++ b/extensions/mermaid-chat-features/chat-webview-src/index-editor.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { initializeMermaidWebview } from './mermaidWebview'; +import { VsCodeApi } from './vscodeApi'; + +declare function acquireVsCodeApi(): VsCodeApi; +const vscode = acquireVsCodeApi(); + + +initializeMermaidWebview(vscode).then(panZoomHandler => { + if (!panZoomHandler) { + return; + } + + // Wire up zoom controls + const zoomInBtn = document.querySelector('.zoom-in-btn'); + const zoomOutBtn = document.querySelector('.zoom-out-btn'); + const zoomResetBtn = document.querySelector('.zoom-reset-btn'); + + zoomInBtn?.addEventListener('click', () => panZoomHandler.zoomIn()); + zoomOutBtn?.addEventListener('click', () => panZoomHandler.zoomOut()); + zoomResetBtn?.addEventListener('click', () => panZoomHandler.reset()); +}); diff --git a/extensions/mermaid-chat-features/chat-webview-src/index.ts b/extensions/mermaid-chat-features/chat-webview-src/index.ts index 9b3c9df71b6..d50d4197115 100644 --- a/extensions/mermaid-chat-features/chat-webview-src/index.ts +++ b/extensions/mermaid-chat-features/chat-webview-src/index.ts @@ -2,72 +2,23 @@ * 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'; +import { initializeMermaidWebview } from './mermaidWebview'; +import { VsCodeApi } from './vscodeApi'; -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'; -}; +declare function acquireVsCodeApi(): VsCodeApi; +const vscode = acquireVsCodeApi(); -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; - } +async function main() { + await initializeMermaidWebview(vscode); - const diagramNode = document.querySelector('.mermaid'); - if (!diagramNode || !(diagramNode instanceof HTMLElement)) { - return; + // Set up the "Open in Editor" button + const openBtn = document.querySelector('.open-in-editor-btn'); + if (openBtn) { + openBtn.addEventListener('click', e => { + e.stopPropagation(); + vscode.postMessage({ type: 'openInEditor' }); + }); } - - 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(); - +main(); diff --git a/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts b/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts new file mode 100644 index 00000000000..fb11ad44e5c --- /dev/null +++ b/extensions/mermaid-chat-features/chat-webview-src/mermaidWebview.ts @@ -0,0 +1,409 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; +import { VsCodeApi } from './vscodeApi'; + +interface PanZoomState { + readonly scale: number; + readonly translateX: number; + readonly translateY: number; +} + +export class PanZoomHandler { + private scale = 1; + private translateX = 0; + private translateY = 0; + + private isPanning = false; + private hasDragged = false; + private hasInteracted = false; + private startX = 0; + private startY = 0; + + private readonly minScale = 0.1; + private readonly maxScale = 5; + private readonly zoomFactor = 0.002; + + constructor( + private readonly container: HTMLElement, + private readonly content: HTMLElement, + private readonly vscode: VsCodeApi + ) { + this.container = container; + this.content = content; + this.content.style.transformOrigin = '0 0'; + this.container.style.overflow = 'hidden'; + this.container.style.cursor = 'default'; + this.setupEventListeners(); + } + + /** + * Initializes the pan/zoom state - either restores from saved state or centers the content. + */ + public initialize(): void { + if (!this.restoreState()) { + // Use requestAnimationFrame to ensure layout is updated before centering + requestAnimationFrame(() => { + this.centerContent(); + }); + } + } + + private setupEventListeners(): void { + // Pan with mouse drag + this.container.addEventListener('mousedown', e => this.handleMouseDown(e)); + document.addEventListener('mousemove', e => this.handleMouseMove(e)); + document.addEventListener('mouseup', () => this.handleMouseUp()); + + // Click to zoom (Alt+click = zoom in, Alt+Shift+click = zoom out) + this.container.addEventListener('click', e => this.handleClick(e)); + + // Trackpad: pinch = zoom, Alt + two-finger scroll = zoom + this.container.addEventListener('wheel', e => this.handleWheel(e), { passive: false }); + + // Update cursor when Alt/Option key is pressed + this.container.addEventListener('mousemove', e => this.updateCursorFromModifier(e)); + this.container.addEventListener('mouseenter', e => this.updateCursorFromModifier(e)); + window.addEventListener('keydown', e => this.handleKeyChange(e)); + window.addEventListener('keyup', e => this.handleKeyChange(e)); + + // Re-center on resize if user hasn't interacted yet + window.addEventListener('resize', () => this.handleResize()); + } + + private handleKeyChange(e: KeyboardEvent): void { + if ((e.key === 'Alt' || e.key === 'Shift') && !this.isPanning) { + e.preventDefault(); + if (e.altKey && !e.shiftKey) { + this.container.style.cursor = 'grab'; + } else if (e.altKey && e.shiftKey) { + this.container.style.cursor = 'zoom-out'; + } else { + this.container.style.cursor = 'default'; + } + } + } + + private updateCursorFromModifier(e: MouseEvent): void { + if (this.isPanning) { + return; + } + if (e.altKey && !e.shiftKey) { + this.container.style.cursor = 'grab'; + } else if (e.altKey && e.shiftKey) { + this.container.style.cursor = 'zoom-out'; + } else { + this.container.style.cursor = 'default'; + } + } + + private handleClick(e: MouseEvent): void { + // Only zoom on click if Alt is held and we didn't drag + if (!e.altKey || this.hasDragged) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + const rect = this.container.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + // Alt+Shift+click = zoom out, Alt+click = zoom in + const factor = e.shiftKey ? 0.8 : 1.25; + this.zoomAtPoint(factor, x, y); + } + + private handleWheel(e: WheelEvent): void { + // Only zoom when Alt is held (or ctrlKey for pinch-to-zoom gestures) + // ctrlKey is set by browsers for pinch-to-zoom gestures + const isPinchZoom = e.ctrlKey; + + if (!e.altKey && !isPinchZoom) { + // Allow normal scrolling when Alt is not held + return; + } + + if (isPinchZoom || e.altKey) { + // Pinch gesture or Alt + two-finger drag = zoom + e.preventDefault(); + e.stopPropagation(); + + const rect = this.container.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + // Calculate zoom (scroll up = zoom in, scroll down = zoom out) + // Pinch gestures have smaller deltaY values, so use a higher factor + const effectiveZoomFactor = isPinchZoom ? this.zoomFactor * 5 : this.zoomFactor; + const delta = -e.deltaY * effectiveZoomFactor; + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * (1 + delta))); + + // Zoom toward mouse position + const scaleFactor = newScale / this.scale; + this.translateX = mouseX - (mouseX - this.translateX) * scaleFactor; + this.translateY = mouseY - (mouseY - this.translateY) * scaleFactor; + this.scale = newScale; + + this.applyTransform(); + this.saveState(); + } + } + + private handleMouseDown(e: MouseEvent): void { + if (e.button !== 0 || !e.altKey) { + return; + } + e.preventDefault(); + e.stopPropagation(); + this.isPanning = true; + this.hasDragged = false; + this.startX = e.clientX - this.translateX; + this.startY = e.clientY - this.translateY; + this.container.style.cursor = 'grabbing'; + } + + private handleMouseMove(e: MouseEvent): void { + if (!this.isPanning) { + return; + } + + // Handle case where mouse was released outside the webview + if (e.buttons === 0) { + this.handleMouseUp(); + return; + } + + const dx = e.clientX - this.startX - this.translateX; + const dy = e.clientY - this.startY - this.translateY; + if (Math.abs(dx) > 3 || Math.abs(dy) > 3) { + this.hasDragged = true; + } + this.translateX = e.clientX - this.startX; + this.translateY = e.clientY - this.startY; + this.applyTransform(); + } + + private handleMouseUp(): void { + if (this.isPanning) { + this.isPanning = false; + this.container.style.cursor = 'default'; + this.saveState(); + } + } + + private applyTransform(): void { + this.content.style.transform = `translate(${this.translateX}px, ${this.translateY}px) scale(${this.scale})`; + } + + private saveState(): void { + this.hasInteracted = true; + const currentState = this.vscode.getState() || {}; + this.vscode.setState({ + ...currentState, + panZoom: { + scale: this.scale, + translateX: this.translateX, + translateY: this.translateY + } + }); + } + + private restoreState(): boolean { + const state = this.vscode.getState(); + if (state?.panZoom) { + const panZoom = state.panZoom as PanZoomState; + this.scale = panZoom.scale ?? 1; + this.translateX = panZoom.translateX ?? 0; + this.translateY = panZoom.translateY ?? 0; + this.hasInteracted = true; + this.applyTransform(); + return true; + } + return false; + } + + private handleResize(): void { + if (!this.hasInteracted) { + this.centerContent(); + } + } + + /** + * Centers the content within the container. + */ + private centerContent(): void { + const containerRect = this.container.getBoundingClientRect(); + + // Get the SVG element inside the content - mermaid renders to an SVG + const svg = this.content.querySelector('svg'); + if (!svg) { + return; + } + const svgRect = svg.getBoundingClientRect(); + + // Calculate the center position based on the SVG dimensions + this.translateX = (containerRect.width - svgRect.width) / 2; + this.translateY = (containerRect.height - svgRect.height) / 2; + + this.applyTransform(); + } + + public reset(): void { + this.scale = 1; + this.translateX = 0; + this.translateY = 0; + this.hasInteracted = false; + this.applyTransform(); // Apply scale first so content size is correct + + // Clear the saved pan/zoom state + const currentState = this.vscode.getState() || {}; + delete currentState.panZoom; + this.vscode.setState(currentState); + + // Use requestAnimationFrame to ensure layout is updated before centering + requestAnimationFrame(() => { + this.centerContent(); + }); + } + + public zoomIn(): void { + const rect = this.container.getBoundingClientRect(); + this.zoomAtPoint(1.25, rect.width / 2, rect.height / 2); + } + + public zoomOut(): void { + const rect = this.container.getBoundingClientRect(); + this.zoomAtPoint(0.8, rect.width / 2, rect.height / 2); + } + + private zoomAtPoint(factor: number, x: number, y: number): void { + const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * factor)); + const scaleFactor = newScale / this.scale; + this.translateX = x - (x - this.translateX) * scaleFactor; + this.translateY = y - (y - this.translateY) * scaleFactor; + this.scale = newScale; + this.applyTransform(); + this.saveState(); + } +} + +export function getMermaidTheme(): 'dark' | 'default' { + return document.body.classList.contains('vscode-dark') || (document.body.classList.contains('vscode-high-contrast') && !document.body.classList.contains('vscode-high-contrast-light')) + ? 'dark' + : 'default'; +} + +/** + * Unpersisted state + */ +interface LocalState { + readonly mermaidSource: string; + readonly theme: 'dark' | 'default'; +} + +interface PersistedState { + readonly mermaidSource: string; + readonly panZoom?: PanZoomState; +} + +/** + * Re-renders the mermaid diagram when theme changes + */ +async function rerenderMermaidDiagram( + diagramElement: HTMLElement, + diagramText: string, + newTheme: 'dark' | 'default' +): Promise { + diagramElement.textContent = diagramText; + delete diagramElement.dataset.processed; + + mermaid.initialize({ + theme: newTheme, + }); + await mermaid.run({ + nodes: [diagramElement] + }); +} + +export async function initializeMermaidWebview(vscode: VsCodeApi): Promise { + const diagram = document.querySelector('.mermaid'); + if (!diagram) { + return; + } + + // Capture diagram state + const theme = getMermaidTheme(); + const diagramText = diagram.textContent ?? ''; + let state: LocalState = { + mermaidSource: diagramText, + theme + }; + + // Save the mermaid source in the webview state + const currentState: PersistedState = vscode.getState() || {}; + vscode.setState({ + ...currentState, + mermaidSource: diagramText + }); + + // Wrap the diagram for pan/zoom support + const wrapper = document.createElement('div'); + wrapper.className = 'mermaid-wrapper'; + wrapper.style.cssText = 'position: relative; width: 100%; height: 100%; overflow: hidden;'; + + const content = document.createElement('div'); + content.className = 'mermaid-content'; + + // Move the diagram into the content wrapper + diagram.parentNode?.insertBefore(wrapper, diagram); + content.appendChild(diagram); + wrapper.appendChild(content); + + // Run mermaid + const config: MermaidConfig = { + startOnLoad: false, + theme, + }; + mermaid.initialize(config); + await mermaid.run({ nodes: [diagram] }); + + // Show the diagram now that it's rendered + diagram.classList.add('rendered'); + + const panZoomHandler = new PanZoomHandler(wrapper, content, vscode); + panZoomHandler.initialize(); + + // Listen for messages from the extension + window.addEventListener('message', event => { + const message = event.data; + if (message.type === 'resetPanZoom') { + panZoomHandler.reset(); + } + }); + + // Re-render when theme changes + new MutationObserver(() => { + const newTheme = getMermaidTheme(); + if (state?.theme === newTheme) { + return; + } + + const diagramNode = document.querySelector('.mermaid'); + if (!diagramNode || !(diagramNode instanceof HTMLElement)) { + return; + } + + state = { + mermaidSource: state?.mermaidSource ?? '', + theme: newTheme + }; + + rerenderMermaidDiagram(diagramNode, state.mermaidSource, newTheme); + }).observe(document.body, { attributes: true, attributeFilter: ['class'] }); + + return panZoomHandler; +} diff --git a/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json b/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json index a57ffcaeba0..9f2d42c7713 100644 --- a/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json +++ b/extensions/mermaid-chat-features/chat-webview-src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": ".", "outDir": "./dist/", "jsx": "react", "lib": [ diff --git a/extensions/mermaid-chat-features/chat-webview-src/vscodeApi.ts b/extensions/mermaid-chat-features/chat-webview-src/vscodeApi.ts new file mode 100644 index 00000000000..cf21e604536 --- /dev/null +++ b/extensions/mermaid-chat-features/chat-webview-src/vscodeApi.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. + *--------------------------------------------------------------------------------------------*/ + +export interface VsCodeApi { + getState(): any; + setState(state: any): void; + postMessage(message: any): void; +} diff --git a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs b/extensions/mermaid-chat-features/esbuild-chat-webview.mjs index b23de5746fa..e242585b1c3 100644 --- a/extensions/mermaid-chat-features/esbuild-chat-webview.mjs +++ b/extensions/mermaid-chat-features/esbuild-chat-webview.mjs @@ -10,9 +10,16 @@ const srcDir = path.join(import.meta.dirname, 'chat-webview-src'); const outDir = path.join(import.meta.dirname, 'chat-webview-out'); run({ - entryPoints: [ - path.join(srcDir, 'index.ts'), - ], + entryPoints: { + 'index': path.join(srcDir, 'index.ts'), + 'index-editor': path.join(srcDir, 'index-editor.ts'), + 'codicon': path.join(import.meta.dirname, 'node_modules', '@vscode', 'codicons', 'dist', 'codicon.css'), + }, srcDir, outdir: outDir, + additionalOptions: { + loader: { + '.ttf': 'dataurl', + } + } }, process.argv); diff --git a/extensions/mermaid-chat-features/package-lock.json b/extensions/mermaid-chat-features/package-lock.json index 185afcde646..2ec780d5c59 100644 --- a/extensions/mermaid-chat-features/package-lock.json +++ b/extensions/mermaid-chat-features/package-lock.json @@ -13,7 +13,8 @@ "mermaid": "^11.11.0" }, "devDependencies": { - "@types/node": "^22.18.10" + "@types/node": "^22.18.10", + "@vscode/codicons": "^0.0.36" }, "engines": { "vscode": "^1.104.0" @@ -405,6 +406,13 @@ "license": "MIT", "optional": true }, + "node_modules/@vscode/codicons": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.36.tgz", + "integrity": "sha512-wsNOvNMMJ2BY8rC2N2MNBG7yOowV3ov8KlvUE/AiVUlHKTfWsw3OgAOQduX7h0Un6GssKD3aoTVH+TF3DSQwKQ==", + "dev": true, + "license": "CC-BY-4.0" + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", diff --git a/extensions/mermaid-chat-features/package.json b/extensions/mermaid-chat-features/package.json index d7856516218..16b6a03ce48 100644 --- a/extensions/mermaid-chat-features/package.json +++ b/extensions/mermaid-chat-features/package.json @@ -24,25 +24,64 @@ }, "main": "./out/extension", "browser": "./dist/browser/extension", - "activationEvents": [], + "activationEvents": [ + "onWebviewPanel:vscode.chat-mermaid-features.preview" + ], "contributes": { + "commands": [ + { + "command": "_mermaid-chat.resetPanZoom", + "title": "Reset Pan and Zoom" + }, + { + "command": "_mermaid-chat.openInEditor", + "title": "Open Diagram in Editor" + }, + { + "command": "_mermaid-chat.copySource", + "title": "Copy Diagram Source" + } + ], + "menus": { + "commandPalette": [ + { + "command": "_mermaid-chat.resetPanZoom", + "when": "false" + }, + { + "command": "_mermaid-chat.openInEditor", + "when": "false" + }, + { + "command": "_mermaid-chat.copySource", + "when": "false" + } + ], + "webview/context": [ + { + "command": "_mermaid-chat.resetPanZoom", + "when": "webviewId == 'vscode.chat-mermaid-features.chatOutputItem'" + }, + { + "command": "_mermaid-chat.copySource", + "when": "webviewId == 'vscode.chat-mermaid-features.chatOutputItem' || webviewId == 'vscode.chat-mermaid-features.preview'" + } + ] + }, "configuration": { "title": "Mermaid Chat Features", "properties": { "mermaid-chat.enabled": { "type": "boolean", - "default": false, + "default": true, "description": "%config.enabled.description%", - "scope": "application", - "tags": [ - "experimental" - ] + "scope": "application" } } }, "chatOutputRenderers": [ { - "viewType": "vscode.chatMermaidDiagram", + "viewType": "vscode.chat-mermaid-features.chatOutputItem", "mimeTypes": [ "text/vnd.mermaid" ] @@ -63,6 +102,10 @@ "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." + }, + "title": { + "type": "string", + "description": "A short title that describes the diagram." } } } @@ -73,13 +116,14 @@ "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-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs 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/node": "^22.18.10" + "@types/node": "^22.18.10", + "@vscode/codicons": "^0.0.36" }, "dependencies": { "dompurify": "^3.2.7", diff --git a/extensions/mermaid-chat-features/src/chatOutputRenderer.ts b/extensions/mermaid-chat-features/src/chatOutputRenderer.ts new file mode 100644 index 00000000000..c6aacbd748e --- /dev/null +++ b/extensions/mermaid-chat-features/src/chatOutputRenderer.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { MermaidEditorManager } from './editorManager'; +import { MermaidWebviewManager } from './webviewManager'; +import { escapeHtmlText } from './util/html'; +import { generateUuid } from './util/uuid'; +import { disposeAll } from './util/dispose'; + +/** + * Mime type used to identify Mermaid diagram data in chat output. + */ +const mime = 'text/vnd.mermaid'; + +/** + * View type that uniquely identifies the Mermaid chat output renderer. + */ +const viewType = 'vscode.chat-mermaid-features.chatOutputItem'; + +class MermaidChatOutputRenderer implements vscode.ChatOutputRenderer { + + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _webviewManager: MermaidWebviewManager + ) { } + + async renderChatOutput({ value }: vscode.ChatOutputDataItem, chatOutputWebview: vscode.ChatOutputWebview, _ctx: unknown, _token: vscode.CancellationToken): Promise { + const webview = chatOutputWebview.webview; + const decoded = decodeMermaidData(value); + const mermaidSource = decoded.source; + const title = decoded.title; + + // Generate unique ID for this webview + const webviewId = generateUuid(); + + const disposables: vscode.Disposable[] = []; + + // Register and set as active + disposables.push(this._webviewManager.registerWebview(webviewId, webview, mermaidSource, title, 'chat')); + + // Listen for messages from the webview + disposables.push(webview.onDidReceiveMessage(message => { + if (message.type === 'openInEditor') { + vscode.commands.executeCommand('_mermaid-chat.openInEditor', { mermaidWebviewId: webviewId }); + } + })); + + // Dispose resources when webview is disposed + chatOutputWebview.onDidDispose(() => { + disposeAll(disposables); + }); + + // Set the options for the webview + const mediaRoot = vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out'); + webview.options = { + enableScripts: true, + localResourceRoots: [mediaRoot], + }; + + // Set the HTML content for the webview + const nonce = generateUuid(); + const mermaidScript = vscode.Uri.joinPath(mediaRoot, 'index.js'); + const codiconsUri = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'codicon.css')); + + webview.html = ` + + + + + + + Mermaid Diagram + + + + + + + + +
+					${escapeHtmlText(mermaidSource)}
+				
+ + + + `; + } +} + + +export function registerChatSupport( + context: vscode.ExtensionContext, + webviewManager: MermaidWebviewManager, + editorManager: MermaidEditorManager +): vscode.Disposable { + const disposables: vscode.Disposable[] = []; + + disposables.push( + vscode.commands.registerCommand('_mermaid-chat.openInEditor', (ctx?: { mermaidWebviewId?: string }) => { + const webviewInfo = ctx?.mermaidWebviewId ? webviewManager.getWebview(ctx.mermaidWebviewId) : webviewManager.activeWebview; + if (webviewInfo) { + editorManager.openPreview(webviewInfo.mermaidSource, webviewInfo.title); + } + }) + ); + + // Register lm tools + disposables.push( + vscode.lm.registerTool<{ markup: string; title?: string }>('renderMermaidDiagram', { + invoke: async (options, _token) => { + const sourceCode = options.input.markup; + const title = options.input.title; + return writeMermaidToolOutput(sourceCode, title); + }, + }) + ); + + // 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. + const renderer = new MermaidChatOutputRenderer(context.extensionUri, webviewManager); + disposables.push(vscode.chat.registerChatOutputRenderer(viewType, renderer)); + + return vscode.Disposable.from(...disposables); +} + +function writeMermaidToolOutput(sourceCode: string, title: string | undefined): vscode.LanguageModelToolResult { + // Expose the source code as a markdown mermaid code block + const fence = getFenceForContent(sourceCode); + const result = new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(`${fence}mermaid\n${sourceCode}\n${fence}`) + ]); + + // And store custom data in the tool result details to indicate that a custom renderer should be used for it. + // Encode source and optional title as JSON. + const data = JSON.stringify({ source: sourceCode, title }); + // Add cast to use proposed API + (result as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = { + mime, + value: new TextEncoder().encode(data), + }; + + return result; +} + +function getFenceForContent(content: string): string { + const backtickMatch = content.matchAll(/`+/g); + if (!backtickMatch) { + return '```'; + } + + const maxBackticks = Math.max(...Array.from(backtickMatch, s => s[0].length)); + return '`'.repeat(Math.max(3, maxBackticks + 1)); +} + +interface MermaidData { + readonly title: string | undefined; + readonly source: string; +} + +function decodeMermaidData(value: Uint8Array): MermaidData { + const text = new TextDecoder().decode(value); + + // Try to parse as JSON (new format with title), fall back to plain text (legacy format) + try { + const parsed = JSON.parse(text); + if (typeof parsed === 'object' && typeof parsed.source === 'string') { + return { title: parsed.title, source: parsed.source }; + } + } catch { + // Not JSON, treat as legacy plain text format + } + + return { title: undefined, source: text }; +} diff --git a/extensions/mermaid-chat-features/src/editorManager.ts b/extensions/mermaid-chat-features/src/editorManager.ts new file mode 100644 index 00000000000..5ee5cec29da --- /dev/null +++ b/extensions/mermaid-chat-features/src/editorManager.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; +import { generateUuid } from './util/uuid'; +import { MermaidWebviewManager } from './webviewManager'; +import { escapeHtmlText } from './util/html'; +import { Disposable } from './util/dispose'; + +export const mermaidEditorViewType = 'vscode.chat-mermaid-features.preview'; + +interface MermaidPreviewState { + readonly webviewId: string; + readonly mermaidSource: string; +} + +/** + * Manages mermaid diagram editor panels, ensuring only one editor per diagram. + */ +export class MermaidEditorManager extends Disposable implements vscode.WebviewPanelSerializer { + + private readonly _previews = new Map(); + + constructor( + private readonly _extensionUri: vscode.Uri, + private readonly _webviewManager: MermaidWebviewManager + ) { + super(); + + this._register(vscode.window.registerWebviewPanelSerializer(mermaidEditorViewType, this)); + } + + /** + * Opens a preview for the given diagram + * + * If a preview already exists for this diagram, it will be revealed instead of creating a new one. + */ + public openPreview(mermaidSource: string, title?: string): void { + const webviewId = getWebviewId(mermaidSource); + const existingPreview = this._previews.get(webviewId); + if (existingPreview) { + existingPreview.reveal(); + return; + } + + const preview = MermaidPreview.create( + webviewId, + mermaidSource, + title, + this._extensionUri, + this._webviewManager, + vscode.ViewColumn.Active); + + this._registerPreview(preview); + } + + public async deserializeWebviewPanel( + webviewPanel: vscode.WebviewPanel, + state: MermaidPreviewState + ): Promise { + if (!state?.mermaidSource) { + webviewPanel.webview.html = this._getErrorHtml(); + return; + } + + const webviewId = getWebviewId(state.mermaidSource); + + const preview = MermaidPreview.revive( + webviewPanel, + webviewId, + state.mermaidSource, + this._extensionUri, + this._webviewManager + ); + + this._registerPreview(preview); + } + + private _registerPreview(preview: MermaidPreview): void { + this._previews.set(preview.diagramId, preview); + + preview.onDispose(() => { + this._previews.delete(preview.diagramId); + }); + } + + private _getErrorHtml(): string { + return /* html */` + + + + + Mermaid Preview + + + + +

An unexpected error occurred while restoring the Mermaid preview.

+ + `; + } + + public override dispose(): void { + super.dispose(); + + for (const preview of this._previews.values()) { + preview.dispose(); + } + this._previews.clear(); + } +} + +class MermaidPreview extends Disposable { + + private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter()); + public readonly onDispose = this._onDisposeEmitter.event; + + public static create( + diagramId: string, + mermaidSource: string, + title: string | undefined, + extensionUri: vscode.Uri, + webviewManager: MermaidWebviewManager, + viewColumn: vscode.ViewColumn + ): MermaidPreview { + const webviewPanel = vscode.window.createWebviewPanel( + mermaidEditorViewType, + title ?? vscode.l10n.t('Mermaid Diagram'), + viewColumn, + { + retainContextWhenHidden: false, + } + ); + + return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager); + } + + public static revive( + webviewPanel: vscode.WebviewPanel, + diagramId: string, + mermaidSource: string, + extensionUri: vscode.Uri, + webviewManager: MermaidWebviewManager + ): MermaidPreview { + return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager); + } + + private constructor( + private readonly _webviewPanel: vscode.WebviewPanel, + public readonly diagramId: string, + private readonly _mermaidSource: string, + private readonly _extensionUri: vscode.Uri, + private readonly _webviewManager: MermaidWebviewManager + ) { + super(); + + this._webviewPanel.iconPath = new vscode.ThemeIcon('graph'); + + this._webviewPanel.webview.options = { + enableScripts: true, + localResourceRoots: [ + vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out') + ], + }; + + this._webviewPanel.webview.html = this._getHtml(); + + // Register with the webview manager + this._register(this._webviewManager.registerWebview(this.diagramId, this._webviewPanel.webview, this._mermaidSource, undefined, 'editor')); + + this._register(this._webviewPanel.onDidChangeViewState(e => { + if (e.webviewPanel.active) { + this._webviewManager.setActiveWebview(this.diagramId); + } + })); + + this._register(this._webviewPanel.onDidDispose(() => { + this._onDisposeEmitter.fire(); + this.dispose(); + })); + } + + public reveal(): void { + this._webviewPanel.reveal(); + } + + public override dispose() { + this._onDisposeEmitter.fire(); + + super.dispose(); + + this._webviewPanel.dispose(); + } + + private _getHtml(): string { + const nonce = generateUuid(); + + const mediaRoot = vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out'); + const scriptUri = this._webviewPanel.webview.asWebviewUri( + vscode.Uri.joinPath(mediaRoot, 'index-editor.js') + ); + const codiconsUri = this._webviewPanel.webview.asWebviewUri( + vscode.Uri.joinPath(mediaRoot, 'codicon.css') + ); + + return /* html */` + + + + + Mermaid Diagram + + + + + +
+ + + +
+
+					${escapeHtmlText(this._mermaidSource)}
+				
+ + + `; + } +} + + +/** + * Generates a unique ID for a diagram based on its content. + * This ensures the same diagram content always gets the same ID. + */ +function getWebviewId(source: string): string { + // Simple hash function for generating a content-based ID + let hash = 0; + for (let i = 0; i < source.length; i++) { + const char = source.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32-bit integer + } + return Math.abs(hash).toString(16); +} diff --git a/extensions/mermaid-chat-features/src/extension.ts b/extensions/mermaid-chat-features/src/extension.ts index 51294649f4f..e0b13e6f0b4 100644 --- a/extensions/mermaid-chat-features/src/extension.ts +++ b/extensions/mermaid-chat-features/src/extension.ts @@ -3,98 +3,33 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { generateUuid } from './uuid'; +import { registerChatSupport } from './chatOutputRenderer'; +import { MermaidEditorManager } from './editorManager'; +import { MermaidWebviewManager } from './webviewManager'; -/** - * 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) { + const webviewManager = new MermaidWebviewManager(); + + const editorManager = new MermaidEditorManager(context.extensionUri, webviewManager); + context.subscriptions.push(editorManager); + + // Register chat support + context.subscriptions.push(registerChatSupport(context, webviewManager, editorManager)); - // Register tools + // Register commands context.subscriptions.push( - vscode.lm.registerTool<{ markup: string }>('renderMermaidDiagram', { - invoke: async (options, _token) => { - const sourceCode = options.input.markup; - return writeMermaidToolOutput(sourceCode); - }, + vscode.commands.registerCommand('_mermaid-chat.resetPanZoom', (ctx?: { mermaidWebviewId?: string }) => { + webviewManager.resetPanZoom(ctx?.mermaidWebviewId); }) ); - // 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 = generateUuid(); - 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, '''); + vscode.commands.registerCommand('_mermaid-chat.copySource', (ctx?: { mermaidWebviewId?: string }) => { + const webviewInfo = ctx?.mermaidWebviewId ? webviewManager.getWebview(ctx.mermaidWebviewId) : webviewManager.activeWebview; + if (webviewInfo) { + vscode.env.clipboard.writeText(webviewInfo.mermaidSource); + } + }) + ); } - - diff --git a/extensions/mermaid-chat-features/src/util/dispose.ts b/extensions/mermaid-chat-features/src/util/dispose.ts new file mode 100644 index 00000000000..175acf7b367 --- /dev/null +++ b/extensions/mermaid-chat-features/src/util/dispose.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +export function disposeAll(disposables: vscode.Disposable[]) { + while (disposables.length) { + const item = disposables.pop(); + item?.dispose(); + } +} + +export abstract class Disposable { + private _isDisposed = false; + + protected _disposables: vscode.Disposable[] = []; + + public dispose(): any { + if (this._isDisposed) { + return; + } + this._isDisposed = true; + disposeAll(this._disposables); + } + + protected _register(value: T): T { + if (this._isDisposed) { + value.dispose(); + } else { + this._disposables.push(value); + } + return value; + } + + protected get isDisposed() { + return this._isDisposed; + } +} diff --git a/extensions/mermaid-chat-features/src/util/html.ts b/extensions/mermaid-chat-features/src/util/html.ts new file mode 100644 index 00000000000..fc52f45ea4b --- /dev/null +++ b/extensions/mermaid-chat-features/src/util/html.ts @@ -0,0 +1,12 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +export function escapeHtmlText(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/extensions/mermaid-chat-features/src/uuid.ts b/extensions/mermaid-chat-features/src/util/uuid.ts similarity index 100% rename from extensions/mermaid-chat-features/src/uuid.ts rename to extensions/mermaid-chat-features/src/util/uuid.ts diff --git a/extensions/mermaid-chat-features/src/webviewManager.ts b/extensions/mermaid-chat-features/src/webviewManager.ts new file mode 100644 index 00000000000..253ae92b959 --- /dev/null +++ b/extensions/mermaid-chat-features/src/webviewManager.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 * as vscode from 'vscode'; + +export interface MermaidWebviewInfo { + readonly id: string; + readonly webview: vscode.Webview; + readonly mermaidSource: string; + readonly title: string | undefined; + readonly type: 'chat' | 'editor'; +} + +/** + * Manages all mermaid webviews (both chat output renderers and editor previews). + * Tracks the active webview and provides methods for interacting with webviews. + */ +export class MermaidWebviewManager { + + private _activeWebviewId: string | undefined; + private readonly _webviews = new Map(); + + /** + * Gets the currently active webview info. + */ + public get activeWebview(): MermaidWebviewInfo | undefined { + return this._activeWebviewId ? this._webviews.get(this._activeWebviewId) : undefined; + } + + public registerWebview(id: string, webview: vscode.Webview, mermaidSource: string, title: string | undefined, type: 'chat' | 'editor'): vscode.Disposable { + if (this._webviews.has(id)) { + throw new Error(`Webview with id ${id} is already registered.`); + } + + const info: MermaidWebviewInfo = { + id, + webview, + mermaidSource, + title, + type + }; + this._webviews.set(id, info); + return { dispose: () => this.unregisterWebview(id) }; + } + + private unregisterWebview(id: string): void { + this._webviews.delete(id); + + // Clear active if this was the active webview + if (this._activeWebviewId === id) { + this._activeWebviewId = undefined; + } + } + + public setActiveWebview(id: string): void { + if (this._webviews.has(id)) { + this._activeWebviewId = id; + } + } + + public getWebview(id: string): MermaidWebviewInfo | undefined { + return this._webviews.get(id); + } + + /** + * Sends a reset pan/zoom message to a specific webview by ID. + */ + public resetPanZoom(id: string | undefined): void { + const target = id ? this._webviews.get(id) : this.activeWebview; + target?.webview.postMessage({ type: 'resetPanZoom' }); + } +} diff --git a/extensions/mermaid-chat-features/tsconfig.json b/extensions/mermaid-chat-features/tsconfig.json index 35a9a9ad8a0..3df59ab7f3d 100644 --- a/extensions/mermaid-chat-features/tsconfig.json +++ b/extensions/mermaid-chat-features/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/microsoft-authentication/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore index e7feddb5da8..e2daf4b8a89 100644 --- a/extensions/microsoft-authentication/.vscodeignore +++ b/extensions/microsoft-authentication/.vscodeignore @@ -3,7 +3,6 @@ out/test/** out/** extension.webpack.config.js -extension-browser.webpack.config.js package-lock.json src/** .gitignore diff --git a/extensions/microsoft-authentication/extension-browser.webpack.config.js b/extensions/microsoft-authentication/extension-browser.webpack.config.js deleted file mode 100644 index daf3fdf8447..00000000000 --- a/extensions/microsoft-authentication/extension-browser.webpack.config.js +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -// @ts-check -import path from 'path'; -import { browser as withBrowserDefaults } from '../shared.webpack.config.mjs'; - -export default withBrowserDefaults({ - context: import.meta.dirname, - node: { - global: true, - __filename: false, - __dirname: false, - }, - entry: { - extension: './src/extension.ts', - }, - resolve: { - alias: { - './node/authServer': path.resolve(import.meta.dirname, 'src/browser/authServer'), - './node/buffer': path.resolve(import.meta.dirname, 'src/browser/buffer'), - './node/fetch': path.resolve(import.meta.dirname, 'src/browser/fetch'), - './node/authProvider': path.resolve(import.meta.dirname, 'src/browser/authProvider'), - } - } -}); diff --git a/extensions/microsoft-authentication/extension.webpack.config.js b/extensions/microsoft-authentication/extension.webpack.config.js index 2182e98e213..a46d5a527df 100644 --- a/extensions/microsoft-authentication/extension.webpack.config.js +++ b/extensions/microsoft-authentication/extension.webpack.config.js @@ -8,20 +8,41 @@ import CopyWebpackPlugin from 'copy-webpack-plugin'; import path from 'path'; const isWindows = process.platform === 'win32'; -const windowsArches = ['x64']; const isMacOS = process.platform === 'darwin'; -const macOSArches = ['arm64']; +const isLinux = !isWindows && !isMacOS; + +const windowsArches = ['x64']; +const linuxArches = ['x64']; + +let platformFolder; +switch (process.platform) { + case 'win32': + platformFolder = 'windows'; + break; + case 'darwin': + platformFolder = 'macos'; + break; + case 'linux': + platformFolder = 'linux'; + break; + default: + throw new Error(`Unsupported platform: ${process.platform}`); +} -const arch = process.arch; +const arch = process.env.VSCODE_ARCH || process.arch; console.log(`Building Microsoft Authentication Extension for ${process.platform} (${arch})`); const plugins = [...nodePlugins(import.meta.dirname)]; -if ((isWindows && windowsArches.includes(arch)) || (isMacOS && macOSArches.includes(arch))) { +if ( + (isWindows && windowsArches.includes(arch)) || + isMacOS || + (isLinux && linuxArches.includes(arch)) +) { plugins.push(new CopyWebpackPlugin({ patterns: [ { // The native files we need to ship with the extension - from: '**/dist/(lib|)msal*.(node|dll|dylib)', + from: `**/dist/${platformFolder}/${arch}/(lib|)msal*.(node|dll|dylib|so)`, to: '[name][ext]' } ] diff --git a/extensions/microsoft-authentication/package-lock.json b/extensions/microsoft-authentication/package-lock.json index d3442978d36..850b8b9277a 100644 --- a/extensions/microsoft-authentication/package-lock.json +++ b/extensions/microsoft-authentication/package-lock.json @@ -10,8 +10,8 @@ "license": "MIT", "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^3.8.0", - "@azure/msal-node-extensions": "^1.5.23", + "@azure/msal-node": "^3.8.3", + "@azure/msal-node-extensions": "^1.5.25", "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" @@ -33,21 +33,21 @@ "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "node_modules/@azure/msal-common": { - "version": "15.13.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.0.tgz", - "integrity": "sha512-8oF6nj02qX7eE/6+wFT5NluXRHc05AgdCC3fJnkjiJooq8u7BcLmxaYYSwc2AfEkWRMRi6Eyvvbeqk4U4412Ag==", + "version": "15.13.2", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.13.2.tgz", + "integrity": "sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.0.tgz", - "integrity": "sha512-23BXm82Mp5XnRhrcd4mrHa0xuUNRp96ivu3nRatrfdAqjoeWAGyD0eEAafxAOHAEWWmdlyFK4ELFcdziXyw2sA==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.3.tgz", + "integrity": "sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.13.0", + "@azure/msal-common": "15.13.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -56,14 +56,14 @@ } }, "node_modules/@azure/msal-node-extensions": { - "version": "1.5.23", - "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-1.5.23.tgz", - "integrity": "sha512-9i9GibDBxEUiYon/3Ecisde4SDFJD89nW+VCnvlzbFnVyo2TSaV047anLA/lk2ena52GSJvBGGdZLpAQqxwo3w==", + "version": "1.5.25", + "resolved": "https://registry.npmjs.org/@azure/msal-node-extensions/-/msal-node-extensions-1.5.25.tgz", + "integrity": "sha512-8UtOy6McoHQUbvi75Cx+ftpbTuOB471j4V4yZJmRM3KJ30bMO7forXrVV+/xArvWdgZ9VkBvq26OclFstJUo8Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.13.0", - "@azure/msal-node-runtime": "^0.19.0", + "@azure/msal-common": "15.13.2", + "@azure/msal-node-runtime": "^0.20.0", "keytar": "^7.8.0" }, "engines": { @@ -71,9 +71,9 @@ } }, "node_modules/@azure/msal-node-runtime": { - "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==", + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.20.1.tgz", + "integrity": "sha512-WVbMedbJHjt9M+qeZMH/6U1UmjXsKaMB6fN8OZUtGY7UVNYofrowZNx4nVvWN/ajPKBQCEW4Rr/MwcRuA8HGcQ==", "hasInstallScript": true, "license": "MIT" }, @@ -268,7 +268,8 @@ "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", @@ -324,6 +325,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" } @@ -520,21 +522,23 @@ } }, "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "1.0.1", + "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -635,7 +639,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/semver": { "version": "7.6.2", diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 178475bb711..6e80b8927be 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -16,7 +16,8 @@ "enabledApiProposals": [ "nativeWindowHandle", "authIssuers", - "authenticationChallenges" + "authenticationChallenges", + "envIsAppPortable" ], "capabilities": { "virtualWorkspaces": true, @@ -110,13 +111,11 @@ "default": "msal", "enum": [ "msal", - "msal-no-broker", - "classic" + "msal-no-broker" ], "enumDescriptions": [ "%microsoft-authentication.implementation.enumDescriptions.msal%", - "%microsoft-authentication.implementation.enumDescriptions.msal-no-broker%", - "%microsoft-authentication.implementation.enumDescriptions.classic%" + "%microsoft-authentication.implementation.enumDescriptions.msal-no-broker%" ], "markdownDescription": "%microsoft-authentication.implementation.description%", "tags": [ @@ -129,13 +128,10 @@ }, "aiKey": "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255", "main": "./out/extension.js", - "browser": "./dist/browser/extension.js", "scripts": { "vscode:prepublish": "npm run compile", "compile": "gulp compile-extension:microsoft-authentication", - "compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none", - "watch": "gulp watch-extension:microsoft-authentication", - "watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose" + "watch": "gulp watch-extension:microsoft-authentication" }, "devDependencies": { "@types/node": "22.x", @@ -146,8 +142,8 @@ }, "dependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@azure/msal-node": "^3.8.0", - "@azure/msal-node-extensions": "^1.5.23", + "@azure/msal-node": "^3.8.3", + "@azure/msal-node-extensions": "^1.5.25", "@vscode/extension-telemetry": "^0.9.8", "keytar": "file:./packageMocks/keytar", "vscode-tas-client": "^0.1.84" diff --git a/extensions/microsoft-authentication/package.nls.json b/extensions/microsoft-authentication/package.nls.json index 3b14adfa58e..4fcd2d27b74 100644 --- a/extensions/microsoft-authentication/package.nls.json +++ b/extensions/microsoft-authentication/package.nls.json @@ -3,16 +3,9 @@ "description": "Microsoft authentication provider", "signIn": "Sign In", "signOut": "Sign Out", - "microsoft-authentication.implementation.description": { - "message": "The authentication implementation to use for signing in with a Microsoft account.\n\n*NOTE: The `classic` implementation is deprecated and will be removed in a future release. If the `msal` implementation does not work for you, please [open an issue](command:workbench.action.openIssueReporter) and explain what you are trying to log in to.*", - "comment": [ - "{Locked='[(command:workbench.action.openIssueReporter)]'}", - "The `command:` syntax will turn into a link. Do not translate it." - ] - }, + "microsoft-authentication.implementation.description": "The authentication implementation to use for signing in with a Microsoft account.", "microsoft-authentication.implementation.enumDescriptions.msal": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account.", "microsoft-authentication.implementation.enumDescriptions.msal-no-broker": "Use the Microsoft Authentication Library (MSAL) to sign in with a Microsoft account using a browser. This is useful if you are having issues with the native broker.", - "microsoft-authentication.implementation.enumDescriptions.classic": "(deprecated) Use the classic authentication flow to sign in with a Microsoft account.", "microsoft-sovereign-cloud.environment.description": { "message": "The Sovereign Cloud to use for authentication. If you select `custom`, you must also set the `#microsoft-sovereign-cloud.customEnvironment#` setting.", "comment": [ diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts deleted file mode 100644 index 1246b2ec40e..00000000000 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ /dev/null @@ -1,963 +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 * as vscode from 'vscode'; -import * as path from 'path'; -import { isSupportedEnvironment } from './common/uri'; -import { IntervalTimer, raceCancellationAndTimeoutError, SequencerByKey } from './common/async'; -import { generateCodeChallenge, generateCodeVerifier, randomUUID } from './cryptoUtils'; -import { BetterTokenStorage, IDidChangeInOtherWindowEvent } from './betterSecretStorage'; -import { LoopbackAuthServer } from './node/authServer'; -import { base64Decode } from './node/buffer'; -import fetch from './node/fetch'; -import { UriEventHandler } from './UriEventHandler'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import { Environment } from '@azure/ms-rest-azure-env'; - -const redirectUrl = 'https://vscode.dev/redirect'; -const defaultActiveDirectoryEndpointUrl = Environment.AzureCloud.activeDirectoryEndpointUrl; -const DEFAULT_CLIENT_ID = 'aebc6443-996d-45c2-90f0-388ff96faa56'; -const DEFAULT_TENANT = 'organizations'; -const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad'; -const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'; - -const enum MicrosoftAccountType { - AAD = 'aad', - MSA = 'msa', - Unknown = 'unknown' -} - -interface IToken { - accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined - idToken?: string; // depending on the scopes can be either supplied or empty - - expiresIn?: number; // How long access token is valid, in seconds - expiresAt?: number; // UNIX epoch time at which token will expire - refreshToken: string; - - account: { - label: string; - id: string; - type: MicrosoftAccountType; - }; - scope: string; - sessionId: string; // The account id + the scope -} - -export interface IStoredSession { - id: string; - refreshToken: string; - scope: string; // Scopes are alphabetized and joined with a space - account: { - label: string; - id: string; - }; - endpoint: string | undefined; -} - -export interface ITokenResponse { - access_token: string; - expires_in: number; - ext_expires_in: number; - refresh_token: string; - scope: string; - token_type: string; - id_token?: string; -} - -export interface IMicrosoftTokens { - accessToken: string; - idToken?: string; -} - -interface IScopeData { - originalScopes?: string[]; - scopes: string[]; - scopeStr: string; - scopesToSend: string; - clientId: string; - tenant: string; -} - -export const REFRESH_NETWORK_FAILURE = 'Network failure'; - -export class AzureActiveDirectoryService { - // For details on why this is set to 2/3... see https://github.com/microsoft/vscode/issues/133201#issuecomment-966668197 - private static REFRESH_TIMEOUT_MODIFIER = 1000 * 2 / 3; - private static POLLING_CONSTANT = 1000 * 60 * 30; - - private _tokens: IToken[] = []; - private _refreshTimeouts: Map = new Map(); - private _sessionChangeEmitter: vscode.EventEmitter = new vscode.EventEmitter(); - - // Used to keep track of current requests when not using the local server approach. - private _pendingNonces = new Map(); - private _codeExchangePromises = new Map>(); - private _codeVerfifiers = new Map(); - - // Used to keep track of tokens that we need to store but can't because we aren't the focused window. - private _pendingTokensToStore: Map = new Map(); - - // Used to sequence requests to the same scope. - private _sequencer = new SequencerByKey(); - - constructor( - private readonly _logger: vscode.LogOutputChannel, - _context: vscode.ExtensionContext, - private readonly _uriHandler: UriEventHandler, - private readonly _tokenStorage: BetterTokenStorage, - private readonly _telemetryReporter: TelemetryReporter, - private readonly _env: Environment - ) { - _context.subscriptions.push(this._tokenStorage.onDidChangeInOtherWindow((e) => this.checkForUpdates(e))); - _context.subscriptions.push(vscode.window.onDidChangeWindowState(async (e) => e.focused && await this.storePendingTokens())); - - // In the event that a window isn't focused for a long time, we should still try to store the tokens at some point. - const timer = new IntervalTimer(); - timer.cancelAndSet( - () => !vscode.window.state.focused && this.storePendingTokens(), - // 5 hours + random extra 0-30 seconds so that each window doesn't try to store at the same time - (18000000) + Math.floor(Math.random() * 30000)); - _context.subscriptions.push(timer); - } - - public async initialize(): Promise { - this._logger.trace('Reading sessions from secret storage...'); - const sessions = await this._tokenStorage.getAll(item => this.sessionMatchesEndpoint(item)); - this._logger.trace(`Got ${sessions.length} stored sessions`); - - const refreshes = sessions.map(async session => { - this._logger.trace(`[${session.scope}] '${session.id}' Read stored session`); - const scopes = session.scope.split(' '); - const scopeData: IScopeData = { - scopes, - scopeStr: session.scope, - // filter our special scopes - scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - clientId: this.getClientId(scopes), - tenant: this.getTenantId(scopes), - }; - try { - await this.refreshToken(session.refreshToken, scopeData, session.id); - } catch (e) { - // If we aren't connected to the internet, then wait and try to refresh again later. - if (e.message === REFRESH_NETWORK_FAILURE) { - this._tokens.push({ - accessToken: undefined, - refreshToken: session.refreshToken, - account: { - ...session.account, - type: MicrosoftAccountType.Unknown - }, - scope: session.scope, - sessionId: session.id - }); - } else { - vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.')); - this._logger.error(e); - await this.removeSessionByIToken({ - accessToken: undefined, - refreshToken: session.refreshToken, - account: { - ...session.account, - type: MicrosoftAccountType.Unknown - }, - scope: session.scope, - sessionId: session.id - }); - } - } - }); - - const result = await Promise.allSettled(refreshes); - for (const res of result) { - if (res.status === 'rejected') { - this._logger.error(`Failed to initialize stored data: ${res.reason}`); - this.clearSessions(); - break; - } - } - - for (const token of this._tokens) { - /* __GDPR__ - "account" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." }, - "accountType": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what account types are being used." } - } - */ - this._telemetryReporter.sendTelemetryEvent('account', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(token.scope.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}').split(' ')), - accountType: token.account.type - }); - } - } - - //#region session operations - - public get onDidChangeSessions(): vscode.Event { - return this._sessionChangeEmitter.event; - } - - public getSessions(scopes: string[] | undefined, { account, authorizationServer }: vscode.AuthenticationProviderSessionOptions = {}): Promise { - if (!scopes) { - this._logger.info('Getting sessions for all scopes...'); - const sessions = this._tokens - .filter(token => !account?.label || token.account.label === account.label) - .map(token => this.convertToSessionSync(token)); - this._logger.info(`Got ${sessions.length} sessions for all scopes${account ? ` for account '${account.label}'` : ''}...`); - return Promise.resolve(sessions); - } - - let modifiedScopes = [...scopes]; - if (!modifiedScopes.includes('openid')) { - modifiedScopes.push('openid'); - } - if (!modifiedScopes.includes('email')) { - modifiedScopes.push('email'); - } - if (!modifiedScopes.includes('profile')) { - modifiedScopes.push('profile'); - } - if (!modifiedScopes.includes('offline_access')) { - modifiedScopes.push('offline_access'); - } - if (authorizationServer) { - const tenant = authorizationServer.path.split('/')[1]; - if (tenant) { - modifiedScopes.push(`VSCODE_TENANT:${tenant}`); - } - } - modifiedScopes = modifiedScopes.sort(); - - const modifiedScopesStr = modifiedScopes.join(' '); - const clientId = this.getClientId(scopes); - const scopeData: IScopeData = { - clientId, - originalScopes: scopes, - scopes: modifiedScopes, - scopeStr: modifiedScopesStr, - // filter our special scopes - scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - tenant: this.getTenantId(modifiedScopes), - }; - - this._logger.trace(`[${scopeData.scopeStr}] Queued getting sessions` + account ? ` for ${account?.label}` : ''); - return this._sequencer.queue(modifiedScopesStr, () => this.doGetSessions(scopeData, account)); - } - - private async doGetSessions(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { - this._logger.info(`[${scopeData.scopeStr}] Getting sessions` + account ? ` for ${account?.label}` : ''); - - const matchingTokens = this._tokens - .filter(token => token.scope === scopeData.scopeStr) - .filter(token => !account?.label || token.account.label === account.label); - // If we still don't have a matching token try to get a new token from an existing token by using - // the refreshToken. This is documented here: - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token - // "Refresh tokens are valid for all permissions that your client has already received consent for." - if (!matchingTokens.length) { - // Get a token with the correct client id and account. - let token: IToken | undefined; - for (const t of this._tokens) { - // No refresh token, so we can't make a new token from this session - if (!t.refreshToken) { - continue; - } - // Need to make sure the account matches if we were provided one - if (account?.label && t.account.label !== account.label) { - continue; - } - // If the client id is the default client id, then check for the absence of the VSCODE_CLIENT_ID scope - if (scopeData.clientId === DEFAULT_CLIENT_ID && !t.scope.includes('VSCODE_CLIENT_ID')) { - token = t; - break; - } - // If the client id is not the default client id, then check for the matching VSCODE_CLIENT_ID scope - if (scopeData.clientId !== DEFAULT_CLIENT_ID && t.scope.includes(`VSCODE_CLIENT_ID:${scopeData.clientId}`)) { - token = t; - break; - } - } - - if (token) { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Found a matching token with a different scopes '${token.scope}'. Attempting to get a new session using the existing session.`); - try { - const itoken = await this.doRefreshToken(token.refreshToken, scopeData); - this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(itoken)], removed: [], changed: [] }); - matchingTokens.push(itoken); - } catch (err) { - this._logger.error(`[${scopeData.scopeStr}] Attempted to get a new session using the existing session with scopes '${token.scope}' but it failed due to: ${err.message ?? err}`); - } - } - } - - this._logger.info(`[${scopeData.scopeStr}] Got ${matchingTokens.length} sessions`); - const results = await Promise.allSettled(matchingTokens.map(token => this.convertToSession(token, scopeData))); - return results - .filter(result => result.status === 'fulfilled') - .map(result => (result as PromiseFulfilledResult).value); - } - - public createSession(scopes: string[], { account, authorizationServer }: vscode.AuthenticationProviderSessionOptions = {}): Promise { - let modifiedScopes = [...scopes]; - if (!modifiedScopes.includes('openid')) { - modifiedScopes.push('openid'); - } - if (!modifiedScopes.includes('email')) { - modifiedScopes.push('email'); - } - if (!modifiedScopes.includes('profile')) { - modifiedScopes.push('profile'); - } - if (!modifiedScopes.includes('offline_access')) { - modifiedScopes.push('offline_access'); - } - if (authorizationServer) { - const tenant = authorizationServer.path.split('/')[1]; - if (tenant) { - modifiedScopes.push(`VSCODE_TENANT:${tenant}`); - } - } - modifiedScopes = modifiedScopes.sort(); - const scopeData: IScopeData = { - originalScopes: scopes, - scopes: modifiedScopes, - scopeStr: modifiedScopes.join(' '), - // filter our special scopes - scopesToSend: modifiedScopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - clientId: this.getClientId(scopes), - tenant: this.getTenantId(modifiedScopes), - }; - - this._logger.trace(`[${scopeData.scopeStr}] Queued creating session`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doCreateSession(scopeData, account)); - } - - private async doCreateSession(scopeData: IScopeData, account?: vscode.AuthenticationSessionAccountInformation): Promise { - this._logger.info(`[${scopeData.scopeStr}] Creating session` + account ? ` for ${account?.label}` : ''); - - const runsRemote = vscode.env.remoteName !== undefined; - const runsServerless = vscode.env.remoteName === undefined && vscode.env.uiKind === vscode.UIKind.Web; - - if (runsServerless && this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) { - throw new Error('Sign in to non-public clouds is not supported on the web.'); - } - - return await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Signing in to your account...'), cancellable: true }, async (_progress, token) => { - if (runsRemote || runsServerless) { - return await this.createSessionWithoutLocalServer(scopeData, account?.label, token); - } - - try { - return await this.createSessionWithLocalServer(scopeData, account?.label, token); - } catch (e) { - this._logger.error(`[${scopeData.scopeStr}] Error creating session: ${e}`); - - // If the error was about starting the server, try directly hitting the login endpoint instead - if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - return this.createSessionWithoutLocalServer(scopeData, account?.label, token); - } - - throw e; - } - }); - } - - private async createSessionWithLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { - this._logger.trace(`[${scopeData.scopeStr}] Starting login flow with local server`); - const codeVerifier = generateCodeVerifier(); - const codeChallenge = await generateCodeChallenge(codeVerifier); - const qs = new URLSearchParams({ - response_type: 'code', - response_mode: 'query', - client_id: scopeData.clientId, - redirect_uri: redirectUrl, - scope: scopeData.scopesToSend, - code_challenge_method: 'S256', - code_challenge: codeChallenge, - }); - if (loginHint) { - qs.set('login_hint', loginHint); - } else { - qs.set('prompt', 'select_account'); - } - const loginUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize?${qs.toString()}`, this._env.activeDirectoryEndpointUrl).toString(); - const server = new LoopbackAuthServer(path.join(__dirname, '../media'), loginUrl); - await server.start(); - - let codeToExchange; - try { - vscode.env.openExternal(vscode.Uri.parse(`http://127.0.0.1:${server.port}/signin?nonce=${encodeURIComponent(server.nonce)}`)); - const { code } = await raceCancellationAndTimeoutError(server.waitForOAuthResponse(), token, 1000 * 60 * 5); // 5 minutes - codeToExchange = code; - } finally { - setTimeout(() => { - void server.stop(); - }, 5000); - } - - const session = await this.exchangeCodeForSession(codeToExchange, codeVerifier, scopeData); - this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Sending change event for added session`); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); - this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); - return session; - } - - private async createSessionWithoutLocalServer(scopeData: IScopeData, loginHint: string | undefined, token: vscode.CancellationToken): Promise { - this._logger.trace(`[${scopeData.scopeStr}] Starting login flow without local server`); - let callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); - const nonce = generateCodeVerifier(); - const callbackQuery = new URLSearchParams(callbackUri.query); - callbackQuery.set('nonce', encodeURIComponent(nonce)); - callbackUri = callbackUri.with({ - query: callbackQuery.toString() - }); - const state = encodeURIComponent(callbackUri.toString(true)); - const codeVerifier = generateCodeVerifier(); - const codeChallenge = await generateCodeChallenge(codeVerifier); - const signInUrl = new URL(`${scopeData.tenant}/oauth2/v2.0/authorize`, this._env.activeDirectoryEndpointUrl); - const qs = new URLSearchParams({ - response_type: 'code', - client_id: encodeURIComponent(scopeData.clientId), - response_mode: 'query', - redirect_uri: redirectUrl, - state, - scope: scopeData.scopesToSend, - code_challenge_method: 'S256', - code_challenge: codeChallenge, - }); - if (loginHint) { - qs.append('login_hint', loginHint); - } else { - qs.append('prompt', 'select_account'); - } - signInUrl.search = qs.toString(); - const uri = vscode.Uri.parse(signInUrl.toString()); - vscode.env.openExternal(uri); - - - const existingNonces = this._pendingNonces.get(scopeData.scopeStr) || []; - this._pendingNonces.set(scopeData.scopeStr, [...existingNonces, nonce]); - - // Register a single listener for the URI callback, in case the user starts the login process multiple times - // before completing it. - let existingPromise = this._codeExchangePromises.get(scopeData.scopeStr); - let inputBox: vscode.InputBox | undefined; - if (!existingPromise) { - if (isSupportedEnvironment(callbackUri)) { - existingPromise = this.handleCodeResponse(scopeData); - } else { - // This code path shouldn't be hit often, so just surface an error. - throw new Error('Unsupported environment for authentication'); - } - this._codeExchangePromises.set(scopeData.scopeStr, existingPromise); - } - - this._codeVerfifiers.set(nonce, codeVerifier); - - return await raceCancellationAndTimeoutError(existingPromise, token, 1000 * 60 * 5) // 5 minutes - .finally(() => { - this._pendingNonces.delete(scopeData.scopeStr); - this._codeExchangePromises.delete(scopeData.scopeStr); - this._codeVerfifiers.delete(nonce); - inputBox?.dispose(); - }); - } - - public async removeSessionById(sessionId: string, writeToDisk: boolean = true): Promise { - const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId); - if (tokenIndex === -1) { - this._logger.warn(`'${sessionId}' Session not found to remove`); - return Promise.resolve(undefined); - } - - const token = this._tokens.splice(tokenIndex, 1)[0]; - this._logger.trace(`[${token.scope}] '${sessionId}' Queued removing session`); - return this._sequencer.queue(token.scope, () => this.removeSessionByIToken(token, writeToDisk)); - } - - public async clearSessions() { - this._logger.trace('Logging out of all sessions'); - this._tokens = []; - await this._tokenStorage.deleteAll(item => this.sessionMatchesEndpoint(item)); - - this._refreshTimeouts.forEach(timeout => { - clearTimeout(timeout); - }); - - this._refreshTimeouts.clear(); - this._logger.trace('All sessions logged out'); - } - - private async removeSessionByIToken(token: IToken, writeToDisk: boolean = true): Promise { - this._logger.info(`[${token.scope}] '${token.sessionId}' Logging out of session`); - this.removeSessionTimeout(token.sessionId); - - if (writeToDisk) { - await this._tokenStorage.delete(token.sessionId); - } - - const tokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId); - if (tokenIndex !== -1) { - this._tokens.splice(tokenIndex, 1); - } - - const session = this.convertToSessionSync(token); - this._logger.trace(`[${token.scope}] '${token.sessionId}' Sending change event for session that was removed`); - this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); - this._logger.info(`[${token.scope}] '${token.sessionId}' Logged out of session successfully!`); - return session; - } - - //#endregion - - //#region timeout - - private setSessionTimeout(sessionId: string, refreshToken: string, scopeData: IScopeData, timeout: number) { - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Setting refresh timeout for ${timeout} milliseconds`); - this.removeSessionTimeout(sessionId); - this._refreshTimeouts.set(sessionId, setTimeout(async () => { - try { - const refreshedToken = await this.refreshToken(refreshToken, scopeData, sessionId); - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Sending change event for session that was refreshed`); - this._sessionChangeEmitter.fire({ added: [], removed: [], changed: [this.convertToSessionSync(refreshedToken)] }); - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' refresh timeout complete`); - } catch (e) { - if (e.message !== REFRESH_NETWORK_FAILURE) { - vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.')); - await this.removeSessionById(sessionId); - } - } - }, timeout)); - } - - private removeSessionTimeout(sessionId: string): void { - const timeout = this._refreshTimeouts.get(sessionId); - if (timeout) { - clearTimeout(timeout); - this._refreshTimeouts.delete(sessionId); - } - } - - //#endregion - - //#region convert operations - - private convertToTokenSync(json: ITokenResponse, scopeData: IScopeData, existingId?: string): IToken { - let claims = undefined; - this._logger.trace(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse token response.`); - - try { - if (json.id_token) { - claims = JSON.parse(base64Decode(json.id_token.split('.')[1])); - } else { - this._logger.warn(`[${scopeData.scopeStr}] '${existingId ?? 'new'}' Attempting to parse access_token instead since no id_token was included in the response.`); - claims = JSON.parse(base64Decode(json.access_token.split('.')[1])); - } - } catch (e) { - throw e; - } - - const id = `${claims.tid}/${(claims.oid ?? (claims.altsecid ?? '' + claims.ipd))}`; - const sessionId = existingId || `${id}/${randomUUID()}`; - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId}' Token response parsed successfully.`); - return { - expiresIn: json.expires_in, - expiresAt: json.expires_in ? Date.now() + json.expires_in * 1000 : undefined, - accessToken: json.access_token, - idToken: json.id_token, - refreshToken: json.refresh_token, - scope: scopeData.scopeStr, - sessionId, - account: { - label: claims.preferred_username ?? claims.email ?? claims.unique_name ?? 'user@example.com', - id, - type: claims.tid === MSA_TID || claims.tid === MSA_PASSTHRU_TID ? MicrosoftAccountType.MSA : MicrosoftAccountType.AAD - } - }; - } - - /** - * Return a session object without checking for expiry and potentially refreshing. - * @param token The token information. - */ - private convertToSessionSync(token: IToken): vscode.AuthenticationSession { - return { - id: token.sessionId, - accessToken: token.accessToken!, - idToken: token.idToken, - account: token.account, - scopes: token.scope.split(' ') - }; - } - - private async convertToSession(token: IToken, scopeData: IScopeData): Promise { - if (token.accessToken && (!token.expiresAt || token.expiresAt > Date.now())) { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token available from cache${token.expiresAt ? `, expires in ${token.expiresAt - Date.now()} milliseconds` : ''}.`); - return { - id: token.sessionId, - accessToken: token.accessToken, - idToken: token.idToken, - account: token.account, - scopes: scopeData.originalScopes ?? scopeData.scopes - }; - } - - try { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token expired or unavailable, trying refresh`); - const refreshedToken = await this.refreshToken(token.refreshToken, scopeData, token.sessionId); - if (refreshedToken.accessToken) { - return { - id: token.sessionId, - accessToken: refreshedToken.accessToken, - idToken: refreshedToken.idToken, - account: token.account, - // We always prefer the original scopes requested since that array is used as a key in the AuthService - scopes: scopeData.originalScopes ?? scopeData.scopes - }; - } else { - throw new Error(); - } - } catch (e) { - throw new Error('Unavailable due to network problems'); - } - } - - //#endregion - - //#region refresh logic - - private refreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Queued refreshing token`); - return this._sequencer.queue(scopeData.scopeStr, () => this.doRefreshToken(refreshToken, scopeData, sessionId)); - } - - private async doRefreshToken(refreshToken: string, scopeData: IScopeData, sessionId?: string): Promise { - this._logger.trace(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token`); - const postData = new URLSearchParams({ - refresh_token: refreshToken, - client_id: scopeData.clientId, - grant_type: 'refresh_token', - scope: scopeData.scopesToSend - }).toString(); - - try { - const json = await this.fetchTokenResponse(postData, scopeData); - const token = this.convertToTokenSync(json, scopeData, sessionId); - if (token.expiresIn) { - this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER); - } - this.setToken(token, scopeData); - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Token refresh success`); - return token; - } catch (e) { - if (e.message === REFRESH_NETWORK_FAILURE) { - // We were unable to refresh because of a network failure (i.e. the user lost internet access). - // so set up a timeout to try again later. We only do this if we have a session id to reference later. - if (sessionId) { - this.setSessionTimeout(sessionId, refreshToken, scopeData, AzureActiveDirectoryService.POLLING_CONSTANT); - } - throw e; - } - this._logger.error(`[${scopeData.scopeStr}] '${sessionId ?? 'new'}' Refreshing token failed: ${e.message}`); - throw e; - } - } - - //#endregion - - //#region scope parsers - - private getClientId(scopes: string[]) { - return scopes.reduce((prev, current) => { - if (current.startsWith('VSCODE_CLIENT_ID:')) { - return current.split('VSCODE_CLIENT_ID:')[1]; - } - return prev; - }, undefined) ?? DEFAULT_CLIENT_ID; - } - - private getTenantId(scopes: string[]) { - return scopes.reduce((prev, current) => { - if (current.startsWith('VSCODE_TENANT:')) { - return current.split('VSCODE_TENANT:')[1]; - } - return prev; - }, undefined) ?? DEFAULT_TENANT; - } - - //#endregion - - //#region oauth flow - - private async handleCodeResponse(scopeData: IScopeData): Promise { - let uriEventListener: vscode.Disposable; - return new Promise((resolve: (value: vscode.AuthenticationSession) => void, reject) => { - uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { - try { - const query = new URLSearchParams(uri.query); - let code = query.get('code'); - let nonce = query.get('nonce'); - if (Array.isArray(code)) { - code = code[0]; - } - if (!code) { - throw new Error('No code included in query'); - } - if (Array.isArray(nonce)) { - nonce = nonce[0]; - } - if (!nonce) { - throw new Error('No nonce included in query'); - } - - const acceptedStates = this._pendingNonces.get(scopeData.scopeStr) || []; - // Workaround double encoding issues of state in web - if (!acceptedStates.includes(nonce) && !acceptedStates.includes(decodeURIComponent(nonce))) { - throw new Error('Nonce does not match.'); - } - - const verifier = this._codeVerfifiers.get(nonce) ?? this._codeVerfifiers.get(decodeURIComponent(nonce)); - if (!verifier) { - throw new Error('No available code verifier'); - } - - const session = await this.exchangeCodeForSession(code, verifier, scopeData); - this._sessionChangeEmitter.fire({ added: [session], removed: [], changed: [] }); - this._logger.info(`[${scopeData.scopeStr}] '${session.id}' session successfully created!`); - resolve(session); - } catch (err) { - reject(err); - } - }); - }).then(result => { - uriEventListener.dispose(); - return result; - }).catch(err => { - uriEventListener.dispose(); - throw err; - }); - } - - private async exchangeCodeForSession(code: string, codeVerifier: string, scopeData: IScopeData): Promise { - this._logger.trace(`[${scopeData.scopeStr}] Exchanging login code for session`); - let token: IToken | undefined; - try { - const postData = new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_id: scopeData.clientId, - scope: scopeData.scopesToSend, - code_verifier: codeVerifier, - redirect_uri: redirectUrl - }).toString(); - - const json = await this.fetchTokenResponse(postData, scopeData); - this._logger.trace(`[${scopeData.scopeStr}] Exchanging code for token succeeded!`); - token = this.convertToTokenSync(json, scopeData); - } catch (e) { - this._logger.error(`[${scopeData.scopeStr}] Error exchanging code for token: ${e}`); - throw e; - } - - if (token.expiresIn) { - this.setSessionTimeout(token.sessionId, token.refreshToken, scopeData, token.expiresIn * AzureActiveDirectoryService.REFRESH_TIMEOUT_MODIFIER); - } - this.setToken(token, scopeData); - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Exchanging login code for session succeeded!`); - return await this.convertToSession(token, scopeData); - } - - private async fetchTokenResponse(postData: string, scopeData: IScopeData): Promise { - let endpointUrl: string; - if (this._env.activeDirectoryEndpointUrl !== defaultActiveDirectoryEndpointUrl) { - // If this is for sovereign clouds, don't try using the proxy endpoint, which supports only public cloud - endpointUrl = this._env.activeDirectoryEndpointUrl; - } else { - const proxyEndpoints: { [providerId: string]: string } | undefined = await vscode.commands.executeCommand('workbench.getCodeExchangeProxyEndpoints'); - endpointUrl = proxyEndpoints?.microsoft || this._env.activeDirectoryEndpointUrl; - } - const endpoint = new URL(`${scopeData.tenant}/oauth2/v2.0/token`, endpointUrl); - - let attempts = 0; - while (attempts <= 3) { - attempts++; - let result; - let errorMessage: string | undefined; - try { - result = await fetch(endpoint.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body: postData - }); - } catch (e) { - errorMessage = e.message ?? e; - } - - if (!result || result.status > 499) { - if (attempts > 3) { - this._logger.error(`[${scopeData.scopeStr}] Fetching token failed: ${result ? await result.text() : errorMessage}`); - break; - } - // Exponential backoff - await new Promise(resolve => setTimeout(resolve, 5 * attempts * attempts * 1000)); - continue; - } else if (!result.ok) { - // For 4XX errors, the user may actually have an expired token or have changed - // their password recently which is throwing a 4XX. For this, we throw an error - // so that the user can be prompted to sign in again. - throw new Error(await result.text()); - } - - return await result.json() as ITokenResponse; - } - - throw new Error(REFRESH_NETWORK_FAILURE); - } - - //#endregion - - //#region storage operations - - private setToken(token: IToken, scopeData: IScopeData): void { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Setting token`); - - const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId); - if (existingTokenIndex > -1) { - this._tokens.splice(existingTokenIndex, 1, token); - } else { - this._tokens.push(token); - } - - // Don't await because setting the token is only useful for any new windows that open. - void this.storeToken(token, scopeData); - } - - private async storeToken(token: IToken, scopeData: IScopeData): Promise { - if (!vscode.window.state.focused) { - if (this._pendingTokensToStore.has(token.sessionId)) { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, replacing token to be stored`); - } else { - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Window is not focused, pending storage of token`); - } - this._pendingTokensToStore.set(token.sessionId, token); - return; - } - - await this._tokenStorage.store(token.sessionId, { - id: token.sessionId, - refreshToken: token.refreshToken, - scope: token.scope, - account: token.account, - endpoint: this._env.activeDirectoryEndpointUrl, - }); - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Stored token`); - } - - private async storePendingTokens(): Promise { - if (this._pendingTokensToStore.size === 0) { - this._logger.trace('No pending tokens to store'); - return; - } - - const tokens = [...this._pendingTokensToStore.values()]; - this._pendingTokensToStore.clear(); - - this._logger.trace(`Storing ${tokens.length} pending tokens...`); - await Promise.allSettled(tokens.map(async token => { - this._logger.trace(`[${token.scope}] '${token.sessionId}' Storing pending token`); - await this._tokenStorage.store(token.sessionId, { - id: token.sessionId, - refreshToken: token.refreshToken, - scope: token.scope, - account: token.account, - endpoint: this._env.activeDirectoryEndpointUrl, - }); - this._logger.trace(`[${token.scope}] '${token.sessionId}' Stored pending token`); - })); - this._logger.trace('Done storing pending tokens'); - } - - private async checkForUpdates(e: IDidChangeInOtherWindowEvent): Promise { - for (const key of e.added) { - const session = await this._tokenStorage.get(key); - if (!session) { - this._logger.error('session not found that was apparently just added'); - continue; - } - - if (!this.sessionMatchesEndpoint(session)) { - // If the session wasn't made for this login endpoint, ignore this update - continue; - } - - const matchesExisting = this._tokens.some(token => token.scope === session.scope && token.sessionId === session.id); - if (!matchesExisting && session.refreshToken) { - try { - const scopes = session.scope.split(' '); - const scopeData: IScopeData = { - scopes, - scopeStr: session.scope, - // filter our special scopes - scopesToSend: scopes.filter(s => !s.startsWith('VSCODE_')).join(' '), - clientId: this.getClientId(scopes), - tenant: this.getTenantId(scopes), - }; - this._logger.trace(`[${scopeData.scopeStr}] '${session.id}' Session added in another window`); - const token = await this.refreshToken(session.refreshToken, scopeData, session.id); - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Sending change event for session that was added`); - this._sessionChangeEmitter.fire({ added: [this.convertToSessionSync(token)], removed: [], changed: [] }); - this._logger.trace(`[${scopeData.scopeStr}] '${token.sessionId}' Session added in another window added here`); - continue; - } catch (e) { - // Network failures will automatically retry on next poll. - if (e.message !== REFRESH_NETWORK_FAILURE) { - vscode.window.showErrorMessage(vscode.l10n.t('You have been signed out because reading stored authentication information failed.')); - await this.removeSessionById(session.id); - } - continue; - } - } - } - - for (const { value } of e.removed) { - this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window`); - if (!this.sessionMatchesEndpoint(value)) { - // If the session wasn't made for this login endpoint, ignore this update - this._logger.trace(`[${value.scope}] '${value.id}' Session doesn't match endpoint. Skipping...`); - continue; - } - - await this.removeSessionById(value.id, false); - this._logger.trace(`[${value.scope}] '${value.id}' Session removed in another window removed here`); - } - - // NOTE: We don't need to handle changed sessions because all that really would give us is a new refresh token - // because access tokens are not stored in Secret Storage due to their short lifespan. This new refresh token - // is not useful in this window because we really only care about the lifetime of the _access_ token which we - // are already managing (see usages of `setSessionTimeout`). - // However, in order to minimize the amount of times we store tokens, if a token was stored via another window, - // we cancel any pending token storage operations. - for (const sessionId of e.updated) { - if (this._pendingTokensToStore.delete(sessionId)) { - this._logger.trace(`'${sessionId}' Cancelled pending token storage because token was updated in another window`); - } - } - } - - private sessionMatchesEndpoint(session: IStoredSession): boolean { - // For older sessions with no endpoint set, it can be assumed to be the default endpoint - session.endpoint ||= defaultActiveDirectoryEndpointUrl; - - return session.endpoint === this._env.activeDirectoryEndpointUrl; - } - - //#endregion -} diff --git a/extensions/microsoft-authentication/src/browser/authProvider.ts b/extensions/microsoft-authentication/src/browser/authProvider.ts deleted file mode 100644 index 3b4da5b18fa..00000000000 --- a/extensions/microsoft-authentication/src/browser/authProvider.ts +++ /dev/null @@ -1,29 +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 { AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationSession, EventEmitter } from 'vscode'; - -export class MsalAuthProvider implements AuthenticationProvider { - private _onDidChangeSessions = new EventEmitter(); - onDidChangeSessions = this._onDidChangeSessions.event; - - initialize(): Thenable { - throw new Error('Method not implemented.'); - } - - getSessions(): Thenable { - throw new Error('Method not implemented.'); - } - createSession(): Thenable { - throw new Error('Method not implemented.'); - } - removeSession(): Thenable { - throw new Error('Method not implemented.'); - } - - dispose() { - this._onDidChangeSessions.dispose(); - } -} diff --git a/extensions/microsoft-authentication/src/browser/authServer.ts b/extensions/microsoft-authentication/src/browser/authServer.ts deleted file mode 100644 index 60b53c713a8..00000000000 --- a/extensions/microsoft-authentication/src/browser/authServer.ts +++ /dev/null @@ -1,12 +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 function startServer(_: any): any { - throw new Error('Not implemented'); -} - -export function createServer(_: any): any { - throw new Error('Not implemented'); -} diff --git a/extensions/microsoft-authentication/src/browser/buffer.ts b/extensions/microsoft-authentication/src/browser/buffer.ts deleted file mode 100644 index 794bb19f579..00000000000 --- a/extensions/microsoft-authentication/src/browser/buffer.ts +++ /dev/null @@ -1,17 +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 function base64Encode(text: string): string { - return btoa(text); -} - -export function base64Decode(text: string): string { - // modification of https://stackoverflow.com/a/38552302 - const replacedCharacters = text.replace(/-/g, '+').replace(/_/g, '/'); - const decodedText = decodeURIComponent(atob(replacedCharacters).split('').map(function (c) { - return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); - }).join('')); - return decodedText; -} diff --git a/extensions/microsoft-authentication/src/browser/fetch.ts b/extensions/microsoft-authentication/src/browser/fetch.ts deleted file mode 100644 index c61281ca8f8..00000000000 --- a/extensions/microsoft-authentication/src/browser/fetch.ts +++ /dev/null @@ -1,6 +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 default fetch; diff --git a/extensions/microsoft-authentication/src/common/telemetryReporter.ts b/extensions/microsoft-authentication/src/common/telemetryReporter.ts index 67b202982ce..5fe773a877e 100644 --- a/extensions/microsoft-authentication/src/common/telemetryReporter.ts +++ b/extensions/microsoft-authentication/src/common/telemetryReporter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AuthError } from '@azure/msal-node'; +import { AuthError, ClientAuthError } from '@azure/msal-node'; import TelemetryReporter, { TelemetryEventProperties } from '@vscode/extension-telemetry'; import { IExperimentationTelemetry } from 'vscode-tas-client'; @@ -43,17 +43,6 @@ export class MicrosoftAuthenticationTelemetryReporter implements IExperimentatio this._telemetryReporter.sendTelemetryEvent('activatingmsalnobroker'); } - sendActivatedWithClassicImplementationEvent(reason: 'setting' | 'web'): void { - /* __GDPR__ - "activatingClassic" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine how often users use the classic login flow.", - "reason": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Why classic was used" } - } - */ - this._telemetryReporter.sendTelemetryEvent('activatingClassic', { reason }); - } - sendLoginEvent(scopes: readonly string[]): void { /* __GDPR__ "login" : { @@ -119,6 +108,43 @@ export class MicrosoftAuthenticationTelemetryReporter implements IExperimentatio }); } + sendTelemetryClientAuthErrorEvent(error: AuthError): void { + const errorCode = error.errorCode; + const correlationId = error.correlationId; + const errorName = error.name; + let brokerErrorCode: string | undefined; + let brokerStatusCode: string | undefined; + let brokerTag: string | undefined; + + // Extract platform broker error information if available + if (error.platformBrokerError) { + brokerErrorCode = error.platformBrokerError.errorCode; + brokerStatusCode = `${error.platformBrokerError.statusCode}`; + brokerTag = error.platformBrokerError.tag; + } + + /* __GDPR__ + "msalClientAuthError" : { + "owner": "TylerLeonhardt", + "comment": "Used to determine how often users run into client auth errors during the login flow.", + "errorName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The name of the client auth error." }, + "errorCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The client auth error code." }, + "correlationId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The client auth error correlation id." }, + "brokerErrorCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The broker error code." }, + "brokerStatusCode": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The broker error status code." }, + "brokerTag": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The broker error tag." } + } + */ + this._telemetryReporter.sendTelemetryErrorEvent('msalClientAuthError', { + errorName, + errorCode, + correlationId, + brokerErrorCode, + brokerStatusCode, + brokerTag + }); + } + /** * Sends an event for an account type available at startup. * @param scopes The scopes for the session diff --git a/extensions/microsoft-authentication/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts index 620a10e1a29..c7cf62b15e3 100644 --- a/extensions/microsoft-authentication/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -3,13 +3,71 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { commands, ExtensionContext, l10n, window, workspace } from 'vscode'; -import * as extensionV1 from './extensionV1'; -import * as extensionV2 from './extensionV2'; -import { MicrosoftAuthenticationTelemetryReporter } from './common/telemetryReporter'; +import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; +import Logger from './logger'; +import { MsalAuthProvider } from './node/authProvider'; +import { UriEventHandler } from './UriEventHandler'; +import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable, Uri } from 'vscode'; +import { MicrosoftAuthenticationTelemetryReporter, MicrosoftSovereignCloudAuthenticationTelemetryReporter } from './common/telemetryReporter'; -let implementation: 'msal' | 'msal-no-broker' | 'classic' = 'msal'; -const getImplementation = () => workspace.getConfiguration('microsoft-authentication').get<'msal' | 'msal-no-broker' | 'classic'>('implementation') ?? 'msal'; +let implementation: 'msal' | 'msal-no-broker' = 'msal'; +const getImplementation = () => workspace.getConfiguration('microsoft-authentication').get<'msal' | 'msal-no-broker'>('implementation') ?? 'msal'; + +async function initMicrosoftSovereignCloudAuthProvider( + context: ExtensionContext, + uriHandler: UriEventHandler +): Promise { + const environment = workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); + let authProviderName: string | undefined; + if (!environment) { + return undefined; + } + + if (environment === 'custom') { + const customEnv = workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); + if (!customEnv) { + const res = await window.showErrorMessage(l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), l10n.t('Open settings')); + if (res) { + await commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + try { + Environment.add(customEnv); + } catch (e) { + const res = await window.showErrorMessage(l10n.t('Error validating custom environment setting: {0}', e.message), l10n.t('Open settings')); + if (res) { + await commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); + } + return undefined; + } + authProviderName = customEnv.name; + } else { + authProviderName = environment; + } + + const env = Environment.get(authProviderName); + if (!env) { + await window.showErrorMessage(l10n.t('The environment `{0}` is not a valid environment.', authProviderName), l10n.t('Open settings')); + return undefined; + } + + const authProvider = await MsalAuthProvider.create( + context, + new MicrosoftSovereignCloudAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), + window.createOutputChannel(l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), + uriHandler, + env + ); + const disposable = authentication.registerAuthenticationProvider( + 'microsoft-sovereign-cloud', + authProviderName, + authProvider, + { supportsMultipleAccounts: true, supportsChallenges: true } + ); + context.subscriptions.push(disposable); + return disposable; +} export async function activate(context: ExtensionContext) { const mainTelemetryReporter = new MicrosoftAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey); @@ -39,34 +97,48 @@ export async function activate(context: ExtensionContext) { commands.executeCommand('workbench.action.reloadWindow'); } })); - const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; - - // Only activate the new extension if we are not running in a browser environment - if (!isNodeEnvironment) { - mainTelemetryReporter.sendActivatedWithClassicImplementationEvent('web'); - return await extensionV1.activate(context, mainTelemetryReporter.telemetryReporter); - } switch (implementation) { case 'msal-no-broker': mainTelemetryReporter.sendActivatedWithMsalNoBrokerEvent(); - await extensionV2.activate(context, mainTelemetryReporter); - break; - case 'classic': - mainTelemetryReporter.sendActivatedWithClassicImplementationEvent('setting'); - await extensionV1.activate(context, mainTelemetryReporter.telemetryReporter); break; case 'msal': default: - await extensionV2.activate(context, mainTelemetryReporter); break; } + + const uriHandler = new UriEventHandler(); + context.subscriptions.push(uriHandler); + const authProvider = await MsalAuthProvider.create( + context, + mainTelemetryReporter, + Logger, + uriHandler + ); + context.subscriptions.push(authentication.registerAuthenticationProvider( + 'microsoft', + 'Microsoft', + authProvider, + { + supportsMultipleAccounts: true, + supportsChallenges: true, + supportedAuthorizationServers: [ + Uri.parse('https://login.microsoftonline.com/*'), + Uri.parse('https://login.microsoftonline.com/*/v2.0') + ] + } + )); + + let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); + + context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration('microsoft-sovereign-cloud')) { + microsoftSovereignCloudAuthProviderDisposable?.dispose(); + microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); + } + })); } export function deactivate() { - if (implementation !== 'classic') { - extensionV2.deactivate(); - } else { - extensionV1.deactivate(); - } + Logger.info('Microsoft Authentication is deactivating...'); } diff --git a/extensions/microsoft-authentication/src/extensionV1.ts b/extensions/microsoft-authentication/src/extensionV1.ts deleted file mode 100644 index 02248dd989d..00000000000 --- a/extensions/microsoft-authentication/src/extensionV1.ts +++ /dev/null @@ -1,193 +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 * as vscode from 'vscode'; -import { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; -import { AzureActiveDirectoryService, IStoredSession } from './AADHelper'; -import { BetterTokenStorage } from './betterSecretStorage'; -import { UriEventHandler } from './UriEventHandler'; -import TelemetryReporter from '@vscode/extension-telemetry'; -import Logger from './logger'; - -async function initMicrosoftSovereignCloudAuthProvider(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter, uriHandler: UriEventHandler, tokenStorage: BetterTokenStorage): Promise { - const environment = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); - let authProviderName: string | undefined; - if (!environment) { - return undefined; - } - - if (environment === 'custom') { - const customEnv = vscode.workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); - if (!customEnv) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), vscode.l10n.t('Open settings')); - if (res) { - await vscode.commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; - } - try { - Environment.add(customEnv); - } catch (e) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('Error validating custom environment setting: {0}', e.message), vscode.l10n.t('Open settings')); - if (res) { - await vscode.commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; - } - authProviderName = customEnv.name; - } else { - authProviderName = environment; - } - - const env = Environment.get(authProviderName); - if (!env) { - const res = await vscode.window.showErrorMessage(vscode.l10n.t('The environment `{0}` is not a valid environment.', authProviderName), vscode.l10n.t('Open settings')); - return undefined; - } - - const aadService = new AzureActiveDirectoryService( - vscode.window.createOutputChannel(vscode.l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), - context, - uriHandler, - tokenStorage, - telemetryReporter, - env); - await aadService.initialize(); - - const disposable = vscode.authentication.registerAuthenticationProvider('microsoft-sovereign-cloud', authProviderName, { - onDidChangeSessions: aadService.onDidChangeSessions, - getSessions: (scopes: string[]) => aadService.getSessions(scopes), - createSession: async (scopes: string[]) => { - try { - /* __GDPR__ - "loginMicrosoftSovereignCloud" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Sovereign Cloud Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } - } - */ - telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloud', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), - }); - - return await aadService.createSession(scopes); - } catch (e) { - /* __GDPR__ - "loginMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } - */ - telemetryReporter.sendTelemetryEvent('loginMicrosoftSovereignCloudFailed'); - - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logoutMicrosoftSovereignCloud" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloud'); - - await aadService.removeSessionById(id); - } catch (e) { - /* __GDPR__ - "logoutMicrosoftSovereignCloudFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutMicrosoftSovereignCloudFailed'); - } - } - }, { supportsMultipleAccounts: true }); - - context.subscriptions.push(disposable); - return disposable; -} - -export async function activate(context: vscode.ExtensionContext, telemetryReporter: TelemetryReporter) { - // If we ever activate the old flow, then mark that we will need to migrate when the user upgrades to v2. - // TODO: MSAL Migration. Remove this when we remove the old flow. - context.globalState.update('msalMigration', false); - - const uriHandler = new UriEventHandler(); - context.subscriptions.push(uriHandler); - const betterSecretStorage = new BetterTokenStorage('microsoft.login.keylist', context); - - const loginService = new AzureActiveDirectoryService( - Logger, - context, - uriHandler, - betterSecretStorage, - telemetryReporter, - Environment.AzureCloud); - await loginService.initialize(); - - context.subscriptions.push(vscode.authentication.registerAuthenticationProvider( - 'microsoft', - 'Microsoft', - { - onDidChangeSessions: loginService.onDidChangeSessions, - getSessions: (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => loginService.getSessions(scopes, options), - createSession: async (scopes: string[], options?: vscode.AuthenticationProviderSessionOptions) => { - try { - /* __GDPR__ - "login" : { - "owner": "TylerLeonhardt", - "comment": "Used to determine the usage of the Microsoft Auth Provider.", - "scopes": { "classification": "PublicNonPersonalData", "purpose": "FeatureInsight", "comment": "Used to determine what scope combinations are being requested." } - } - */ - telemetryReporter.sendTelemetryEvent('login', { - // Get rid of guids from telemetry. - scopes: JSON.stringify(scopes.map(s => s.replace(/[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}/i, '{guid}'))), - }); - - return await loginService.createSession(scopes, options); - } catch (e) { - /* __GDPR__ - "loginFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users run into issues with the login flow." } - */ - telemetryReporter.sendTelemetryEvent('loginFailed'); - - throw e; - } - }, - removeSession: async (id: string) => { - try { - /* __GDPR__ - "logout" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often users log out." } - */ - telemetryReporter.sendTelemetryEvent('logout'); - - await loginService.removeSessionById(id); - } catch (e) { - /* __GDPR__ - "logoutFailed" : { "owner": "TylerLeonhardt", "comment": "Used to determine how often fail to log out." } - */ - telemetryReporter.sendTelemetryEvent('logoutFailed'); - } - } - }, - { - supportsMultipleAccounts: true, - supportedAuthorizationServers: [ - vscode.Uri.parse('https://login.microsoftonline.com/*'), - vscode.Uri.parse('https://login.microsoftonline.com/*/v2.0') - ] - } - )); - - let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); - - context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration('microsoft-sovereign-cloud')) { - microsoftSovereignCloudAuthProviderDisposable?.dispose(); - microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, telemetryReporter, uriHandler, betterSecretStorage); - } - })); - - return; -} - -// this method is called when your extension is deactivated -export function deactivate() { } diff --git a/extensions/microsoft-authentication/src/extensionV2.ts b/extensions/microsoft-authentication/src/extensionV2.ts deleted file mode 100644 index bafc8454f8c..00000000000 --- a/extensions/microsoft-authentication/src/extensionV2.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 { Environment, EnvironmentParameters } from '@azure/ms-rest-azure-env'; -import Logger from './logger'; -import { MsalAuthProvider } from './node/authProvider'; -import { UriEventHandler } from './UriEventHandler'; -import { authentication, commands, ExtensionContext, l10n, window, workspace, Disposable, Uri } from 'vscode'; -import { MicrosoftAuthenticationTelemetryReporter, MicrosoftSovereignCloudAuthenticationTelemetryReporter } from './common/telemetryReporter'; - -async function initMicrosoftSovereignCloudAuthProvider( - context: ExtensionContext, - uriHandler: UriEventHandler -): Promise { - const environment = workspace.getConfiguration('microsoft-sovereign-cloud').get('environment'); - let authProviderName: string | undefined; - if (!environment) { - return undefined; - } - - if (environment === 'custom') { - const customEnv = workspace.getConfiguration('microsoft-sovereign-cloud').get('customEnvironment'); - if (!customEnv) { - const res = await window.showErrorMessage(l10n.t('You must also specify a custom environment in order to use the custom environment auth provider.'), l10n.t('Open settings')); - if (res) { - await commands.executeCommand('workbench.action.openSettingsJson', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; - } - try { - Environment.add(customEnv); - } catch (e) { - const res = await window.showErrorMessage(l10n.t('Error validating custom environment setting: {0}', e.message), l10n.t('Open settings')); - if (res) { - await commands.executeCommand('workbench.action.openSettings', 'microsoft-sovereign-cloud.customEnvironment'); - } - return undefined; - } - authProviderName = customEnv.name; - } else { - authProviderName = environment; - } - - const env = Environment.get(authProviderName); - if (!env) { - await window.showErrorMessage(l10n.t('The environment `{0}` is not a valid environment.', authProviderName), l10n.t('Open settings')); - return undefined; - } - - const authProvider = await MsalAuthProvider.create( - context, - new MicrosoftSovereignCloudAuthenticationTelemetryReporter(context.extension.packageJSON.aiKey), - window.createOutputChannel(l10n.t('Microsoft Sovereign Cloud Authentication'), { log: true }), - uriHandler, - env - ); - const disposable = authentication.registerAuthenticationProvider( - 'microsoft-sovereign-cloud', - authProviderName, - authProvider, - { supportsMultipleAccounts: true, supportsChallenges: true } - ); - context.subscriptions.push(disposable); - return disposable; -} - -export async function activate(context: ExtensionContext, mainTelemetryReporter: MicrosoftAuthenticationTelemetryReporter) { - const uriHandler = new UriEventHandler(); - context.subscriptions.push(uriHandler); - const authProvider = await MsalAuthProvider.create( - context, - mainTelemetryReporter, - Logger, - uriHandler - ); - context.subscriptions.push(authentication.registerAuthenticationProvider( - 'microsoft', - 'Microsoft', - authProvider, - { - supportsMultipleAccounts: true, - supportsChallenges: true, - supportedAuthorizationServers: [ - Uri.parse('https://login.microsoftonline.com/*'), - Uri.parse('https://login.microsoftonline.com/*/v2.0') - ] - } - )); - - let microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); - - context.subscriptions.push(workspace.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration('microsoft-sovereign-cloud')) { - microsoftSovereignCloudAuthProviderDisposable?.dispose(); - microsoftSovereignCloudAuthProviderDisposable = await initMicrosoftSovereignCloudAuthProvider(context, uriHandler); - } - })); -} - -export function deactivate() { } diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index ac57bf0680b..95921b9e3a3 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccountInfo, AuthenticationResult, ClientAuthError, ClientAuthErrorCodes, ServerError, SilentFlowRequest } from '@azure/msal-node'; +import { AccountInfo, AuthenticationResult, AuthError, ClientAuthError, ClientAuthErrorCodes, ServerError } from '@azure/msal-node'; import { AuthenticationChallenge, AuthenticationConstraint, AuthenticationGetSessionOptions, AuthenticationProvider, AuthenticationProviderAuthenticationSessionsChangeEvent, AuthenticationProviderSessionOptions, AuthenticationSession, AuthenticationSessionAccountInformation, CancellationError, env, EventEmitter, ExtensionContext, ExtensionKind, l10n, LogOutputChannel, Uri, window } from 'vscode'; import { Environment } from '@azure/ms-rest-azure-env'; import { CachedPublicClientApplicationManager } from './publicClientCache'; @@ -12,7 +12,6 @@ import { MicrosoftAccountType, MicrosoftAuthenticationTelemetryReporter } from ' import { ScopeData } from '../common/scopeData'; import { EventBufferer } from '../common/event'; import { BetterTokenStorage } from '../betterSecretStorage'; -import { IStoredSession } from '../AADHelper'; import { ExtensionHost, getMsalFlows } from './flows'; import { base64Decode } from './buffer'; import { Config } from '../common/config'; @@ -21,6 +20,22 @@ import { isSupportedClient } from '../common/env'; const MSA_TID = '9188040d-6c67-4c5b-b112-36a304b66dad'; const MSA_PASSTHRU_TID = 'f8cdef31-a31e-4b4a-93e4-5f571e91255a'; +/** + * Interface for sessions stored from the old authentication flow. + * Used for migration purposes when upgrading to MSAL. + * TODO: Remove this after one or two releases. + */ +export interface IStoredSession { + id: string; + refreshToken: string; + scope: string; // Scopes are alphabetized and joined with a space + account: { + label: string; + id: string; + }; + endpoint: string | undefined; +} + export class MsalAuthProvider implements AuthenticationProvider { private readonly _disposables: { dispose(): void }[]; @@ -209,14 +224,12 @@ export class MsalAuthProvider implements AuthenticationProvider { } }; - const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`)); const flows = getMsalFlows({ - extensionHost: isNodeEnvironment - ? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote - : ExtensionHost.WebWorker, + extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote, supportedClient: isSupportedClient(callbackUri), - isBrokerSupported: cachedPca.isBrokerAvailable + isBrokerSupported: cachedPca.isBrokerAvailable, + isPortableMode: env.isAppPortable }); const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); @@ -348,14 +361,12 @@ export class MsalAuthProvider implements AuthenticationProvider { } }; - const isNodeEnvironment = typeof process !== 'undefined' && typeof process?.versions?.node === 'string'; const callbackUri = await env.asExternalUri(Uri.parse(`${env.uriScheme}://vscode.microsoft-authentication`)); const flows = getMsalFlows({ - extensionHost: isNodeEnvironment - ? this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote - : ExtensionHost.WebWorker, + extensionHost: this._context.extension.extensionKind === ExtensionKind.UI ? ExtensionHost.Local : ExtensionHost.Remote, isBrokerSupported: cachedPca.isBrokerAvailable, - supportedClient: isSupportedClient(callbackUri) + supportedClient: isSupportedClient(callbackUri), + isPortableMode: env.isAppPortable }); const authority = new URL(scopeData.tenant, this._env.activeDirectoryEndpointUrl).toString(); @@ -501,6 +512,7 @@ export class MsalAuthProvider implements AuthenticationProvider { if (cachedPca.isBrokerAvailable && process.platform === 'darwin') { redirectUri = Config.macOSBrokerRedirectUri; } + this._logger.trace(`[getAllSessionsForPca] [${scopeData.scopeStr}] [${account.environment}] [${account.username}] acquiring token silently with${forceRefresh ? ' ' : 'out '}force refresh${claims ? ' and claims' : ''}...`); const result = await cachedPca.acquireTokenSilent({ account, authority, @@ -513,7 +525,11 @@ export class MsalAuthProvider implements AuthenticationProvider { } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again - this._telemetryReporter.sendTelemetryErrorEvent(e); + if (e instanceof AuthError) { + this._telemetryReporter.sendTelemetryClientAuthErrorEvent(e); + } else { + this._telemetryReporter.sendTelemetryErrorEvent(e); + } this._logger.info(`[getAllSessionsForPca] [${scopeData.scopeStr}] [${account.username}] failed to acquire token silently, skipping account`, JSON.stringify(e)); continue; } diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts index 1f0e528c99f..e86269833a8 100644 --- a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -51,9 +51,7 @@ export class CachedPublicClientApplication implements ICachedPublicClientApplica const loggerOptions = new MsalLoggerOptions(_logger, telemetryReporter); let broker: BrokerOptions | undefined; - if (process.platform !== 'win32' && process.platform !== 'darwin') { - this._logger.info(`[${this._clientId}] Native Broker is only available on Windows and macOS`); - } else if (env.uiKind === UIKind.Web) { + if (env.uiKind === UIKind.Web) { this._logger.info(`[${this._clientId}] Native Broker is not available in web UI`); } 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`); diff --git a/extensions/microsoft-authentication/src/node/flows.ts b/extensions/microsoft-authentication/src/node/flows.ts index 4a3c877691b..e5105fc58c5 100644 --- a/extensions/microsoft-authentication/src/node/flows.ts +++ b/extensions/microsoft-authentication/src/node/flows.ts @@ -14,16 +14,15 @@ import { Config } from '../common/config'; const DEFAULT_REDIRECT_URI = 'https://vscode.dev/redirect'; export const enum ExtensionHost { - WebWorker, Remote, Local } interface IMsalFlowOptions { supportsRemoteExtensionHost: boolean; - supportsWebWorkerExtensionHost: boolean; supportsUnsupportedClient: boolean; supportsBroker: boolean; + supportsPortableMode: boolean; } interface IMsalFlowTriggerOptions { @@ -48,9 +47,9 @@ class DefaultLoopbackFlow implements IMsalFlow { label = 'default'; options: IMsalFlowOptions = { supportsRemoteExtensionHost: false, - supportsWebWorkerExtensionHost: false, supportsUnsupportedClient: true, - supportsBroker: true + supportsBroker: true, + supportsPortableMode: true }; async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger }: IMsalFlowTriggerOptions): Promise { @@ -78,9 +77,9 @@ class UrlHandlerFlow implements IMsalFlow { label = 'protocol handler'; options: IMsalFlowOptions = { supportsRemoteExtensionHost: true, - supportsWebWorkerExtensionHost: false, supportsUnsupportedClient: false, - supportsBroker: false + supportsBroker: false, + supportsPortableMode: false }; async trigger({ cachedPca, authority, scopes, claims, loginHint, windowHandle, logger, uriHandler, callbackUri }: IMsalFlowTriggerOptions): Promise { @@ -108,9 +107,9 @@ class DeviceCodeFlow implements IMsalFlow { label = 'device code'; options: IMsalFlowOptions = { supportsRemoteExtensionHost: true, - supportsWebWorkerExtensionHost: false, supportsUnsupportedClient: true, - supportsBroker: false + supportsBroker: false, + supportsPortableMode: true }; async trigger({ cachedPca, authority, scopes, claims, logger }: IMsalFlowTriggerOptions): Promise { @@ -133,22 +132,19 @@ export interface IMsalFlowQuery { extensionHost: ExtensionHost; supportedClient: boolean; isBrokerSupported: boolean; + isPortableMode: boolean; } export function getMsalFlows(query: IMsalFlowQuery): IMsalFlow[] { const flows = []; for (const flow of allFlows) { let useFlow: boolean = true; - switch (query.extensionHost) { - case ExtensionHost.Remote: - useFlow &&= flow.options.supportsRemoteExtensionHost; - break; - case ExtensionHost.WebWorker: - useFlow &&= flow.options.supportsWebWorkerExtensionHost; - break; + if (query.extensionHost === ExtensionHost.Remote) { + useFlow &&= flow.options.supportsRemoteExtensionHost; } useFlow &&= flow.options.supportsBroker || !query.isBrokerSupported; useFlow &&= flow.options.supportsUnsupportedClient || query.supportedClient; + useFlow &&= flow.options.supportsPortableMode || !query.isPortableMode; if (useFlow) { flows.push(flow); } diff --git a/extensions/microsoft-authentication/src/node/test/flows.test.ts b/extensions/microsoft-authentication/src/node/test/flows.test.ts index 1cd4bd6077a..9be191e8feb 100644 --- a/extensions/microsoft-authentication/src/node/test/flows.test.ts +++ b/extensions/microsoft-authentication/src/node/test/flows.test.ts @@ -11,7 +11,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: true, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 3); @@ -24,7 +25,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: true, - isBrokerSupported: true + isBrokerSupported: true, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); @@ -35,7 +37,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Remote, supportedClient: true, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 2); @@ -43,21 +46,12 @@ suite('getMsalFlows', () => { assert.strictEqual(flows[1].label, 'device code'); }); - test('should return no flows for web worker extension host', () => { - const query: IMsalFlowQuery = { - extensionHost: ExtensionHost.WebWorker, - supportedClient: true, - isBrokerSupported: false - }; - const flows = getMsalFlows(query); - assert.strictEqual(flows.length, 0); - }); - test('should return only default and device code flows for local extension host with unsupported client and no broker', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: false, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 2); @@ -69,7 +63,8 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Remote, supportedClient: false, - isBrokerSupported: false + isBrokerSupported: false, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); @@ -80,10 +75,24 @@ suite('getMsalFlows', () => { const query: IMsalFlowQuery = { extensionHost: ExtensionHost.Local, supportedClient: false, - isBrokerSupported: true + isBrokerSupported: true, + isPortableMode: false }; const flows = getMsalFlows(query); assert.strictEqual(flows.length, 1); assert.strictEqual(flows[0].label, 'default'); }); + + test('should exclude protocol handler flow in portable mode', () => { + const query: IMsalFlowQuery = { + extensionHost: ExtensionHost.Local, + supportedClient: true, + isBrokerSupported: false, + isPortableMode: true + }; + const flows = getMsalFlows(query); + assert.strictEqual(flows.length, 2); + assert.strictEqual(flows[0].label, 'default'); + assert.strictEqual(flows[1].label, 'device code'); + }); }); diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json index 7f97ae747b4..47df27d5bdb 100644 --- a/extensions/microsoft-authentication/tsconfig.json +++ b/extensions/microsoft-authentication/tsconfig.json @@ -1,19 +1,18 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "noFallthroughCasesInSwitch": true, "noUnusedLocals": false, - "skipLibCheck": true, - "lib": [ - "WebWorker" - ] + "skipLibCheck": true }, "include": [ "src/**/*", "../../src/vscode-dts/vscode.d.ts", "../../src/vscode-dts/vscode.proposed.nativeWindowHandle.d.ts", "../../src/vscode-dts/vscode.proposed.authIssuers.d.ts", - "../../src/vscode-dts/vscode.proposed.authenticationChallenges.d.ts" + "../../src/vscode-dts/vscode.proposed.authenticationChallenges.d.ts", + "../../src/vscode-dts/vscode.proposed.envIsAppPortable.d.ts" ] } diff --git a/extensions/notebook-renderers/package-lock.json b/extensions/notebook-renderers/package-lock.json index f2f7af8abf8..85357e3c855 100644 --- a/extensions/notebook-renderers/package-lock.json +++ b/extensions/notebook-renderers/package-lock.json @@ -116,6 +116,20 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "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-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -205,6 +219,21 @@ "node": ">=12" } }, + "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/entities": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", @@ -217,6 +246,55 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "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" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "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" + } + }, "node_modules/escodegen": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", @@ -277,19 +355,126 @@ "dev": true }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "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/function-bind": { + "version": "1.1.2", + "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.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", + "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" + }, + "funding": { + "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/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" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -405,6 +590,16 @@ "node": ">= 0.8.0" } }, + "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/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index 6c3205fa7b7..09d4129e817 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -65,8 +65,7 @@ const domEval = (container: Element) => { for (const key of preservedScriptAttributes) { const val = node[key] || node.getAttribute && node.getAttribute(key); if (val) { - // eslint-disable-next-line local/code-no-any-casts - scriptTag.setAttribute(key, val as any); + scriptTag.setAttribute(key, val as unknown as string); } } @@ -76,8 +75,8 @@ const domEval = (container: Element) => { }; function getAltText(outputInfo: OutputItem) { - const metadata = outputInfo.metadata; - if (typeof metadata === 'object' && metadata && 'vscode_altText' in metadata && typeof metadata.vscode_altText === 'string') { + const metadata = outputInfo.metadata as Record | undefined; + if (typeof metadata === 'object' && metadata && typeof metadata.vscode_altText === 'string') { return metadata.vscode_altText; } return undefined; @@ -337,9 +336,9 @@ function findScrolledHeight(container: HTMLElement): number | undefined { } function scrollingEnabled(output: OutputItem, options: RenderOptions) { - const metadata = output.metadata; + const metadata = output.metadata as Record | undefined; return (typeof metadata === 'object' && metadata - && 'scrollable' in metadata && typeof metadata.scrollable === 'boolean') ? + && typeof metadata.scrollable === 'boolean') ? metadata.scrollable : options.outputScrolling; } diff --git a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index 999116152c8..a193ce38d72 100644 --- a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -127,15 +127,13 @@ suite('Notebook builtin output renderer', () => { return text; }, blob() { - // eslint-disable-next-line local/code-no-any-casts - return [] as any; + return new Blob([text], { type: mime }); }, json() { return '{ }'; }, data() { - // eslint-disable-next-line local/code-no-any-casts - return [] as any; + return new Uint8Array(); }, metadata: {} }; diff --git a/extensions/notebook-renderers/tsconfig.json b/extensions/notebook-renderers/tsconfig.json index 07c5ef470f5..0bc7baa21be 100644 --- a/extensions/notebook-renderers/tsconfig.json +++ b/extensions/notebook-renderers/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "lib": [ "es2024", diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 352ee31ae9f..694e98b5e12 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -150,9 +150,10 @@ } }, "node_modules/js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" diff --git a/extensions/npm/tsconfig.json b/extensions/npm/tsconfig.json index 0c84fcc94e3..5252f6bd127 100644 --- a/extensions/npm/tsconfig.json +++ b/extensions/npm/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/package-lock.json b/extensions/package-lock.json index acaba35fbf0..01f27f13a4b 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -13,15 +13,15 @@ "typescript": "^5.9.3" }, "devDependencies": { - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", - "esbuild": "0.25.0", + "@parcel/watcher": "^2.5.6", + "esbuild": "0.27.2", "vscode-grammar-updater": "^1.1.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", - "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -36,9 +36,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", - "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -53,9 +53,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", - "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -70,9 +70,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", - "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -87,9 +87,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", - "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -104,9 +104,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", - "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -121,9 +121,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", - "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -138,9 +138,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", - "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -155,9 +155,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", - "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -172,9 +172,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", - "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -189,9 +189,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", - "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -206,9 +206,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", - "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -223,9 +223,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", - "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -240,9 +240,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", - "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -257,9 +257,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", - "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -274,9 +274,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", - "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -291,9 +291,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", - "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -308,9 +308,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", - "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -325,9 +325,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", - "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -342,9 +342,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", - "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -359,9 +359,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", - "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -375,10 +375,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", - "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -393,9 +410,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", - "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -410,9 +427,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", - "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -427,9 +444,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", - "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -444,18 +461,54 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "git+ssh://git@github.com/parcel-bundler/watcher.git#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", - "integrity": "sha512-Z0lk8pM5vwuOJU6pfheRXHrOpQYIIEnVl/z8DY6370D4+ZnrOTvFa5BUdf3pGxahT5ILbPWwQSm2Wthy4q1OTg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">= 10.0.0" }, @@ -464,16 +517,256 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "fill-range": "^7.1.1" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/coffeescript": { @@ -512,9 +805,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", - "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -525,31 +818,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.0", - "@esbuild/android-arm": "0.25.0", - "@esbuild/android-arm64": "0.25.0", - "@esbuild/android-x64": "0.25.0", - "@esbuild/darwin-arm64": "0.25.0", - "@esbuild/darwin-x64": "0.25.0", - "@esbuild/freebsd-arm64": "0.25.0", - "@esbuild/freebsd-x64": "0.25.0", - "@esbuild/linux-arm": "0.25.0", - "@esbuild/linux-arm64": "0.25.0", - "@esbuild/linux-ia32": "0.25.0", - "@esbuild/linux-loong64": "0.25.0", - "@esbuild/linux-mips64el": "0.25.0", - "@esbuild/linux-ppc64": "0.25.0", - "@esbuild/linux-riscv64": "0.25.0", - "@esbuild/linux-s390x": "0.25.0", - "@esbuild/linux-x64": "0.25.0", - "@esbuild/netbsd-arm64": "0.25.0", - "@esbuild/netbsd-x64": "0.25.0", - "@esbuild/openbsd-arm64": "0.25.0", - "@esbuild/openbsd-x64": "0.25.0", - "@esbuild/sunos-x64": "0.25.0", - "@esbuild/win32-arm64": "0.25.0", - "@esbuild/win32-ia32": "0.25.0", - "@esbuild/win32-x64": "0.25.0" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/fast-plist": { @@ -558,18 +852,6 @@ "integrity": "sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg= sha512-2HxzrqJhmMoxVzARjYFvkzkL2dCBB8sogU5sD8gqcZWv5UCivK9/cXM9KIPDRwU+eD3mbRDN/GhW8bO/4dtMfg==", "dev": true }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -591,29 +873,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "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/node-addon-api": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", @@ -625,29 +884,18 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/extensions/package.json b/extensions/package.json index 7b1d8defa3e..7fba7893b60 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -10,8 +10,8 @@ "postinstall": "node ./postinstall.mjs" }, "devDependencies": { - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", - "esbuild": "0.25.0", + "@parcel/watcher": "^2.5.6", + "esbuild": "0.27.2", "vscode-grammar-updater": "^1.1.0" }, "overrides": { diff --git a/extensions/php-language-features/tsconfig.json b/extensions/php-language-features/tsconfig.json index 29a69e99d98..60b4cc148d8 100644 --- a/extensions/php-language-features/tsconfig.json +++ b/extensions/php-language-features/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/php/cgmanifest.json b/extensions/php/cgmanifest.json index fe4db8d4ac6..090bdf642f9 100644 --- a/extensions/php/cgmanifest.json +++ b/extensions/php/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/KapitanOczywisty/language-php", - "commitHash": "b17fdadac1756fc13a0853c26fca2f0b4495c0bd" + "commitHash": "6941b924add3b2587a5be789248176edf5f14595" } }, "license": "MIT", diff --git a/extensions/php/language-configuration.json b/extensions/php/language-configuration.json index 4d9a3238d40..fca0a338042 100644 --- a/extensions/php/language-configuration.json +++ b/extensions/php/language-configuration.json @@ -93,20 +93,16 @@ ] ], "indentationRules": { - "increaseIndentPattern": "({(?!.*}).*|\\(|\\[|((else(\\s)?)?if|else|for(each)?|while|switch|case).*:)\\s*((/[/*].*|)?$|\\?>)", - "decreaseIndentPattern": "^(.*\\*\\/)?\\s*((\\})|(\\)+[;,])|(\\]\\)*[;,])|\\b(else:)|\\b((end(if|for(each)?|while|switch));))", + "increaseIndentPattern": "((else\\s?)?if|else|for(each)?|while|switch|case).*:\\s*((/[/*].*|)?$|\\?>)", + "decreaseIndentPattern": "^(.*\\*\\/)?\\s*(\\b(else:)|\\bend(if|for(each)?|while|switch);)", // e.g. * ...| or */| or *-----*/| - "unIndentedLinePattern": { - "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$" - }, - "indentNextLinePattern": { - "pattern": "^\\s*(((if|else ?if|while|for|foreach)\\s*\\(.*\\)\\s*)|else\\s*)$" - } + "unIndentedLinePattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$|^(\\t|[ ])*[ ]\\*/\\s*$|^(\\t|[ ])*\\*([ ]([^\\*]|\\*(?!/))*)?$", + "indentNextLinePattern": "^\\s*(((if|else ?if|while|for|foreach)\\s*\\(.*\\)\\s*)|else\\s*)$" }, "folding": { "markers": { - "start": "^\\s*(#|\/\/)region\\b", - "end": "^\\s*(#|\/\/)endregion\\b" + "start": "^\\s*(#|\/\/|\/\/ #)region\\b", + "end": "^\\s*(#|\/\/|\/\/ #)endregion\\b" } }, "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\-\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", diff --git a/extensions/php/syntaxes/php.tmLanguage.json b/extensions/php/syntaxes/php.tmLanguage.json index 3875a74b032..efb122c98d3 100644 --- a/extensions/php/syntaxes/php.tmLanguage.json +++ b/extensions/php/syntaxes/php.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/KapitanOczywisty/language-php/commit/b17fdadac1756fc13a0853c26fca2f0b4495c0bd", + "version": "https://github.com/KapitanOczywisty/language-php/commit/6941b924add3b2587a5be789248176edf5f14595", "scopeName": "source.php", "patterns": [ { @@ -982,9 +982,13 @@ "name": "keyword.operator.arithmetic.php" }, { - "match": "(?i)(!|&&|\\|\\|)|\\b(and|or|xor|as)\\b", + "match": "(?i)(!|&&|\\|\\|)|\\b(and|or|xor)\\b", "name": "keyword.operator.logical.php" }, + { + "match": "(?i)\\bas\\b", + "name": "keyword.operator.as.php" + }, { "include": "#function-call" }, diff --git a/extensions/prompt-basics/package.json b/extensions/prompt-basics/package.json index 323c2b8ce68..1765ac15d8c 100644 --- a/extensions/prompt-basics/package.json +++ b/extensions/prompt-basics/package.json @@ -20,8 +20,7 @@ "prompt" ], "extensions": [ - ".prompt.md", - "copilot-instructions.md" + ".prompt.md" ], "configuration": "./language-configuration.json" }, @@ -51,6 +50,17 @@ "**/.github/agents/*.md" ], "configuration": "./language-configuration.json" + }, + { + "id": "skill", + "aliases": [ + "Skill", + "skill" + ], + "filenames": [ + "SKILL.md" + ], + "configuration": "./language-configuration.json" } ], "grammars": [ @@ -80,6 +90,15 @@ "markup.underline.link.markdown", "punctuation.definition.list.begin.markdown" ] + }, + { + "language": "skill", + "path": "./syntaxes/prompt.tmLanguage.json", + "scopeName": "text.html.markdown.prompt", + "unbalancedBracketScopes": [ + "markup.underline.link.markdown", + "punctuation.definition.list.begin.markdown" + ] } ], "configurationDefaults": { @@ -93,9 +112,10 @@ "editor.wordWrap": "on", "editor.quickSuggestions": { "comments": "off", - "strings": "off", - "other": "off" - } + "strings": "on", + "other": "on" + }, + "editor.wordBasedSuggestions": "off" }, "[instructions]": { "editor.insertSpaces": true, @@ -107,9 +127,10 @@ "editor.wordWrap": "on", "editor.quickSuggestions": { "comments": "off", - "strings": "off", - "other": "off" - } + "strings": "on", + "other": "on" + }, + "editor.wordBasedSuggestions": "off" }, "[chatagent]": { "editor.insertSpaces": true, @@ -121,9 +142,25 @@ "editor.wordWrap": "on", "editor.quickSuggestions": { "comments": "off", - "strings": "off", - "other": "off" - } + "strings": "on", + "other": "on" + }, + "editor.wordBasedSuggestions": "off" + }, + "[skill]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "editor.unicodeHighlight.ambiguousCharacters": false, + "editor.unicodeHighlight.invisibleCharacters": false, + "diffEditor.ignoreTrimWhitespace": false, + "editor.wordWrap": "on", + "editor.quickSuggestions": { + "comments": "off", + "strings": "on", + "other": "on" + }, + "editor.wordBasedSuggestions": "off" } } }, diff --git a/extensions/r/package.json b/extensions/r/package.json index 9d655808b86..f4edd6a4f5c 100644 --- a/extensions/r/package.json +++ b/extensions/r/package.json @@ -17,9 +17,9 @@ { "id": "r", "extensions": [ - ".r", - ".rhistory", - ".rprofile", + ".R", + ".Rhistory", + ".Rprofile", ".rt" ], "aliases": [ diff --git a/extensions/razor/cgmanifest.json b/extensions/razor/cgmanifest.json index 188b960c4ec..e28699e3bd4 100644 --- a/extensions/razor/cgmanifest.json +++ b/extensions/razor/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "dotnet/razor", "repositoryUrl": "https://github.com/dotnet/razor", - "commitHash": "9b1e979b6c3fe7cfbe30f595b9b0994d20bd482c" + "commitHash": "743f32a68c61809b22fd84e8748c3686ef1bb8b8" } }, "license": "MIT", diff --git a/extensions/razor/syntaxes/cshtml.tmLanguage.json b/extensions/razor/syntaxes/cshtml.tmLanguage.json index 286005474c5..f4f57003e31 100644 --- a/extensions/razor/syntaxes/cshtml.tmLanguage.json +++ b/extensions/razor/syntaxes/cshtml.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/dotnet/razor/commit/9b1e979b6c3fe7cfbe30f595b9b0994d20bd482c", + "version": "https://github.com/dotnet/razor/commit/743f32a68c61809b22fd84e8748c3686ef1bb8b8", "name": "ASP.NET Razor", "scopeName": "text.html.cshtml", "injections": { @@ -27,6 +27,13 @@ "include": "#implicit-expression" } ] + }, + "source.cs": { + "patterns": [ + { + "include": "#inline-template" + } + ] } }, "patterns": [ @@ -129,6 +136,9 @@ { "include": "#text-tag" }, + { + "include": "#inline-template" + }, { "include": "#wellformed-html" }, @@ -183,6 +193,111 @@ } } }, + "inline-template": { + "patterns": [ + { + "include": "#inline-template-void-tag" + }, + { + "include": "#inline-template-non-void-tag" + } + ] + }, + "inline-template-void-tag": { + "name": "meta.tag.structure.$4.void.html", + "begin": "(?i)(@)(<)(!)?(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)(?=\\s|/?>)", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "punctuation.definition.tag.begin.html" + }, + "3": { + "name": "constant.character.escape.razor.tagHelperOptOut" + }, + "4": { + "name": "entity.name.tag.html" + } + }, + "patterns": [ + { + "include": "#razor-control-structures" + }, + { + "include": "text.html.derivative" + } + ], + "end": "/?>", + "endCaptures": { + "0": { + "name": "punctuation.definition.tag.end.html" + } + } + }, + "inline-template-non-void-tag": { + "begin": "(@)(<)(!)?([^/\\s>]+)(?=\\s|/?>)", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#transition" + } + ] + }, + "2": { + "name": "punctuation.definition.tag.begin.html" + }, + "3": { + "name": "constant.character.escape.razor.tagHelperOptOut" + }, + "4": { + "name": "entity.name.tag.html" + } + }, + "end": "()|(/>)", + "endCaptures": { + "1": { + "name": "punctuation.definition.tag.begin.html" + }, + "2": { + "name": "entity.name.tag.html" + }, + "3": { + "name": "punctuation.definition.tag.end.html" + }, + "4": { + "name": "punctuation.definition.tag.end.html" + } + }, + "patterns": [ + { + "begin": "(?<=>)(?!$)", + "end": "(?=|=>|==|=~|!~|!=|;|$|\n if|else|elsif|then|do|end|unless|while|until|or|and\n )\n |\n $\n)", + "begin": "(?x)\n(?|=>|==|=~|!~|!=|;|$|\n if|else|elsif|then|do|end|unless|while|until|or|and\n )\n)", "captures": { "1": { "name": "string.regexp.interpolated.ruby" @@ -1581,13 +1581,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1))", "comment": "Heredoc with embedded HTML", "end": "(?!\\G)", "name": "meta.embedded.block.html", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HTML)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1618,13 +1618,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1))", "comment": "Heredoc with embedded HAML", "end": "(?!\\G)", "name": "meta.embedded.block.haml", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)HAML)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1655,13 +1655,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1))", "comment": "Heredoc with embedded XML", "end": "(?!\\G)", "name": "meta.embedded.block.xml", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)XML)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1692,13 +1692,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1))", "comment": "Heredoc with embedded SQL", "end": "(?!\\G)", "name": "meta.embedded.block.sql", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SQL)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1729,13 +1729,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1))", "comment": "Heredoc with embedded GraphQL", "end": "(?!\\G)", "name": "meta.embedded.block.graphql", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:GRAPHQL|GQL))\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1766,13 +1766,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1))", "comment": "Heredoc with embedded CSS", "end": "(?!\\G)", "name": "meta.embedded.block.css", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CSS)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1803,13 +1803,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1))", "comment": "Heredoc with embedded C++", "end": "(?!\\G)", "name": "meta.embedded.block.cpp", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)CPP)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1840,13 +1840,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)C)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)C)\\b\\1))", "comment": "Heredoc with embedded C", "end": "(?!\\G)", "name": "meta.embedded.block.c", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)C)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)C)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1877,13 +1877,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1))", "comment": "Heredoc with embedded Javascript", "end": "(?!\\G)", "name": "meta.embedded.block.js", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:JS|JAVASCRIPT))\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1914,13 +1914,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1))", "comment": "Heredoc with embedded jQuery Javascript", "end": "(?!\\G)", "name": "meta.embedded.block.js.jquery", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)JQUERY)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1951,13 +1951,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1))", "comment": "Heredoc with embedded Shell", "end": "(?!\\G)", "name": "meta.embedded.block.shell", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:SH|SHELL))\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -1988,13 +1988,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1))", "comment": "Heredoc with embedded Lua", "end": "(?!\\G)", "name": "meta.embedded.block.lua", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)LUA)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -2025,13 +2025,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1))", "comment": "Heredoc with embedded Ruby", "end": "(?!\\G)", "name": "meta.embedded.block.ruby", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)RUBY)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -2062,13 +2062,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1))", "comment": "Heredoc with embedded YAML", "end": "(?!\\G)", "name": "meta.embedded.block.yaml", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)(?:YAML|YML))\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -2099,13 +2099,13 @@ ] }, { - "begin": "(?=(?><<[-~]([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1))", + "begin": "(?=(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1))", "comment": "Heredoc with embedded Slim", "end": "(?!\\G)", "name": "meta.embedded.block.slim", "patterns": [ { - "begin": "(?><<[-~]([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1)", + "begin": "(?><<[-~]?([\"'`]?)((?:[_\\w]+_|)SLIM)\\b\\1)", "beginCaptures": { "0": { "name": "string.definition.begin.ruby" @@ -2162,7 +2162,7 @@ ] }, { - "begin": "(?>((<<[-~]([\"'`]?)(\\w+)\\3,\\s?)*<<[-~]([\"'`]?)(\\w+)\\5))(.*)", + "begin": "(?>((<<[-~]?([\"'`]?)(\\w+)\\3,\\s?)*<<[-~]?([\"'`]?)(\\w+)\\5))(.*)", "beginCaptures": { "1": { "name": "string.definition.begin.ruby" diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index 155ed6ae658..1119636313f 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -16,7 +16,7 @@ ], "scripts": { "generate-grammar": "node ./syntaxes/generateTMLanguage.js", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:search-result ./tsconfig.json" + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:search-result ./tsconfig.json" }, "capabilities": { "virtualWorkspaces": true, diff --git a/extensions/search-result/tsconfig.json b/extensions/search-result/tsconfig.json index 796a159a61c..e723410bedf 100644 --- a/extensions/search-result/tsconfig.json +++ b/extensions/search-result/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/shared.webpack.config.mjs b/extensions/shared.webpack.config.mjs index f54499dc227..12b1ea522a4 100644 --- a/extensions/shared.webpack.config.mjs +++ b/extensions/shared.webpack.config.mjs @@ -42,17 +42,21 @@ function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfi rules: [{ test: /\.ts$/, exclude: /node_modules/, - use: [{ - // configure TypeScript loader: - // * enable sources maps for end-to-end source maps - loader: 'ts-loader', - options: tsLoaderOptions - }, { - loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - options: { - configFile: path.join(extConfig.context, 'tsconfig.json') + use: [ + { + // configure TypeScript loader: + // * enable sources maps for end-to-end source maps + loader: 'ts-loader', + options: tsLoaderOptions }, - },] + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, 'tsconfig.json') + // }, + // }, + ] }] }, externals: { @@ -135,12 +139,13 @@ function withBrowserDefaults(/**@type WebpackConfig & { context: string }*/extCo // ...(additionalOptions ? {} : { configFile: additionalOptions.configFile }), } }, - { - loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), - options: { - configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') - }, - }, + // disable mangling for now, SEE https://github.com/microsoft/vscode/issues/204692 + // { + // loader: path.resolve(import.meta.dirname, 'mangle-loader.js'), + // options: { + // configFile: path.join(extConfig.context, additionalOptions?.configFile ?? 'tsconfig.json') + // }, + // }, ] }, { test: /\.wasm$/, diff --git a/extensions/simple-browser/package.json b/extensions/simple-browser/package.json index 6dd737b08f5..0d558eeebf6 100644 --- a/extensions/simple-browser/package.json +++ b/extensions/simple-browser/package.json @@ -51,6 +51,16 @@ "default": true, "title": "Focus Lock Indicator Enabled", "description": "%configuration.focusLockIndicator.enabled.description%" + }, + "simpleBrowser.useIntegratedBrowser": { + "type": "boolean", + "default": false, + "markdownDescription": "%configuration.useIntegratedBrowser.description%", + "scope": "application", + "tags": [ + "experimental", + "onExP" + ] } } } @@ -60,7 +70,7 @@ "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:simple-browser ./tsconfig.json", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs 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/simple-browser/package.nls.json b/extensions/simple-browser/package.nls.json index 496dc28dfdd..3b6b41530fa 100644 --- a/extensions/simple-browser/package.nls.json +++ b/extensions/simple-browser/package.nls.json @@ -1,5 +1,6 @@ { "displayName": "Simple Browser", "description": "A very basic built-in webview for displaying web content.", - "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser." + "configuration.focusLockIndicator.enabled.description": "Enable/disable the floating indicator that shows when focused in the simple browser.", + "configuration.useIntegratedBrowser.description": "When enabled, the `simpleBrowser.show` command will open URLs in the integrated browser instead of the Simple Browser webview. **Note:** This setting is experimental and only available on desktop." } diff --git a/extensions/simple-browser/preview-src/tsconfig.json b/extensions/simple-browser/preview-src/tsconfig.json index e8e5336a66b..714d398b880 100644 --- a/extensions/simple-browser/preview-src/tsconfig.json +++ b/extensions/simple-browser/preview-src/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": ".", "outDir": "./dist/", "jsx": "react", "lib": [ diff --git a/extensions/simple-browser/src/extension.ts b/extensions/simple-browser/src/extension.ts index 885afe28712..6eb0bb0837f 100644 --- a/extensions/simple-browser/src/extension.ts +++ b/extensions/simple-browser/src/extension.ts @@ -14,6 +14,8 @@ declare class URL { const openApiCommand = 'simpleBrowser.api.open'; const showCommand = 'simpleBrowser.show'; +const integratedBrowserCommand = 'workbench.action.browser.open'; +const useIntegratedBrowserSetting = 'simpleBrowser.useIntegratedBrowser'; const enabledHosts = new Set([ 'localhost', @@ -31,6 +33,27 @@ const enabledHosts = new Set([ const openerId = 'simpleBrowser.open'; +/** + * Checks if the integrated browser should be used instead of the simple browser + */ +async function shouldUseIntegratedBrowser(): Promise { + const config = vscode.workspace.getConfiguration(); + if (!config.get(useIntegratedBrowserSetting, false)) { + return false; + } + + // Verify that the integrated browser command is available + const commands = await vscode.commands.getCommands(true); + return commands.includes(integratedBrowserCommand); +} + +/** + * Opens a URL in the integrated browser + */ +async function openInIntegratedBrowser(url?: string): Promise { + await vscode.commands.executeCommand(integratedBrowserCommand, url); +} + export function activate(context: vscode.ExtensionContext) { const manager = new SimpleBrowserManager(context.extensionUri); @@ -43,6 +66,10 @@ export function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.commands.registerCommand(showCommand, async (url?: string) => { + if (await shouldUseIntegratedBrowser()) { + return openInIntegratedBrowser(url); + } + if (!url) { url = await vscode.window.showInputBox({ placeHolder: vscode.l10n.t("https://example.com"), @@ -55,11 +82,15 @@ export function activate(context: vscode.ExtensionContext) { } })); - context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, (url: vscode.Uri, showOptions?: { + context.subscriptions.push(vscode.commands.registerCommand(openApiCommand, async (url: vscode.Uri, showOptions?: { preserveFocus?: boolean; viewColumn: vscode.ViewColumn; }) => { - manager.show(url, showOptions); + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(url.toString(true)); + } else { + manager.show(url, showOptions); + } })); context.subscriptions.push(vscode.window.registerExternalUriOpener(openerId, { @@ -74,10 +105,14 @@ export function activate(context: vscode.ExtensionContext) { return vscode.ExternalUriOpenerPriority.None; }, - openExternalUri(resolveUri: vscode.Uri) { - return manager.show(resolveUri, { - viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active - }); + async openExternalUri(resolveUri: vscode.Uri) { + if (await shouldUseIntegratedBrowser()) { + await openInIntegratedBrowser(resolveUri.toString(true)); + } else { + return manager.show(resolveUri, { + viewColumn: vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active + }); + } } }, { schemes: ['http', 'https'], diff --git a/extensions/simple-browser/tsconfig.json b/extensions/simple-browser/tsconfig.json index 43ed762ce7d..8f53af26c09 100644 --- a/extensions/simple-browser/tsconfig.json +++ b/extensions/simple-browser/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/sql/build/update-grammar.mjs b/extensions/sql/build/update-grammar.mjs index 40e2102e1e4..d7c589727ce 100644 --- a/extensions/sql/build/update-grammar.mjs +++ b/extensions/sql/build/update-grammar.mjs @@ -5,4 +5,4 @@ import * as vscodeGrammarUpdater from 'vscode-grammar-updater'; -vscodeGrammarUpdater.update('microsoft/vscode-mssql', 'syntaxes/SQL.plist', './syntaxes/sql.tmLanguage.json', undefined, 'main'); +vscodeGrammarUpdater.update('microsoft/vscode-mssql', 'extensions/mssql/syntaxes/SQL.plist', './syntaxes/sql.tmLanguage.json', undefined, 'main'); diff --git a/extensions/sql/cgmanifest.json b/extensions/sql/cgmanifest.json index a702e17d932..c338d174d2a 100644 --- a/extensions/sql/cgmanifest.json +++ b/extensions/sql/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "microsoft/vscode-mssql", "repositoryUrl": "https://github.com/microsoft/vscode-mssql", - "commitHash": "ff0c7d3b3582100856fe5b839663b2a8704dc4e4" + "commitHash": "5fd71951c51e0a8385fb546ccebf0716a549fedf" } }, "license": "MIT", diff --git a/extensions/sql/syntaxes/sql.tmLanguage.json b/extensions/sql/syntaxes/sql.tmLanguage.json index d0eefc0c22b..4be5b47334c 100644 --- a/extensions/sql/syntaxes/sql.tmLanguage.json +++ b/extensions/sql/syntaxes/sql.tmLanguage.json @@ -1,10 +1,10 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/microsoft/vscode-mssql/blob/master/syntaxes/SQL.plist", + "This file has been converted from https://github.com/microsoft/vscode-mssql/blob/master/extensions/mssql/syntaxes/SQL.plist", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/microsoft/vscode-mssql/commit/ff0c7d3b3582100856fe5b839663b2a8704dc4e4", + "version": "https://github.com/microsoft/vscode-mssql/commit/5fd71951c51e0a8385fb546ccebf0716a549fedf", "name": "SQL", "scopeName": "source.sql", "patterns": [ diff --git a/extensions/terminal-suggest/package.json b/extensions/terminal-suggest/package.json index 66e99b20e11..734e3e91c82 100644 --- a/extensions/terminal-suggest/package.json +++ b/extensions/terminal-suggest/package.json @@ -36,8 +36,8 @@ "scripts": { "compile": "npx gulp compile-extension:terminal-suggest", "watch": "npx gulp watch-extension:terminal-suggest", - "pull-zshbuiltins": "ts-node ./scripts/pullZshBuiltins.ts", - "pull-fishbuiltins": "ts-node ./scripts/pullFishBuiltins.ts" + "pull-zshbuiltins": "node ./scripts/pullZshBuiltins.ts", + "pull-fishbuiltins": "node ./scripts/pullFishBuiltins.ts" }, "main": "./out/terminalSuggestMain", "activationEvents": [ diff --git a/extensions/terminal-suggest/src/completions/azd.ts b/extensions/terminal-suggest/src/completions/azd.ts index 1b5609b0433..2a4adb84d62 100644 --- a/extensions/terminal-suggest/src/completions/azd.ts +++ b/extensions/terminal-suggest/src/completions/azd.ts @@ -191,6 +191,16 @@ const completionSpec: Fig.Spec = { name: ['add'], description: 'Add a component to your project.', }, + { + name: ['ai'], + description: 'Extension for the Foundry Agent Service. (Preview)', + subcommands: [ + { + name: ['agent'], + description: 'Extension for the Foundry Agent Service. (Preview)', + }, + ], + }, { name: ['auth'], description: 'Authenticate with Azure.', @@ -274,6 +284,10 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['coding-agent'], + description: 'This extension configures GitHub Copilot Coding Agent access to Azure', + }, { name: ['completion'], description: 'Generate shell completion scripts.', @@ -351,6 +365,10 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['demo'], + description: 'This extension provides examples of the AZD extension framework.', + }, { name: ['deploy'], description: 'Deploy your project code to Azure.', @@ -407,6 +425,10 @@ const completionSpec: Fig.Spec = { isDangerous: true, }, ], + args: { + name: 'layer', + isOptional: true, + }, }, { name: ['env'], @@ -499,6 +521,15 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['--layer'], + description: 'Provisioning layer to refresh the environment from.', + args: [ + { + name: 'layer', + }, + ], + }, ], args: { name: 'environment', @@ -647,7 +678,8 @@ const completionSpec: Fig.Spec = { }, ], args: { - name: 'extension-name', + name: 'extension-id', + generators: azdGenerators.listExtensions, }, }, { @@ -1273,6 +1305,10 @@ const completionSpec: Fig.Spec = { description: 'Preview changes to Azure resources.', }, ], + args: { + name: 'layer', + isOptional: true, + }, }, { name: ['publish'], @@ -1474,6 +1510,10 @@ const completionSpec: Fig.Spec = { name: ['version'], description: 'Print the version number of Azure Developer CLI.', }, + { + name: ['x'], + description: 'This extension provides a set of tools for AZD extension developers to test and debug their extensions.', + }, { name: ['help'], description: 'Help about any command', @@ -1482,6 +1522,16 @@ const completionSpec: Fig.Spec = { name: ['add'], description: 'Add a component to your project.', }, + { + name: ['ai'], + description: 'Extension for the Foundry Agent Service. (Preview)', + subcommands: [ + { + name: ['agent'], + description: 'Extension for the Foundry Agent Service. (Preview)', + }, + ], + }, { name: ['auth'], description: 'Authenticate with Azure.', @@ -1496,6 +1546,10 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['coding-agent'], + description: 'This extension configures GitHub Copilot Coding Agent access to Azure', + }, { name: ['completion'], description: 'Generate shell completion scripts.', @@ -1552,6 +1606,10 @@ const completionSpec: Fig.Spec = { }, ], }, + { + name: ['demo'], + description: 'This extension provides examples of the AZD extension framework.', + }, { name: ['deploy'], description: 'Deploy your project code to Azure.', @@ -1768,6 +1826,10 @@ const completionSpec: Fig.Spec = { name: ['version'], description: 'Print the version number of Azure Developer CLI.', }, + { + name: ['x'], + description: 'This extension provides a set of tools for AZD extension developers to test and debug their extensions.', + }, ], }, ], diff --git a/extensions/terminal-suggest/src/completions/git.ts b/extensions/terminal-suggest/src/completions/git.ts index 21beac98c0c..68cc6fda2d7 100644 --- a/extensions/terminal-suggest/src/completions/git.ts +++ b/extensions/terminal-suggest/src/completions/git.ts @@ -99,6 +99,7 @@ const postProcessBranches = } let description = "Branch"; + if (insertWithoutRemotes && name.startsWith("remotes/")) { name = name.slice(name.indexOf("/", 8) + 1); description = "Remote branch"; @@ -287,7 +288,7 @@ export const gitGenerators = { "refs/remotes/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), - } satisfies Fig.Generator, + }, localBranches: { script: [ @@ -295,26 +296,44 @@ export const gitGenerators = { "refs/heads/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), - } satisfies Fig.Generator, + }, // custom generator to display local branches by default or // remote branches if '-r' flag is used. See branch -d for use localOrRemoteBranches: { custom: async (tokens, executeShellCommand) => { const pp = postProcessBranches({ insertWithoutRemotes: true }); - const refs = tokens.includes("-r") ? "refs/remotes/" : "refs/heads/"; - return pp?.( - ( - await executeShellCommand({ - command: gitBranchForEachRefArgs[0], - args: [ - ...gitBranchForEachRefArgs.slice(1), - refs, - ], - }) - ).stdout, - tokens - ); + if (tokens.includes("-r")) { + return pp?.( + ( + await executeShellCommand({ + command: "git", + args: [ + "--no-optional-locks", + "-r", + "--no-color", + "--sort=-committerdate", + ], + }) + ).stdout, + tokens + ); + } else { + return pp?.( + ( + await executeShellCommand({ + command: "git", + args: [ + "--no-optional-locks", + "branch", + "--no-color", + "--sort=-committerdate", + ], + }) + ).stdout, + tokens + ); + } }, } satisfies Fig.Generator, @@ -6337,6 +6356,504 @@ const completionSpec: Fig.Spec = { name: "pattern", }, }, + { + name: "--committer", + description: "Search for commits by a particular committer", + requiresSeparator: true, + args: { + name: "pattern", + }, + }, + { + name: "--graph", + description: "Draw a text-based graphical representation of the commit history", + }, + { + name: "--all", + description: "Show all branches", + }, + { + name: "--decorate", + description: "Show ref names of commits", + }, + { + name: "--no-decorate", + description: "Do not show ref names of commits", + }, + { + name: "--abbrev-commit", + description: "Show only the first few characters of the SHA-1 checksum", + }, + { + name: ["-n", "--max-count"], + description: "Limit the number of commits to output", + requiresSeparator: true, + args: { + name: "number", + }, + }, + { + name: "--since", + description: "Show commits more recent than a specific date", + requiresSeparator: true, + args: { + name: "date", + }, + }, + { + name: "--after", + description: "Show commits more recent than a specific date", + requiresSeparator: true, + args: { + name: "date", + }, + }, + { + name: "--until", + description: "Show commits older than a specific date", + requiresSeparator: true, + args: { + name: "date", + }, + }, + { + name: "--before", + description: "Show commits older than a specific date", + requiresSeparator: true, + args: { + name: "date", + }, + }, + { + name: "--merges", + description: "Show only merge commits", + }, + { + name: "--no-merges", + description: "Do not show merge commits", + }, + { + name: "--first-parent", + description: "Follow only the first parent commit upon seeing a merge commit", + }, + { + name: "--reverse", + description: "Output the commits in reverse order", + }, + { + name: "--relative-date", + description: "Show dates relative to the current time", + }, + { + name: "--date", + description: "Format dates (iso, rfc, short, relative, local, etc.)", + requiresSeparator: true, + args: { + name: "format", + suggestions: [ + { name: "relative", description: "Relative to current time" }, + { name: "local", description: "Local timezone" }, + { name: "iso", description: "ISO 8601 format" }, + { name: "iso8601", description: "ISO 8601 format" }, + { name: "iso-strict", description: "Strict ISO 8601 format" }, + { name: "rfc", description: "RFC 2822 format" }, + { name: "rfc2822", description: "RFC 2822 format" }, + { name: "short", description: "YYYY-MM-DD format" }, + { name: "raw", description: "Seconds since epoch + timezone" }, + { name: "human", description: "Human-readable format" }, + { name: "unix", description: "Unix timestamp (seconds since epoch)" }, + { name: "default", description: "Default ctime-like format" }, + { name: "format:", description: "Custom strftime format" }, + ], + }, + }, + { + name: "--pretty", + description: "Pretty-print the contents of the commit logs", + requiresSeparator: true, + args: { + name: "format", + suggestions: [ + { name: "oneline", description: "Show each commit as a single line" }, + { name: "short", description: "Show commit and author" }, + { name: "medium", description: "Show commit, author, and date (default)" }, + { name: "full", description: "Show commit, author, and committer" }, + { name: "fuller", description: "Show commit, author, committer, and dates" }, + { name: "reference", description: "Abbreviated hash with title and date" }, + { name: "email", description: "Format as email with headers" }, + { name: "mboxrd", description: "Email format with quoted From lines" }, + { name: "raw", description: "Show raw commit object" }, + { name: "format:", description: "Custom format string with placeholders" }, + { name: "tformat:", description: "Custom format with terminator semantics" }, + ], + }, + }, + { + name: "--format", + description: "Pretty-print the contents of the commit logs in a given format", + requiresSeparator: true, + args: { + name: "format", + }, + }, + { + name: "--name-only", + description: "Show only names of changed files", + }, + { + name: "--name-status", + description: "Show names and status of changed files", + }, + { + name: "--shortstat", + description: "Output only the last line of the --stat format", + }, + { + name: "-S", + description: "Look for differences that change the number of occurrences of the specified string", + requiresSeparator: true, + args: { + name: "string", + }, + }, + { + name: "-G", + description: "Look for differences whose patch text contains added/removed lines that match ", + requiresSeparator: true, + args: { + name: "regex", + }, + }, + { + name: "--no-walk", + description: "Only display the given commits, but do not traverse their ancestors", + }, + { + name: "--cherry-pick", + description: "Omit any commit that introduces the same change as another commit", + }, + { + name: ["-i", "--regexp-ignore-case"], + description: "Match patterns case-insensitively", + }, + { + name: ["-E", "--extended-regexp"], + description: "Use extended regular expressions for patterns", + }, + { + name: ["-F", "--fixed-strings"], + description: "Use fixed string matching instead of patterns", + }, + { + name: ["-P", "--perl-regexp"], + description: "Use Perl-compatible regular expressions", + }, + { + name: "--all-match", + description: "Match all --grep patterns instead of any", + }, + { + name: "--invert-grep", + description: "Show commits that don't match the --grep pattern", + }, + { + name: "--skip", + description: "Skip a number of commits before starting to show output", + requiresSeparator: true, + args: { + name: "number", + }, + }, + { + name: "--min-parents", + description: "Show only commits with at least this many parents", + requiresSeparator: true, + args: { + name: "number", + }, + }, + { + name: "--max-parents", + description: "Show only commits with at most this many parents", + requiresSeparator: true, + args: { + name: "number", + }, + }, + { + name: "--branches", + description: "Show commits from all branches", + args: { + name: "pattern", + isOptional: true, + }, + }, + { + name: "--tags", + description: "Show commits from all tags", + args: { + name: "pattern", + isOptional: true, + }, + }, + { + name: "--remotes", + description: "Show commits from all remote-tracking branches", + args: { + name: "pattern", + isOptional: true, + }, + }, + { + name: "--glob", + description: "Show commits from refs matching the given shell glob pattern", + requiresSeparator: true, + args: { + name: "pattern", + }, + }, + { + name: "--exclude", + description: "Exclude refs matching the given shell glob pattern", + requiresSeparator: true, + args: { + name: "pattern", + }, + }, + { + name: ["-g", "--walk-reflogs"], + description: "Walk reflog entries from most recent to oldest", + }, + { + name: "--boundary", + description: "Output excluded boundary commits", + }, + { + name: "--date-order", + description: "Show commits in date order", + }, + { + name: "--author-date-order", + description: "Show commits in author date order", + }, + { + name: "--topo-order", + description: "Show commits in topological order", + }, + { + name: "--parents", + description: "Print parent commit hashes", + }, + { + name: "--children", + description: "Print child commit hashes", + }, + { + name: "--left-right", + description: "Mark commits with < or > for left or right side of symmetric difference", + }, + { + name: "--cherry-mark", + description: "Mark equivalent commits with = and others with +", + }, + { + name: "--left-only", + description: "Show only commits on the left side of a symmetric difference", + }, + { + name: "--right-only", + description: "Show only commits on the right side of a symmetric difference", + }, + { + name: "--cherry", + description: "Synonym for --right-only --cherry-mark --no-merges", + }, + { + name: "--full-history", + description: "Show full commit history without simplification", + }, + { + name: "--simplify-merges", + description: "Remove unnecessary merges from history", + }, + { + name: "--ancestry-path", + description: "Only display commits between the specified range that are ancestors of the end commit", + }, + { + name: "--numstat", + description: "Show number of added and deleted lines in decimal notation", + }, + { + name: "--no-patch", + description: "Suppress diff output", + }, + { + name: "--raw", + description: "Show output in raw format", + }, + { + name: "-m", + description: "Show diffs for merge commits", + }, + { + name: "-c", + description: "Show combined diff format for merge commits", + }, + { + name: "--cc", + description: "Show condensed combined diff format for merge commits", + }, + { + name: "--notes", + description: "Show notes attached to commits", + args: { + name: "ref", + isOptional: true, + }, + }, + { + name: "--no-notes", + description: "Do not show notes", + }, + { + name: "--show-notes", + description: "Show notes (default when showing commit messages)", + }, + { + name: "-L", + description: "Trace the evolution of a line range or function", + requiresSeparator: true, + args: { + name: "range:file", + }, + }, + { + name: "--no-abbrev-commit", + description: "Show full 40-byte hexadecimal commit object names", + }, + { + name: "--encoding", + description: "Re-encode commit messages in the specified character encoding", + requiresSeparator: true, + args: { + name: "encoding", + }, + }, + { + name: "--no-commit-id", + description: "Suppress commit IDs in output", + }, + { + name: "--diff-filter", + description: "Select only files that are Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), etc.", + requiresSeparator: true, + args: { + name: "filter", + suggestions: [ + { name: "A", description: "Added files" }, + { name: "C", description: "Copied files" }, + { name: "D", description: "Deleted files" }, + { name: "M", description: "Modified files" }, + { name: "R", description: "Renamed files" }, + { name: "T", description: "Type changed files" }, + { name: "U", description: "Unmerged files" }, + { name: "X", description: "Unknown files" }, + { name: "B", description: "Broken files" }, + ], + }, + }, + { + name: "--full-diff", + description: "Show full diff, not just for specified paths", + }, + { + name: "--log-size", + description: "Include log size information", + }, + { + name: ["-U", "--unified"], + description: "Generate diffs with lines of context", + requiresSeparator: true, + args: { + name: "lines", + }, + }, + { + name: "--summary", + description: "Show a diffstat summary of created, renamed, and mode changes", + }, + { + name: "--patch-with-stat", + description: "Synonym for -p --stat", + }, + { + name: "--ignore-space-change", + description: "Ignore changes in whitespace", + }, + { + name: "--ignore-all-space", + description: "Ignore all whitespace when comparing lines", + }, + { + name: "--ignore-blank-lines", + description: "Ignore changes whose lines are all blank", + }, + { + name: "--function-context", + description: "Show whole function as context", + }, + { + name: "--ext-diff", + description: "Allow external diff helper to be executed", + }, + { + name: "--no-ext-diff", + description: "Disallow external diff helper", + }, + { + name: "--textconv", + description: "Allow external text conversion filters for binary files", + }, + { + name: "--no-textconv", + description: "Disallow external text conversion filters", + }, + { + name: "--color", + description: "Show colored diff", + args: { + name: "when", + isOptional: true, + suggestions: [ + { name: "always", description: "Always use colors" }, + { name: "never", description: "Never use colors" }, + { name: "auto", description: "Use colors when output is to a terminal" }, + ], + }, + }, + { + name: "--no-color", + description: "Turn off colored diff", + }, + { + name: "--word-diff", + description: "Show word diff", + args: { + name: "mode", + isOptional: true, + suggestions: [ + { name: "color", description: "Highlight changed words using colors" }, + { name: "plain", description: "Show words with [-removed-] and {+added+}" }, + { name: "porcelain", description: "Use special line-based format" }, + { name: "none", description: "Disable word diff" }, + ], + }, + }, + { + name: "--color-words", + description: "Equivalent to --word-diff=color", + }, ], args: [ { diff --git a/extensions/terminal-suggest/src/completions/npm.ts b/extensions/terminal-suggest/src/completions/npm.ts new file mode 100644 index 00000000000..f031d467aff --- /dev/null +++ b/extensions/terminal-suggest/src/completions/npm.ts @@ -0,0 +1,1625 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +function uninstallSubcommand(named: string | string[]): Fig.Subcommand { + return { + name: named, + description: 'Uninstall a package', + args: { + name: 'package', + generators: dependenciesGenerator, + filterStrategy: 'fuzzy', + isVariadic: true, + }, + options: npmUninstallOptions, + }; +} + +const atsInStr = (s: string) => (s.match(/@/g) || []).length; + +export const createNpmSearchHandler = + (keywords?: string[]) => + async ( + context: string[], + executeShellCommand: Fig.ExecuteCommandFunction, + shellContext: Fig.ShellContext + ): Promise => { + const searchTerm = context[context.length - 1]; + if (searchTerm === '') { + return []; + } + // Add optional keyword parameter + const keywordParameter = + keywords && keywords.length > 0 ? `+keywords:${keywords.join(',')}` : ''; + + const queryPackagesUrl = keywordParameter + ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` + : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; + + // Query the API with the package name + const queryPackages = [ + '-s', + '-H', + 'Accept: application/json', + queryPackagesUrl, + ]; + // We need to remove the '@' at the end of the searchTerm before querying versions + const queryVersions = [ + '-s', + '-H', + 'Accept: application/vnd.npm.install-v1+json', + `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, + ]; + // If the end of our token is '@', then we want to generate version suggestions + // Otherwise, we want packages + const out = (query: string) => + executeShellCommand({ + command: 'curl', + args: query[query.length - 1] === '@' ? queryVersions : queryPackages, + }); + // If our token starts with '@', then a 2nd '@' tells us we want + // versions. + // Otherwise, '@' anywhere else in the string will indicate the same. + const shouldGetVersion = searchTerm.startsWith('@') + ? atsInStr(searchTerm) > 1 + : searchTerm.includes('@'); + + try { + const data = JSON.parse((await out(searchTerm)).stdout); + if (shouldGetVersion) { + // create dist tags suggestions + const versions = Object.entries(data['dist-tags'] || {}).map( + ([key, value]) => ({ + name: key, + description: value, + }) + ) as Fig.Suggestion[]; + // create versions + versions.push( + ...Object.keys(data.versions) + .map((version) => ({ name: version })) + .reverse() + ); + return versions; + } + + const results = keywordParameter ? data.results : data; + return results.map( + (item: { package: { name: string; description: string } }) => ({ + name: item.package.name, + description: item.package.description, + }) + ) as Fig.Suggestion[]; + } catch (error) { + console.error({ error }); + return []; + } + }; + +// GENERATORS +export const npmSearchGenerator: Fig.Generator = { + trigger: (newToken, oldToken) => { + // If the package name starts with '@', we want to trigger when + // the 2nd '@' is typed because we'll need to generate version + // suggetsions + // e.g. @typescript-eslint/types + if (oldToken.startsWith('@')) { + return !(atsInStr(oldToken) > 1 && atsInStr(newToken) > 1); + } + + // If the package name doesn't start with '@', then trigger when + // we see the first '@' so we can generate version suggestions + return !(oldToken.includes('@') && newToken.includes('@')); + }, + getQueryTerm: '@', + cache: { + ttl: 1000 * 60 * 60 * 24 * 2, // 2 days + }, + custom: createNpmSearchHandler(), +}; + +const workspaceGenerator: Fig.Generator = { + // script: "cat $(npm prefix)/package.json", + custom: async (tokens, executeShellCommand) => { + const { stdout: npmPrefix } = await executeShellCommand({ + command: 'npm', + args: ['prefix'], + }); + + const { stdout: out } = await executeShellCommand({ + command: 'cat', + args: [`${npmPrefix}/package.json`], + }); + + const suggestions: Fig.Suggestion[] = []; + try { + if (out.trim() === '') { + return suggestions; + } + + const packageContent = JSON.parse(out); + const workspaces = packageContent['workspaces']; + + if (workspaces) { + for (const workspace of workspaces) { + suggestions.push({ + name: workspace, + description: 'Workspaces', + }); + } + } + } catch (e) { + console.log(e); + } + return suggestions; + }, +}; + +/** Generator that lists package.json dependencies */ +export const dependenciesGenerator: Fig.Generator = { + trigger: (newToken) => newToken === '-g' || newToken === '--global', + custom: async function (tokens, executeShellCommand) { + if (!tokens.includes('-g') && !tokens.includes('--global')) { + const { stdout: npmPrefix } = await executeShellCommand({ + command: 'npm', + args: ['prefix'], + }); + const { stdout: out } = await executeShellCommand({ + command: 'cat', + args: [`${npmPrefix}/package.json`], + }); + const packageContent = JSON.parse(out); + const dependencies = packageContent['dependencies'] ?? {}; + const devDependencies = packageContent['devDependencies']; + const optionalDependencies = packageContent['optionalDependencies'] ?? {}; + Object.assign(dependencies, devDependencies, optionalDependencies); + + return Object.keys(dependencies) + .filter((pkgName) => { + const isListed = tokens.some((current) => current === pkgName); + return !isListed; + }) + .map((pkgName) => ({ + name: pkgName, + description: dependencies[pkgName] + ? 'dependency' + : optionalDependencies[pkgName] + ? 'optionalDependency' + : 'devDependency', + })); + } else { + const { stdout } = await executeShellCommand({ + command: 'bash', + args: ['-c', 'ls -1 `npm root -g`'], + }); + return stdout.split('\n').map((name) => ({ + name, + description: 'Global dependency', + })); + } + }, +}; + +/** Generator that lists package.json scripts (with the respect to the `fig` field) */ +export const npmScriptsGenerator: Fig.Generator = { + cache: { + strategy: 'stale-while-revalidate', + cacheByDirectory: true, + }, + script: [ + 'bash', + '-c', + 'until [[ -f package.json ]] || [[ $PWD = \' / \' ]]; do cd ..; done; cat package.json', + ], + postProcess: function (out, [npmClient]) { + if (out.trim() === '') { + return []; + } + + try { + const packageContent = JSON.parse(out); + const scripts = packageContent['scripts']; + const figCompletions = packageContent['fig'] || {}; + + if (scripts) { + return Object.entries(scripts).map(([scriptName, scriptContents]) => { + const icon = + npmClient === 'yarn' + ? 'fig://icon?type=yarn' + : 'fig://icon?type=npm'; + const customScripts: Fig.Suggestion = figCompletions[scriptName]; + return { + name: scriptName, + icon, + description: scriptContents as string, + priority: 51, + /** + * If there are custom definitions for the scripts + * we want to override the default values + * */ + ...customScripts, + }; + }); + } + } catch (e) { + console.error(e); + } + + return []; + }, +}; + +const globalOption: Fig.Option = { + name: ['-g', '--global'], + description: + 'Operates in \'global\' mode, so that packages are installed into the prefix folder instead of the current working directory', +}; + +const jsonOption: Fig.Option = { + name: '--json', + description: 'Show output in json format', +}; + +const omitOption: Fig.Option = { + name: '--omit', + description: 'Dependency types to omit from the installation tree on disk', + args: { + name: 'Package type', + default: 'dev', + suggestions: ['dev', 'optional', 'peer'], + }, + isRepeatable: 3, +}; + +const parseableOption: Fig.Option = { + name: ['-p', '--parseable'], + description: + 'Output parseable results from commands that write to standard output', +}; + +const longOption: Fig.Option = { + name: ['-l', '--long'], + description: 'Show extended information', +}; + +const workSpaceOptions: Fig.Option[] = [ + { + name: ['-w', '--workspace'], + description: + 'Enable running a command in the context of the configured workspaces of the current project', + args: { + name: 'workspace', + generators: workspaceGenerator, + isVariadic: true, + }, + }, + { + name: ['-ws', '--workspaces'], + description: + 'Enable running a command in the context of all the configured workspaces', + }, +]; + +const npmUninstallOptions: Fig.Option[] = [ + { + name: ['-S', '--save'], + description: 'Package will be removed from your dependencies', + }, + { + name: ['-D', '--save-dev'], + description: 'Package will appear in your `devDependencies`', + }, + { + name: ['-O', '--save-optional'], + description: 'Package will appear in your `optionalDependencies`', + }, + { + name: '--no-save', + description: 'Prevents saving to `dependencies`', + }, + { + name: '-g', + description: 'Uninstall global package', + }, + ...workSpaceOptions, +]; + +const npmListOptions: Fig.Option[] = [ + { + name: ['-a', '-all'], + description: 'Show all outdated or installed packages', + }, + jsonOption, + longOption, + parseableOption, + { + name: '--depth', + description: 'The depth to go when recursing packages', + args: { name: 'depth' }, + }, + { + name: '--link', + description: 'Limits output to only those packages that are linked', + }, + { + name: '--package-lock-only', + description: + 'Current operation will only use the package-lock.json, ignoring node_modules', + }, + { + name: '--no-unicode', + description: 'Uses unicode characters in the tree output', + }, + globalOption, + omitOption, + ...workSpaceOptions, +]; + +const registryOption: Fig.Option = { + name: '--registry', + description: 'The base URL of the npm registry', + args: { name: 'registry' }, +}; + +const verboseOption: Fig.Option = { + name: '--verbose', + description: 'Show extra information', + args: { name: 'verbose' }, +}; + +const otpOption: Fig.Option = { + name: '--otp', + description: 'One-time password from a two-factor authenticator', + args: { name: 'otp' }, +}; + +const ignoreScriptsOption: Fig.Option = { + name: '--ignore-scripts', + description: + 'If true, npm does not run scripts specified in package.json files', +}; + +const scriptShellOption: Fig.Option = { + name: '--script-shell', + description: + 'The shell to use for scripts run with the npm exec, npm run and npm init commands', + args: { name: 'script-shell' }, +}; + +const dryRunOption: Fig.Option = { + name: '--dry-run', + description: + 'Indicates that you don\'t want npm to make any changes and that it should only report what it would have done', +}; + +const completionSpec: Fig.Spec = { + name: 'npm', + parserDirectives: { + flagsArePosixNoncompliant: true, + }, + description: 'Node package manager', + subcommands: [ + { + name: ['install', 'i', 'add'], + description: 'Install a package and its dependencies', + args: { + name: 'package', + isOptional: true, + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [ + { + name: ['-P', '--save-prod'], + description: + 'Package will appear in your `dependencies`. This is the default unless `-D` or `-O` are present', + }, + { + name: ['-D', '--save-dev'], + description: 'Package will appear in your `devDependencies`', + }, + { + name: ['-O', '--save-optional'], + description: 'Package will appear in your `optionalDependencies`', + }, + { + name: '--no-save', + description: 'Prevents saving to `dependencies`', + }, + { + name: ['-E', '--save-exact'], + description: + 'Saved dependencies will be configured with an exact version rather than using npm\'s default semver range operator', + }, + { + name: ['-B', '--save-bundle'], + description: + 'Saved dependencies will also be added to your bundleDependencies list', + }, + globalOption, + { + name: '--global-style', + description: + 'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder', + }, + { + name: '--legacy-bundling', + description: + 'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package', + }, + { + name: '--legacy-peer-deps', + description: + 'Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6', + }, + { + name: '--package-lock-only', + description: 'Only update the `package-lock.json`, instead of checking `node_modules` and downloading dependencies.', + }, + { + name: '--strict-peer-deps', + description: + 'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure', + }, + { + name: '--no-package-lock', + description: 'Ignores package-lock.json files when installing', + }, + registryOption, + verboseOption, + omitOption, + ignoreScriptsOption, + { + name: '--no-audit', + description: + 'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', + }, + { + name: '--no-bin-links', + description: + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', + }, + { + name: '--no-fund', + description: + 'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding', + }, + dryRunOption, + ...workSpaceOptions, + ], + }, + { + name: ['run', 'run-script'], + description: 'Run arbitrary package scripts', + options: [ + ...workSpaceOptions, + { + name: '--if-present', + description: + 'Npm will not exit with an error code when run-script is invoked for a script that isn\'t defined in the scripts section of package.json', + }, + { + name: '--silent', + description: '', + }, + ignoreScriptsOption, + scriptShellOption, + { + name: '--', + args: { + name: 'args', + isVariadic: true, + // TODO: load the spec based on the runned script (see yarn spec `yarnScriptParsedDirectives`) + }, + }, + ], + args: { + name: 'script', + description: 'Script to run from your package.json', + filterStrategy: 'fuzzy', + generators: npmScriptsGenerator, + }, + }, + { + name: 'init', + description: 'Trigger the initialization', + options: [ + { + name: ['-y', '--yes'], + description: + 'Automatically answer \'yes\' to any prompts that npm might print on the command line', + }, + { + name: '-w', + description: + 'Create the folders and boilerplate expected while also adding a reference to your project workspaces property', + args: { name: 'dir' }, + }, + ], + }, + { name: 'access', description: 'Set access controls on private packages' }, + { + name: ['adduser', 'login'], + description: 'Add a registry user account', + options: [ + registryOption, + { + name: '--scope', + description: + 'Associate an operation with a scope for a scoped registry', + args: { + name: 'scope', + description: 'Scope name', + }, + }, + ], + }, + { + name: 'audit', + description: 'Run a security audit', + subcommands: [ + { + name: 'fix', + description: + 'If the fix argument is provided, then remediations will be applied to the package tree', + options: [ + dryRunOption, + { + name: ['-f', '--force'], + description: + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input', + isDangerous: true, + }, + ...workSpaceOptions, + ], + }, + ], + options: [ + ...workSpaceOptions, + { + name: '--audit-level', + description: + 'The minimum level of vulnerability for npm audit to exit with a non-zero exit code', + args: { + name: 'audit', + suggestions: [ + 'info', + 'low', + 'moderate', + 'high', + 'critical', + 'none', + ], + }, + }, + { + name: '--package-lock-only', + description: + 'Current operation will only use the package-lock.json, ignoring node_modules', + }, + jsonOption, + omitOption, + ], + }, + { + name: 'bin', + description: 'Print the folder where npm will install executables', + options: [globalOption], + }, + { + name: ['bugs', 'issues'], + description: 'Report bugs for a package in a web browser', + args: { + name: 'package', + isOptional: true, + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [ + { + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], + }, + { + name: '--browser', + description: + 'The browser that is called by the npm bugs command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], + }, + registryOption, + ], + }, + { + name: 'cache', + description: 'Manipulates packages cache', + subcommands: [ + { + name: 'add', + description: 'Add the specified packages to the local cache', + }, + { + name: 'clean', + description: 'Delete all data out of the cache folder', + }, + { + name: 'verify', + description: + 'Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data', + }, + ], + options: [ + { + name: '--cache', + args: { name: 'cache' }, + description: 'The location of npm\'s cache directory', + }, + ], + }, + { + name: ['ci', 'clean-install', 'install-clean'], + description: 'Install a project with a clean slate', + options: [ + { + name: '--audit', + description: + 'When \'true\' submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', + args: { + name: 'audit', + suggestions: ['true', 'false'], + }, + exclusiveOn: ['--no-audit'], + }, + { + name: '--no-audit', + description: + 'Do not submit audit reports alongside the current npm command', + exclusiveOn: ['--audit'], + }, + ignoreScriptsOption, + scriptShellOption, + verboseOption, + registryOption, + ], + }, + { + name: 'cit', + description: 'Install a project with a clean slate and run tests', + }, + { + name: 'clean-install-test', + description: 'Install a project with a clean slate and run tests', + }, + { name: 'completion', description: 'Tab completion for npm' }, + { + name: ['config', 'c'], + description: 'Manage the npm configuration files', + subcommands: [ + { + name: 'set', + description: 'Sets the config key to the value', + args: [{ name: 'key' }, { name: 'value' }], + options: [ + { name: ['-g', '--global'], description: 'Sets it globally' }, + ], + }, + { + name: 'get', + description: 'Echo the config value to stdout', + args: { name: 'key' }, + }, + { + name: 'list', + description: 'Show all the config settings', + options: [ + { name: '-g', description: 'Lists globally installed packages' }, + { name: '-l', description: 'Also shows defaults' }, + jsonOption, + ], + }, + { + name: 'delete', + description: 'Deletes the key from all configuration files', + args: { name: 'key' }, + }, + { + name: 'edit', + description: 'Opens the config file in an editor', + options: [ + { name: '--global', description: 'Edits the global config' }, + ], + }, + ], + }, + { name: 'create', description: 'Create a package.json file' }, + { + name: ['dedupe', 'ddp'], + description: 'Reduce duplication in the package tree', + }, + { + name: 'deprecate', + description: 'Deprecate a version of a package', + options: [registryOption], + }, + { name: 'dist-tag', description: 'Modify package distribution tags' }, + { + name: ['docs', 'home'], + description: 'Open documentation for a package in a web browser', + args: { + name: 'package', + isOptional: true, + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [ + ...workSpaceOptions, + registryOption, + { + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], + }, + { + name: '--browser', + description: + 'The browser that is called by the npm docs command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], + }, + ], + }, + { + name: 'doctor', + description: 'Check your npm environment', + options: [registryOption], + }, + { + name: 'edit', + description: 'Edit an installed package', + options: [ + { + name: '--editor', + description: 'The command to run for npm edit or npm config edit', + }, + ], + }, + { + name: ['explain', 'why'], + description: 'Explain installed packages', + args: { + name: 'package-spec', + description: 'Package name or path to folder within node_modules', + isVariadic: true, + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + }, + options: [jsonOption, ...workSpaceOptions], + }, + { + name: 'explore', + description: 'Browse an installed package', + args: { + name: 'package', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + }, + }, + { name: 'fund', description: 'Retrieve funding information' }, + { name: 'get', description: 'Echo the config value to stdout' }, + { + name: 'help', + description: 'Get help on npm', + args: { + name: 'term', + isVariadic: true, + description: 'Terms to search for', + }, + options: [ + { + name: '--viewer', + description: 'The program to use to view help content', + args: { + name: 'viewer', + }, + }, + ], + }, + { + name: 'help-search', + description: 'Search npm help documentation', + args: { + name: 'text', + description: 'Text to search for', + }, + options: [longOption], + }, + { name: 'hook', description: 'Manage registry hooks' }, + { + name: 'install-ci-test', + description: 'Install a project with a clean slate and run tests', + }, + { name: 'install-test', description: 'Install package(s) and run tests' }, + { name: 'it', description: 'Install package(s) and run tests' }, + { + name: 'link', + description: 'Symlink a package folder', + args: { name: 'path', template: 'filepaths' }, + }, + { name: 'ln', description: 'Symlink a package folder' }, + { + name: 'logout', + description: 'Log out of the registry', + options: [ + registryOption, + { + name: '--scope', + description: + 'Associate an operation with a scope for a scoped registry', + args: { + name: 'scope', + description: 'Scope name', + }, + }, + ], + }, + { + name: ['ls', 'list'], + description: 'List installed packages', + options: npmListOptions, + args: { name: '[@scope]/pkg', isVariadic: true }, + }, + { + name: 'org', + description: 'Manage orgs', + subcommands: [ + { + name: 'set', + description: 'Add a user to an org or manage roles', + args: [ + { + name: 'orgname', + description: 'Organization name', + }, + { + name: 'username', + description: 'User name', + }, + { + name: 'role', + isOptional: true, + suggestions: ['developer', 'admin', 'owner'], + }, + ], + options: [registryOption, otpOption], + }, + { + name: 'rm', + description: 'Remove a user from an org', + args: [ + { + name: 'orgname', + description: 'Organization name', + }, + { + name: 'username', + description: 'User name', + }, + ], + options: [registryOption, otpOption], + }, + { + name: 'ls', + description: + 'List users in an org or see what roles a particular user has in an org', + args: [ + { + name: 'orgname', + description: 'Organization name', + }, + { + name: 'username', + description: 'User name', + isOptional: true, + }, + ], + options: [registryOption, otpOption, jsonOption, parseableOption], + }, + ], + }, + { + name: 'outdated', + description: 'Check for outdated packages', + args: { + name: '[<@scope>/]', + isVariadic: true, + isOptional: true, + }, + options: [ + { + name: ['-a', '-all'], + description: 'Show all outdated or installed packages', + }, + jsonOption, + longOption, + parseableOption, + { + name: '-g', + description: 'Checks globally', + }, + ...workSpaceOptions, + ], + }, + { + name: ['owner', 'author'], + description: 'Manage package owners', + subcommands: [ + { + name: 'ls', + description: + 'List all the users who have access to modify a package and push new versions. Handy when you need to know who to bug for help', + args: { name: '[@scope/]pkg' }, + options: [registryOption], + }, + { + name: 'add', + description: + 'Add a new user as a maintainer of a package. This user is enabled to modify metadata, publish new versions, and add other owners', + args: [{ name: 'user' }, { name: '[@scope/]pkg' }], + options: [registryOption, otpOption], + }, + { + name: 'rm', + description: + 'Remove a user from the package owner list. This immediately revokes their privileges', + args: [{ name: 'user' }, { name: '[@scope/]pkg' }], + options: [registryOption, otpOption], + }, + ], + }, + { + name: 'pack', + description: 'Create a tarball from a package', + args: { + name: '[<@scope>/]', + }, + options: [ + jsonOption, + dryRunOption, + ...workSpaceOptions, + { + name: '--pack-destination', + description: 'Directory in which npm pack will save tarballs', + args: { + name: 'pack-destination', + template: ['folders'], + }, + }, + ], + }, + { + name: 'ping', + description: 'Ping npm registry', + options: [registryOption], + }, + { + name: 'pkg', + description: 'Manages your package.json', + subcommands: [ + { + name: 'get', + description: + 'Retrieves a value key, defined in your package.json file. It is possible to get multiple values and values for child fields', + args: { + name: 'field', + description: + 'Name of the field to get. You can view child fields by separating them with a period', + isVariadic: true, + }, + options: [jsonOption, ...workSpaceOptions], + }, + { + name: 'set', + description: + 'Sets a value in your package.json based on the field value. It is possible to set multiple values and values for child fields', + args: { + // Format is =. How to achieve this? + name: 'field', + description: + 'Name of the field to set. You can set child fields by separating them with a period', + isVariadic: true, + }, + options: [ + jsonOption, + ...workSpaceOptions, + { + name: ['-f', '--force'], + description: + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg', + isDangerous: true, + }, + ], + }, + { + name: 'delete', + description: 'Deletes a key from your package.json', + args: { + name: 'key', + description: + 'Name of the key to delete. You can delete child fields by separating them with a period', + isVariadic: true, + }, + options: [ + ...workSpaceOptions, + { + name: ['-f', '--force'], + description: + 'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg', + isDangerous: true, + }, + ], + }, + ], + }, + { + name: 'prefix', + description: 'Display prefix', + options: [ + { + name: ['-g', '--global'], + description: 'Print the global prefix to standard out', + }, + ], + }, + { + name: 'profile', + description: 'Change settings on your registry profile', + subcommands: [ + { + name: 'get', + description: + 'Display all of the properties of your profile, or one or more specific properties', + args: { + name: 'property', + isOptional: true, + description: 'Property name', + }, + options: [registryOption, jsonOption, parseableOption, otpOption], + }, + { + name: 'set', + description: 'Set the value of a profile property', + args: [ + { + name: 'property', + description: 'Property name', + suggestions: [ + 'email', + 'fullname', + 'homepage', + 'freenode', + 'twitter', + 'github', + ], + }, + { + name: 'value', + description: 'Property value', + }, + ], + options: [registryOption, jsonOption, parseableOption, otpOption], + subcommands: [ + { + name: 'password', + description: + 'Change your password. This is interactive, you\'ll be prompted for your current password and a new password', + }, + ], + }, + { + name: 'enable-2fa', + description: 'Enables two-factor authentication', + args: { + name: 'mode', + description: + 'Mode for two-factor authentication. Defaults to auth-and-writes mode', + isOptional: true, + suggestions: [ + { + name: 'auth-only', + description: + 'Require an OTP when logging in or making changes to your account\'s authentication', + }, + { + name: 'auth-and-writes', + description: + 'Requires an OTP at all the times auth-only does, and also requires one when publishing a module, setting the latest dist-tag, or changing access via npm access and npm owner', + }, + ], + }, + options: [registryOption, otpOption], + }, + { + name: 'disable-2fa', + description: 'Disables two-factor authentication', + options: [registryOption, otpOption], + }, + ], + }, + { + name: 'prune', + description: 'Remove extraneous packages', + args: { + name: '[<@scope>/]', + isOptional: true, + }, + options: [ + omitOption, + dryRunOption, + jsonOption, + { + name: '--production', + description: 'Remove the packages specified in your devDependencies', + }, + ...workSpaceOptions, + ], + }, + { + name: 'publish', + description: 'Publish a package', + args: { + name: 'tarball|folder', + isOptional: true, + description: + 'A url or file path to a gzipped tar archive containing a single folder with a package.json file inside | A folder containing a package.json file', + template: ['folders'], + }, + options: [ + { + name: '--tag', + description: 'Registers the published package with the given tag', + args: { name: 'tag' }, + }, + ...workSpaceOptions, + { + name: '--access', + description: + 'Sets scoped package to be publicly viewable if set to \'public\'', + args: { + default: 'restricted', + suggestions: ['restricted', 'public'], + }, + }, + dryRunOption, + otpOption, + ], + }, + { + name: ['rebuild', 'rb'], + description: 'Rebuild a package', + args: { + name: '[<@scope>/][@]', + }, + options: [ + globalOption, + ...workSpaceOptions, + ignoreScriptsOption, + { + name: '--no-bin-links', + description: + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', + }, + ], + }, + { + name: 'repo', + description: 'Open package repository page in the browser', + args: { + name: 'package', + isOptional: true, + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [ + ...workSpaceOptions, + { + name: '--no-browser', + description: 'Display in command line instead of browser', + exclusiveOn: ['--browser'], + }, + { + name: '--browser', + description: + 'The browser that is called by the npm repo command to open websites', + args: { name: 'browser' }, + exclusiveOn: ['--no-browser'], + }, + ], + }, + { + name: 'restart', + description: 'Restart a package', + options: [ + ignoreScriptsOption, + scriptShellOption, + { + name: '--', + args: { + name: 'arg', + description: 'Arguments to be passed to the restart script', + }, + }, + ], + }, + { + name: 'root', + description: 'Display npm root', + options: [ + { + name: ['-g', '--global'], + description: + 'Print the effective global node_modules folder to standard out', + }, + ], + }, + { + name: ['search', 's', 'se', 'find'], + description: 'Search for packages', + args: { + name: 'search terms', + isVariadic: true, + }, + options: [ + longOption, + jsonOption, + { + name: '--color', + description: 'Show colors', + args: { + name: 'always', + suggestions: ['always'], + description: 'Always show colors', + }, + exclusiveOn: ['--no-color'], + }, + { + name: '--no-color', + description: 'Do not show colors', + exclusiveOn: ['--color'], + }, + parseableOption, + { + name: '--no-description', + description: 'Do not show descriptions', + }, + { + name: '--searchopts', + description: + 'Space-separated options that are always passed to search', + args: { + name: 'searchopts', + }, + }, + { + name: '--searchexclude', + description: + 'Space-separated options that limit the results from search', + args: { + name: 'searchexclude', + }, + }, + registryOption, + { + name: '--prefer-online', + description: + 'If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data', + exclusiveOn: ['--prefer-offline', '--offline'], + }, + { + name: '--prefer-offline', + description: + 'If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server', + exclusiveOn: ['--prefer-online', '--offline'], + }, + { + name: '--offline', + description: + 'Force offline mode: no network requests will be done during install', + exclusiveOn: ['--prefer-online', '--prefer-offline'], + }, + ], + }, + { name: 'set', description: 'Sets the config key to the value' }, + { + name: 'set-script', + description: 'Set tasks in the scripts section of package.json', + args: [ + { + name: 'script', + description: + 'Name of the task to be added to the scripts section of package.json', + }, + { + name: 'command', + description: 'Command to run when script is called', + }, + ], + options: workSpaceOptions, + }, + { + name: 'shrinkwrap', + description: 'Lock down dependency versions for publication', + }, + { + name: 'star', + description: 'Mark your favorite packages', + args: { + name: 'pkg', + description: 'Package to mark as favorite', + }, + options: [ + registryOption, + { + name: '--no-unicode', + description: 'Do not use unicode characters in the tree output', + }, + ], + }, + { + name: 'stars', + description: 'View packages marked as favorites', + args: { + name: 'user', + isOptional: true, + description: 'View packages marked as favorites by ', + }, + options: [registryOption], + }, + { + name: 'start', + description: 'Start a package', + options: [ + ignoreScriptsOption, + scriptShellOption, + { + name: '--', + args: { + name: 'arg', + description: 'Arguments to be passed to the start script', + }, + }, + ], + }, + { + name: 'stop', + description: 'Stop a package', + options: [ + ignoreScriptsOption, + scriptShellOption, + { + name: '--', + args: { + name: 'arg', + description: 'Arguments to be passed to the stop script', + }, + }, + ], + }, + { + name: 'team', + description: 'Manage organization teams and team memberships', + subcommands: [ + { + name: 'create', + args: { name: 'scope:team' }, + options: [registryOption, otpOption], + }, + { + name: 'destroy', + args: { name: 'scope:team' }, + options: [registryOption, otpOption], + }, + { + name: 'add', + args: [{ name: 'scope:team' }, { name: 'user' }], + options: [registryOption, otpOption], + }, + { + name: 'rm', + args: [{ name: 'scope:team' }, { name: 'user' }], + options: [registryOption, otpOption], + }, + { + name: 'ls', + args: { name: 'scope|scope:team' }, + options: [registryOption, jsonOption, parseableOption], + }, + ], + }, + { + name: ['test', 'tst', 't'], + description: 'Test a package', + options: [ignoreScriptsOption, scriptShellOption], + }, + { + name: 'token', + description: 'Manage your authentication tokens', + subcommands: [ + { + name: 'list', + description: 'Shows a table of all active authentication tokens', + options: [jsonOption, parseableOption], + }, + { + name: 'create', + description: 'Create a new authentication token', + options: [ + { + name: '--read-only', + description: + 'This is used to mark a token as unable to publish when configuring limited access tokens with the npm token create command', + }, + { + name: '--cidr', + description: + 'This is a list of CIDR address to be used when configuring limited access tokens with the npm token create command', + isRepeatable: true, + args: { + name: 'cidr', + }, + }, + ], + }, + { + name: 'revoke', + description: + 'Immediately removes an authentication token from the registry. You will no longer be able to use it', + args: { name: 'idtoken' }, + }, + ], + options: [registryOption, otpOption], + }, + uninstallSubcommand('uninstall'), + uninstallSubcommand(['r', 'rm']), + uninstallSubcommand('un'), + uninstallSubcommand('remove'), + uninstallSubcommand('unlink'), + { + name: 'unpublish', + description: 'Remove a package from the registry', + args: { + name: '[<@scope>/][@]', + }, + options: [ + dryRunOption, + { + name: ['-f', '--force'], + description: + 'Allow unpublishing all versions of a published package. Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input', + isDangerous: true, + }, + ...workSpaceOptions, + ], + }, + { + name: 'unstar', + description: 'Remove an item from your favorite packages', + args: { + name: 'pkg', + description: 'Package to unmark as favorite', + }, + options: [ + registryOption, + otpOption, + { + name: '--no-unicode', + description: 'Do not use unicode characters in the tree output', + }, + ], + }, + { + name: ['update', 'upgrade', 'up'], + description: 'Update a package', + options: [ + { name: '-g', description: 'Update global package' }, + { + name: '--global-style', + description: + 'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder', + }, + { + name: '--legacy-bundling', + description: + 'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package', + }, + { + name: '--strict-peer-deps', + description: + 'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure', + }, + { + name: '--no-package-lock', + description: 'Ignores package-lock.json files when installing', + }, + omitOption, + ignoreScriptsOption, + { + name: '--no-audit', + description: + 'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', + }, + { + name: '--no-bin-links', + description: + 'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables', + }, + { + name: '--no-fund', + description: + 'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding', + }, + { + name: '--save', + description: + 'Update the semver values of direct dependencies in your project package.json', + }, + dryRunOption, + ...workSpaceOptions, + ], + }, + { + name: 'version', + description: 'Bump a package version', + options: [ + ...workSpaceOptions, + jsonOption, + { + name: '--allow-same-version', + description: + 'Prevents throwing an error when npm version is used to set the new version to the same value as the current version', + }, + { + name: '--no-commit-hooks', + description: + 'Do not run git commit hooks when using the npm version command', + }, + { + name: '--no-git-tag-version', + description: + 'Do not tag the commit when using the npm version command', + }, + { + name: '--preid', + description: + 'The \'prerelease identifier\' to use as a prefix for the \'prerelease\' part of a semver. Like the rc in 1.2.0-rc.8', + args: { + name: 'prerelease-id', + }, + }, + { + name: '--sign-git-tag', + description: + 'If set to true, then the npm version command will tag the version using -s to add a signature', + }, + ], + }, + { + name: ['view', 'v', 'info', 'show'], + description: 'View registry info', + options: [...workSpaceOptions, jsonOption], + }, + { + name: 'whoami', + description: 'Display npm username', + options: [registryOption], + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/pnpm.ts b/extensions/terminal-suggest/src/completions/pnpm.ts new file mode 100644 index 00000000000..12b71e358e1 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/pnpm.ts @@ -0,0 +1,1031 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// GENERATORS + +import { npmScriptsGenerator, npmSearchGenerator } from './npm'; +import { dependenciesGenerator, nodeClis } from './yarn'; + +const filterMessages = (out: string): string => { + return out.startsWith('warning:') || out.startsWith('error:') + ? out.split('\n').slice(1).join('\n') + : out; +}; + +const searchBranches: Fig.Generator = { + script: ['git', 'branch', '--no-color'], + postProcess: function (out) { + const output = filterMessages(out); + + if (output.startsWith('fatal:')) { + return []; + } + + return output.split('\n').map((elm) => { + let name = elm.trim(); + const parts = elm.match(/\S+/g); + if (parts && parts.length > 1) { + if (parts[0] === '*') { + // Current branch. + return { + name: elm.replace('*', '').trim(), + description: 'Current branch', + }; + } else if (parts[0] === '+') { + // Branch checked out in another worktree. + name = elm.replace('+', '').trim(); + } + } + + return { + name, + description: 'Branch', + icon: 'fig://icon?type=git', + }; + }); + }, +}; + +const generatorInstalledPackages: Fig.Generator = { + script: ['pnpm', 'ls'], + postProcess: function (out) { + /** + * out + * @example + * ``` + * Legend: production dependency, optional only, dev only + * + * /xxxx/xxxx/ (PRIVATE) + * + * dependencies: + * lodash 4.17.21 + * foo link:packages/foo + * + * devDependencies: + * typescript 4.7.4 + * ``` + */ + if (out.includes('ERR_PNPM')) { + return []; + } + + const output = out + .split('\n') + .slice(3) + // remove empty lines, '*dependencies:' lines, local workspace packages (eg: 'foo':'workspace:*') + .filter( + (item) => + !!item && + !item.toLowerCase().includes('dependencies') && + !item.includes('link:') + ) + .map((item) => item.replace(/\s/, '@')); // typescript 4.7.4 -> typescript@4.7.4 + + return output.map((pkg) => { + return { + name: pkg, + icon: 'fig://icon?type=package', + }; + }); + }, +}; + +const FILTER_OPTION: Fig.Option = { + name: '--filter', + args: { + template: 'filepaths', + name: 'Filepath / Package', + description: + 'To only select packages under the specified directory, you may specify any absolute path, typically in POSIX format', + }, + description: `Filtering allows you to restrict commands to specific subsets of packages. +pnpm supports a rich selector syntax for picking packages by name or by relation. +More details: https://pnpm.io/filtering`, +}; + +/** Options that being appended for `pnpm i` and `add` */ +const INSTALL_BASE_OPTIONS: Fig.Option[] = [ + { + name: '--offline', + description: + 'If true, pnpm will use only packages already available in the store. If a package won\'t be found locally, the installation will fail', + }, + { + name: '--prefer-offline', + description: + 'If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server. To force full offline mode, use --offline', + }, + { + name: '--ignore-scripts', + description: + 'Do not execute any scripts defined in the project package.json and its dependencies', + }, + { + name: '--reporter', + description: `Allows you to choose the reporter that will log debug info to the terminal about the installation progress`, + args: { + name: 'Reporter Type', + suggestions: ['silent', 'default', 'append-only', 'ndjson'], + }, + }, +]; + +/** Base options for pnpm i when run without any arguments */ +const INSTALL_OPTIONS: Fig.Option[] = [ + { + name: ['-P', '--save-prod'], + description: `Pnpm will not install any package listed in devDependencies if the NODE_ENV environment variable is set to production. +Use this flag to instruct pnpm to ignore NODE_ENV and take its production status from this flag instead`, + }, + { + name: ['-D', '--save-dev'], + description: + 'Only devDependencies are installed regardless of the NODE_ENV', + }, + { + name: '--no-optional', + description: 'OptionalDependencies are not installed', + }, + { + name: '--lockfile-only', + description: + 'When used, only updates pnpm-lock.yaml and package.json instead of checking node_modules and downloading dependencies', + }, + { + name: '--frozen-lockfile', + description: + 'If true, pnpm doesn\'t generate a lockfile and fails to install if the lockfile is out of sync with the manifest / an update is needed or no lockfile is present', + }, + { + name: '--use-store-server', + description: + 'Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run pnpm server stop', + }, + { + name: '--shamefully-hoist', + description: + 'Creates a flat node_modules structure, similar to that of npm or yarn. WARNING: This is highly discouraged', + }, +]; + +/** Base options for pnpm add */ +const INSTALL_PACKAGE_OPTIONS: Fig.Option[] = [ + { + name: ['-P', '--save-prod'], + description: 'Install the specified packages as regular dependencies', + }, + { + name: ['-D', '--save-dev'], + description: 'Install the specified packages as devDependencies', + }, + { + name: ['-O', '--save-optional'], + description: 'Install the specified packages as optionalDependencies', + }, + { + name: '--no-save', + description: 'Prevents saving to `dependencies`', + }, + { + name: ['-E', '--save-exact'], + description: + 'Saved dependencies will be configured with an exact version rather than using pnpm\'s default semver range operator', + }, + { + name: '--save-peer', + description: + 'Using --save-peer will add one or more packages to peerDependencies and install them as dev dependencies', + }, + { + name: ['--ignore-workspace-root-check', '-W#'], + description: `Adding a new dependency to the root workspace package fails, unless the --ignore-workspace-root-check or -W flag is used. +For instance, pnpm add debug -W`, + }, + { + name: ['--global', '-g'], + description: `Install a package globally`, + }, + { + name: '--workspace', + description: `Only adds the new dependency if it is found in the workspace`, + }, + FILTER_OPTION, +]; + +// SUBCOMMANDS +const SUBCOMMANDS_MANAGE_DEPENDENCIES: Fig.Subcommand[] = [ + { + name: 'add', + description: `Installs a package and any packages that it depends on. By default, any new package is installed as a production dependency`, + args: { + name: 'package', + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [...INSTALL_BASE_OPTIONS, ...INSTALL_PACKAGE_OPTIONS], + }, + { + name: ['install', 'i'], + description: `Pnpm install is used to install all dependencies for a project. +In a CI environment, installation fails if a lockfile is present but needs an update. +Inside a workspace, pnpm install installs all dependencies in all the projects. +If you want to disable this behavior, set the recursive-install setting to false`, + async generateSpec(tokens) { + // `pnpm i` with args is an `pnpm add` alias + const hasArgs = + tokens.filter((token) => token.trim() !== '' && !token.startsWith('-')) + .length > 2; + + return { + name: 'install', + options: [ + ...INSTALL_BASE_OPTIONS, + ...(hasArgs ? INSTALL_PACKAGE_OPTIONS : INSTALL_OPTIONS), + ], + }; + }, + args: { + name: 'package', + isOptional: true, + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + }, + { + name: ['install-test', 'it'], + description: + 'Runs pnpm install followed immediately by pnpm test. It takes exactly the same arguments as pnpm install', + options: [...INSTALL_BASE_OPTIONS, ...INSTALL_OPTIONS], + }, + { + name: ['update', 'upgrade', 'up'], + description: `Pnpm update updates packages to their latest version based on the specified range. +When used without arguments, updates all dependencies. You can use patterns to update specific dependencies`, + args: { + name: 'Package', + isOptional: true, + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + options: [ + { + name: ['--recursive', '-r'], + description: + 'Concurrently runs update in all subdirectories with a package.json (excluding node_modules)', + }, + { + name: ['--latest', '-L'], + description: + 'Ignores the version range specified in package.json. Instead, the version specified by the latest tag will be used (potentially upgrading the packages across major versions)', + }, + { + name: '--global', + description: 'Update global packages', + }, + { + name: ['-P', '--save-prod'], + description: `Only update packages in dependencies and optionalDependencies`, + }, + { + name: ['-D', '--save-dev'], + description: 'Only update packages in devDependencies', + }, + { + name: '--no-optional', + description: 'Don\'t update packages in optionalDependencies', + }, + { + name: ['--interactive', '-i'], + description: + 'Show outdated dependencies and select which ones to update', + }, + { + name: '--workspace', + description: `Tries to link all packages from the workspace. Versions are updated to match the versions of packages inside the workspace. +If specific packages are updated, the command will fail if any of the updated dependencies are not found inside the workspace. For instance, the following command fails if express is not a workspace package: pnpm up -r --workspace express`, + }, + FILTER_OPTION, + ], + }, + { + name: ['remove', 'rm', 'uninstall', 'un'], + description: `Removes packages from node_modules and from the project's package.json`, + args: { + name: 'Package', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + options: [ + { + name: ['--recursive', '-r'], + description: `When used inside a workspace, removes a dependency (or dependencies) from every workspace package. +When used not inside a workspace, removes a dependency (or dependencies) from every package found in subdirectories`, + }, + { + name: '--global', + description: 'Remove a global package', + }, + { + name: ['-P', '--save-prod'], + description: `Only remove the dependency from dependencies`, + }, + { + name: ['-D', '--save-dev'], + description: 'Only remove the dependency from devDependencies', + }, + { + name: ['--save-optional', '-O'], + description: 'Only remove the dependency from optionalDependencies', + }, + FILTER_OPTION, + ], + }, + { + name: ['link', 'ln'], + description: `Makes the current local package accessible system-wide, or in another location`, + args: [ + { + name: 'Package', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + { template: 'filepaths' }, + ], + options: [ + { + name: ['--dir', '-C'], + description: `Changes the link location to `, + }, + { + name: '--global', + description: + 'Links the specified package () from global node_modules to the node_nodules of package from where this command was executed or specified via --dir option', + }, + ], + }, + { + name: 'unlink', + description: `Unlinks a system-wide package (inverse of pnpm link). +If called without arguments, all linked dependencies will be unlinked. +This is similar to yarn unlink, except pnpm re-installs the dependency after removing the external link`, + args: [ + { + name: 'Package', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + { template: 'filepaths' }, + ], + options: [ + { + name: ['--recursive', '-r'], + description: `Unlink in every package found in subdirectories or in every workspace package, when executed inside a workspace`, + }, + FILTER_OPTION, + ], + }, + { + name: 'import', + description: + 'Pnpm import generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file', + }, + { + name: ['rebuild', 'rb'], + description: `Rebuild a package`, + args: [ + { + name: 'Package', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + { template: 'filepaths' }, + ], + options: [ + { + name: ['--recursive', '-r'], + description: `This command runs the pnpm rebuild command in every package of the monorepo`, + }, + FILTER_OPTION, + ], + }, + { + name: 'prune', + description: `Removes unnecessary packages`, + options: [ + { + name: '--prod', + description: `Remove the packages specified in devDependencies`, + }, + { + name: '--no-optional', + description: `Remove the packages specified in optionalDependencies`, + }, + ], + }, + { + name: 'fetch', + description: `EXPERIMENTAL FEATURE: Fetch packages from a lockfile into virtual store, package manifest is ignored: https://pnpm.io/cli/fetch`, + options: [ + { + name: '--prod', + description: `Development packages will not be fetched`, + }, + { + name: '--dev', + description: `Only development packages will be fetched`, + }, + ], + }, + { + name: 'patch', + description: `This command will cause a package to be extracted in a temporary directory intended to be editable at will`, + args: { + name: 'package', + generators: generatorInstalledPackages, + }, + options: [ + { + name: '--edit-dir', + description: `The package that needs to be patched will be extracted to this directory`, + }, + ], + }, + { + name: 'patch-commit', + args: { + name: 'dir', + }, + description: `Generate a patch out of a directory`, + }, + { + name: 'patch-remove', + args: { + name: 'package', + isVariadic: true, + // TODO: would be nice to have a generator of all patched packages + }, + }, +]; + +const SUBCOMMANDS_RUN_SCRIPTS: Fig.Subcommand[] = [ + { + name: ['run', 'run-script'], + description: 'Runs a script defined in the package\'s manifest file', + args: { + name: 'Scripts', + filterStrategy: 'fuzzy', + generators: npmScriptsGenerator, + isVariadic: true, + }, + options: [ + { + name: ['-r', '--recursive'], + description: `This runs an arbitrary command from each package's 'scripts' object. If a package doesn't have the command, it is skipped. If none of the packages have the command, the command fails`, + }, + { + name: '--if-present', + description: + 'You can use the --if-present flag to avoid exiting with a non-zero exit code when the script is undefined. This lets you run potentially undefined scripts without breaking the execution chain', + }, + { + name: '--parallel', + description: + 'Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process', + }, + { + name: '--stream', + description: + 'Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved', + }, + FILTER_OPTION, + ], + }, + { + name: 'exec', + description: `Execute a shell command in scope of a project. +node_modules/.bin is added to the PATH, so pnpm exec allows executing commands of dependencies`, + args: { + name: 'Scripts', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + options: [ + { + name: ['-r', '--recursive'], + description: `Execute the shell command in every project of the workspace. +The name of the current package is available through the environment variable PNPM_PACKAGE_NAME (supported from pnpm v2.22.0 onwards)`, + }, + { + name: '--parallel', + description: + 'Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process', + }, + FILTER_OPTION, + ], + }, + { + name: ['test', 't', 'tst'], + description: `Runs an arbitrary command specified in the package's test property of its scripts object. +The intended usage of the property is to specify a command that runs unit or integration testing for your program`, + }, + { + name: 'start', + description: `Runs an arbitrary command specified in the package's start property of its scripts object. If no start property is specified on the scripts object, it will attempt to run node server.js as a default, failing if neither are present. +The intended usage of the property is to specify a command that starts your program`, + }, +]; + +const SUBCOMMANDS_REVIEW_DEPS: Fig.Subcommand[] = [ + { + name: 'audit', + description: `Checks for known security issues with the installed packages. +If security issues are found, try to update your dependencies via pnpm update. +If a simple update does not fix all the issues, use overrides to force versions that are not vulnerable. +For instance, if lodash@<2.1.0 is vulnerable, use overrides to force lodash@^2.1.0. +Details at: https://pnpm.io/cli/audit`, + options: [ + { + name: '--audit-level', + description: `Only print advisories with severity greater than or equal to `, + args: { + name: 'Audit Level', + default: 'low', + suggestions: ['low', 'moderate', 'high', 'critical'], + }, + }, + { + name: '--fix', + description: + 'Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies', + }, + { + name: '--json', + description: `Output audit report in JSON format`, + }, + { + name: ['--dev', '-D'], + description: `Only audit dev dependencies`, + }, + { + name: ['--prod', '-P'], + description: `Only audit production dependencies`, + }, + { + name: '--no-optional', + description: `Don't audit optionalDependencies`, + }, + { + name: '--ignore-registry-errors', + description: `If the registry responds with a non-200 status code, the process should exit with 0. So the process will fail only if the registry actually successfully responds with found vulnerabilities`, + }, + ], + }, + { + name: ['list', 'ls'], + description: `This command will output all the versions of packages that are installed, as well as their dependencies, in a tree-structure. +Positional arguments are name-pattern@version-range identifiers, which will limit the results to only the packages named. For example, pnpm list 'babel-*' 'eslint-*' semver@5`, + options: [ + { + name: ['--recursive', '-r'], + description: `Perform command on every package in subdirectories or on every workspace package, when executed inside a workspace`, + }, + { + name: '--json', + description: `Log output in JSON format`, + }, + { + name: '--long', + description: `Show extended information`, + }, + { + name: '--parseable', + description: `Outputs package directories in a parseable format instead of their tree view`, + }, + { + name: '--global', + description: `List packages in the global install directory instead of in the current project`, + }, + { + name: '--depth', + description: `Max display depth of the dependency tree. +pnpm ls --depth 0 will list direct dependencies only. pnpm ls --depth -1 will list projects only. Useful inside a workspace when used with the -r option`, + args: { name: 'number' }, + }, + { + name: ['--dev', '-D'], + description: `Only list dev dependencies`, + }, + { + name: ['--prod', '-P'], + description: `Only list production dependencies`, + }, + { + name: '--no-optional', + description: `Don't list optionalDependencies`, + }, + FILTER_OPTION, + ], + }, + { + name: 'outdated', + description: `Checks for outdated packages. The check can be limited to a subset of the installed packages by providing arguments (patterns are supported)`, + options: [ + { + name: ['--recursive', '-r'], + description: `Check for outdated dependencies in every package found in subdirectories, or in every workspace package when executed inside a workspace`, + }, + { + name: '--long', + description: `Print details`, + }, + { + name: '--global', + description: `List outdated global packages`, + }, + { + name: '--no-table', + description: `Prints the outdated dependencies in a list format instead of the default table. Good for small consoles`, + }, + { + name: '--compatible', + description: `Prints only versions that satisfy specifications in package.json`, + }, + { + name: ['--dev', '-D'], + description: `Only list dev dependencies`, + }, + { + name: ['--prod', '-P'], + description: `Only list production dependencies`, + }, + { + name: '--no-optional', + description: `Doesn't check optionalDependencies`, + }, + ], + }, + { + name: 'why', + description: `Shows all packages that depend on the specified package`, + args: { + name: 'Scripts', + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + options: [ + { + name: ['--recursive', '-r'], + description: `Show the dependency tree for the specified package on every package in subdirectories or on every workspace package when executed inside a workspace`, + }, + { + name: '--json', + description: `Log output in JSON format`, + }, + { + name: '--long', + description: `Show verbose output`, + }, + { + name: '--parseable', + description: `Show parseable output instead of tree view`, + }, + { + name: '--global', + description: `List packages in the global install directory instead of in the current project`, + }, + { + name: ['--dev', '-D'], + description: `Only display the dependency tree for packages in devDependencies`, + }, + { + name: ['--prod', '-P'], + description: `Only display the dependency tree for packages in dependencies`, + }, + FILTER_OPTION, + ], + }, +]; + +const SUBCOMMANDS_MISC: Fig.Subcommand[] = [ + { + name: 'publish', + description: `Publishes a package to the registry. +When publishing a package inside a workspace, the LICENSE file from the root of the workspace is packed with the package (unless the package has a license of its own). +You may override some fields before publish, using the publishConfig field in package.json. You also can use the publishConfig.directory to customize the published subdirectory (usually using third party build tools). +When running this command recursively (pnpm -r publish), pnpm will publish all the packages that have versions not yet published to the registry`, + args: { + name: 'Branch', + generators: searchBranches, + }, + options: [ + { + name: '--tag', + description: `Publishes the package with the given tag. By default, pnpm publish updates the latest tag`, + args: { + name: '', + }, + }, + { + name: '--dry-run', + description: `Does everything a publish would do except actually publishing to the registry`, + }, + { + name: '--ignore-scripts', + description: `Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)`, + }, + { + name: '--no-git-checks', + description: `Don't check if current branch is your publish branch, clean, and up-to-date`, + }, + { + name: '--access', + description: `Tells the registry whether the published package should be public or restricted`, + args: { + name: 'Type', + suggestions: ['public', 'private'], + }, + }, + { + name: '--force', + description: `Try to publish packages even if their current version is already found in the registry`, + }, + { + name: '--report-summary', + description: `Save the list of published packages to pnpm-publish-summary.json. Useful when some other tooling is used to report the list of published packages`, + }, + FILTER_OPTION, + ], + }, + { + name: ['recursive', 'm', 'multi', '-r'], + description: `Runs a pnpm command recursively on all subdirectories in the package or every available workspace`, + options: [ + { + name: '--link-workspace-packages', + description: `Link locally available packages in workspaces of a monorepo into node_modules instead of re-downloading them from the registry. This emulates functionality similar to yarn workspaces. +When this is set to deep, local packages can also be linked to subdependencies. +Be advised that it is encouraged instead to use npmrc for this setting, to enforce the same behaviour in all environments. This option exists solely so you may override that if necessary`, + args: { + name: 'bool or `deep`', + suggestions: ['dee['], + }, + }, + { + name: '--workspace-concurrency', + description: `Set the maximum number of tasks to run simultaneously. For unlimited concurrency use Infinity`, + args: { name: '' }, + }, + { + name: '--bail', + description: `Stops when a task throws an error`, + }, + { + name: '--no-bail', + description: `Don't stop when a task throws an error`, + }, + { + name: '--sort', + description: `Packages are sorted topologically (dependencies before dependents)`, + }, + { + name: '--no-sort', + description: `Disable packages sorting`, + }, + { + name: '--reverse', + description: `The order of packages is reversed`, + }, + FILTER_OPTION, + ], + }, + { + name: 'server', + description: `Manage a store server`, + subcommands: [ + { + name: 'start', + description: + 'Starts a server that performs all interactions with the store. Other commands will delegate any store-related tasks to this server', + options: [ + { + name: '--background', + description: `Runs the server in the background, similar to daemonizing on UNIX systems`, + }, + { + name: '--network-concurrency', + description: `The maximum number of network requests to process simultaneously`, + args: { name: 'number' }, + }, + { + name: '--protocol', + description: `The communication protocol used by the server. When this is set to auto, IPC is used on all systems except for Windows, which uses TCP`, + args: { + name: 'Type', + suggestions: ['auto', 'tcp', 'ipc'], + }, + }, + { + name: '--port', + description: `The port number to use when TCP is used for communication. If a port is specified and the protocol is set to auto, regardless of system type, the protocol is automatically set to use TCP`, + args: { name: 'port number' }, + }, + { + name: '--store-dir', + description: `The directory to use for the content addressable store`, + args: { name: 'Path', template: 'filepaths' }, + }, + { + name: '--lock', + description: `Set to make the package store immutable to external processes while the server is running or not`, + }, + { + name: '--no-lock', + description: `Set to make the package store mutable to external processes while the server is running or not`, + }, + { + name: '--ignore-stop-requests', + description: `Prevents you from stopping the server using pnpm server stop`, + }, + { + name: '--ignore-upload-requests', + description: `Prevents creating a new side effect cache during install`, + }, + ], + }, + { + name: 'stop', + description: 'Stops the store server', + }, + { + name: 'status', + description: 'Prints information about the running server', + }, + ], + }, + { + name: 'store', + description: 'Managing the package store', + subcommands: [ + { + name: 'status', + description: `Checks for modified packages in the store. +Returns exit code 0 if the content of the package is the same as it was at the time of unpacking`, + }, + { + name: 'add', + description: `Functionally equivalent to pnpm add, +except this adds new packages to the store directly without modifying any projects or files outside of the store`, + }, + { + name: 'prune', + description: `Removes orphan packages from the store. +Pruning the store will save disk space, however may slow down future installations involving pruned packages. +Ultimately, it is a safe operation, however not recommended if you have orphaned packages from a package you intend to reinstall. +Please read the FAQ for more information on unreferenced packages and best practices. +Please note that this is prohibited when a store server is running`, + }, + { + name: 'path', + description: `Returns the path to the active store directory`, + }, + ], + }, + { + name: 'init', + description: + 'Creates a basic package.json file in the current directory, if it doesn\'t exist already', + }, + { + name: 'doctor', + description: 'Checks for known common issues with pnpm configuration', + }, +]; + +const subcommands = [ + ...SUBCOMMANDS_MANAGE_DEPENDENCIES, + ...SUBCOMMANDS_REVIEW_DEPS, + ...SUBCOMMANDS_RUN_SCRIPTS, + ...SUBCOMMANDS_MISC, +]; + +const recursiveSubcommandsNames = [ + 'add', + 'exec', + 'install', + 'list', + 'outdated', + 'publish', + 'rebuild', + 'remove', + 'run', + 'test', + 'unlink', + 'update', + 'why', +]; + +const recursiveSubcommands = subcommands.filter((subcommand) => { + if (Array.isArray(subcommand.name)) { + return subcommand.name.some((name) => + recursiveSubcommandsNames.includes(name) + ); + } + return recursiveSubcommandsNames.includes(subcommand.name); +}); + +// RECURSIVE SUBCOMMAND INDEX +SUBCOMMANDS_MISC[1].subcommands = recursiveSubcommands; + +// common options +const COMMON_OPTIONS: Fig.Option[] = [ + { + name: ['-C', '--dir'], + args: { + name: 'path', + template: 'folders', + }, + isPersistent: true, + description: + 'Run as if pnpm was started in instead of the current working directory', + }, + { + name: ['-w', '--workspace-root'], + args: { + name: 'workspace', + }, + isPersistent: true, + description: + 'Run as if pnpm was started in the root of the instead of the current working directory', + }, + { + name: ['-h', '--help'], + isPersistent: true, + description: 'Output usage information', + }, + { + name: ['-v', '--version'], + description: 'Show pnpm\'s version', + }, +]; + +// SPEC +const completionSpec: Fig.Spec = { + name: 'pnpm', + description: 'Fast, disk space efficient package manager', + args: { + name: 'Scripts', + filterStrategy: 'fuzzy', + generators: npmScriptsGenerator, + isVariadic: true, + }, + filterStrategy: 'fuzzy', + generateSpec: async (tokens, executeShellCommand) => { + const { script, postProcess } = dependenciesGenerator as Fig.Generator & { + script: string[]; + }; + + if (postProcess === undefined) { + return undefined; + } + + const packages = postProcess( + ( + await executeShellCommand({ + command: script[0], + args: script.slice(1), + }) + ).stdout, + tokens + ) + ?.filter((e) => e !== null) + .map(({ name }) => name as string); + + const subcommands = packages + ?.filter((name) => nodeClis.has(name)) + .map((name) => ({ + name, + loadSpec: name, + icon: 'fig://icon?type=package', + })); + + return { + name: 'pnpm', + subcommands, + }; + }, + subcommands, + options: COMMON_OPTIONS, +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/env.ts b/extensions/terminal-suggest/src/completions/upstream/env.ts index f7b4eb0337a..fa215dbcf05 100644 --- a/extensions/terminal-suggest/src/completions/upstream/env.ts +++ b/extensions/terminal-suggest/src/completions/upstream/env.ts @@ -1,4 +1,4 @@ -const enviromentVariables: Fig.Generator = { +const environmentVariables: Fig.Generator = { custom: async (_tokens, _executeCommand, generatorContext) => { return Object.values(generatorContext.environmentVariables).map( (envVar) => ({ @@ -31,7 +31,7 @@ const completionSpec: Fig.Spec = { description: "Remove variable from the environment", args: { name: "name", - generators: enviromentVariables, + generators: environmentVariables, }, }, { diff --git a/extensions/terminal-suggest/src/completions/upstream/npm.ts b/extensions/terminal-suggest/src/completions/upstream/npm.ts deleted file mode 100644 index aa142e05661..00000000000 --- a/extensions/terminal-suggest/src/completions/upstream/npm.ts +++ /dev/null @@ -1,1610 +0,0 @@ -function uninstallSubcommand(named: string | string[]): Fig.Subcommand { - return { - name: named, - description: "Uninstall a package", - args: { - name: "package", - generators: dependenciesGenerator, - filterStrategy: "fuzzy", - isVariadic: true, - }, - options: npmUninstallOptions, - }; -} - -const atsInStr = (s: string) => (s.match(/@/g) || []).length; - -export const createNpmSearchHandler = - (keywords?: string[]) => - async ( - context: string[], - executeShellCommand: Fig.ExecuteCommandFunction, - shellContext: Fig.ShellContext - ): Promise => { - const searchTerm = context[context.length - 1]; - if (searchTerm === "") { - return []; - } - // Add optional keyword parameter - const keywordParameter = - keywords && keywords.length > 0 ? `+keywords:${keywords.join(",")}` : ""; - - const queryPackagesUrl = keywordParameter - ? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}` - : `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`; - - // Query the API with the package name - const queryPackages = [ - "-s", - "-H", - "Accept: application/json", - queryPackagesUrl, - ]; - // We need to remove the '@' at the end of the searchTerm before querying versions - const queryVersions = [ - "-s", - "-H", - "Accept: application/vnd.npm.install-v1+json", - `https://registry.npmjs.org/${searchTerm.slice(0, -1)}`, - ]; - // If the end of our token is '@', then we want to generate version suggestions - // Otherwise, we want packages - const out = (query: string) => - executeShellCommand({ - command: "curl", - args: query[query.length - 1] === "@" ? queryVersions : queryPackages, - }); - // If our token starts with '@', then a 2nd '@' tells us we want - // versions. - // Otherwise, '@' anywhere else in the string will indicate the same. - const shouldGetVersion = searchTerm.startsWith("@") - ? atsInStr(searchTerm) > 1 - : searchTerm.includes("@"); - - try { - const data = JSON.parse((await out(searchTerm)).stdout); - if (shouldGetVersion) { - // create dist tags suggestions - const versions = Object.entries(data["dist-tags"] || {}).map( - ([key, value]) => ({ - name: key, - description: value, - }) - ) as Fig.Suggestion[]; - // create versions - versions.push( - ...Object.keys(data.versions) - .map((version) => ({ name: version }) as Fig.Suggestion) - .reverse() - ); - return versions; - } - - const results = keywordParameter ? data.results : data; - return results.map( - (item: { package: { name: string; description: string } }) => ({ - name: item.package.name, - description: item.package.description, - }) - ) as Fig.Suggestion[]; - } catch (error) { - console.error({ error }); - return []; - } - }; - -// GENERATORS -export const npmSearchGenerator: Fig.Generator = { - trigger: (newToken, oldToken) => { - // If the package name starts with '@', we want to trigger when - // the 2nd '@' is typed because we'll need to generate version - // suggetsions - // e.g. @typescript-eslint/types - if (oldToken.startsWith("@")) { - return !(atsInStr(oldToken) > 1 && atsInStr(newToken) > 1); - } - - // If the package name doesn't start with '@', then trigger when - // we see the first '@' so we can generate version suggestions - return !(oldToken.includes("@") && newToken.includes("@")); - }, - getQueryTerm: "@", - cache: { - ttl: 1000 * 60 * 60 * 24 * 2, // 2 days - }, - custom: createNpmSearchHandler(), -}; - -const workspaceGenerator: Fig.Generator = { - // script: "cat $(npm prefix)/package.json", - custom: async (tokens, executeShellCommand) => { - const { stdout: npmPrefix } = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], - }); - - const { stdout: out } = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: [`${npmPrefix}/package.json`], - }); - - const suggestions: Fig.Suggestion[] = []; - try { - if (out.trim() == "") { - return suggestions; - } - - const packageContent = JSON.parse(out); - const workspaces = packageContent["workspaces"]; - - if (workspaces) { - for (const workspace of workspaces) { - suggestions.push({ - name: workspace, - description: "Workspaces", - }); - } - } - } catch (e) { - console.log(e); - } - return suggestions; - }, -}; - -/** Generator that lists package.json dependencies */ -export const dependenciesGenerator: Fig.Generator = { - trigger: (newToken) => newToken === "-g" || newToken === "--global", - custom: async function (tokens, executeShellCommand) { - if (!tokens.includes("-g") && !tokens.includes("--global")) { - const { stdout: npmPrefix } = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], - }); - const { stdout: out } = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: [`${npmPrefix}/package.json`], - }); - const packageContent = JSON.parse(out); - const dependencies = packageContent["dependencies"] ?? {}; - const devDependencies = packageContent["devDependencies"]; - const optionalDependencies = packageContent["optionalDependencies"] ?? {}; - Object.assign(dependencies, devDependencies, optionalDependencies); - - return Object.keys(dependencies) - .filter((pkgName) => { - const isListed = tokens.some((current) => current === pkgName); - return !isListed; - }) - .map((pkgName) => ({ - name: pkgName, - icon: "📦", - description: dependencies[pkgName] - ? "dependency" - : optionalDependencies[pkgName] - ? "optionalDependency" - : "devDependency", - })); - } else { - const { stdout } = await executeShellCommand({ - command: "bash", - args: ["-c", "ls -1 `npm root -g`"], - }); - return stdout.split("\n").map((name) => ({ - name, - icon: "📦", - description: "Global dependency", - })); - } - }, -}; - -/** Generator that lists package.json scripts (with the respect to the `fig` field) */ -export const npmScriptsGenerator: Fig.Generator = { - cache: { - strategy: "stale-while-revalidate", - cacheByDirectory: true, - }, - script: [ - "bash", - "-c", - "until [[ -f package.json ]] || [[ $PWD = '/' ]]; do cd ..; done; cat package.json", - ], - postProcess: function (out, [npmClient]) { - if (out.trim() == "") { - return []; - } - - try { - const packageContent = JSON.parse(out); - const scripts = packageContent["scripts"]; - const figCompletions = packageContent["fig"] || {}; - - if (scripts) { - return Object.entries(scripts).map(([scriptName, scriptContents]) => { - const icon = - npmClient === "yarn" - ? "fig://icon?type=yarn" - : "fig://icon?type=npm"; - const customScripts: Fig.Suggestion = figCompletions[scriptName]; - return { - name: scriptName, - icon, - description: scriptContents as string, - priority: 51, - /** - * If there are custom definitions for the scripts - * we want to override the default values - * */ - ...customScripts, - }; - }); - } - } catch (e) { - console.error(e); - } - - return []; - }, -}; - -const globalOption: Fig.Option = { - name: ["-g", "--global"], - description: - "Operates in 'global' mode, so that packages are installed into the prefix folder instead of the current working directory", -}; - -const jsonOption: Fig.Option = { - name: "--json", - description: "Show output in json format", -}; - -const omitOption: Fig.Option = { - name: "--omit", - description: "Dependency types to omit from the installation tree on disk", - args: { - name: "Package type", - default: "dev", - suggestions: ["dev", "optional", "peer"], - }, - isRepeatable: 3, -}; - -const parseableOption: Fig.Option = { - name: ["-p", "--parseable"], - description: - "Output parseable results from commands that write to standard output", -}; - -const longOption: Fig.Option = { - name: ["-l", "--long"], - description: "Show extended information", -}; - -const workSpaceOptions: Fig.Option[] = [ - { - name: ["-w", "--workspace"], - description: - "Enable running a command in the context of the configured workspaces of the current project", - args: { - name: "workspace", - generators: workspaceGenerator, - isVariadic: true, - }, - }, - { - name: ["-ws", "--workspaces"], - description: - "Enable running a command in the context of all the configured workspaces", - }, -]; - -const npmUninstallOptions: Fig.Option[] = [ - { - name: ["-S", "--save"], - description: "Package will be removed from your dependencies", - }, - { - name: ["-D", "--save-dev"], - description: "Package will appear in your `devDependencies`", - }, - { - name: ["-O", "--save-optional"], - description: "Package will appear in your `optionalDependencies`", - }, - { - name: "--no-save", - description: "Prevents saving to `dependencies`", - }, - { - name: "-g", - description: "Uninstall global package", - }, - ...workSpaceOptions, -]; - -const npmListOptions: Fig.Option[] = [ - { - name: ["-a", "-all"], - description: "Show all outdated or installed packages", - }, - jsonOption, - longOption, - parseableOption, - { - name: "--depth", - description: "The depth to go when recursing packages", - args: { name: "depth" }, - }, - { - name: "--link", - description: "Limits output to only those packages that are linked", - }, - { - name: "--package-lock-only", - description: - "Current operation will only use the package-lock.json, ignoring node_modules", - }, - { - name: "--no-unicode", - description: "Uses unicode characters in the tree output", - }, - globalOption, - omitOption, - ...workSpaceOptions, -]; - -const registryOption: Fig.Option = { - name: "--registry", - description: "The base URL of the npm registry", - args: { name: "registry" }, -}; - -const verboseOption: Fig.Option = { - name: "--verbose", - description: "Show extra information", - args: { name: "verbose" }, -}; - -const otpOption: Fig.Option = { - name: "--otp", - description: "One-time password from a two-factor authenticator", - args: { name: "otp" }, -}; - -const ignoreScriptsOption: Fig.Option = { - name: "--ignore-scripts", - description: - "If true, npm does not run scripts specified in package.json files", -}; - -const scriptShellOption: Fig.Option = { - name: "--script-shell", - description: - "The shell to use for scripts run with the npm exec, npm run and npm init commands", - args: { name: "script-shell" }, -}; - -const dryRunOption: Fig.Option = { - name: "--dry-run", - description: - "Indicates that you don't want npm to make any changes and that it should only report what it would have done", -}; - -const completionSpec: Fig.Spec = { - name: "npm", - parserDirectives: { - flagsArePosixNoncompliant: true, - }, - description: "Node package manager", - subcommands: [ - { - name: ["install", "i", "add"], - description: "Install a package and its dependencies", - args: { - name: "package", - isOptional: true, - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [ - { - name: ["-P", "--save-prod"], - description: - "Package will appear in your `dependencies`. This is the default unless `-D` or `-O` are present", - }, - { - name: ["-D", "--save-dev"], - description: "Package will appear in your `devDependencies`", - }, - { - name: ["-O", "--save-optional"], - description: "Package will appear in your `optionalDependencies`", - }, - { - name: "--no-save", - description: "Prevents saving to `dependencies`", - }, - { - name: ["-E", "--save-exact"], - description: - "Saved dependencies will be configured with an exact version rather than using npm's default semver range operator", - }, - { - name: ["-B", "--save-bundle"], - description: - "Saved dependencies will also be added to your bundleDependencies list", - }, - globalOption, - { - name: "--global-style", - description: - "Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder", - }, - { - name: "--legacy-bundling", - description: - "Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package", - }, - { - name: "--legacy-peer-deps", - description: - "Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6", - }, - { - name: "--strict-peer-deps", - description: - "If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure", - }, - { - name: "--no-package-lock", - description: "Ignores package-lock.json files when installing", - }, - registryOption, - verboseOption, - omitOption, - ignoreScriptsOption, - { - name: "--no-audit", - description: - "Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes", - }, - { - name: "--no-bin-links", - description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", - }, - { - name: "--no-fund", - description: - "Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding", - }, - dryRunOption, - ...workSpaceOptions, - ], - }, - { - name: ["run", "run-script"], - description: "Run arbitrary package scripts", - options: [ - ...workSpaceOptions, - { - name: "--if-present", - description: - "Npm will not exit with an error code when run-script is invoked for a script that isn't defined in the scripts section of package.json", - }, - { - name: "--silent", - description: "", - }, - ignoreScriptsOption, - scriptShellOption, - { - name: "--", - args: { - name: "args", - isVariadic: true, - // TODO: load the spec based on the runned script (see yarn spec `yarnScriptParsedDirectives`) - }, - }, - ], - args: { - name: "script", - description: "Script to run from your package.json", - filterStrategy: "fuzzy", - generators: npmScriptsGenerator, - }, - }, - { - name: "init", - description: "Trigger the initialization", - options: [ - { - name: ["-y", "--yes"], - description: - "Automatically answer 'yes' to any prompts that npm might print on the command line", - }, - { - name: "-w", - description: - "Create the folders and boilerplate expected while also adding a reference to your project workspaces property", - args: { name: "dir" }, - }, - ], - }, - { name: "access", description: "Set access controls on private packages" }, - { - name: ["adduser", "login"], - description: "Add a registry user account", - options: [ - registryOption, - { - name: "--scope", - description: - "Associate an operation with a scope for a scoped registry", - args: { - name: "scope", - description: "Scope name", - }, - }, - ], - }, - { - name: "audit", - description: "Run a security audit", - subcommands: [ - { - name: "fix", - description: - "If the fix argument is provided, then remediations will be applied to the package tree", - options: [ - dryRunOption, - { - name: ["-f", "--force"], - description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input", - isDangerous: true, - }, - ...workSpaceOptions, - ], - }, - ], - options: [ - ...workSpaceOptions, - { - name: "--audit-level", - description: - "The minimum level of vulnerability for npm audit to exit with a non-zero exit code", - args: { - name: "audit", - suggestions: [ - "info", - "low", - "moderate", - "high", - "critical", - "none", - ], - }, - }, - { - name: "--package-lock-only", - description: - "Current operation will only use the package-lock.json, ignoring node_modules", - }, - jsonOption, - omitOption, - ], - }, - { - name: "bin", - description: "Print the folder where npm will install executables", - options: [globalOption], - }, - { - name: ["bugs", "issues"], - description: "Report bugs for a package in a web browser", - args: { - name: "package", - isOptional: true, - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [ - { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], - }, - { - name: "--browser", - description: - "The browser that is called by the npm bugs command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], - }, - registryOption, - ], - }, - { - name: "cache", - description: "Manipulates packages cache", - subcommands: [ - { - name: "add", - description: "Add the specified packages to the local cache", - }, - { - name: "clean", - description: "Delete all data out of the cache folder", - }, - { - name: "verify", - description: - "Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data", - }, - ], - options: [ - { - name: "--cache", - args: { name: "cache" }, - description: "The location of npm's cache directory", - }, - ], - }, - { - name: ["ci", "clean-install", "install-clean"], - description: "Install a project with a clean slate", - options: [ - { - name: "--audit", - description: - 'When "true" submit audit reports alongside the current npm command to the default registry and all registries configured for scopes', - args: { - name: "audit", - suggestions: ["true", "false"], - }, - exclusiveOn: ["--no-audit"], - }, - { - name: "--no-audit", - description: - "Do not submit audit reports alongside the current npm command", - exclusiveOn: ["--audit"], - }, - ignoreScriptsOption, - scriptShellOption, - verboseOption, - registryOption, - ], - }, - { - name: "cit", - description: "Install a project with a clean slate and run tests", - }, - { - name: "clean-install-test", - description: "Install a project with a clean slate and run tests", - }, - { name: "completion", description: "Tab completion for npm" }, - { - name: ["config", "c"], - description: "Manage the npm configuration files", - subcommands: [ - { - name: "set", - description: "Sets the config key to the value", - args: [{ name: "key" }, { name: "value" }], - options: [ - { name: ["-g", "--global"], description: "Sets it globally" }, - ], - }, - { - name: "get", - description: "Echo the config value to stdout", - args: { name: "key" }, - }, - { - name: "list", - description: "Show all the config settings", - options: [ - { name: "-g", description: "Lists globally installed packages" }, - { name: "-l", description: "Also shows defaults" }, - jsonOption, - ], - }, - { - name: "delete", - description: "Deletes the key from all configuration files", - args: { name: "key" }, - }, - { - name: "edit", - description: "Opens the config file in an editor", - options: [ - { name: "--global", description: "Edits the global config" }, - ], - }, - ], - }, - { name: "create", description: "Create a package.json file" }, - { - name: ["dedupe", "ddp"], - description: "Reduce duplication in the package tree", - }, - { - name: "deprecate", - description: "Deprecate a version of a package", - options: [registryOption], - }, - { name: "dist-tag", description: "Modify package distribution tags" }, - { - name: ["docs", "home"], - description: "Open documentation for a package in a web browser", - args: { - name: "package", - isOptional: true, - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [ - ...workSpaceOptions, - registryOption, - { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], - }, - { - name: "--browser", - description: - "The browser that is called by the npm docs command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], - }, - ], - }, - { - name: "doctor", - description: "Check your npm environment", - options: [registryOption], - }, - { - name: "edit", - description: "Edit an installed package", - options: [ - { - name: "--editor", - description: "The command to run for npm edit or npm config edit", - }, - ], - }, - { - name: "explore", - description: "Browse an installed package", - args: { - name: "package", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - }, - }, - { name: "fund", description: "Retrieve funding information" }, - { name: "get", description: "Echo the config value to stdout" }, - { - name: "help", - description: "Get help on npm", - args: { - name: "term", - isVariadic: true, - description: "Terms to search for", - }, - options: [ - { - name: "--viewer", - description: "The program to use to view help content", - args: { - name: "viewer", - }, - }, - ], - }, - { - name: "help-search", - description: "Search npm help documentation", - args: { - name: "text", - description: "Text to search for", - }, - options: [longOption], - }, - { name: "hook", description: "Manage registry hooks" }, - { - name: "install-ci-test", - description: "Install a project with a clean slate and run tests", - }, - { name: "install-test", description: "Install package(s) and run tests" }, - { name: "it", description: "Install package(s) and run tests" }, - { - name: "link", - description: "Symlink a package folder", - args: { name: "path", template: "filepaths" }, - }, - { name: "ln", description: "Symlink a package folder" }, - { - name: "logout", - description: "Log out of the registry", - options: [ - registryOption, - { - name: "--scope", - description: - "Associate an operation with a scope for a scoped registry", - args: { - name: "scope", - description: "Scope name", - }, - }, - ], - }, - { - name: ["ls", "list"], - description: "List installed packages", - options: npmListOptions, - args: { name: "[@scope]/pkg", isVariadic: true }, - }, - { - name: "org", - description: "Manage orgs", - subcommands: [ - { - name: "set", - description: "Add a user to an org or manage roles", - args: [ - { - name: "orgname", - description: "Organization name", - }, - { - name: "username", - description: "User name", - }, - { - name: "role", - isOptional: true, - suggestions: ["developer", "admin", "owner"], - }, - ], - options: [registryOption, otpOption], - }, - { - name: "rm", - description: "Remove a user from an org", - args: [ - { - name: "orgname", - description: "Organization name", - }, - { - name: "username", - description: "User name", - }, - ], - options: [registryOption, otpOption], - }, - { - name: "ls", - description: - "List users in an org or see what roles a particular user has in an org", - args: [ - { - name: "orgname", - description: "Organization name", - }, - { - name: "username", - description: "User name", - isOptional: true, - }, - ], - options: [registryOption, otpOption, jsonOption, parseableOption], - }, - ], - }, - { - name: "outdated", - description: "Check for outdated packages", - args: { - name: "[<@scope>/]", - isVariadic: true, - isOptional: true, - }, - options: [ - { - name: ["-a", "-all"], - description: "Show all outdated or installed packages", - }, - jsonOption, - longOption, - parseableOption, - { - name: "-g", - description: "Checks globally", - }, - ...workSpaceOptions, - ], - }, - { - name: ["owner", "author"], - description: "Manage package owners", - subcommands: [ - { - name: "ls", - description: - "List all the users who have access to modify a package and push new versions. Handy when you need to know who to bug for help", - args: { name: "[@scope/]pkg" }, - options: [registryOption], - }, - { - name: "add", - description: - "Add a new user as a maintainer of a package. This user is enabled to modify metadata, publish new versions, and add other owners", - args: [{ name: "user" }, { name: "[@scope/]pkg" }], - options: [registryOption, otpOption], - }, - { - name: "rm", - description: - "Remove a user from the package owner list. This immediately revokes their privileges", - args: [{ name: "user" }, { name: "[@scope/]pkg" }], - options: [registryOption, otpOption], - }, - ], - }, - { - name: "pack", - description: "Create a tarball from a package", - args: { - name: "[<@scope>/]", - }, - options: [ - jsonOption, - dryRunOption, - ...workSpaceOptions, - { - name: "--pack-destination", - description: "Directory in which npm pack will save tarballs", - args: { - name: "pack-destination", - template: ["folders"], - }, - }, - ], - }, - { - name: "ping", - description: "Ping npm registry", - options: [registryOption], - }, - { - name: "pkg", - description: "Manages your package.json", - subcommands: [ - { - name: "get", - description: - "Retrieves a value key, defined in your package.json file. It is possible to get multiple values and values for child fields", - args: { - name: "field", - description: - "Name of the field to get. You can view child fields by separating them with a period", - isVariadic: true, - }, - options: [jsonOption, ...workSpaceOptions], - }, - { - name: "set", - description: - "Sets a value in your package.json based on the field value. It is possible to set multiple values and values for child fields", - args: { - // Format is =. How to achieve this? - name: "field", - description: - "Name of the field to set. You can set child fields by separating them with a period", - isVariadic: true, - }, - options: [ - jsonOption, - ...workSpaceOptions, - { - name: ["-f", "--force"], - description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg", - isDangerous: true, - }, - ], - }, - { - name: "delete", - description: "Deletes a key from your package.json", - args: { - name: "key", - description: - "Name of the key to delete. You can delete child fields by separating them with a period", - isVariadic: true, - }, - options: [ - ...workSpaceOptions, - { - name: ["-f", "--force"], - description: - "Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg", - isDangerous: true, - }, - ], - }, - ], - }, - { - name: "prefix", - description: "Display prefix", - options: [ - { - name: ["-g", "--global"], - description: "Print the global prefix to standard out", - }, - ], - }, - { - name: "profile", - description: "Change settings on your registry profile", - subcommands: [ - { - name: "get", - description: - "Display all of the properties of your profile, or one or more specific properties", - args: { - name: "property", - isOptional: true, - description: "Property name", - }, - options: [registryOption, jsonOption, parseableOption, otpOption], - }, - { - name: "set", - description: "Set the value of a profile property", - args: [ - { - name: "property", - description: "Property name", - suggestions: [ - "email", - "fullname", - "homepage", - "freenode", - "twitter", - "github", - ], - }, - { - name: "value", - description: "Property value", - }, - ], - options: [registryOption, jsonOption, parseableOption, otpOption], - subcommands: [ - { - name: "password", - description: - "Change your password. This is interactive, you'll be prompted for your current password and a new password", - }, - ], - }, - { - name: "enable-2fa", - description: "Enables two-factor authentication", - args: { - name: "mode", - description: - "Mode for two-factor authentication. Defaults to auth-and-writes mode", - isOptional: true, - suggestions: [ - { - name: "auth-only", - description: - "Require an OTP when logging in or making changes to your account's authentication", - }, - { - name: "auth-and-writes", - description: - "Requires an OTP at all the times auth-only does, and also requires one when publishing a module, setting the latest dist-tag, or changing access via npm access and npm owner", - }, - ], - }, - options: [registryOption, otpOption], - }, - { - name: "disable-2fa", - description: "Disables two-factor authentication", - options: [registryOption, otpOption], - }, - ], - }, - { - name: "prune", - description: "Remove extraneous packages", - args: { - name: "[<@scope>/]", - isOptional: true, - }, - options: [ - omitOption, - dryRunOption, - jsonOption, - { - name: "--production", - description: "Remove the packages specified in your devDependencies", - }, - ...workSpaceOptions, - ], - }, - { - name: "publish", - description: "Publish a package", - args: { - name: "tarball|folder", - isOptional: true, - description: - "A url or file path to a gzipped tar archive containing a single folder with a package.json file inside | A folder containing a package.json file", - template: ["folders"], - }, - options: [ - { - name: "--tag", - description: "Registers the published package with the given tag", - args: { name: "tag" }, - }, - ...workSpaceOptions, - { - name: "--access", - description: - "Sets scoped package to be publicly viewable if set to 'public'", - args: { - default: "restricted", - suggestions: ["restricted", "public"], - }, - }, - dryRunOption, - otpOption, - ], - }, - { - name: ["rebuild", "rb"], - description: "Rebuild a package", - args: { - name: "[<@scope>/][@]", - }, - options: [ - globalOption, - ...workSpaceOptions, - ignoreScriptsOption, - { - name: "--no-bin-links", - description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", - }, - ], - }, - { - name: "repo", - description: "Open package repository page in the browser", - args: { - name: "package", - isOptional: true, - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [ - ...workSpaceOptions, - { - name: "--no-browser", - description: "Display in command line instead of browser", - exclusiveOn: ["--browser"], - }, - { - name: "--browser", - description: - "The browser that is called by the npm repo command to open websites", - args: { name: "browser" }, - exclusiveOn: ["--no-browser"], - }, - ], - }, - { - name: "restart", - description: "Restart a package", - options: [ - ignoreScriptsOption, - scriptShellOption, - { - name: "--", - args: { - name: "arg", - description: "Arguments to be passed to the restart script", - }, - }, - ], - }, - { - name: "root", - description: "Display npm root", - options: [ - { - name: ["-g", "--global"], - description: - "Print the effective global node_modules folder to standard out", - }, - ], - }, - { - name: ["search", "s", "se", "find"], - description: "Search for packages", - args: { - name: "search terms", - isVariadic: true, - }, - options: [ - longOption, - jsonOption, - { - name: "--color", - description: "Show colors", - args: { - name: "always", - suggestions: ["always"], - description: "Always show colors", - }, - exclusiveOn: ["--no-color"], - }, - { - name: "--no-color", - description: "Do not show colors", - exclusiveOn: ["--color"], - }, - parseableOption, - { - name: "--no-description", - description: "Do not show descriptions", - }, - { - name: "--searchopts", - description: - "Space-separated options that are always passed to search", - args: { - name: "searchopts", - }, - }, - { - name: "--searchexclude", - description: - "Space-separated options that limit the results from search", - args: { - name: "searchexclude", - }, - }, - registryOption, - { - name: "--prefer-online", - description: - "If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data", - exclusiveOn: ["--prefer-offline", "--offline"], - }, - { - name: "--prefer-offline", - description: - "If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server", - exclusiveOn: ["--prefer-online", "--offline"], - }, - { - name: "--offline", - description: - "Force offline mode: no network requests will be done during install", - exclusiveOn: ["--prefer-online", "--prefer-offline"], - }, - ], - }, - { name: "set", description: "Sets the config key to the value" }, - { - name: "set-script", - description: "Set tasks in the scripts section of package.json", - args: [ - { - name: "script", - description: - "Name of the task to be added to the scripts section of package.json", - }, - { - name: "command", - description: "Command to run when script is called", - }, - ], - options: workSpaceOptions, - }, - { - name: "shrinkwrap", - description: "Lock down dependency versions for publication", - }, - { - name: "star", - description: "Mark your favorite packages", - args: { - name: "pkg", - description: "Package to mark as favorite", - }, - options: [ - registryOption, - { - name: "--no-unicode", - description: "Do not use unicode characters in the tree output", - }, - ], - }, - { - name: "stars", - description: "View packages marked as favorites", - args: { - name: "user", - isOptional: true, - description: "View packages marked as favorites by ", - }, - options: [registryOption], - }, - { - name: "start", - description: "Start a package", - options: [ - ignoreScriptsOption, - scriptShellOption, - { - name: "--", - args: { - name: "arg", - description: "Arguments to be passed to the start script", - }, - }, - ], - }, - { - name: "stop", - description: "Stop a package", - options: [ - ignoreScriptsOption, - scriptShellOption, - { - name: "--", - args: { - name: "arg", - description: "Arguments to be passed to the stop script", - }, - }, - ], - }, - { - name: "team", - description: "Manage organization teams and team memberships", - subcommands: [ - { - name: "create", - args: { name: "scope:team" }, - options: [registryOption, otpOption], - }, - { - name: "destroy", - args: { name: "scope:team" }, - options: [registryOption, otpOption], - }, - { - name: "add", - args: [{ name: "scope:team" }, { name: "user" }], - options: [registryOption, otpOption], - }, - { - name: "rm", - args: [{ name: "scope:team" }, { name: "user" }], - options: [registryOption, otpOption], - }, - { - name: "ls", - args: { name: "scope|scope:team" }, - options: [registryOption, jsonOption, parseableOption], - }, - ], - }, - { - name: ["test", "tst", "t"], - description: "Test a package", - options: [ignoreScriptsOption, scriptShellOption], - }, - { - name: "token", - description: "Manage your authentication tokens", - subcommands: [ - { - name: "list", - description: "Shows a table of all active authentication tokens", - options: [jsonOption, parseableOption], - }, - { - name: "create", - description: "Create a new authentication token", - options: [ - { - name: "--read-only", - description: - "This is used to mark a token as unable to publish when configuring limited access tokens with the npm token create command", - }, - { - name: "--cidr", - description: - "This is a list of CIDR address to be used when configuring limited access tokens with the npm token create command", - isRepeatable: true, - args: { - name: "cidr", - }, - }, - ], - }, - { - name: "revoke", - description: - "Immediately removes an authentication token from the registry. You will no longer be able to use it", - args: { name: "idtoken" }, - }, - ], - options: [registryOption, otpOption], - }, - uninstallSubcommand("uninstall"), - uninstallSubcommand(["r", "rm"]), - uninstallSubcommand("un"), - uninstallSubcommand("remove"), - uninstallSubcommand("unlink"), - { - name: "unpublish", - description: "Remove a package from the registry", - args: { - name: "[<@scope>/][@]", - }, - options: [ - dryRunOption, - { - name: ["-f", "--force"], - description: - "Allow unpublishing all versions of a published package. Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input", - isDangerous: true, - }, - ...workSpaceOptions, - ], - }, - { - name: "unstar", - description: "Remove an item from your favorite packages", - args: { - name: "pkg", - description: "Package to unmark as favorite", - }, - options: [ - registryOption, - otpOption, - { - name: "--no-unicode", - description: "Do not use unicode characters in the tree output", - }, - ], - }, - { - name: ["update", "upgrade", "up"], - description: "Update a package", - options: [ - { name: "-g", description: "Update global package" }, - { - name: "--global-style", - description: - "Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder", - }, - { - name: "--legacy-bundling", - description: - "Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package", - }, - { - name: "--strict-peer-deps", - description: - "If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure", - }, - { - name: "--no-package-lock", - description: "Ignores package-lock.json files when installing", - }, - omitOption, - ignoreScriptsOption, - { - name: "--no-audit", - description: - "Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes", - }, - { - name: "--no-bin-links", - description: - "Tells npm to not create symlinks (or .cmd shims on Windows) for package executables", - }, - { - name: "--no-fund", - description: - "Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding", - }, - { - name: "--save", - description: - "Update the semver values of direct dependencies in your project package.json", - }, - dryRunOption, - ...workSpaceOptions, - ], - }, - { - name: "version", - description: "Bump a package version", - options: [ - ...workSpaceOptions, - jsonOption, - { - name: "--allow-same-version", - description: - "Prevents throwing an error when npm version is used to set the new version to the same value as the current version", - }, - { - name: "--no-commit-hooks", - description: - "Do not run git commit hooks when using the npm version command", - }, - { - name: "--no-git-tag-version", - description: - "Do not tag the commit when using the npm version command", - }, - { - name: "--preid", - description: - 'The "prerelease identifier" to use as a prefix for the "prerelease" part of a semver. Like the rc in 1.2.0-rc.8', - args: { - name: "prerelease-id", - }, - }, - { - name: "--sign-git-tag", - description: - "If set to true, then the npm version command will tag the version using -s to add a signature", - }, - ], - }, - { - name: ["view", "v", "info", "show"], - description: "View registry info", - options: [...workSpaceOptions, jsonOption], - }, - { - name: "whoami", - description: "Display npm username", - options: [registryOption], - }, - ], -}; - -export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts b/extensions/terminal-suggest/src/completions/upstream/pnpm.ts deleted file mode 100644 index 9ce7c798208..00000000000 --- a/extensions/terminal-suggest/src/completions/upstream/pnpm.ts +++ /dev/null @@ -1,1027 +0,0 @@ -// GENERATORS - -import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; -import { dependenciesGenerator, nodeClis } from "./yarn"; - -const filterMessages = (out: string): string => { - return out.startsWith("warning:") || out.startsWith("error:") - ? out.split("\n").slice(1).join("\n") - : out; -}; - -const searchBranches: Fig.Generator = { - script: ["git", "branch", "--no-color"], - postProcess: function (out) { - const output = filterMessages(out); - - if (output.startsWith("fatal:")) { - return []; - } - - return output.split("\n").map((elm) => { - let name = elm.trim(); - const parts = elm.match(/\S+/g); - if (parts && parts.length > 1) { - if (parts[0] == "*") { - // Current branch. - return { - name: elm.replace("*", "").trim(), - description: "Current branch", - icon: "⭐️", - }; - } else if (parts[0] == "+") { - // Branch checked out in another worktree. - name = elm.replace("+", "").trim(); - } - } - - return { - name, - description: "Branch", - icon: "fig://icon?type=git", - }; - }); - }, -}; - -const generatorInstalledPackages: Fig.Generator = { - script: ["pnpm", "ls"], - postProcess: function (out) { - /** - * out - * @example - * ``` - * Legend: production dependency, optional only, dev only - * - * /xxxx/xxxx/ (PRIVATE) - * - * dependencies: - * lodash 4.17.21 - * foo link:packages/foo - * - * devDependencies: - * typescript 4.7.4 - * ``` - */ - if (out.includes("ERR_PNPM")) { - return []; - } - - const output = out - .split("\n") - .slice(3) - // remove empty lines, "*dependencies:" lines, local workspace packages (eg: "foo":"workspace:*") - .filter( - (item) => - !!item && - !item.toLowerCase().includes("dependencies") && - !item.includes("link:") - ) - .map((item) => item.replace(/\s/, "@")); // typescript 4.7.4 -> typescript@4.7.4 - - return output.map((pkg) => { - return { - name: pkg, - icon: "fig://icon?type=package", - }; - }); - }, -}; - -const FILTER_OPTION: Fig.Option = { - name: "--filter", - args: { - template: "filepaths", - name: "Filepath / Package", - description: - "To only select packages under the specified directory, you may specify any absolute path, typically in POSIX format", - }, - description: `Filtering allows you to restrict commands to specific subsets of packages. -pnpm supports a rich selector syntax for picking packages by name or by relation. -More details: https://pnpm.io/filtering`, -}; - -/** Options that being appended for `pnpm i` and `add` */ -const INSTALL_BASE_OPTIONS: Fig.Option[] = [ - { - name: "--offline", - description: - "If true, pnpm will use only packages already available in the store. If a package won't be found locally, the installation will fail", - }, - { - name: "--prefer-offline", - description: - "If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server. To force full offline mode, use --offline", - }, - { - name: "--ignore-scripts", - description: - "Do not execute any scripts defined in the project package.json and its dependencies", - }, - { - name: "--reporter", - description: `Allows you to choose the reporter that will log debug info to the terminal about the installation progress`, - args: { - name: "Reporter Type", - suggestions: ["silent", "default", "append-only", "ndjson"], - }, - }, -]; - -/** Base options for pnpm i when run without any arguments */ -const INSTALL_OPTIONS: Fig.Option[] = [ - { - name: ["-P", "--save-prod"], - description: `Pnpm will not install any package listed in devDependencies if the NODE_ENV environment variable is set to production. -Use this flag to instruct pnpm to ignore NODE_ENV and take its production status from this flag instead`, - }, - { - name: ["-D", "--save-dev"], - description: - "Only devDependencies are installed regardless of the NODE_ENV", - }, - { - name: "--no-optional", - description: "OptionalDependencies are not installed", - }, - { - name: "--lockfile-only", - description: - "When used, only updates pnpm-lock.yaml and package.json instead of checking node_modules and downloading dependencies", - }, - { - name: "--frozen-lockfile", - description: - "If true, pnpm doesn't generate a lockfile and fails to install if the lockfile is out of sync with the manifest / an update is needed or no lockfile is present", - }, - { - name: "--use-store-server", - description: - "Starts a store server in the background. The store server will keep running after installation is done. To stop the store server, run pnpm server stop", - }, - { - name: "--shamefully-hoist", - description: - "Creates a flat node_modules structure, similar to that of npm or yarn. WARNING: This is highly discouraged", - }, -]; - -/** Base options for pnpm add */ -const INSTALL_PACKAGE_OPTIONS: Fig.Option[] = [ - { - name: ["-P", "--save-prod"], - description: "Install the specified packages as regular dependencies", - }, - { - name: ["-D", "--save-dev"], - description: "Install the specified packages as devDependencies", - }, - { - name: ["-O", "--save-optional"], - description: "Install the specified packages as optionalDependencies", - }, - { - name: "--no-save", - description: "Prevents saving to `dependencies`", - }, - { - name: ["-E", "--save-exact"], - description: - "Saved dependencies will be configured with an exact version rather than using pnpm's default semver range operator", - }, - { - name: "--save-peer", - description: - "Using --save-peer will add one or more packages to peerDependencies and install them as dev dependencies", - }, - { - name: ["--ignore-workspace-root-check", "-W#"], - description: `Adding a new dependency to the root workspace package fails, unless the --ignore-workspace-root-check or -W flag is used. -For instance, pnpm add debug -W`, - }, - { - name: ["--global", "-g"], - description: `Install a package globally`, - }, - { - name: "--workspace", - description: `Only adds the new dependency if it is found in the workspace`, - }, - FILTER_OPTION, -]; - -// SUBCOMMANDS -const SUBCOMMANDS_MANAGE_DEPENDENCIES: Fig.Subcommand[] = [ - { - name: "add", - description: `Installs a package and any packages that it depends on. By default, any new package is installed as a production dependency`, - args: { - name: "package", - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [...INSTALL_BASE_OPTIONS, ...INSTALL_PACKAGE_OPTIONS], - }, - { - name: ["install", "i"], - description: `Pnpm install is used to install all dependencies for a project. -In a CI environment, installation fails if a lockfile is present but needs an update. -Inside a workspace, pnpm install installs all dependencies in all the projects. -If you want to disable this behavior, set the recursive-install setting to false`, - async generateSpec(tokens) { - // `pnpm i` with args is an `pnpm add` alias - const hasArgs = - tokens.filter((token) => token.trim() !== "" && !token.startsWith("-")) - .length > 2; - - return { - name: "install", - options: [ - ...INSTALL_BASE_OPTIONS, - ...(hasArgs ? INSTALL_PACKAGE_OPTIONS : INSTALL_OPTIONS), - ], - }; - }, - args: { - name: "package", - isOptional: true, - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - }, - { - name: ["install-test", "it"], - description: - "Runs pnpm install followed immediately by pnpm test. It takes exactly the same arguments as pnpm install", - options: [...INSTALL_BASE_OPTIONS, ...INSTALL_OPTIONS], - }, - { - name: ["update", "upgrade", "up"], - description: `Pnpm update updates packages to their latest version based on the specified range. -When used without arguments, updates all dependencies. You can use patterns to update specific dependencies`, - args: { - name: "Package", - isOptional: true, - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - options: [ - { - name: ["--recursive", "-r"], - description: - "Concurrently runs update in all subdirectories with a package.json (excluding node_modules)", - }, - { - name: ["--latest", "-L"], - description: - "Ignores the version range specified in package.json. Instead, the version specified by the latest tag will be used (potentially upgrading the packages across major versions)", - }, - { - name: "--global", - description: "Update global packages", - }, - { - name: ["-P", "--save-prod"], - description: `Only update packages in dependencies and optionalDependencies`, - }, - { - name: ["-D", "--save-dev"], - description: "Only update packages in devDependencies", - }, - { - name: "--no-optional", - description: "Don't update packages in optionalDependencies", - }, - { - name: ["--interactive", "-i"], - description: - "Show outdated dependencies and select which ones to update", - }, - { - name: "--workspace", - description: `Tries to link all packages from the workspace. Versions are updated to match the versions of packages inside the workspace. -If specific packages are updated, the command will fail if any of the updated dependencies are not found inside the workspace. For instance, the following command fails if express is not a workspace package: pnpm up -r --workspace express`, - }, - FILTER_OPTION, - ], - }, - { - name: ["remove", "rm", "uninstall", "un"], - description: `Removes packages from node_modules and from the project's package.json`, - args: { - name: "Package", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - options: [ - { - name: ["--recursive", "-r"], - description: `When used inside a workspace, removes a dependency (or dependencies) from every workspace package. -When used not inside a workspace, removes a dependency (or dependencies) from every package found in subdirectories`, - }, - { - name: "--global", - description: "Remove a global package", - }, - { - name: ["-P", "--save-prod"], - description: `Only remove the dependency from dependencies`, - }, - { - name: ["-D", "--save-dev"], - description: "Only remove the dependency from devDependencies", - }, - { - name: ["--save-optional", "-O"], - description: "Only remove the dependency from optionalDependencies", - }, - FILTER_OPTION, - ], - }, - { - name: ["link", "ln"], - description: `Makes the current local package accessible system-wide, or in another location`, - args: [ - { - name: "Package", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - { template: "filepaths" }, - ], - options: [ - { - name: ["--dir", "-C"], - description: `Changes the link location to `, - }, - { - name: "--global", - description: - "Links the specified package () from global node_modules to the node_nodules of package from where this command was executed or specified via --dir option", - }, - ], - }, - { - name: "unlink", - description: `Unlinks a system-wide package (inverse of pnpm link). -If called without arguments, all linked dependencies will be unlinked. -This is similar to yarn unlink, except pnpm re-installs the dependency after removing the external link`, - args: [ - { - name: "Package", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - { template: "filepaths" }, - ], - options: [ - { - name: ["--recursive", "-r"], - description: `Unlink in every package found in subdirectories or in every workspace package, when executed inside a workspace`, - }, - FILTER_OPTION, - ], - }, - { - name: "import", - description: - "Pnpm import generates a pnpm-lock.yaml from an npm package-lock.json (or npm-shrinkwrap.json) file", - }, - { - name: ["rebuild", "rb"], - description: `Rebuild a package`, - args: [ - { - name: "Package", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - { template: "filepaths" }, - ], - options: [ - { - name: ["--recursive", "-r"], - description: `This command runs the pnpm rebuild command in every package of the monorepo`, - }, - FILTER_OPTION, - ], - }, - { - name: "prune", - description: `Removes unnecessary packages`, - options: [ - { - name: "--prod", - description: `Remove the packages specified in devDependencies`, - }, - { - name: "--no-optional", - description: `Remove the packages specified in optionalDependencies`, - }, - ], - }, - { - name: "fetch", - description: `EXPERIMENTAL FEATURE: Fetch packages from a lockfile into virtual store, package manifest is ignored: https://pnpm.io/cli/fetch`, - options: [ - { - name: "--prod", - description: `Development packages will not be fetched`, - }, - { - name: "--dev", - description: `Only development packages will be fetched`, - }, - ], - }, - { - name: "patch", - description: `This command will cause a package to be extracted in a temporary directory intended to be editable at will`, - args: { - name: "package", - generators: generatorInstalledPackages, - }, - options: [ - { - name: "--edit-dir", - description: `The package that needs to be patched will be extracted to this directory`, - }, - ], - }, - { - name: "patch-commit", - args: { - name: "dir", - }, - description: `Generate a patch out of a directory`, - }, - { - name: "patch-remove", - args: { - name: "package", - isVariadic: true, - // TODO: would be nice to have a generator of all patched packages - }, - }, -]; - -const SUBCOMMANDS_RUN_SCRIPTS: Fig.Subcommand[] = [ - { - name: ["run", "run-script"], - description: "Runs a script defined in the package's manifest file", - args: { - name: "Scripts", - filterStrategy: "fuzzy", - generators: npmScriptsGenerator, - isVariadic: true, - }, - options: [ - { - name: ["-r", "--recursive"], - description: `This runs an arbitrary command from each package's "scripts" object. If a package doesn't have the command, it is skipped. If none of the packages have the command, the command fails`, - }, - { - name: "--if-present", - description: - "You can use the --if-present flag to avoid exiting with a non-zero exit code when the script is undefined. This lets you run potentially undefined scripts without breaking the execution chain", - }, - { - name: "--parallel", - description: - "Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process", - }, - { - name: "--stream", - description: - "Stream output from child processes immediately, prefixed with the originating package directory. This allows output from different packages to be interleaved", - }, - FILTER_OPTION, - ], - }, - { - name: "exec", - description: `Execute a shell command in scope of a project. -node_modules/.bin is added to the PATH, so pnpm exec allows executing commands of dependencies`, - args: { - name: "Scripts", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - options: [ - { - name: ["-r", "--recursive"], - description: `Execute the shell command in every project of the workspace. -The name of the current package is available through the environment variable PNPM_PACKAGE_NAME (supported from pnpm v2.22.0 onwards)`, - }, - { - name: "--parallel", - description: - "Completely disregard concurrency and topological sorting, running a given script immediately in all matching packages with prefixed streaming output. This is the preferred flag for long-running processes over many packages, for instance, a lengthy build process", - }, - FILTER_OPTION, - ], - }, - { - name: ["test", "t", "tst"], - description: `Runs an arbitrary command specified in the package's test property of its scripts object. -The intended usage of the property is to specify a command that runs unit or integration testing for your program`, - }, - { - name: "start", - description: `Runs an arbitrary command specified in the package's start property of its scripts object. If no start property is specified on the scripts object, it will attempt to run node server.js as a default, failing if neither are present. -The intended usage of the property is to specify a command that starts your program`, - }, -]; - -const SUBCOMMANDS_REVIEW_DEPS: Fig.Subcommand[] = [ - { - name: "audit", - description: `Checks for known security issues with the installed packages. -If security issues are found, try to update your dependencies via pnpm update. -If a simple update does not fix all the issues, use overrides to force versions that are not vulnerable. -For instance, if lodash@<2.1.0 is vulnerable, use overrides to force lodash@^2.1.0. -Details at: https://pnpm.io/cli/audit`, - options: [ - { - name: "--audit-level", - description: `Only print advisories with severity greater than or equal to `, - args: { - name: "Audit Level", - default: "low", - suggestions: ["low", "moderate", "high", "critical"], - }, - }, - { - name: "--fix", - description: - "Add overrides to the package.json file in order to force non-vulnerable versions of the dependencies", - }, - { - name: "--json", - description: `Output audit report in JSON format`, - }, - { - name: ["--dev", "-D"], - description: `Only audit dev dependencies`, - }, - { - name: ["--prod", "-P"], - description: `Only audit production dependencies`, - }, - { - name: "--no-optional", - description: `Don't audit optionalDependencies`, - }, - { - name: "--ignore-registry-errors", - description: `If the registry responds with a non-200 status code, the process should exit with 0. So the process will fail only if the registry actually successfully responds with found vulnerabilities`, - }, - ], - }, - { - name: ["list", "ls"], - description: `This command will output all the versions of packages that are installed, as well as their dependencies, in a tree-structure. -Positional arguments are name-pattern@version-range identifiers, which will limit the results to only the packages named. For example, pnpm list "babel-*" "eslint-*" semver@5`, - options: [ - { - name: ["--recursive", "-r"], - description: `Perform command on every package in subdirectories or on every workspace package, when executed inside a workspace`, - }, - { - name: "--json", - description: `Log output in JSON format`, - }, - { - name: "--long", - description: `Show extended information`, - }, - { - name: "--parseable", - description: `Outputs package directories in a parseable format instead of their tree view`, - }, - { - name: "--global", - description: `List packages in the global install directory instead of in the current project`, - }, - { - name: "--depth", - description: `Max display depth of the dependency tree. -pnpm ls --depth 0 will list direct dependencies only. pnpm ls --depth -1 will list projects only. Useful inside a workspace when used with the -r option`, - args: { name: "number" }, - }, - { - name: ["--dev", "-D"], - description: `Only list dev dependencies`, - }, - { - name: ["--prod", "-P"], - description: `Only list production dependencies`, - }, - { - name: "--no-optional", - description: `Don't list optionalDependencies`, - }, - FILTER_OPTION, - ], - }, - { - name: "outdated", - description: `Checks for outdated packages. The check can be limited to a subset of the installed packages by providing arguments (patterns are supported)`, - options: [ - { - name: ["--recursive", "-r"], - description: `Check for outdated dependencies in every package found in subdirectories, or in every workspace package when executed inside a workspace`, - }, - { - name: "--long", - description: `Print details`, - }, - { - name: "--global", - description: `List outdated global packages`, - }, - { - name: "--no-table", - description: `Prints the outdated dependencies in a list format instead of the default table. Good for small consoles`, - }, - { - name: "--compatible", - description: `Prints only versions that satisfy specifications in package.json`, - }, - { - name: ["--dev", "-D"], - description: `Only list dev dependencies`, - }, - { - name: ["--prod", "-P"], - description: `Only list production dependencies`, - }, - { - name: "--no-optional", - description: `Doesn't check optionalDependencies`, - }, - ], - }, - { - name: "why", - description: `Shows all packages that depend on the specified package`, - args: { - name: "Scripts", - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - options: [ - { - name: ["--recursive", "-r"], - description: `Show the dependency tree for the specified package on every package in subdirectories or on every workspace package when executed inside a workspace`, - }, - { - name: "--json", - description: `Log output in JSON format`, - }, - { - name: "--long", - description: `Show verbose output`, - }, - { - name: "--parseable", - description: `Show parseable output instead of tree view`, - }, - { - name: "--global", - description: `List packages in the global install directory instead of in the current project`, - }, - { - name: ["--dev", "-D"], - description: `Only display the dependency tree for packages in devDependencies`, - }, - { - name: ["--prod", "-P"], - description: `Only display the dependency tree for packages in dependencies`, - }, - FILTER_OPTION, - ], - }, -]; - -const SUBCOMMANDS_MISC: Fig.Subcommand[] = [ - { - name: "publish", - description: `Publishes a package to the registry. -When publishing a package inside a workspace, the LICENSE file from the root of the workspace is packed with the package (unless the package has a license of its own). -You may override some fields before publish, using the publishConfig field in package.json. You also can use the publishConfig.directory to customize the published subdirectory (usually using third party build tools). -When running this command recursively (pnpm -r publish), pnpm will publish all the packages that have versions not yet published to the registry`, - args: { - name: "Branch", - generators: searchBranches, - }, - options: [ - { - name: "--tag", - description: `Publishes the package with the given tag. By default, pnpm publish updates the latest tag`, - args: { - name: "", - }, - }, - { - name: "--dry-run", - description: `Does everything a publish would do except actually publishing to the registry`, - }, - { - name: "--ignore-scripts", - description: `Ignores any publish related lifecycle scripts (prepublishOnly, postpublish, and the like)`, - }, - { - name: "--no-git-checks", - description: `Don't check if current branch is your publish branch, clean, and up-to-date`, - }, - { - name: "--access", - description: `Tells the registry whether the published package should be public or restricted`, - args: { - name: "Type", - suggestions: ["public", "private"], - }, - }, - { - name: "--force", - description: `Try to publish packages even if their current version is already found in the registry`, - }, - { - name: "--report-summary", - description: `Save the list of published packages to pnpm-publish-summary.json. Useful when some other tooling is used to report the list of published packages`, - }, - FILTER_OPTION, - ], - }, - { - name: ["recursive", "m", "multi", "-r"], - description: `Runs a pnpm command recursively on all subdirectories in the package or every available workspace`, - options: [ - { - name: "--link-workspace-packages", - description: `Link locally available packages in workspaces of a monorepo into node_modules instead of re-downloading them from the registry. This emulates functionality similar to yarn workspaces. -When this is set to deep, local packages can also be linked to subdependencies. -Be advised that it is encouraged instead to use npmrc for this setting, to enforce the same behaviour in all environments. This option exists solely so you may override that if necessary`, - args: { - name: "bool or `deep`", - suggestions: ["dee["], - }, - }, - { - name: "--workspace-concurrency", - description: `Set the maximum number of tasks to run simultaneously. For unlimited concurrency use Infinity`, - args: { name: "" }, - }, - { - name: "--bail", - description: `Stops when a task throws an error`, - }, - { - name: "--no-bail", - description: `Don't stop when a task throws an error`, - }, - { - name: "--sort", - description: `Packages are sorted topologically (dependencies before dependents)`, - }, - { - name: "--no-sort", - description: `Disable packages sorting`, - }, - { - name: "--reverse", - description: `The order of packages is reversed`, - }, - FILTER_OPTION, - ], - }, - { - name: "server", - description: `Manage a store server`, - subcommands: [ - { - name: "start", - description: - "Starts a server that performs all interactions with the store. Other commands will delegate any store-related tasks to this server", - options: [ - { - name: "--background", - description: `Runs the server in the background, similar to daemonizing on UNIX systems`, - }, - { - name: "--network-concurrency", - description: `The maximum number of network requests to process simultaneously`, - args: { name: "number" }, - }, - { - name: "--protocol", - description: `The communication protocol used by the server. When this is set to auto, IPC is used on all systems except for Windows, which uses TCP`, - args: { - name: "Type", - suggestions: ["auto", "tcp", "ipc"], - }, - }, - { - name: "--port", - description: `The port number to use when TCP is used for communication. If a port is specified and the protocol is set to auto, regardless of system type, the protocol is automatically set to use TCP`, - args: { name: "port number" }, - }, - { - name: "--store-dir", - description: `The directory to use for the content addressable store`, - args: { name: "Path", template: "filepaths" }, - }, - { - name: "--lock", - description: `Set to make the package store immutable to external processes while the server is running or not`, - }, - { - name: "--no-lock", - description: `Set to make the package store mutable to external processes while the server is running or not`, - }, - { - name: "--ignore-stop-requests", - description: `Prevents you from stopping the server using pnpm server stop`, - }, - { - name: "--ignore-upload-requests", - description: `Prevents creating a new side effect cache during install`, - }, - ], - }, - { - name: "stop", - description: "Stops the store server", - }, - { - name: "status", - description: "Prints information about the running server", - }, - ], - }, - { - name: "store", - description: "Managing the package store", - subcommands: [ - { - name: "status", - description: `Checks for modified packages in the store. -Returns exit code 0 if the content of the package is the same as it was at the time of unpacking`, - }, - { - name: "add", - description: `Functionally equivalent to pnpm add, -except this adds new packages to the store directly without modifying any projects or files outside of the store`, - }, - { - name: "prune", - description: `Removes orphan packages from the store. -Pruning the store will save disk space, however may slow down future installations involving pruned packages. -Ultimately, it is a safe operation, however not recommended if you have orphaned packages from a package you intend to reinstall. -Please read the FAQ for more information on unreferenced packages and best practices. -Please note that this is prohibited when a store server is running`, - }, - { - name: "path", - description: `Returns the path to the active store directory`, - }, - ], - }, - { - name: "init", - description: - "Creates a basic package.json file in the current directory, if it doesn't exist already", - }, - { - name: "doctor", - description: "Checks for known common issues with pnpm configuration", - }, -]; - -const subcommands = [ - ...SUBCOMMANDS_MANAGE_DEPENDENCIES, - ...SUBCOMMANDS_REVIEW_DEPS, - ...SUBCOMMANDS_RUN_SCRIPTS, - ...SUBCOMMANDS_MISC, -]; - -const recursiveSubcommandsNames = [ - "add", - "exec", - "install", - "list", - "outdated", - "publish", - "rebuild", - "remove", - "run", - "test", - "unlink", - "update", - "why", -]; - -const recursiveSubcommands = subcommands.filter((subcommand) => { - if (Array.isArray(subcommand.name)) { - return subcommand.name.some((name) => - recursiveSubcommandsNames.includes(name) - ); - } - return recursiveSubcommandsNames.includes(subcommand.name); -}); - -// RECURSIVE SUBCOMMAND INDEX -SUBCOMMANDS_MISC[1].subcommands = recursiveSubcommands; - -// common options -const COMMON_OPTIONS: Fig.Option[] = [ - { - name: ["-C", "--dir"], - args: { - name: "path", - template: "folders", - }, - isPersistent: true, - description: - "Run as if pnpm was started in instead of the current working directory", - }, - { - name: ["-w", "--workspace-root"], - args: { - name: "workspace", - }, - isPersistent: true, - description: - "Run as if pnpm was started in the root of the instead of the current working directory", - }, - { - name: ["-h", "--help"], - isPersistent: true, - description: "Output usage information", - }, - { - name: ["-v", "--version"], - description: "Show pnpm's version", - }, -]; - -// SPEC -const completionSpec: Fig.Spec = { - name: "pnpm", - description: "Fast, disk space efficient package manager", - args: { - name: "Scripts", - filterStrategy: "fuzzy", - generators: npmScriptsGenerator, - isVariadic: true, - }, - filterStrategy: "fuzzy", - generateSpec: async (tokens, executeShellCommand) => { - const { script, postProcess } = dependenciesGenerator as Fig.Generator & { - script: string[]; - }; - - if (postProcess === undefined) { - return undefined; - } - - const packages = postProcess( - ( - await executeShellCommand({ - command: script[0], - args: script.slice(1), - }) - ).stdout, - tokens - ) - ?.filter((e) => e !== null) - .map(({ name }) => name as string); - - const subcommands = packages - ?.filter((name) => nodeClis.has(name)) - .map((name) => ({ - name, - loadSpec: name, - icon: "fig://icon?type=package", - })); - - return { - name: "pnpm", - subcommands, - } as Fig.Spec; - }, - subcommands, - options: COMMON_OPTIONS, -}; - -export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/upstream/yarn.ts b/extensions/terminal-suggest/src/completions/upstream/yarn.ts deleted file mode 100644 index 04c573a151b..00000000000 --- a/extensions/terminal-suggest/src/completions/upstream/yarn.ts +++ /dev/null @@ -1,1674 +0,0 @@ -import { npmScriptsGenerator, npmSearchGenerator } from "./npm"; - -export const yarnScriptParserDirectives: Fig.Arg["parserDirectives"] = { - alias: async (token, executeShellCommand) => { - const npmPrefix = await executeShellCommand({ - command: "npm", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["prefix"], - }); - if (npmPrefix.status !== 0) { - throw new Error("npm prefix command failed"); - } - const packageJson = await executeShellCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: [`${npmPrefix.stdout.trim()}/package.json`], - }); - const script: string = JSON.parse(packageJson.stdout).scripts?.[token]; - if (!script) { - throw new Error(`Script not found: '${token}'`); - } - return script; - }, -}; - -export const nodeClis = new Set([ - "vue", - "vite", - "nuxt", - "react-native", - "degit", - "expo", - "jest", - "next", - "electron", - "prisma", - "eslint", - "prettier", - "tsc", - "typeorm", - "babel", - "remotion", - "autocomplete-tools", - "redwood", - "rw", - "create-completion-spec", - "publish-spec-to-team", - "capacitor", - "cap", -]); - -// generate global package list from global package.json file -const getGlobalPackagesGenerator: Fig.Generator = { - custom: async (tokens, executeCommand, generatorContext) => { - const { stdout: yarnGlobalDir } = await executeCommand({ - command: "yarn", - args: ["global", "dir"], - }); - - const { stdout } = await executeCommand({ - command: "cat", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: [`${yarnGlobalDir.trim()}/package.json`], - }); - - if (stdout.trim() == "") return []; - - try { - const packageContent = JSON.parse(stdout); - const dependencyScripts = packageContent["dependencies"] || {}; - const devDependencyScripts = packageContent["devDependencies"] || {}; - const dependencies = [ - ...Object.keys(dependencyScripts), - ...Object.keys(devDependencyScripts), - ]; - - const filteredDependencies = dependencies.filter( - (dependency) => !tokens.includes(dependency) - ); - - return filteredDependencies.map((dependencyName) => ({ - name: dependencyName, - icon: "📦", - })); - } catch (e) {} - - return []; - }, -}; - -// generate package list of direct and indirect dependencies -const allDependenciesGenerator: Fig.Generator = { - script: ["yarn", "list", "--depth=0", "--json"], - postProcess: (out) => { - if (out.trim() == "") return []; - - try { - const packageContent = JSON.parse(out); - const dependencies = packageContent.data.trees; - return dependencies.map((dependency: { name: string }) => ({ - name: dependency.name.split("@")[0], - icon: "📦", - })); - } catch (e) {} - return []; - }, -}; - -const configList: Fig.Generator = { - script: ["yarn", "config", "list"], - postProcess: function (out) { - if (out.trim() == "") { - return []; - } - - try { - const startIndex = out.indexOf("{"); - const endIndex = out.indexOf("}"); - let output = out.substring(startIndex, endIndex + 1); - // TODO: fix hacky code - // reason: JSON parse was not working without double quotes - output = output - .replace(/\'/gi, '"') - .replace("lastUpdateCheck", '"lastUpdateCheck"') - .replace("registry", '"lastUpdateCheck"'); - const configObject = JSON.parse(output); - if (configObject) { - return Object.keys(configObject).map((key) => ({ name: key })); - } - } catch (e) {} - - return []; - }, -}; - -export const dependenciesGenerator: Fig.Generator = { - script: [ - "bash", - "-c", - "until [[ -f package.json ]] || [[ $PWD = '/' ]]; do cd ..; done; cat package.json", - ], - postProcess: function (out, context = []) { - if (out.trim() === "") { - return []; - } - - try { - const packageContent = JSON.parse(out); - const dependencies = packageContent["dependencies"] ?? {}; - const devDependencies = packageContent["devDependencies"]; - const optionalDependencies = packageContent["optionalDependencies"] ?? {}; - Object.assign(dependencies, devDependencies, optionalDependencies); - - return Object.keys(dependencies) - .filter((pkgName) => { - const isListed = context.some((current) => current === pkgName); - return !isListed; - }) - .map((pkgName) => ({ - name: pkgName, - icon: "📦", - description: dependencies[pkgName] - ? "dependency" - : optionalDependencies[pkgName] - ? "optionalDependency" - : "devDependency", - })); - } catch (e) { - console.error(e); - return []; - } - }, -}; - -const commonOptions: Fig.Option[] = [ - { name: ["-s", "--silent"], description: "Skip Yarn console logs" }, - { - name: "--no-default-rc", - description: - "Prevent Yarn from automatically detecting yarnrc and npmrc files", - }, - { - name: "--use-yarnrc", - description: - "Specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc) (default: )", - args: { name: "path", template: "filepaths" }, - }, - { - name: "--verbose", - description: "Output verbose messages on internal operations", - }, - { - name: "--offline", - description: - "Trigger an error if any required dependencies are not available in local cache", - }, - { - name: "--prefer-offline", - description: - "Use network only if dependencies are not available in local cache", - }, - { - name: ["--enable-pnp", "--pnp"], - description: "Enable the Plug'n'Play installation", - }, - { - name: "--json", - description: "Format Yarn log messages as lines of JSON", - }, - { - name: "--ignore-scripts", - description: "Don't run lifecycle scripts", - }, - { name: "--har", description: "Save HAR output of network traffic" }, - { name: "--ignore-platform", description: "Ignore platform checks" }, - { name: "--ignore-engines", description: "Ignore engines check" }, - { - name: "--ignore-optional", - description: "Ignore optional dependencies", - }, - { - name: "--force", - description: - "Install and build packages even if they were built before, overwrite lockfile", - }, - { - name: "--skip-integrity-check", - description: "Run install without checking if node_modules is installed", - }, - { - name: "--check-files", - description: "Install will verify file tree of packages for consistency", - }, - { - name: "--no-bin-links", - description: "Don't generate bin links when setting up packages", - }, - { name: "--flat", description: "Only allow one version of a package" }, - { - name: ["--prod", "--production"], - description: - "Instruct Yarn to ignore NODE_ENV and take its production-or-not status from this flag instead", - }, - { - name: "--no-lockfile", - description: "Don't read or generate a lockfile", - }, - { name: "--pure-lockfile", description: "Don't generate a lockfile" }, - { - name: "--frozen-lockfile", - description: "Don't generate a lockfile and fail if an update is needed", - }, - { - name: "--update-checksums", - description: "Update package checksums from current repository", - }, - { - name: "--link-duplicates", - description: "Create hardlinks to the repeated modules in node_modules", - }, - { - name: "--link-folder", - description: "Specify a custom folder to store global links", - args: { name: "path", template: "folders" }, - }, - { - name: "--global-folder", - description: "Specify a custom folder to store global packages", - args: { name: "path", template: "folders" }, - }, - { - name: "--modules-folder", - description: - "Rather than installing modules into the node_modules folder relative to the cwd, output them here", - args: { name: "path", template: "folders" }, - }, - { - name: "--preferred-cache-folder", - description: "Specify a custom folder to store the yarn cache if possible", - args: { name: "path", template: "folders" }, - }, - { - name: "--cache-folder", - description: - "Specify a custom folder that must be used to store the yarn cache", - args: { name: "path", template: "folders" }, - }, - { - name: "--mutex", - description: "Use a mutex to ensure only one yarn instance is executing", - args: { name: "type[:specifier]" }, - }, - { - name: "--emoji", - description: "Enables emoji in output", - args: { - default: "true", - suggestions: ["true", "false"], - }, - }, - { - name: "--cwd", - description: "Working directory to use", - args: { name: "cwd", template: "folders" }, - }, - { - name: ["--proxy", "--https-proxy"], - description: "", - args: { name: "host" }, - }, - { - name: "--registry", - description: "Override configuration registry", - args: { name: "url" }, - }, - { name: "--no-progress", description: "Disable progress bar" }, - { - name: "--network-concurrency", - description: "Maximum number of concurrent network requests", - args: { name: "number" }, - }, - { - name: "--network-timeout", - description: "TCP timeout for network requests", - args: { name: "milliseconds" }, - }, - { - name: "--non-interactive", - description: "Do not show interactive prompts", - }, - { - name: "--scripts-prepend-node-path", - description: "Prepend the node executable dir to the PATH in scripts", - }, - { - name: "--no-node-version-check", - description: - "Do not warn when using a potentially unsupported Node version", - }, - { - name: "--focus", - description: - "Focus on a single workspace by installing remote copies of its sibling workspaces", - }, - { - name: "--otp", - description: "One-time password for two factor authentication", - args: { name: "otpcode" }, - }, -]; - -export const createCLIsGenerator: Fig.Generator = { - script: function (context) { - if (context[context.length - 1] === "") return undefined; - const searchTerm = "create-" + context[context.length - 1]; - return [ - "curl", - "-s", - "-H", - "Accept: application/json", - `https://api.npms.io/v2/search?q=${searchTerm}&size=20`, - ]; - }, - cache: { - ttl: 100 * 24 * 60 * 60 * 3, // 3 days - }, - postProcess: function (out) { - try { - return JSON.parse(out).results.map( - (item: { package: { name: string; description: string } }) => - ({ - name: item.package.name.substring(7), - description: item.package.description, - }) as Fig.Suggestion - ) as Fig.Suggestion[]; - } catch (e) { - return []; - } - }, -}; - -const completionSpec: Fig.Spec = { - name: "yarn", - description: "Manage packages and run scripts", - generateSpec: async (tokens, executeShellCommand) => { - const binaries = ( - await executeShellCommand({ - command: "bash", - args: [ - "-c", - `until [[ -d node_modules/ ]] || [[ $PWD = '/' ]]; do cd ..; done; ls -1 node_modules/.bin/`, - ], - }) - ).stdout.split("\n"); - - const subcommands = binaries - .filter((name) => nodeClis.has(name)) - .map((name) => ({ - name: name, - loadSpec: name === "rw" ? "redwood" : name, - icon: "fig://icon?type=package", - })); - - return { - name: "yarn", - subcommands, - } as Fig.Spec; - }, - args: { - generators: npmScriptsGenerator, - filterStrategy: "fuzzy", - parserDirectives: yarnScriptParserDirectives, - isOptional: true, - isCommand: true, - }, - options: [ - { - name: "--disable-pnp", - description: "Disable the Plug'n'Play installation", - }, - { - name: "--emoji", - description: "Enable emoji in output (default: true)", - args: { - name: "bool", - suggestions: [{ name: "true" }, { name: "false" }], - }, - }, - { - name: ["--enable-pnp", "--pnp"], - description: "Enable the Plug'n'Play installation", - }, - { - name: "--flat", - description: "Only allow one version of a package", - }, - { - name: "--focus", - description: - "Focus on a single workspace by installing remote copies of its sibling workspaces", - }, - { - name: "--force", - description: - "Install and build packages even if they were built before, overwrite lockfile", - }, - { - name: "--frozen-lockfile", - description: "Don't generate a lockfile and fail if an update is needed", - }, - { - name: "--global-folder", - description: "Specify a custom folder to store global packages", - args: { - template: "folders", - }, - }, - { - name: "--har", - description: "Save HAR output of network traffic", - }, - { - name: "--https-proxy", - description: "", - args: { - name: "path", - suggestions: [{ name: "https://" }], - }, - }, - { - name: "--ignore-engines", - description: "Ignore engines check", - }, - { - name: "--ignore-optional", - description: "Ignore optional dependencies", - }, - { - name: "--ignore-platform", - description: "Ignore platform checks", - }, - { - name: "--ignore-scripts", - description: "Don't run lifecycle scripts", - }, - { - name: "--json", - description: - "Format Yarn log messages as lines of JSON (see jsonlines.org)", - }, - { - name: "--link-duplicates", - description: "Create hardlinks to the repeated modules in node_modules", - }, - { - name: "--link-folder", - description: "Specify a custom folder to store global links", - args: { - template: "folders", - }, - }, - { - name: "--modules-folder", - description: - "Rather than installing modules into the node_modules folder relative to the cwd, output them here", - args: { - template: "folders", - }, - }, - { - name: "--mutex", - description: "Use a mutex to ensure only one yarn instance is executing", - args: [ - { - name: "type", - suggestions: [{ name: ":" }], - }, - { - name: "specifier", - suggestions: [{ name: ":" }], - }, - ], - }, - { - name: "--network-concurrency", - description: "Maximum number of concurrent network requests", - args: { - name: "number", - }, - }, - { - name: "--network-timeout", - description: "TCP timeout for network requests", - args: { - name: "milliseconds", - }, - }, - { - name: "--no-bin-links", - description: "Don't generate bin links when setting up packages", - }, - { - name: "--no-default-rc", - description: - "Prevent Yarn from automatically detecting yarnrc and npmrc files", - }, - { - name: "--no-lockfile", - description: "Don't read or generate a lockfile", - }, - { - name: "--non-interactive", - description: "Do not show interactive prompts", - }, - { - name: "--no-node-version-check", - description: - "Do not warn when using a potentially unsupported Node version", - }, - { - name: "--no-progress", - description: "Disable progress bar", - }, - { - name: "--offline", - description: - "Trigger an error if any required dependencies are not available in local cache", - }, - { - name: "--otp", - description: "One-time password for two factor authentication", - args: { - name: "otpcode", - }, - }, - { - name: "--prefer-offline", - description: - "Use network only if dependencies are not available in local cache", - }, - { - name: "--preferred-cache-folder", - description: - "Specify a custom folder to store the yarn cache if possible", - args: { - template: "folders", - }, - }, - { - name: ["--prod", "--production"], - description: "", - args: {}, - }, - { - name: "--proxy", - description: "", - args: { - name: "host", - }, - }, - { - name: "--pure-lockfile", - description: "Don't generate a lockfile", - }, - { - name: "--registry", - description: "Override configuration registry", - args: { - name: "url", - }, - }, - { - name: ["-s", "--silent"], - description: - "Skip Yarn console logs, other types of logs (script output) will be printed", - }, - { - name: "--scripts-prepend-node-path", - description: "Prepend the node executable dir to the PATH in scripts", - args: { - suggestions: [{ name: "true" }, { name: "false" }], - }, - }, - { - name: "--skip-integrity-check", - description: "Run install without checking if node_modules is installed", - }, - { - name: "--strict-semver", - description: "", - }, - ...commonOptions, - { - name: ["-v", "--version"], - description: "Output the version number", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - subcommands: [ - { - name: "add", - description: "Installs a package and any packages that it depends on", - args: { - name: "package", - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - options: [ - ...commonOptions, - { - name: ["-W", "--ignore-workspace-root-check"], - description: "Required to run yarn add inside a workspace root", - }, - { - name: ["-D", "--dev"], - description: "Save package to your `devDependencies`", - }, - { - name: ["-P", "--peer"], - description: "Save package to your `peerDependencies`", - }, - { - name: ["-O", "--optional"], - description: "Save package to your `optionalDependencies`", - }, - { - name: ["-E", "--exact"], - description: "Install exact version", - dependsOn: ["--latest"], - }, - { - name: ["-T", "--tilde"], - description: - "Install most recent release with the same minor version", - }, - { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "audit", - description: - "Perform a vulnerability audit against the installed packages", - options: [ - { - name: "--summary", - description: "Only print the summary", - }, - { - name: "--groups", - description: - "Only audit dependencies from listed groups. Default: devDependencies, dependencies, optionalDependencies", - args: { - name: "group_name", - isVariadic: true, - }, - }, - { - name: "--level", - description: - "Only print advisories with severity greater than or equal to one of the following: info|low|moderate|high|critical. Default: info", - args: { - name: "severity", - suggestions: [ - { name: "info" }, - { name: "low" }, - { name: "moderate" }, - { name: "high" }, - { name: "critical" }, - ], - }, - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "autoclean", - description: - "Cleans and removes unnecessary files from package dependencies", - options: [ - { - name: ["-h", "--help"], - description: "Output usage information", - }, - { - name: ["-i", "--init"], - description: - "Creates the .yarnclean file if it does not exist, and adds the default entries", - }, - { - name: ["-f", "--force"], - description: "If a .yarnclean file exists, run the clean process", - }, - ], - }, - { - name: "bin", - description: "Displays the location of the yarn bin folder", - options: [ - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "cache", - description: "Yarn cache list will print out every cached package", - options: [ - ...commonOptions, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - subcommands: [ - { - name: "clean", - description: "Clear global cache", - }, - { - name: "dir", - description: "Print yarn’s global cache path", - }, - { - name: "list", - description: "Print out every cached package", - options: [ - { - name: "--pattern", - description: "Filter cached packages by pattern", - args: { - name: "pattern", - }, - }, - ], - }, - ], - }, - { - name: "config", - description: "Configure yarn", - options: [ - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - subcommands: [ - { - name: "set", - description: "Sets the config key to a certain value", - options: [ - { - name: ["-g", "--global"], - description: "Set global config", - }, - ], - }, - { - name: "get", - description: "Print the value for a given key", - args: { - generators: configList, - }, - }, - { - name: "delete", - description: "Deletes a given key from the config", - args: { - generators: configList, - }, - }, - { - name: "list", - description: "Displays the current configuration", - }, - ], - }, - { - name: "create", - description: "Creates new projects from any create-* starter kits", - args: { - name: "cli", - generators: createCLIsGenerator, - loadSpec: async (token) => ({ - name: "create-" + token, - type: "global", - }), - isCommand: true, - }, - options: [ - ...commonOptions, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "exec", - description: "", - options: [ - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "generate-lock-entry", - description: "Generates a lock file entry", - options: [ - { - name: "--use-manifest", - description: - "Specify which manifest file to use for generating lock entry", - args: { - template: "filepaths", - }, - }, - { - name: "--resolved", - description: "Generate from <*.tgz>#", - args: { - template: "filepaths", - }, - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "global", - description: "Manage yarn globally", - subcommands: [ - { - name: "add", - description: "Install globally packages on your operating system", - args: { - name: "package", - generators: npmSearchGenerator, - debounce: true, - isVariadic: true, - }, - }, - { - name: "bin", - description: "Displays the location of the yarn global bin folder", - }, - { - name: "dir", - description: - "Displays the location of the global installation folder", - }, - { - name: "ls", - description: "List globally installed packages (deprecated)", - }, - { - name: "list", - description: "List globally installed packages", - }, - { - name: "remove", - description: "Remove globally installed packages", - args: { - name: "package", - filterStrategy: "fuzzy", - generators: getGlobalPackagesGenerator, - isVariadic: true, - }, - options: [ - ...commonOptions, - { - name: ["-W", "--ignore-workspace-root-check"], - description: - "Required to run yarn remove inside a workspace root", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "upgrade", - description: "Upgrade globally installed packages", - options: [ - ...commonOptions, - { - name: ["-S", "--scope"], - description: "Upgrade packages under the specified scope", - args: { name: "scope" }, - }, - { - name: ["-L", "--latest"], - description: "List the latest version of packages", - }, - { - name: ["-E", "--exact"], - description: - "Install exact version. Only used when --latest is specified", - dependsOn: ["--latest"], - }, - { - name: ["-P", "--pattern"], - description: "Upgrade packages that match pattern", - args: { name: "pattern" }, - }, - { - name: ["-T", "--tilde"], - description: - "Install most recent release with the same minor version. Only used when --latest is specified", - }, - { - name: ["-C", "--caret"], - description: - "Install most recent release with the same major version. Only used when --latest is specified", - dependsOn: ["--latest"], - }, - { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", - }, - { name: ["-h", "--help"], description: "Output usage information" }, - ], - }, - { - name: "upgrade-interactive", - description: - "Display the outdated packages before performing any upgrade", - options: [ - { - name: "--latest", - description: "Use the version tagged latest in the registry", - }, - ], - }, - ], - options: [ - ...commonOptions, - { - name: "--prefix", - description: "Bin prefix to use to install binaries", - args: { - name: "prefix", - }, - }, - { - name: "--latest", - description: "Bin prefix to use to install binaries", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "help", - description: "Output usage information", - }, - { - name: "import", - description: "Generates yarn.lock from an npm package-lock.json file", - }, - { - name: "info", - description: "Show information about a package", - }, - { - name: "init", - description: "Interactively creates or updates a package.json file", - options: [ - ...commonOptions, - { - name: ["-y", "--yes"], - description: "Use default options", - }, - { - name: ["-p", "--private"], - description: "Use default options and private true", - }, - { - name: ["-i", "--install"], - description: "Install a specific Yarn release", - args: { - name: "version", - }, - }, - { - name: "-2", - description: "Generates the project using Yarn 2", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "install", - description: "Install all the dependencies listed within package.json", - options: [ - ...commonOptions, - { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "licenses", - description: "", - subcommands: [ - { - name: "list", - description: "List licenses for installed packages", - }, - { - name: "generate-disclaimer", - description: "List of licenses from all the packages", - }, - ], - }, - { - name: "link", - description: "Symlink a package folder during development", - args: { - isOptional: true, - name: "package", - }, - options: [ - ...commonOptions, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "list", - description: "Lists all dependencies for the current working directory", - options: [ - { - name: "--depth", - description: "Restrict the depth of the dependencies", - }, - { - name: "--pattern", - description: "Filter the list of dependencies by the pattern", - }, - ], - }, - { - name: "login", - description: "Store registry username and email", - }, - { - name: "logout", - description: "Clear registry username and email", - }, - { - name: "node", - description: "", - }, - { - name: "outdated", - description: "Checks for outdated package dependencies", - options: [ - ...commonOptions, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "owner", - description: "Manage package owners", - subcommands: [ - { - name: "list", - description: "Lists all of the owners of a package", - args: { - name: "package", - }, - }, - { - name: "add", - description: "Adds the user as an owner of the package", - args: { - name: "package", - }, - }, - { - name: "remove", - description: "Removes the user as an owner of the package", - args: [ - { - name: "user", - }, - { - name: "package", - }, - ], - }, - ], - }, - { - name: "pack", - description: "Creates a compressed gzip archive of package dependencies", - options: [ - { - name: "--filename", - description: - "Creates a compressed gzip archive of package dependencies and names the file filename", - }, - ], - }, - { - name: "policies", - description: "Defines project-wide policies for your project", - subcommands: [ - { - name: "set-version", - description: "Will download the latest stable release", - options: [ - { - name: "--rc", - description: "Download the latest rc release", - }, - ], - }, - ], - }, - { - name: "publish", - description: "Publishes a package to the npm registry", - args: { name: "Tarball or Folder", template: "folders" }, - options: [ - ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, - { - name: "--major", - description: "Auto-increment major version number", - }, - { - name: "--minor", - description: "Auto-increment minor version number", - }, - { - name: "--patch", - description: "Auto-increment patch version number", - }, - { - name: "--premajor", - description: "Auto-increment premajor version number", - }, - { - name: "--preminor", - description: "Auto-increment preminor version number", - }, - { - name: "--prepatch", - description: "Auto-increment prepatch version number", - }, - { - name: "--prerelease", - description: "Auto-increment prerelease version number", - }, - { - name: "--preid", - description: "Add a custom identifier to the prerelease", - args: { name: "preid" }, - }, - { - name: "--message", - description: "Message", - args: { name: "message" }, - }, - { name: "--no-git-tag-version", description: "No git tag version" }, - { - name: "--no-commit-hooks", - description: "Bypass git hooks when committing new version", - }, - { name: "--access", description: "Access", args: { name: "access" } }, - { name: "--tag", description: "Tag", args: { name: "tag" } }, - ], - }, - { - name: "remove", - description: "Remove installed package", - args: { - filterStrategy: "fuzzy", - generators: dependenciesGenerator, - isVariadic: true, - }, - options: [ - ...commonOptions, - { - name: ["-W", "--ignore-workspace-root-check"], - description: "Required to run yarn remove inside a workspace root", - }, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - ], - }, - { - name: "run", - description: "Runs a defined package script", - options: [ - ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, - ], - args: [ - { - name: "script", - description: "Script to run from your package.json", - generators: npmScriptsGenerator, - filterStrategy: "fuzzy", - parserDirectives: yarnScriptParserDirectives, - isCommand: true, - }, - { - name: "env", - suggestions: ["env"], - description: "Lists environment variables available to scripts", - isOptional: true, - }, - ], - }, - { - name: "tag", - description: "Add, remove, or list tags on a package", - }, - { - name: "team", - description: "Maintain team memberships", - subcommands: [ - { - name: "create", - description: "Create a new team", - args: { - name: "", - }, - }, - { - name: "destroy", - description: "Destroys an existing team", - args: { - name: "", - }, - }, - { - name: "add", - description: "Add a user to an existing team", - args: [ - { - name: "", - }, - { - name: "", - }, - ], - }, - { - name: "remove", - description: "Remove a user from a team they belong to", - args: { - name: " ", - }, - }, - { - name: "list", - description: - "If performed on an organization name, will return a list of existing teams under that organization. If performed on a team, it will instead return a list of all users belonging to that particular team", - args: { - name: "|", - }, - }, - ], - }, - { - name: "unlink", - description: "Unlink a previously created symlink for a package", - }, - { - name: "unplug", - description: "", - }, - { - name: "upgrade", - description: - "Upgrades packages to their latest version based on the specified range", - args: { - name: "package", - generators: dependenciesGenerator, - filterStrategy: "fuzzy", - isVariadic: true, - isOptional: true, - }, - options: [ - ...commonOptions, - { - name: ["-S", "--scope"], - description: "Upgrade packages under the specified scope", - args: { name: "scope" }, - }, - { - name: ["-L", "--latest"], - description: "List the latest version of packages", - }, - { - name: ["-E", "--exact"], - description: - "Install exact version. Only used when --latest is specified", - dependsOn: ["--latest"], - }, - { - name: ["-P", "--pattern"], - description: "Upgrade packages that match pattern", - args: { name: "pattern" }, - }, - { - name: ["-T", "--tilde"], - description: - "Install most recent release with the same minor version. Only used when --latest is specified", - }, - { - name: ["-C", "--caret"], - description: - "Install most recent release with the same major version. Only used when --latest is specified", - dependsOn: ["--latest"], - }, - { - name: ["-A", "--audit"], - description: "Run vulnerability audit on installed packages", - }, - { name: ["-h", "--help"], description: "Output usage information" }, - ], - }, - { - name: "upgrade-interactive", - description: "Upgrades packages in interactive mode", - options: [ - { - name: "--latest", - description: "Use the version tagged latest in the registry", - }, - ], - }, - { - name: "version", - description: "Update version of your package", - options: [ - ...commonOptions, - { name: ["-h", "--help"], description: "Output usage information" }, - { - name: "--new-version", - description: "New version", - args: { name: "version" }, - }, - { - name: "--major", - description: "Auto-increment major version number", - }, - { - name: "--minor", - description: "Auto-increment minor version number", - }, - { - name: "--patch", - description: "Auto-increment patch version number", - }, - { - name: "--premajor", - description: "Auto-increment premajor version number", - }, - { - name: "--preminor", - description: "Auto-increment preminor version number", - }, - { - name: "--prepatch", - description: "Auto-increment prepatch version number", - }, - { - name: "--prerelease", - description: "Auto-increment prerelease version number", - }, - { - name: "--preid", - description: "Add a custom identifier to the prerelease", - args: { name: "preid" }, - }, - { - name: "--message", - description: "Message", - args: { name: "message" }, - }, - { name: "--no-git-tag-version", description: "No git tag version" }, - { - name: "--no-commit-hooks", - description: "Bypass git hooks when committing new version", - }, - { name: "--access", description: "Access", args: { name: "access" } }, - { name: "--tag", description: "Tag", args: { name: "tag" } }, - ], - }, - { - name: "versions", - description: - "Displays version information of the currently installed Yarn, Node.js, and its dependencies", - }, - { - name: "why", - description: "Show information about why a package is installed", - args: { - name: "package", - filterStrategy: "fuzzy", - generators: allDependenciesGenerator, - }, - options: [ - ...commonOptions, - { - name: ["-h", "--help"], - description: "Output usage information", - }, - { - name: "--peers", - description: - "Print the peer dependencies that match the specified name", - }, - { - name: ["-R", "--recursive"], - description: - "List, for each workspace, what are all the paths that lead to the dependency", - }, - ], - }, - { - name: "workspace", - description: "Manage workspace", - filterStrategy: "fuzzy", - generateSpec: async (_tokens, executeShellCommand) => { - const version = ( - await executeShellCommand({ - command: "yarn", - // eslint-disable-next-line @withfig/fig-linter/no-useless-arrays - args: ["--version"], - }) - ).stdout; - const isYarnV1 = version.startsWith("1."); - - const getWorkspacesDefinitionsV1 = async () => { - const { stdout } = await executeShellCommand({ - command: "yarn", - args: ["workspaces", "info"], - }); - - const startJson = stdout.indexOf("{"); - const endJson = stdout.lastIndexOf("}"); - - return Object.entries( - JSON.parse(stdout.slice(startJson, endJson + 1)) as Record< - string, - { location: string } - > - ).map(([name, { location }]) => ({ - name, - location, - })); - }; - - // For yarn >= 2.0.0 - const getWorkspacesDefinitionsVOther = async () => { - // yarn workspaces list --json - const out = ( - await executeShellCommand({ - command: "yarn", - args: ["workspaces", "list", "--json"], - }) - ).stdout; - return out.split("\n").map((line) => JSON.parse(line.trim())); - }; - - try { - const workspacesDefinitions = isYarnV1 - ? // transform Yarn V1 output to array of workspaces like Yarn V2 - await getWorkspacesDefinitionsV1() - : // in yarn v>=2.0.0, workspaces definitions are a list of JSON lines - await getWorkspacesDefinitionsVOther(); - - const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( - ({ name, location }: { name: string; location: string }) => ({ - name, - description: "Workspaces", - args: { - name: "script", - generators: { - cache: { - strategy: "stale-while-revalidate", - ttl: 60_000, // 60s - }, - script: ["cat", `${location}/package.json`], - postProcess: function (out: string) { - if (out.trim() == "") { - return []; - } - try { - const packageContent = JSON.parse(out); - const scripts = packageContent["scripts"]; - if (scripts) { - return Object.keys(scripts).map((script) => ({ - name: script, - })); - } - } catch (e) {} - return []; - }, - }, - }, - }) - ); - - return { - name: "workspace", - subcommands, - }; - } catch (e) { - console.error(e); - } - return { name: "workspaces" }; - }, - }, - { - name: "workspaces", - description: "Show information about your workspaces", - options: [ - { - name: "subcommand", - description: "", - args: { - suggestions: [{ name: "info" }, { name: "run" }], - }, - }, - { - name: "flags", - description: "", - }, - ], - }, - { - name: "set", - description: "Set global Yarn options", - subcommands: [ - { - name: "resolution", - description: "Enforce a package resolution", - args: [ - { - name: "descriptor", - description: - "A descriptor for the package, in the form of 'lodash@npm:^1.2.3'", - }, - { - name: "resolution", - description: "The version of the package to resolve", - }, - ], - options: [ - { - name: ["-s", "--save"], - description: - "Persist the resolution inside the top-level manifest", - }, - ], - }, - { - name: "version", - description: "Lock the Yarn version used by the project", - args: { - name: "version", - description: - "Use the specified version, which can also be a Yarn 2 build (e.g 2.0.0-rc.30) or a Yarn 1 build (e.g 1.22.1)", - template: "filepaths", - suggestions: [ - { - name: "from-sources", - insertValue: "from sources", - }, - "latest", - "canary", - "classic", - "self", - ], - }, - options: [ - { - name: "--only-if-needed", - description: - "Only lock the Yarn version if it isn't already locked", - }, - ], - }, - ], - }, - ], -}; - -export default completionSpec; diff --git a/extensions/terminal-suggest/src/completions/yarn.ts b/extensions/terminal-suggest/src/completions/yarn.ts new file mode 100644 index 00000000000..7b0750ba2b1 --- /dev/null +++ b/extensions/terminal-suggest/src/completions/yarn.ts @@ -0,0 +1,1677 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { npmScriptsGenerator, npmSearchGenerator } from './npm'; + +export const yarnScriptParserDirectives: Fig.Arg['parserDirectives'] = { + alias: async (token, executeShellCommand) => { + const npmPrefix = await executeShellCommand({ + command: 'npm', + args: ['prefix'], + }); + if (npmPrefix.status !== 0) { + throw new Error('npm prefix command failed'); + } + const packageJson = await executeShellCommand({ + command: 'cat', + args: [`${npmPrefix.stdout.trim()}/package.json`], + }); + const script: string = JSON.parse(packageJson.stdout).scripts?.[token]; + if (!script) { + throw new Error(`Script not found: '${token}'`); + } + return script; + }, +}; + +export const nodeClis = new Set([ + 'vue', + 'vite', + 'nuxt', + 'react-native', + 'degit', + 'expo', + 'jest', + 'next', + 'electron', + 'prisma', + 'eslint', + 'prettier', + 'tsc', + 'typeorm', + 'babel', + 'remotion', + 'autocomplete-tools', + 'redwood', + 'rw', + 'create-completion-spec', + 'publish-spec-to-team', + 'capacitor', + 'cap', +]); + +// generate global package list from global package.json file +const getGlobalPackagesGenerator: Fig.Generator = { + custom: async (tokens, executeCommand, generatorContext) => { + const { stdout: yarnGlobalDir } = await executeCommand({ + command: 'yarn', + args: ['global', 'dir'], + }); + + const { stdout } = await executeCommand({ + command: 'cat', + args: [`${yarnGlobalDir.trim()}/package.json`], + }); + + if (stdout.trim() === '') { + return []; + } + + try { + const packageContent = JSON.parse(stdout); + const dependencyScripts = packageContent['dependencies'] || {}; + const devDependencyScripts = packageContent['devDependencies'] || {}; + const dependencies = [ + ...Object.keys(dependencyScripts), + ...Object.keys(devDependencyScripts), + ]; + + const filteredDependencies = dependencies.filter( + (dependency) => !tokens.includes(dependency) + ); + + return filteredDependencies.map((dependencyName) => ({ + name: dependencyName, + })); + } catch (e) { } + + return []; + }, +}; + +// generate package list of direct and indirect dependencies +const allDependenciesGenerator: Fig.Generator = { + script: ['yarn', 'list', '--depth=0', '--json'], + postProcess: (out) => { + if (out.trim() === '') { + return []; + } + + try { + const packageContent = JSON.parse(out); + const dependencies = packageContent.data.trees; + return dependencies.map((dependency: { name: string }) => ({ + name: dependency.name.split('@')[0], + })); + } catch (e) { } + return []; + }, +}; + +const configList: Fig.Generator = { + script: ['yarn', 'config', 'list'], + postProcess: function (out) { + if (out.trim() === '') { + return []; + } + + try { + const startIndex = out.indexOf('{'); + const endIndex = out.indexOf('}'); + let output = out.substring(startIndex, endIndex + 1); + // TODO: fix hacky code + // reason: JSON parse was not working without double quotes + output = output + .replace(/\'/gi, '\'') + .replace('lastUpdateCheck', '\'lastUpdateCheck\'') + .replace('registry', '\'lastUpdateCheck\''); + const configObject = JSON.parse(output); + if (configObject) { + return Object.keys(configObject).map((key) => ({ name: key })); + } + } catch (e) { } + + return []; + }, +}; + +export const dependenciesGenerator: Fig.Generator = { + script: [ + 'bash', + '-c', + 'until [[ -f package.json ]] || [[ $PWD = \' / \' ]]; do cd ..; done; cat package.json', + ], + postProcess: function (out, context = []) { + if (out.trim() === '') { + return []; + } + + try { + const packageContent = JSON.parse(out); + const dependencies = packageContent['dependencies'] ?? {}; + const devDependencies = packageContent['devDependencies']; + const optionalDependencies = packageContent['optionalDependencies'] ?? {}; + Object.assign(dependencies, devDependencies, optionalDependencies); + + return Object.keys(dependencies) + .filter((pkgName) => { + const isListed = context.some((current) => current === pkgName); + return !isListed; + }) + .map((pkgName) => ({ + name: pkgName, + description: dependencies[pkgName] + ? 'dependency' + : optionalDependencies[pkgName] + ? 'optionalDependency' + : 'devDependency', + })); + } catch (e) { + console.error(e); + return []; + } + }, +}; + +const commonOptions: Fig.Option[] = [ + { name: ['-s', '--silent'], description: 'Skip Yarn console logs' }, + { + name: '--no-default-rc', + description: + 'Prevent Yarn from automatically detecting yarnrc and npmrc files', + }, + { + name: '--use-yarnrc', + description: + 'Specifies a yarnrc file that Yarn should use (.yarnrc only, not .npmrc) (default: )', + args: { name: 'path', template: 'filepaths' }, + }, + { + name: '--verbose', + description: 'Output verbose messages on internal operations', + }, + { + name: '--offline', + description: + 'Trigger an error if any required dependencies are not available in local cache', + }, + { + name: '--prefer-offline', + description: + 'Use network only if dependencies are not available in local cache', + }, + { + name: ['--enable-pnp', '--pnp'], + description: 'Enable the Plug\'n\'Play installation', + }, + { + name: '--json', + description: 'Format Yarn log messages as lines of JSON', + }, + { + name: '--ignore-scripts', + description: 'Don\'t run lifecycle scripts', + }, + { name: '--har', description: 'Save HAR output of network traffic' }, + { name: '--ignore-platform', description: 'Ignore platform checks' }, + { name: '--ignore-engines', description: 'Ignore engines check' }, + { + name: '--ignore-optional', + description: 'Ignore optional dependencies', + }, + { + name: '--force', + description: + 'Install and build packages even if they were built before, overwrite lockfile', + }, + { + name: '--skip-integrity-check', + description: 'Run install without checking if node_modules is installed', + }, + { + name: '--check-files', + description: 'Install will verify file tree of packages for consistency', + }, + { + name: '--no-bin-links', + description: 'Don\'t generate bin links when setting up packages', + }, + { name: '--flat', description: 'Only allow one version of a package' }, + { + name: ['--prod', '--production'], + description: + 'Instruct Yarn to ignore NODE_ENV and take its production-or-not status from this flag instead', + }, + { + name: '--no-lockfile', + description: 'Don\'t read or generate a lockfile', + }, + { + name: '--pure-lockfile', description: 'Don\'t generate a lockfile' + }, + { + name: '--frozen-lockfile', + description: 'Don\'t generate a lockfile and fail if an update is needed', + }, + { + name: '--update-checksums', + description: 'Update package checksums from current repository', + }, + { + name: '--link-duplicates', + description: 'Create hardlinks to the repeated modules in node_modules', + }, + { + name: '--link-folder', + description: 'Specify a custom folder to store global links', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--global-folder', + description: 'Specify a custom folder to store global packages', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--modules-folder', + description: + 'Rather than installing modules into the node_modules folder relative to the cwd, output them here', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--preferred-cache-folder', + description: 'Specify a custom folder to store the yarn cache if possible', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--cache-folder', + description: + 'Specify a custom folder that must be used to store the yarn cache', + args: { name: 'path', template: 'folders' }, + }, + { + name: '--mutex', + description: 'Use a mutex to ensure only one yarn instance is executing', + args: { name: 'type[:specifier]' }, + }, + { + name: '--emoji', + description: 'Enables emoji in output', + args: { + default: 'true', + suggestions: ['true', 'false'], + }, + }, + { + name: '--cwd', + description: 'Working directory to use', + args: { name: 'cwd', template: 'folders' }, + }, + { + name: ['--proxy', '--https-proxy'], + description: '', + args: { name: 'host' }, + }, + { + name: '--registry', + description: 'Override configuration registry', + args: { name: 'url' }, + }, + { name: '--no-progress', description: 'Disable progress bar' }, + { + name: '--network-concurrency', + description: 'Maximum number of concurrent network requests', + args: { name: 'number' }, + }, + { + name: '--network-timeout', + description: 'TCP timeout for network requests', + args: { name: 'milliseconds' }, + }, + { + name: '--non-interactive', + description: 'Do not show interactive prompts', + }, + { + name: '--scripts-prepend-node-path', + description: 'Prepend the node executable dir to the PATH in scripts', + }, + { + name: '--no-node-version-check', + description: + 'Do not warn when using a potentially unsupported Node version', + }, + { + name: '--focus', + description: + 'Focus on a single workspace by installing remote copies of its sibling workspaces', + }, + { + name: '--otp', + description: 'One-time password for two factor authentication', + args: { name: 'otpcode' }, + }, +]; + +export const createCLIsGenerator: Fig.Generator = { + script: function (context) { + if (context[context.length - 1] === '') { + return undefined; + } + const searchTerm = 'create-' + context[context.length - 1]; + return [ + 'curl', + '-s', + '-H', + 'Accept: application/json', + `https://api.npms.io/v2/search?q=${searchTerm}&size=20`, + ]; + }, + cache: { + ttl: 100 * 24 * 60 * 60 * 3, // 3 days + }, + postProcess: function (out) { + try { + return JSON.parse(out).results.map((item: { package: { name: string; description: string } }) => ({ + name: item.package.name.substring(7), + description: item.package.description, + })) as Fig.Suggestion[]; + } catch (e) { + return []; + } + }, +}; + +const completionSpec: Fig.Spec = { + name: 'yarn', + description: 'Manage packages and run scripts', + generateSpec: async (tokens, executeShellCommand) => { + const binaries = ( + await executeShellCommand({ + command: 'bash', + args: [ + '-c', + `until [[ -d node_modules/ ]] || [[ $PWD = '/' ]]; do cd ..; done; ls -1 node_modules/.bin/`, + ], + }) + ).stdout.split('\n'); + + const subcommands = binaries + .filter((name) => nodeClis.has(name)) + .map((name) => ({ + name: name, + loadSpec: name === 'rw' ? 'redwood' : name, + icon: 'fig://icon?type=package', + })); + + return { + name: 'yarn', + subcommands, + }; + }, + args: { + generators: npmScriptsGenerator, + filterStrategy: 'fuzzy', + parserDirectives: yarnScriptParserDirectives, + isOptional: true, + isCommand: true, + }, + options: [ + { + name: '--disable-pnp', + description: 'Disable the Plug\'n\'Play installation', + }, + { + name: '--emoji', + description: 'Enable emoji in output (default: true)', + args: { + name: 'bool', + suggestions: [{ name: 'true' }, { name: 'false' }], + }, + }, + { + name: ['--enable-pnp', '--pnp'], + description: 'Enable the Plug\'n\'Play installation', + }, + { + name: '--flat', + description: 'Only allow one version of a package', + }, + { + name: '--focus', + description: + 'Focus on a single workspace by installing remote copies of its sibling workspaces', + }, + { + name: '--force', + description: + 'Install and build packages even if they were built before, overwrite lockfile', + }, + { + name: '--frozen-lockfile', + description: 'Don\'t generate a lockfile and fail if an update is needed', + }, + { + name: '--global-folder', + description: 'Specify a custom folder to store global packages', + args: { + template: 'folders', + }, + }, + { + name: '--har', + description: 'Save HAR output of network traffic', + }, + { + name: '--https-proxy', + description: '', + args: { + name: 'path', + suggestions: [{ name: 'https://' }], + }, + }, + { + name: '--ignore-engines', + description: 'Ignore engines check', + }, + { + name: '--ignore-optional', + description: 'Ignore optional dependencies', + }, + { + name: '--ignore-platform', + description: 'Ignore platform checks', + }, + { + name: '--ignore-scripts', + description: 'Don\'t run lifecycle scripts', + }, + { + name: '--json', + description: + 'Format Yarn log messages as lines of JSON (see jsonlines.org)', + }, + { + name: '--link-duplicates', + description: 'Create hardlinks to the repeated modules in node_modules', + }, + { + name: '--link-folder', + description: 'Specify a custom folder to store global links', + args: { + template: 'folders', + }, + }, + { + name: '--modules-folder', + description: + 'Rather than installing modules into the node_modules folder relative to the cwd, output them here', + args: { + template: 'folders', + }, + }, + { + name: '--mutex', + description: 'Use a mutex to ensure only one yarn instance is executing', + args: [ + { + name: 'type', + suggestions: [{ name: ':' }], + }, + { + name: 'specifier', + suggestions: [{ name: ':' }], + }, + ], + }, + { + name: '--network-concurrency', + description: 'Maximum number of concurrent network requests', + args: { + name: 'number', + }, + }, + { + name: '--network-timeout', + description: 'TCP timeout for network requests', + args: { + name: 'milliseconds', + }, + }, + { + name: '--no-bin-links', + description: 'Don\'t generate bin links when setting up packages', + }, + { + name: '--no-default-rc', + description: + 'Prevent Yarn from automatically detecting yarnrc and npmrc files', + }, + { + name: '--no-lockfile', + description: 'Don\'t read or generate a lockfile', + }, + { + name: '--non-interactive', + description: 'Do not show interactive prompts', + }, + { + name: '--no-node-version-check', + description: + 'Do not warn when using a potentially unsupported Node version', + }, + { + name: '--no-progress', + description: 'Disable progress bar', + }, + { + name: '--offline', + description: + 'Trigger an error if any required dependencies are not available in local cache', + }, + { + name: '--otp', + description: 'One-time password for two factor authentication', + args: { + name: 'otpcode', + }, + }, + { + name: '--prefer-offline', + description: + 'Use network only if dependencies are not available in local cache', + }, + { + name: '--preferred-cache-folder', + description: + 'Specify a custom folder to store the yarn cache if possible', + args: { + template: 'folders', + }, + }, + { + name: ['--prod', '--production'], + description: '', + args: {}, + }, + { + name: '--proxy', + description: '', + args: { + name: 'host', + }, + }, + { + name: '--pure-lockfile', + description: 'Don\'t generate a lockfile', + }, + { + name: '--registry', + description: 'Override configuration registry', + args: { + name: 'url', + }, + }, + { + name: ['-s', '--silent'], + description: + 'Skip Yarn console logs, other types of logs (script output) will be printed', + }, + { + name: '--scripts-prepend-node-path', + description: 'Prepend the node executable dir to the PATH in scripts', + args: { + suggestions: [{ name: 'true' }, { name: 'false' }], + }, + }, + { + name: '--skip-integrity-check', + description: 'Run install without checking if node_modules is installed', + }, + { + name: '--strict-semver', + description: '', + }, + ...commonOptions, + { + name: ['-v', '--version'], + description: 'Output the version number', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + subcommands: [ + { + name: 'add', + description: 'Installs a package and any packages that it depends on', + args: { + name: 'package', + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + options: [ + ...commonOptions, + { + name: ['-W', '--ignore-workspace-root-check'], + description: 'Required to run yarn add inside a workspace root', + }, + { + name: ['-D', '--dev'], + description: 'Save package to your `devDependencies`', + }, + { + name: ['-P', '--peer'], + description: 'Save package to your `peerDependencies`', + }, + { + name: ['-O', '--optional'], + description: 'Save package to your `optionalDependencies`', + }, + { + name: ['-E', '--exact'], + description: 'Install exact version', + dependsOn: ['--latest'], + }, + { + name: ['-T', '--tilde'], + description: + 'Install most recent release with the same minor version', + }, + { + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'audit', + description: + 'Perform a vulnerability audit against the installed packages', + options: [ + { + name: '--summary', + description: 'Only print the summary', + }, + { + name: '--groups', + description: + 'Only audit dependencies from listed groups. Default: devDependencies, dependencies, optionalDependencies', + args: { + name: 'group_name', + isVariadic: true, + }, + }, + { + name: '--level', + description: + 'Only print advisories with severity greater than or equal to one of the following: info|low|moderate|high|critical. Default: info', + args: { + name: 'severity', + suggestions: [ + { name: 'info' }, + { name: 'low' }, + { name: 'moderate' }, + { name: 'high' }, + { name: 'critical' }, + ], + }, + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'autoclean', + description: + 'Cleans and removes unnecessary files from package dependencies', + options: [ + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + { + name: ['-i', '--init'], + description: + 'Creates the .yarnclean file if it does not exist, and adds the default entries', + }, + { + name: ['-f', '--force'], + description: 'If a .yarnclean file exists, run the clean process', + }, + ], + }, + { + name: 'bin', + description: 'Displays the location of the yarn bin folder', + options: [ + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'cache', + description: 'Yarn cache list will print out every cached package', + options: [ + ...commonOptions, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + subcommands: [ + { + name: 'clean', + description: 'Clear global cache', + }, + { + name: 'dir', + description: 'Print yarn\'s global cache path', + }, + { + name: 'list', + description: 'Print out every cached package', + options: [ + { + name: '--pattern', + description: 'Filter cached packages by pattern', + args: { + name: 'pattern', + }, + }, + ], + }, + ], + }, + { + name: 'config', + description: 'Configure yarn', + options: [ + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + subcommands: [ + { + name: 'set', + description: 'Sets the config key to a certain value', + options: [ + { + name: ['-g', '--global'], + description: 'Set global config', + }, + ], + }, + { + name: 'get', + description: 'Print the value for a given key', + args: { + generators: configList, + }, + }, + { + name: 'delete', + description: 'Deletes a given key from the config', + args: { + generators: configList, + }, + }, + { + name: 'list', + description: 'Displays the current configuration', + }, + ], + }, + { + name: 'create', + description: 'Creates new projects from any create-* starter kits', + args: { + name: 'cli', + generators: createCLIsGenerator, + loadSpec: async (token) => ({ + name: 'create-' + token, + type: 'global', + }), + isCommand: true, + }, + options: [ + ...commonOptions, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'exec', + description: '', + options: [ + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'generate-lock-entry', + description: 'Generates a lock file entry', + options: [ + { + name: '--use-manifest', + description: + 'Specify which manifest file to use for generating lock entry', + args: { + template: 'filepaths', + }, + }, + { + name: '--resolved', + description: 'Generate from <*.tgz>#', + args: { + template: 'filepaths', + }, + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'global', + description: 'Manage yarn globally', + subcommands: [ + { + name: 'add', + description: 'Install globally packages on your operating system', + args: { + name: 'package', + generators: npmSearchGenerator, + debounce: true, + isVariadic: true, + }, + }, + { + name: 'bin', + description: 'Displays the location of the yarn global bin folder', + }, + { + name: 'dir', + description: + 'Displays the location of the global installation folder', + }, + { + name: 'ls', + description: 'List globally installed packages (deprecated)', + }, + { + name: 'list', + description: 'List globally installed packages', + }, + { + name: 'remove', + description: 'Remove globally installed packages', + args: { + name: 'package', + filterStrategy: 'fuzzy', + generators: getGlobalPackagesGenerator, + isVariadic: true, + }, + options: [ + ...commonOptions, + { + name: ['-W', '--ignore-workspace-root-check'], + description: + 'Required to run yarn remove inside a workspace root', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'upgrade', + description: 'Upgrade globally installed packages', + options: [ + ...commonOptions, + { + name: ['-S', '--scope'], + description: 'Upgrade packages under the specified scope', + args: { name: 'scope' }, + }, + { + name: ['-L', '--latest'], + description: 'List the latest version of packages', + }, + { + name: ['-E', '--exact'], + description: + 'Install exact version. Only used when --latest is specified', + dependsOn: ['--latest'], + }, + { + name: ['-P', '--pattern'], + description: 'Upgrade packages that match pattern', + args: { name: 'pattern' }, + }, + { + name: ['-T', '--tilde'], + description: + 'Install most recent release with the same minor version. Only used when --latest is specified', + }, + { + name: ['-C', '--caret'], + description: + 'Install most recent release with the same major version. Only used when --latest is specified', + dependsOn: ['--latest'], + }, + { + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', + }, + { name: ['-h', '--help'], description: 'Output usage information' }, + ], + }, + { + name: 'upgrade-interactive', + description: + 'Display the outdated packages before performing any upgrade', + options: [ + { + name: '--latest', + description: 'Use the version tagged latest in the registry', + }, + ], + }, + ], + options: [ + ...commonOptions, + { + name: '--prefix', + description: 'Bin prefix to use to install binaries', + args: { + name: 'prefix', + }, + }, + { + name: '--latest', + description: 'Bin prefix to use to install binaries', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'help', + description: 'Output usage information', + }, + { + name: 'import', + description: 'Generates yarn.lock from an npm package-lock.json file', + }, + { + name: 'info', + description: 'Show information about a package', + }, + { + name: 'init', + description: 'Interactively creates or updates a package.json file', + options: [ + ...commonOptions, + { + name: ['-y', '--yes'], + description: 'Use default options', + }, + { + name: ['-p', '--private'], + description: 'Use default options and private true', + }, + { + name: ['-i', '--install'], + description: 'Install a specific Yarn release', + args: { + name: 'version', + }, + }, + { + name: '-2', + description: 'Generates the project using Yarn 2', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'install', + description: 'Install all the dependencies listed within package.json', + options: [ + ...commonOptions, + { + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'licenses', + description: '', + subcommands: [ + { + name: 'list', + description: 'List licenses for installed packages', + }, + { + name: 'generate-disclaimer', + description: 'List of licenses from all the packages', + }, + ], + }, + { + name: 'link', + description: 'Symlink a package folder during development', + args: { + isOptional: true, + name: 'package', + }, + options: [ + ...commonOptions, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'list', + description: 'Lists all dependencies for the current working directory', + options: [ + { + name: '--depth', + description: 'Restrict the depth of the dependencies', + }, + { + name: '--pattern', + description: 'Filter the list of dependencies by the pattern', + }, + ], + }, + { + name: 'login', + description: 'Store registry username and email', + }, + { + name: 'logout', + description: 'Clear registry username and email', + }, + { + name: 'node', + description: '', + }, + { + name: 'outdated', + description: 'Checks for outdated package dependencies', + options: [ + ...commonOptions, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'owner', + description: 'Manage package owners', + subcommands: [ + { + name: 'list', + description: 'Lists all of the owners of a package', + args: { + name: 'package', + }, + }, + { + name: 'add', + description: 'Adds the user as an owner of the package', + args: { + name: 'package', + }, + }, + { + name: 'remove', + description: 'Removes the user as an owner of the package', + args: [ + { + name: 'user', + }, + { + name: 'package', + }, + ], + }, + ], + }, + { + name: 'pack', + description: 'Creates a compressed gzip archive of package dependencies', + options: [ + { + name: '--filename', + description: + 'Creates a compressed gzip archive of package dependencies and names the file filename', + }, + ], + }, + { + name: 'policies', + description: 'Defines project-wide policies for your project', + subcommands: [ + { + name: 'set-version', + description: 'Will download the latest stable release', + options: [ + { + name: '--rc', + description: 'Download the latest rc release', + }, + ], + }, + ], + }, + { + name: 'publish', + description: 'Publishes a package to the npm registry', + args: { name: 'Tarball or Folder', template: 'folders' }, + options: [ + ...commonOptions, + { name: ['-h', '--help'], description: 'Output usage information' }, + { + name: '--major', + description: 'Auto-increment major version number', + }, + { + name: '--minor', + description: 'Auto-increment minor version number', + }, + { + name: '--patch', + description: 'Auto-increment patch version number', + }, + { + name: '--premajor', + description: 'Auto-increment premajor version number', + }, + { + name: '--preminor', + description: 'Auto-increment preminor version number', + }, + { + name: '--prepatch', + description: 'Auto-increment prepatch version number', + }, + { + name: '--prerelease', + description: 'Auto-increment prerelease version number', + }, + { + name: '--preid', + description: 'Add a custom identifier to the prerelease', + args: { name: 'preid' }, + }, + { + name: '--message', + description: 'Message', + args: { name: 'message' }, + }, + { name: '--no-git-tag-version', description: 'No git tag version' }, + { + name: '--no-commit-hooks', + description: 'Bypass git hooks when committing new version', + }, + { name: '--access', description: 'Access', args: { name: 'access' } }, + { name: '--tag', description: 'Tag', args: { name: 'tag' } }, + ], + }, + { + name: 'remove', + description: 'Remove installed package', + args: { + filterStrategy: 'fuzzy', + generators: dependenciesGenerator, + isVariadic: true, + }, + options: [ + ...commonOptions, + { + name: ['-W', '--ignore-workspace-root-check'], + description: 'Required to run yarn remove inside a workspace root', + }, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + ], + }, + { + name: 'run', + description: 'Runs a defined package script', + options: [ + ...commonOptions, + { name: ['-h', '--help'], description: 'Output usage information' }, + ], + args: [ + { + name: 'script', + description: 'Script to run from your package.json', + generators: npmScriptsGenerator, + filterStrategy: 'fuzzy', + parserDirectives: yarnScriptParserDirectives, + isCommand: true, + }, + { + name: 'env', + suggestions: ['env'], + description: 'Lists environment variables available to scripts', + isOptional: true, + }, + ], + }, + { + name: 'tag', + description: 'Add, remove, or list tags on a package', + }, + { + name: 'team', + description: 'Maintain team memberships', + subcommands: [ + { + name: 'create', + description: 'Create a new team', + args: { + name: '', + }, + }, + { + name: 'destroy', + description: 'Destroys an existing team', + args: { + name: '', + }, + }, + { + name: 'add', + description: 'Add a user to an existing team', + args: [ + { + name: '', + }, + { + name: '', + }, + ], + }, + { + name: 'remove', + description: 'Remove a user from a team they belong to', + args: { + name: ' ', + }, + }, + { + name: 'list', + description: + 'If performed on an organization name, will return a list of existing teams under that organization. If performed on a team, it will instead return a list of all users belonging to that particular team', + args: { + name: '|', + }, + }, + ], + }, + { + name: 'unlink', + description: 'Unlink a previously created symlink for a package', + }, + { + name: 'unplug', + description: '', + }, + { + name: 'upgrade', + description: + 'Upgrades packages to their latest version based on the specified range', + args: { + name: 'package', + generators: dependenciesGenerator, + filterStrategy: 'fuzzy', + isVariadic: true, + isOptional: true, + }, + options: [ + ...commonOptions, + { + name: ['-S', '--scope'], + description: 'Upgrade packages under the specified scope', + args: { name: 'scope' }, + }, + { + name: ['-L', '--latest'], + description: 'List the latest version of packages', + }, + { + name: ['-E', '--exact'], + description: + 'Install exact version. Only used when --latest is specified', + dependsOn: ['--latest'], + }, + { + name: ['-P', '--pattern'], + description: 'Upgrade packages that match pattern', + args: { name: 'pattern' }, + }, + { + name: ['-T', '--tilde'], + description: + 'Install most recent release with the same minor version. Only used when --latest is specified', + }, + { + name: ['-C', '--caret'], + description: + 'Install most recent release with the same major version. Only used when --latest is specified', + dependsOn: ['--latest'], + }, + { + name: ['-A', '--audit'], + description: 'Run vulnerability audit on installed packages', + }, + { name: ['-h', '--help'], description: 'Output usage information' }, + ], + }, + { + name: 'upgrade-interactive', + description: 'Upgrades packages in interactive mode', + options: [ + { + name: '--latest', + description: 'Use the version tagged latest in the registry', + }, + ], + }, + { + name: 'version', + description: 'Update version of your package', + options: [ + ...commonOptions, + { name: ['-h', '--help'], description: 'Output usage information' }, + { + name: '--new-version', + description: 'New version', + args: { name: 'version' }, + }, + { + name: '--major', + description: 'Auto-increment major version number', + }, + { + name: '--minor', + description: 'Auto-increment minor version number', + }, + { + name: '--patch', + description: 'Auto-increment patch version number', + }, + { + name: '--premajor', + description: 'Auto-increment premajor version number', + }, + { + name: '--preminor', + description: 'Auto-increment preminor version number', + }, + { + name: '--prepatch', + description: 'Auto-increment prepatch version number', + }, + { + name: '--prerelease', + description: 'Auto-increment prerelease version number', + }, + { + name: '--preid', + description: 'Add a custom identifier to the prerelease', + args: { name: 'preid' }, + }, + { + name: '--message', + description: 'Message', + args: { name: 'message' }, + }, + { name: '--no-git-tag-version', description: 'No git tag version' }, + { + name: '--no-commit-hooks', + description: 'Bypass git hooks when committing new version', + }, + { name: '--access', description: 'Access', args: { name: 'access' } }, + { name: '--tag', description: 'Tag', args: { name: 'tag' } }, + ], + }, + { + name: 'versions', + description: + 'Displays version information of the currently installed Yarn, Node.js, and its dependencies', + }, + { + name: 'why', + description: 'Show information about why a package is installed', + args: { + name: 'package', + filterStrategy: 'fuzzy', + generators: allDependenciesGenerator, + }, + options: [ + ...commonOptions, + { + name: ['-h', '--help'], + description: 'Output usage information', + }, + { + name: '--peers', + description: + 'Print the peer dependencies that match the specified name', + }, + { + name: ['-R', '--recursive'], + description: + 'List, for each workspace, what are all the paths that lead to the dependency', + }, + ], + }, + { + name: 'workspace', + description: 'Manage workspace', + filterStrategy: 'fuzzy', + generateSpec: async (_tokens, executeShellCommand) => { + const version = ( + await executeShellCommand({ + command: 'yarn', + args: ['--version'], + }) + ).stdout; + const isYarnV1 = version.startsWith('1.'); + + const getWorkspacesDefinitionsV1 = async () => { + const { stdout } = await executeShellCommand({ + command: 'yarn', + args: ['workspaces', 'info'], + }); + + const startJson = stdout.indexOf('{'); + const endJson = stdout.lastIndexOf('}'); + + return Object.entries( + JSON.parse(stdout.slice(startJson, endJson + 1)) as Record< + string, + { location: string } + > + ).map(([name, { location }]) => ({ + name, + location, + })); + }; + + // For yarn >= 2.0.0 + const getWorkspacesDefinitionsVOther = async () => { + // yarn workspaces list --json + const out = ( + await executeShellCommand({ + command: 'yarn', + args: ['workspaces', 'list', '--json'], + }) + ).stdout; + return out.split('\n').map((line) => JSON.parse(line.trim())); + }; + + try { + const workspacesDefinitions = isYarnV1 + ? // transform Yarn V1 output to array of workspaces like Yarn V2 + await getWorkspacesDefinitionsV1() + : // in yarn v>=2.0.0, workspaces definitions are a list of JSON lines + await getWorkspacesDefinitionsVOther(); + + const subcommands: Fig.Subcommand[] = workspacesDefinitions.map( + ({ name, location }: { name: string; location: string }) => ({ + name, + description: 'Workspaces', + args: { + name: 'script', + generators: { + cache: { + strategy: 'stale-while-revalidate', + ttl: 60_000, // 60s + }, + script: ['cat', `${location}/package.json`], + postProcess: function (out: string) { + if (out.trim() === '') { + return []; + } + try { + const packageContent = JSON.parse(out); + const scripts = packageContent['scripts']; + if (scripts) { + return Object.keys(scripts).map((script) => ({ + name: script, + })); + } + } catch (e) { } + return []; + }, + }, + }, + }) + ); + + return { + name: 'workspace', + subcommands, + }; + } catch (e) { + console.error(e); + } + return { name: 'workspaces' }; + }, + }, + { + name: 'workspaces', + description: 'Show information about your workspaces', + options: [ + { + name: 'subcommand', + description: '', + args: { + suggestions: [{ name: 'info' }, { name: 'run' }], + }, + }, + { + name: 'flags', + description: '', + }, + ], + }, + { + name: 'set', + description: 'Set global Yarn options', + subcommands: [ + { + name: 'resolution', + description: 'Enforce a package resolution', + args: [ + { + name: 'descriptor', + description: + 'A descriptor for the package, in the form of \'lodash@npm:^ 1.2.3\'', + }, + { + name: 'resolution', + description: 'The version of the package to resolve', + }, + ], + options: [ + { + name: ['-s', '--save'], + description: + 'Persist the resolution inside the top-level manifest', + }, + ], + }, + { + name: 'version', + description: 'Lock the Yarn version used by the project', + args: { + name: 'version', + description: + 'Use the specified version, which can also be a Yarn 2 build (e.g 2.0.0-rc.30) or a Yarn 1 build (e.g 1.22.1)', + template: 'filepaths', + suggestions: [ + { + name: 'from-sources', + insertValue: 'from sources', + }, + 'latest', + 'canary', + 'classic', + 'self', + ], + }, + options: [ + { + name: '--only-if-needed', + description: + 'Only lock the Yarn version if it isn\'t already locked', + }, + ], + }, + ], + }, + ], +}; + +export default completionSpec; diff --git a/extensions/terminal-suggest/src/constants.ts b/extensions/terminal-suggest/src/constants.ts index 7d877e73961..db376c2c3b4 100644 --- a/extensions/terminal-suggest/src/constants.ts +++ b/extensions/terminal-suggest/src/constants.ts @@ -113,10 +113,7 @@ export const upstreamSpecs = [ // JavaScript / TypeScript 'node', - 'npm', 'nvm', - 'pnpm', - 'yarn', 'yo', // Python diff --git a/extensions/terminal-suggest/src/env/pathExecutableCache.ts b/extensions/terminal-suggest/src/env/pathExecutableCache.ts index 6576bb59894..8fae425393b 100644 --- a/extensions/terminal-suggest/src/env/pathExecutableCache.ts +++ b/extensions/terminal-suggest/src/env/pathExecutableCache.ts @@ -5,13 +5,11 @@ import * as fs from 'fs/promises'; import * as vscode from 'vscode'; -import { isExecutable } from '../helpers/executable'; +import { isExecutable, WindowsExecutableExtensionsCache } from '../helpers/executable'; import { osIsWindows } from '../helpers/os'; import type { ICompletionResource } from '../types'; import { getFriendlyResourcePath } from '../helpers/uri'; import { SettingsIds } from '../constants'; -import * as filesystem from 'fs'; -import * as path from 'path'; import { TerminalShellType } from '../terminalSuggestMain'; const isWindows = osIsWindows(); @@ -24,7 +22,7 @@ export interface IExecutablesInPath { export class PathExecutableCache implements vscode.Disposable { private _disposables: vscode.Disposable[] = []; - private _cachedWindowsExeExtensions: { [key: string]: boolean | undefined } | undefined; + private readonly _windowsExecutableExtensionsCache: WindowsExecutableExtensionsCache | undefined; private _cachedExes: Map | undefined> = new Map(); private _inProgressRequest: { @@ -35,10 +33,10 @@ export class PathExecutableCache implements vscode.Disposable { constructor() { if (isWindows) { - this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._windowsExecutableExtensionsCache = new WindowsExecutableExtensionsCache(this._getConfiguredWindowsExecutableExtensions()); this._disposables.push(vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(SettingsIds.CachedWindowsExecutableExtensions)) { - this._cachedWindowsExeExtensions = vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); + this._windowsExecutableExtensionsCache?.update(this._getConfiguredWindowsExecutableExtensions()); this._cachedExes.clear(); } })); @@ -161,6 +159,7 @@ export class PathExecutableCache implements vscode.Disposable { const result = new Set(); const fileResource = vscode.Uri.file(path); const files = await vscode.workspace.fs.readDirectory(fileResource); + const windowsExecutableExtensions = this._windowsExecutableExtensionsCache?.getExtensions(); await Promise.all( files.map(([file, fileType]) => (async () => { let kind: vscode.TerminalCompletionItemKind | undefined; @@ -177,7 +176,7 @@ export class PathExecutableCache implements vscode.Disposable { if (lstat.isSymbolicLink()) { try { const symlinkRealPath = await fs.realpath(resource.fsPath); - const isExec = await isExecutable(symlinkRealPath, this._cachedWindowsExeExtensions); + const isExec = await isExecutable(symlinkRealPath, windowsExecutableExtensions); if (!isExec) { return; } @@ -199,7 +198,7 @@ export class PathExecutableCache implements vscode.Disposable { return; } - const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(formattedPath, this._cachedWindowsExeExtensions); + const isExec = kind === vscode.TerminalCompletionItemKind.Method || await isExecutable(resource.fsPath, windowsExecutableExtensions); if (!isExec) { return; } @@ -218,47 +217,9 @@ export class PathExecutableCache implements vscode.Disposable { return undefined; } } -} - -export async function watchPathDirectories(context: vscode.ExtensionContext, env: ITerminalEnvironment, pathExecutableCache: PathExecutableCache | undefined): Promise { - const pathDirectories = new Set(); - - const envPath = env.PATH; - if (envPath) { - envPath.split(path.delimiter).forEach(p => pathDirectories.add(p)); - } - - const activeWatchers = new Set(); - - // Watch each directory - for (const dir of pathDirectories) { - try { - if (activeWatchers.has(dir)) { - // Skip if already watching or directory doesn't exist - continue; - } - - const stat = await fs.stat(dir); - if (!stat.isDirectory()) { - continue; - } - const watcher = filesystem.watch(dir, { persistent: false }, () => { - if (pathExecutableCache) { - // Refresh cache when directory contents change - pathExecutableCache.refresh(dir); - } - }); - - activeWatchers.add(dir); - - context.subscriptions.push(new vscode.Disposable(() => { - try { - watcher.close(); - activeWatchers.delete(dir); - } catch { } { } - })); - } catch { } + private _getConfiguredWindowsExecutableExtensions(): { [key: string]: boolean | undefined } | undefined { + return vscode.workspace.getConfiguration(SettingsIds.SuggestPrefix).get(SettingsIds.CachedWindowsExecutableExtensionsSuffixOnly); } } diff --git a/extensions/terminal-suggest/src/fig/figInterface.ts b/extensions/terminal-suggest/src/fig/figInterface.ts index 387eaa340ef..0f1fcc20d6d 100644 --- a/extensions/terminal-suggest/src/fig/figInterface.ts +++ b/extensions/terminal-suggest/src/fig/figInterface.ts @@ -21,7 +21,7 @@ import { IFigExecuteExternals } from './execute'; export interface IFigSpecSuggestionsResult { showFiles: boolean; - showFolders: boolean; + showDirectories: boolean; fileExtensions?: string[]; hasCurrentArg: boolean; items: vscode.TerminalCompletionItem[]; @@ -41,7 +41,7 @@ export async function getFigSuggestions( ): Promise { const result: IFigSpecSuggestionsResult = { showFiles: false, - showFolders: false, + showDirectories: false, hasCurrentArg: false, items: [], }; @@ -107,7 +107,7 @@ export async function getFigSuggestions( result.hasCurrentArg ||= !!completionItemResult?.hasCurrentArg; if (completionItemResult) { result.showFiles ||= completionItemResult.showFiles; - result.showFolders ||= completionItemResult.showFolders; + result.showDirectories ||= completionItemResult.showDirectories; result.fileExtensions ||= completionItemResult.fileExtensions; if (completionItemResult.items) { result.items = result.items.concat(completionItemResult.items); @@ -129,7 +129,7 @@ async function getFigSpecSuggestions( token?: vscode.CancellationToken, ): Promise { let showFiles = false; - let showFolders = false; + let showDirectories = false; let fileExtensions: string[] | undefined; const command = getCommand(terminalContext.commandLine, {}, terminalContext.cursorIndex); @@ -154,13 +154,13 @@ async function getFigSpecSuggestions( if (completionItemResult) { showFiles = completionItemResult.showFiles; - showFolders = completionItemResult.showFolders; + showDirectories = completionItemResult.showDirectories; fileExtensions = completionItemResult.fileExtensions; } return { showFiles: showFiles, - showFolders: showFolders, + showDirectories: showDirectories, fileExtensions, hasCurrentArg: !!parsedArguments.currentArg, items, @@ -178,9 +178,9 @@ export async function collectCompletionItemResult( env: Record, items: vscode.TerminalCompletionItem[], executeExternals: IFigExecuteExternals -): Promise<{ showFiles: boolean; showFolders: boolean; fileExtensions: string[] | undefined } | undefined> { +): Promise<{ showFiles: boolean; showDirectories: boolean; fileExtensions: string[] | undefined } | undefined> { let showFiles = false; - let showFolders = false; + let showDirectories = false; let fileExtensions: string[] | undefined; const addSuggestions = async (specArgs: SpecArg[] | Record | undefined, kind: vscode.TerminalCompletionItemKind, parsedArguments?: ArgumentParserResult) => { @@ -223,11 +223,11 @@ export async function collectCompletionItemResult( for (const item of (await generatorResult?.request) ?? []) { if (item.type === 'file') { showFiles = true; - showFolders = true; + showDirectories = true; fileExtensions = item._internal?.fileExtensions as string[] | undefined; } if (item.type === 'folder') { - showFolders = true; + showDirectories = true; } if (!item.name) { @@ -258,14 +258,14 @@ export async function collectCompletionItemResult( if (template === 'filepaths') { showFiles = true; } else if (template === 'folders') { - showFolders = true; + showDirectories = true; } } } } } if (!specArgs) { - return { showFiles, showFolders }; + return { showFiles, showDirectories }; } const flagsToExclude = kind === vscode.TerminalCompletionItemKind.Flag ? parsedArguments?.passedOptions.map(option => option.name).flat() : undefined; @@ -341,9 +341,10 @@ export async function collectCompletionItemResult( } if (parsedArguments.suggestionFlags & SuggestionFlag.Options) { await addSuggestions(parsedArguments.completionObj.options, vscode.TerminalCompletionItemKind.Flag, parsedArguments); + await addSuggestions(parsedArguments.completionObj.persistentOptions, vscode.TerminalCompletionItemKind.Flag, parsedArguments); } - return { showFiles, showFolders, fileExtensions }; + return { showFiles, showDirectories, fileExtensions }; } function convertEnvRecordToArray(env: Record): EnvironmentVariable[] { diff --git a/extensions/terminal-suggest/src/helpers/executable.ts b/extensions/terminal-suggest/src/helpers/executable.ts index 00f56f09cbd..7cd854c8ba4 100644 --- a/extensions/terminal-suggest/src/helpers/executable.ts +++ b/extensions/terminal-suggest/src/helpers/executable.ts @@ -6,10 +6,10 @@ import { osIsWindows } from './os'; import * as fs from 'fs/promises'; -export function isExecutable(filePath: string, configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined } | undefined): Promise | boolean { +export function isExecutable(filePath: string, windowsExecutableExtensions?: Set): Promise | boolean { if (osIsWindows()) { - const resolvedWindowsExecutableExtensions = resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions); - return resolvedWindowsExecutableExtensions.find(ext => filePath.endsWith(ext)) !== undefined; + const extensions = windowsExecutableExtensions ?? defaultWindowsExecutableExtensionsSet; + return hasWindowsExecutableExtension(filePath, extensions); } return isExecutableUnix(filePath); } @@ -25,22 +25,6 @@ export async function isExecutableUnix(filePath: string): Promise { } } - -function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): string[] { - const resolvedWindowsExecutableExtensions: string[] = windowsDefaultExecutableExtensions; - const excluded = new Set(); - if (configuredWindowsExecutableExtensions) { - for (const [key, value] of Object.entries(configuredWindowsExecutableExtensions)) { - if (value === true) { - resolvedWindowsExecutableExtensions.push(key); - } else { - excluded.add(key); - } - } - } - return Array.from(new Set(resolvedWindowsExecutableExtensions)).filter(ext => !excluded.has(ext)); -} - export const windowsDefaultExecutableExtensions: string[] = [ '.exe', // Executable file '.bat', // Batch file @@ -59,3 +43,65 @@ export const windowsDefaultExecutableExtensions: string[] = [ '.pl', // Perl script (requires Perl interpreter) '.sh', // Shell script (via WSL or third-party tools) ]; + +const defaultWindowsExecutableExtensionsSet = new Set(); +for (const ext of windowsDefaultExecutableExtensions) { + defaultWindowsExecutableExtensionsSet.add(ext); +} + +export class WindowsExecutableExtensionsCache { + private _rawConfig: { [key: string]: boolean | undefined } | undefined; + private _cachedExtensions: Set | undefined; + + constructor(rawConfig?: { [key: string]: boolean | undefined }) { + this._rawConfig = rawConfig; + } + + update(rawConfig: { [key: string]: boolean | undefined } | undefined): void { + this._rawConfig = rawConfig; + this._cachedExtensions = undefined; + } + + getExtensions(): Set { + if (!this._cachedExtensions) { + this._cachedExtensions = resolveWindowsExecutableExtensions(this._rawConfig); + } + return this._cachedExtensions; + } +} + +function hasWindowsExecutableExtension(filePath: string, extensions: Set): boolean { + const fileName = filePath.slice(Math.max(filePath.lastIndexOf('\\'), filePath.lastIndexOf('/')) + 1); + for (const ext of extensions) { + if (fileName.endsWith(ext)) { + return true; + } + } + return false; +} + +function resolveWindowsExecutableExtensions(configuredWindowsExecutableExtensions?: { [key: string]: boolean | undefined }): Set { + const extensions = new Set(); + const configured = configuredWindowsExecutableExtensions ?? {}; + const excluded = new Set(); + + for (const [ext, value] of Object.entries(configured)) { + if (value !== true) { + excluded.add(ext); + } + } + + for (const ext of windowsDefaultExecutableExtensions) { + if (!excluded.has(ext)) { + extensions.add(ext); + } + } + + for (const [ext, value] of Object.entries(configured)) { + if (value === true) { + extensions.add(ext); + } + } + + return extensions; +} diff --git a/extensions/terminal-suggest/src/terminalSuggestMain.ts b/extensions/terminal-suggest/src/terminalSuggestMain.ts index 13f2399d908..599f4b93849 100644 --- a/extensions/terminal-suggest/src/terminalSuggestMain.ts +++ b/extensions/terminal-suggest/src/terminalSuggestMain.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ExecOptionsWithStringEncoding } from 'child_process'; +import * as fs from 'fs'; +import { basename, delimiter } from 'path'; import * as vscode from 'vscode'; import azdSpec from './completions/azd'; import cdSpec from './completions/cd'; @@ -14,10 +16,13 @@ import codeTunnelInsidersCompletionSpec from './completions/code-tunnel-insiders import copilotSpec from './completions/copilot'; import gitCompletionSpec from './completions/git'; import ghCompletionSpec from './completions/gh'; +import npmCompletionSpec from './completions/npm'; import npxCompletionSpec from './completions/npx'; +import pnpmCompletionSpec from './completions/pnpm'; import setLocationSpec from './completions/set-location'; +import yarnCompletionSpec from './completions/yarn'; import { upstreamSpecs } from './constants'; -import { ITerminalEnvironment, PathExecutableCache, watchPathDirectories } from './env/pathExecutableCache'; +import { ITerminalEnvironment, PathExecutableCache } from './env/pathExecutableCache'; import { executeCommand, executeCommandTimeout, IFigExecuteExternals } from './fig/execute'; import { getFigSuggestions } from './fig/figInterface'; import { createCompletionItem } from './helpers/completionItem'; @@ -30,8 +35,6 @@ import { getPwshGlobals } from './shell/pwsh'; import { getZshGlobals } from './shell/zsh'; import { defaultShellTypeResetChars, getTokenType, shellTypeResetChars, TokenType } from './tokens'; import type { ICompletionResource } from './types'; -import { basename } from 'path'; - export const enum TerminalShellType { Bash = 'bash', Fish = 'fish', @@ -69,8 +72,11 @@ export const availableSpecs: Fig.Spec[] = [ copilotSpec, gitCompletionSpec, ghCompletionSpec, + npmCompletionSpec, npxCompletionSpec, + pnpmCompletionSpec, setLocationSpec, + yarnCompletionSpec, ]; for (const spec of upstreamSpecs) { availableSpecs.push(require(`./completions/upstream/${spec}`).default); @@ -309,11 +315,11 @@ export async function activate(context: vscode.ExtensionContext) { } const cwd = result.cwd ?? terminal.shellIntegration?.cwd; - if (cwd && (result.showFiles || result.showFolders)) { + if (cwd && (result.showFiles || result.showDirectories)) { const globPattern = createFileGlobPattern(result.fileExtensions); return new vscode.TerminalCompletionList(result.items, { showFiles: result.showFiles, - showDirectories: result.showFolders, + showDirectories: result.showDirectories, globPattern, cwd, }); @@ -321,13 +327,63 @@ export async function activate(context: vscode.ExtensionContext) { return result.items; } }, '/', '\\')); - await watchPathDirectories(context, currentTerminalEnv, pathExecutableCache); + watchPathDirectories(context, currentTerminalEnv, pathExecutableCache); context.subscriptions.push(vscode.commands.registerCommand('terminal.integrated.suggest.clearCachedGlobals', () => { cachedGlobals.clear(); })); } +async function watchPathDirectories(context: vscode.ExtensionContext, env: ITerminalEnvironment, pathExecutableCache: PathExecutableCache | undefined): Promise { + const pathDirectories = new Set(); + + const envPath = env.PATH; + if (envPath) { + envPath.split(delimiter).forEach(p => pathDirectories.add(p)); + } + + const activeWatchers = new Set(); + + let debounceTimer: NodeJS.Timeout | undefined; // debounce in case many file events fire at once + function handleChange() { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + debounceTimer = setTimeout(() => { + pathExecutableCache?.refresh(); + debounceTimer = undefined; + }, 300); + } + + // Watch each directory + for (const dir of pathDirectories) { + if (activeWatchers.has(dir)) { + // Skip if already watching this directory + continue; + } + + try { + const stat = await fs.promises.stat(dir); + if (!stat.isDirectory()) { + continue; + } + } catch { + // File not found + continue; + } + + const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(dir), '*')); + context.subscriptions.push( + watcher, + watcher.onDidCreate(() => handleChange()), + watcher.onDidChange(() => handleChange()), + watcher.onDidDelete(() => handleChange()) + ); + + activeWatchers.add(dir); + } +} + /** * Adjusts the current working directory based on a given current command string if it is a folder. * @param currentCommandString - The current command string, which might contain a folder path prefix. @@ -357,6 +413,13 @@ export async function resolveCwdFromCurrentCommandString(currentCommandString: s } const relativeFolder = lastSlashIndex === -1 ? '' : prefix.slice(0, lastSlashIndex); + // Don't pre-resolve paths with .. segments - let the completion service handle those + // to avoid double-navigation (e.g., typing ../ would resolve cwd to parent here, + // then completion service would navigate up again from the already-parent cwd) + if (relativeFolder.includes('..')) { + return undefined; + } + // Use vscode.Uri.joinPath for path resolution const resolvedUri = vscode.Uri.joinPath(currentCwd, relativeFolder); @@ -423,10 +486,10 @@ export async function getCompletionItemsFromSpecs( name: string, token?: vscode.CancellationToken, executeExternals?: IFigExecuteExternals, -): Promise<{ items: vscode.TerminalCompletionItem[]; showFiles: boolean; showFolders: boolean; fileExtensions?: string[]; cwd?: vscode.Uri }> { +): Promise<{ items: vscode.TerminalCompletionItem[]; showFiles: boolean; showDirectories: boolean; fileExtensions?: string[]; cwd?: vscode.Uri }> { let items: vscode.TerminalCompletionItem[] = []; let showFiles = false; - let showFolders = false; + let showDirectories = false; let hasCurrentArg = false; let fileExtensions: string[] | undefined; @@ -460,7 +523,7 @@ export async function getCompletionItemsFromSpecs( if (result) { hasCurrentArg ||= result.hasCurrentArg; showFiles ||= result.showFiles; - showFolders ||= result.showFolders; + showDirectories ||= result.showDirectories; fileExtensions = result.fileExtensions; if (result.items) { items = items.concat(result.items); @@ -496,18 +559,18 @@ export async function getCompletionItemsFromSpecs( } } showFiles = true; - showFolders = true; - } else if (!items.length && !showFiles && !showFolders && !hasCurrentArg) { + showDirectories = true; + } else if (!items.length && !showFiles && !showDirectories && !hasCurrentArg) { showFiles = true; - showFolders = true; + showDirectories = true; } let cwd: vscode.Uri | undefined; - if (shellIntegrationCwd && (showFiles || showFolders)) { + if (shellIntegrationCwd && (showFiles || showDirectories)) { cwd = await resolveCwdFromCurrentCommandString(currentCommandString, shellIntegrationCwd); } - return { items, showFiles, showFolders, fileExtensions, cwd }; + return { items, showFiles, showDirectories, fileExtensions, cwd }; } function getEnvAsRecord(shellIntegrationEnv: ITerminalEnvironment): Record { diff --git a/extensions/terminal-suggest/src/test/completions/cd.test.ts b/extensions/terminal-suggest/src/test/completions/cd.test.ts index a40d5ee2103..cee2f62e55a 100644 --- a/extensions/terminal-suggest/src/test/completions/cd.test.ts +++ b/extensions/terminal-suggest/src/test/completions/cd.test.ts @@ -37,7 +37,8 @@ export const cdTestSuiteSpec: ISuiteSpec = { // Relative directories (changes cwd due to /) { input: 'cd child/|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdChild } }, - { input: 'cd ../|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, - { input: 'cd ../sibling|', expectedCompletions, expectedResourceRequests: { type: 'folders', cwd: testPaths.cwdParent } }, + // Paths with .. are handled by the completion service to avoid double-navigation (no cwd resolution) + { input: 'cd ../|', expectedCompletions, expectedResourceRequests: { type: 'folders' } }, + { input: 'cd ../sibling|', expectedCompletions, expectedResourceRequests: { type: 'folders' } }, ] }; diff --git a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts index e08b755e60a..1b06db30546 100644 --- a/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts +++ b/extensions/terminal-suggest/src/test/completions/upstream/ls.test.ts @@ -84,8 +84,9 @@ export const lsTestSuiteSpec: ISuiteSpec = { // Relative directories (changes cwd due to /) { input: 'ls child/|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdChild } }, - { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, - { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both', cwd: testPaths.cwdParent } }, + // Paths with .. are handled by the completion service to avoid double-navigation (no cwd resolution) + { input: 'ls ../|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both' } }, + { input: 'ls ../sibling|', expectedCompletions: allOptions, expectedResourceRequests: { type: 'both' } }, ] }; diff --git a/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts index 42b28d6d1dd..83e006a391a 100644 --- a/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts +++ b/extensions/terminal-suggest/src/test/env/pathExecutableCache.test.ts @@ -7,6 +7,7 @@ import 'mocha'; import { deepStrictEqual, strictEqual } from 'node:assert'; import type { MarkdownString } from 'vscode'; import { PathExecutableCache } from '../../env/pathExecutableCache'; +import { WindowsExecutableExtensionsCache, windowsDefaultExecutableExtensions } from '../../helpers/executable'; suite('PathExecutableCache', () => { test('cache should return empty for empty PATH', async () => { @@ -67,4 +68,43 @@ suite('PathExecutableCache', () => { strictEqual(symlinkDoc, `${symlinkPath} -> ${realPath}`); }); } + + if (process.platform === 'win32') { + suite('WindowsExecutableExtensionsCache', () => { + test('returns default extensions when not configured', () => { + const cache = new WindowsExecutableExtensionsCache(); + const extensions = cache.getExtensions(); + + for (const ext of windowsDefaultExecutableExtensions) { + strictEqual(extensions.has(ext), true, `expected default extension ${ext}`); + } + }); + + test('honors configured additions and removals', () => { + const cache = new WindowsExecutableExtensionsCache({ + '.added': true, + '.bat': false + }); + + const extensions = cache.getExtensions(); + strictEqual(extensions.has('.added'), true); + strictEqual(extensions.has('.bat'), false); + strictEqual(extensions.has('.exe'), true); + }); + + test('recomputes only after update is called', () => { + const cache = new WindowsExecutableExtensionsCache({ '.one': true }); + + const first = cache.getExtensions(); + const second = cache.getExtensions(); + strictEqual(first, second, 'expected cached set to be reused'); + + cache.update({ '.two': true }); + const third = cache.getExtensions(); + strictEqual(third.has('.two'), true); + strictEqual(third.has('.one'), false); + strictEqual(third === first, false, 'expected cache to recompute after update'); + }); + }); + } }); diff --git a/extensions/terminal-suggest/src/test/fig.test.ts b/extensions/terminal-suggest/src/test/fig.test.ts index b8144219e88..c3aeee4fcf1 100644 --- a/extensions/terminal-suggest/src/test/fig.test.ts +++ b/extensions/terminal-suggest/src/test/fig.test.ts @@ -183,6 +183,56 @@ export const figGenericTestSuites: ISuiteSpec[] = [ { input: 'foo b|', expectedCompletions: ['b', 'foo'] }, { input: 'foo c|', expectedCompletions: ['c', 'foo'] }, ] + }, + { + name: 'Fig persistent options', + completionSpecs: [ + { + name: 'foo', + description: 'Foo', + options: [ + { name: '--help', description: 'Show help', isPersistent: true }, + { name: '--docs', description: 'Show docs' }, + { name: '--version', description: 'Version info', isPersistent: false } + ], + subcommands: [ + { + name: 'bar', + description: 'Bar subcommand', + options: [ + { name: '--local', description: 'Local option' } + ] + }, + { + name: 'baz', + description: 'Baz subcommand', + options: [ + { name: '--another', description: 'Another option' } + ], + subcommands: [ + { + name: 'nested', + description: 'Nested subcommand' + } + ] + } + ] + } + ], + availableCommands: 'foo', + testSpecs: [ + // Top-level should show all options including persistent + { input: 'foo |', expectedCompletions: ['--help', '--docs', '--version', 'bar', 'baz'] }, + // First-level subcommand should only inherit persistent options (not --docs or --version) + { input: 'foo bar |', expectedCompletions: ['--help', '--local'] }, + // Another first-level subcommand should also inherit only persistent options + { input: 'foo baz |', expectedCompletions: ['--help', '--another', 'nested'] }, + // Nested subcommand should inherit persistent options from top level + { input: 'foo baz nested |', expectedCompletions: ['--help'] }, + // Persistent options should be available even after using local options + { input: 'foo bar --local |', expectedCompletions: ['--help'] }, + ] } ]; + diff --git a/extensions/terminal-suggest/src/test/helpers.ts b/extensions/terminal-suggest/src/test/helpers.ts index a4101a49194..b5080535fcf 100644 --- a/extensions/terminal-suggest/src/test/helpers.ts +++ b/extensions/terminal-suggest/src/test/helpers.ts @@ -21,7 +21,7 @@ export interface ITestSpec { input: string; expectedResourceRequests?: { type: 'files' | 'folders' | 'both'; - cwd: Uri; + cwd?: Uri; }; expectedCompletions?: (string | ICompletionResource)[]; } diff --git a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts index cb4cbbddd5d..57749d2df68 100644 --- a/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts +++ b/extensions/terminal-suggest/src/test/terminalSuggestMain.test.ts @@ -85,7 +85,7 @@ suite('Terminal Suggest', () => { let expectedString = testSpec.expectedCompletions ? `[${testSpec.expectedCompletions.map(e => `'${e}'`).join(', ')}]` : '[]'; if (testSpec.expectedResourceRequests) { expectedString += ` + ${testSpec.expectedResourceRequests.type}`; - if (testSpec.expectedResourceRequests.cwd.fsPath !== testPaths.cwd.fsPath) { + if (testSpec.expectedResourceRequests.cwd && testSpec.expectedResourceRequests.cwd.fsPath !== testPaths.cwd.fsPath) { expectedString += ` @ ${basename(testSpec.expectedResourceRequests.cwd.fsPath)}/`; } } @@ -94,7 +94,7 @@ suite('Terminal Suggest', () => { const cursorIndex = testSpec.input.indexOf('|'); const currentCommandString = getCurrentCommandAndArgs(commandLine, cursorIndex, undefined); const showFiles = testSpec.expectedResourceRequests?.type === 'files' || testSpec.expectedResourceRequests?.type === 'both'; - const showFolders = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; + const showDirectories = testSpec.expectedResourceRequests?.type === 'folders' || testSpec.expectedResourceRequests?.type === 'both'; const terminalContext = { commandLine, cursorIndex }; const result = await getCompletionItemsFromSpecs( completionSpecs, @@ -119,7 +119,7 @@ suite('Terminal Suggest', () => { (testSpec.expectedCompletions ?? []).sort() ); strictEqual(result.showFiles, showFiles, 'Show files different than expected, got: ' + result.showFiles); - strictEqual(result.showFolders, showFolders, 'Show folders different than expected, got: ' + result.showFolders); + strictEqual(result.showDirectories, showDirectories, 'Show directories different than expected, got: ' + result.showDirectories); if (testSpec.expectedResourceRequests?.cwd) { strictEqual(result.cwd?.fsPath, testSpec.expectedResourceRequests.cwd.fsPath, 'Non matching cwd'); } diff --git a/extensions/terminal-suggest/tsconfig.json b/extensions/terminal-suggest/tsconfig.json index 1265a62536f..2792930bc21 100644 --- a/extensions/terminal-suggest/tsconfig.json +++ b/extensions/terminal-suggest/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "types": [ "node" diff --git a/extensions/theme-2026/.vscodeignore b/extensions/theme-2026/.vscodeignore new file mode 100644 index 00000000000..7ef29eaaabf --- /dev/null +++ b/extensions/theme-2026/.vscodeignore @@ -0,0 +1,5 @@ +CUSTOMIZATION.md +node_modules/** +.vscode/** +.gitignore +**/*.map diff --git a/extensions/theme-2026/package.json b/extensions/theme-2026/package.json new file mode 100644 index 00000000000..305cc066c89 --- /dev/null +++ b/extensions/theme-2026/package.json @@ -0,0 +1,38 @@ +{ + "name": "theme-2026", + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "version": "0.1.0", + "publisher": "vscode", + "license": "MIT", + "engines": { + "vscode": "^1.85.0" + }, + "enabledApiProposals": [ + "css" + ], + "categories": [ + "Themes" + ], + "contributes": { + "themes": [ + { + "id": "Experimental Light", + "label": "VS Code Light", + "uiTheme": "vs", + "path": "./themes/2026-light.json" + }, + { + "id": "Experimental Dark", + "label": "VS Code Dark", + "uiTheme": "vs-dark", + "path": "./themes/2026-dark.json" + } + ], + "css": [ + { + "path": "./themes/styles.css" + } + ] + } +} diff --git a/extensions/theme-2026/package.nls.json b/extensions/theme-2026/package.nls.json new file mode 100644 index 00000000000..639cf87f44e --- /dev/null +++ b/extensions/theme-2026/package.nls.json @@ -0,0 +1,6 @@ +{ + "displayName": "2026 Themes", + "description": "Modern, minimal light and dark themes for 2026 with consistent neutral palette and accessible color contrast", + "2026-light-label": "2026 Light", + "2026-dark-label": "2026 Dark" +} diff --git a/extensions/theme-2026/themes/2026-dark.json b/extensions/theme-2026/themes/2026-dark.json new file mode 100644 index 00000000000..3e3c676bac1 --- /dev/null +++ b/extensions/theme-2026/themes/2026-dark.json @@ -0,0 +1,552 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Dark", + "type": "dark", + "colors": { + "foreground": "#bfbfbf", + "disabledForeground": "#444444", + "errorForeground": "#f48771", + "descriptionForeground": "#999999", + "icon.foreground": "#888888", + "focusBorder": "#3994BCB3", + "textBlockQuote.background": "#242526", + "textBlockQuote.border": "#2A2B2CFF", + "textCodeBlock.background": "#242526", + "textLink.foreground": "#48A0C7", + "textLink.activeForeground": "#53A5CA", + "textPreformat.foreground": "#888888", + "textSeparator.foreground": "#2a2a2aFF", + "button.background": "#3994BC", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#3E9BC4", + "button.border": "#2A2B2CFF", + "checkbox.background": "#242526", + "checkbox.border": "#333536", + "checkbox.foreground": "#bfbfbf", + "dropdown.background": "#191A1B", + "dropdown.border": "#333536", + "dropdown.foreground": "#bfbfbf", + "dropdown.listBackground": "#202122", + "input.background": "#191A1B", + "input.border": "#333536FF", + "input.foreground": "#bfbfbf", + "input.placeholderForeground": "#777777", + "inputOption.activeBackground": "#3994BC33", + "inputOption.activeForeground": "#bfbfbf", + "inputOption.activeBorder": "#2A2B2CFF", + "inputValidation.errorBackground": "#191A1B", + "inputValidation.errorBorder": "#2A2B2CFF", + "inputValidation.errorForeground": "#bfbfbf", + "inputValidation.infoBackground": "#191A1B", + "inputValidation.infoBorder": "#2A2B2CFF", + "inputValidation.infoForeground": "#bfbfbf", + "inputValidation.warningBackground": "#191A1B", + "inputValidation.warningBorder": "#2A2B2CFF", + "inputValidation.warningForeground": "#bfbfbf", + "scrollbar.shadow": "#191B1D4D", + "scrollbarSlider.background": "#83848533", + "scrollbarSlider.hoverBackground": "#83848566", + "scrollbarSlider.activeBackground": "#83848599", + "badge.background": "#3994BC", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#878889", + "list.activeSelectionBackground": "#3994BC55", + "list.activeSelectionForeground": "#bfbfbf", + "list.inactiveSelectionBackground": "#2C2D2E", + "list.inactiveSelectionForeground": "#bfbfbf", + "list.hoverBackground": "#262728", + "list.hoverForeground": "#bfbfbf", + "list.dropBackground": "#3994BC1A", + "list.focusBackground": "#3994BC26", + "list.focusForeground": "#bfbfbf", + "list.focusOutline": "#3994BCB3", + "list.highlightForeground": "#48A0C7", + "list.invalidItemForeground": "#444444", + "list.errorForeground": "#f48771", + "list.warningForeground": "#e5ba7d", + "activityBar.background": "#191A1B", + "activityBar.foreground": "#bfbfbf", + "activityBar.inactiveForeground": "#888888", + "activityBar.border": "#2A2B2CFF", + "activityBar.activeBorder": "#bfbfbf", + "activityBar.activeFocusBorder": "#3994BCB3", + "activityBarBadge.background": "#3994BC", + "activityBarBadge.foreground": "#FFFFFF", + "activityBarTop.activeBorder": "#bfbfbf", + "sideBar.background": "#191A1B", + "sideBar.foreground": "#bfbfbf", + "sideBar.border": "#2A2B2CFF", + "sideBarTitle.foreground": "#bfbfbf", + "sideBarSectionHeader.background": "#191A1B", + "sideBarSectionHeader.foreground": "#bfbfbf", + "sideBarSectionHeader.border": "#2A2B2CFF", + "titleBar.activeBackground": "#191A1B", + "titleBar.activeForeground": "#bfbfbf", + "titleBar.inactiveBackground": "#191A1B", + "titleBar.inactiveForeground": "#888888", + "titleBar.border": "#2A2B2CFF", + "menubar.selectionBackground": "#242526", + "menubar.selectionForeground": "#bfbfbf", + "menu.background": "#202122", + "menu.foreground": "#bfbfbf", + "menu.selectionBackground": "#3994BC26", + "menu.selectionForeground": "#bfbfbf", + "menu.separatorBackground": "#838485", + "menu.border": "#2A2B2CFF", + "commandCenter.foreground": "#bfbfbf", + "commandCenter.activeForeground": "#bfbfbf", + "commandCenter.background": "#191A1B", + "commandCenter.activeBackground": "#191A1BCC", + "commandCenter.border": "#333536BB", + "editor.background": "#121314", + "editor.foreground": "#BBBEBF", + "editorLineNumber.foreground": "#858889", + "editorLineNumber.activeForeground": "#BBBEBF", + "editorCursor.foreground": "#BBBEBF", + "editor.selectionBackground": "#3994BC33", + "editor.inactiveSelectionBackground": "#3994BC80", + "editor.selectionHighlightBackground": "#3994BC1A", + "editor.wordHighlightBackground": "#3994BC33", + "editor.wordHighlightStrongBackground": "#3994BC33", + "editor.findMatchBackground": "#3994BC4D", + "editor.findMatchHighlightBackground": "#3994BC26", + "editor.findRangeHighlightBackground": "#242526", + "editor.hoverHighlightBackground": "#242526", + "editor.lineHighlightBackground": "#242526", + "editor.rangeHighlightBackground": "#242526", + "editorLink.activeForeground": "#3a94bc", + "editorWhitespace.foreground": "#8888884D", + "editorIndentGuide.background": "#8384854D", + "editorIndentGuide.activeBackground": "#838485", + "editorRuler.foreground": "#848484", + "editorCodeLens.foreground": "#888888", + "editorBracketMatch.background": "#3994BC55", + "editorBracketMatch.border": "#2A2B2CFF", + "editorWidget.background": "#202122AA", + "editorWidget.border": "#2A2B2CFF", + "editorWidget.foreground": "#bfbfbf", + "editorSuggestWidget.background": "#202122", + "editorSuggestWidget.border": "#2A2B2CFF", + "editorSuggestWidget.foreground": "#bfbfbf", + "editorSuggestWidget.highlightForeground": "#bfbfbf", + "editorSuggestWidget.selectedBackground": "#3994BC26", + "editorHoverWidget.background": "#20212266", + "editorHoverWidget.border": "#2A2B2CFF", + "peekView.border": "#2A2B2CFF", + "peekViewEditor.background": "#191A1B", + "peekViewEditor.matchHighlightBackground": "#3994BC33", + "peekViewResult.background": "#191A1B", + "peekViewResult.fileForeground": "#bfbfbf", + "peekViewResult.lineForeground": "#888888", + "peekViewResult.matchHighlightBackground": "#3994BC33", + "peekViewResult.selectionBackground": "#3994BC26", + "peekViewResult.selectionForeground": "#bfbfbf", + "peekViewTitle.background": "#242526", + "peekViewTitleDescription.foreground": "#888888", + "peekViewTitleLabel.foreground": "#bfbfbf", + "editorGutter.background": "#121314", + "editorGutter.addedBackground": "#72C892", + "editorGutter.deletedBackground": "#F28772", + "diffEditor.insertedTextBackground": "#72C89233", + "diffEditor.removedTextBackground": "#F2877233", + "editorOverviewRuler.border": "#2A2B2CFF", + "editorOverviewRuler.findMatchForeground": "#3a94bc99", + "editorOverviewRuler.modifiedForeground": "#6ab890", + "editorOverviewRuler.addedForeground": "#73c991", + "editorOverviewRuler.deletedForeground": "#f48771", + "editorOverviewRuler.errorForeground": "#f48771", + "editorOverviewRuler.warningForeground": "#e5ba7d", + "panel.background": "#191A1B", + "panel.border": "#2A2B2CFF", + "panelTitle.activeBorder": "#3994BC", + "panelTitle.activeForeground": "#bfbfbf", + "panelTitle.inactiveForeground": "#888888", + "statusBar.background": "#191A1B", + "statusBar.foreground": "#888888", + "statusBar.border": "#2A2B2CFF", + "statusBar.focusBorder": "#3994BCB3", + "statusBar.debuggingBackground": "#3994BC", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#191A1B", + "statusBar.noFolderForeground": "#888888", + "statusBarItem.activeBackground": "#4B4C4D", + "statusBarItem.hoverBackground": "#262728", + "statusBarItem.focusBorder": "#3994BCB3", + "statusBarItem.prominentBackground": "#3994BC", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#3994BC", + "tab.activeBackground": "#121314", + "tab.activeForeground": "#bfbfbf", + "tab.inactiveBackground": "#191A1B", + "tab.inactiveForeground": "#888888", + "tab.border": "#2A2B2CFF", + "tab.lastPinnedBorder": "#2A2B2CFF", + "tab.activeBorder": "#121314", + "tab.activeBorderTop": "#3994BC", + "tab.hoverBackground": "#262728", + "tab.hoverForeground": "#bfbfbf", + "tab.unfocusedActiveBackground": "#121314", + "tab.unfocusedActiveForeground": "#888888", + "tab.unfocusedInactiveBackground": "#191A1B", + "tab.unfocusedInactiveForeground": "#444444", + "editorGroupHeader.tabsBackground": "#191A1B", + "editorGroupHeader.tabsBorder": "#2A2B2CFF", + "breadcrumb.foreground": "#888888", + "breadcrumb.background": "#121314", + "breadcrumb.focusForeground": "#bfbfbf", + "breadcrumb.activeSelectionForeground": "#bfbfbf", + "breadcrumbPicker.background": "#202122", + "notificationCenter.border": "#2A2B2CFF", + "notificationCenterHeader.foreground": "#bfbfbf", + "notificationCenterHeader.background": "#242526", + "notificationToast.border": "#2A2B2CFF", + "notifications.foreground": "#bfbfbf", + "notifications.background": "#202122", + "notifications.border": "#2A2B2CFF", + "notificationLink.foreground": "#3a94bc", + "notificationsWarningIcon.foreground": "#CCA700", + "notificationsErrorIcon.foreground": "#f48771", + "notificationsInfoIcon.foreground": "#3a94bc", + "activityWarningBadge.foreground": "#202020", + "activityWarningBadge.background": "#CCA700", + "activityErrorBadge.foreground": "#FFFFFF", + "activityErrorBadge.background": "#f48771", + "extensionButton.prominentBackground": "#3994BC", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#3E9BC4", + "pickerGroup.border": "#2A2B2CFF", + "pickerGroup.foreground": "#bfbfbf", + "quickInput.background": "#202122", + "quickInput.foreground": "#bfbfbf", + "quickInputList.focusBackground": "#3994BC26", + "quickInputList.focusForeground": "#bfbfbf", + "quickInputList.focusIconForeground": "#bfbfbf", + "quickInputList.hoverBackground": "#515253", + "terminal.selectionBackground": "#3994BC33", + "terminal.background": "#121314", + "terminal.border": "#2A2B2CFF", + "terminal.tab.activeBorder": "#3994BC00", + "terminalCursor.foreground": "#bfbfbf", + "terminalCursor.background": "#191A1B", + "gitDecoration.addedResourceForeground": "#73c991", + "gitDecoration.modifiedResourceForeground": "#e5ba7d", + "gitDecoration.deletedResourceForeground": "#f48771", + "gitDecoration.untrackedResourceForeground": "#73c991", + "gitDecoration.ignoredResourceForeground": "#8C8C8C", + "gitDecoration.conflictingResourceForeground": "#f48771", + "gitDecoration.stageModifiedResourceForeground": "#e5ba7d", + "gitDecoration.stageDeletedResourceForeground": "#f48771", + "quickInputTitle.background": "#202122", + "commandCenter.activeBorder": "#333536", + "quickInput.border": "#333536", + "gauge.foreground": "#59a4f9", + "gauge.background": "#58A4F94D", + "gauge.border": "#2A2C2EFF", + "gauge.warningForeground": "#e5ba7d", + "gauge.warningBackground": "#E3B97E4D", + "gauge.errorForeground": "#f48771", + "gauge.errorBackground": "#F287724D", + "chat.requestBubbleBackground": "#488FAE26", + "chat.requestBubbleHoverBackground": "#488FAE46", + "editorCommentsWidget.rangeBackground": "#488FAE26", + "editorCommentsWidget.rangeActiveBackground": "#488FAE46", + "charts.foreground": "#CCCCCC", + "charts.lines": "#C8CACC80", + "charts.blue": "#57A3F8", + "charts.red": "#EF8773", + "charts.yellow": "#E0B97F", + "charts.orange": "#CD861A", + "charts.green": "#86CF86", + "charts.purple": "#AD80D7" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#6F9B60" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type", + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.instanceof" + ], + "settings": { + "foreground": "#4F8FDD" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#C48081" + } + }, + { + "name": "Language constants", + "scope": [ + "constant.language" + ], + "settings": { + "foreground": "#4F8FDD" + } + }, + { + "name": "HTML/XML tags", + "scope": [ + "entity.name.tag", + "meta.tag.sgml", + "markup.deleted.git_gutter" + ], + "settings": { + "foreground": "#4F9BDD" + } + }, + { + "name": "HTML/XML tag punctuation", + "scope": [ + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#7A828B" + } + }, + { + "name": "HTML/XML attribute names", + "scope": [ + "entity.other.attribute-name" + ], + "settings": { + "foreground": "#90D5FF" + } + }, + { + "name": "Operators", + "scope": [ + "keyword.operator" + ], + "settings": { + "foreground": "#C5CCD6" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars", + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" + ], + "settings": { + "foreground": "#D1D6AE" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.other.attribute", + "entity.name.scope-resolution", + "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", + "storage.type.error.go", + "storage.type.rune.go", + "storage.type.cs", + "storage.type.generic.cs", + "storage.type.modifier.cs", + "storage.type.variable.cs", + "storage.type.annotation.java", + "storage.type.generic.java", + "storage.type.java", + "storage.type.object.array.java", + "storage.type.primitive.array.java", + "storage.type.primitive.java", + "storage.type.token.java", + "storage.type.groovy", + "storage.type.annotation.groovy", + "storage.type.parameters.groovy", + "storage.type.generic.groovy", + "storage.type.object.array.groovy", + "storage.type.primitive.array.groovy", + "storage.type.primitive.groovy" + ], + "settings": { + "foreground": "#48C9C4" + } + }, + { + "name": "Types declaration and references, TS grammar specific", + "scope": [ + "meta.type.cast.expr", + "meta.type.new.expr", + "support.constant.math", + "support.constant.dom", + "support.constant.json", + "entity.other.inherited-class", + "punctuation.separator.namespace.ruby" + ], + "settings": { + "foreground": "#48C9B9" + } + }, + { + "name": "Control flow / Special keywords", + "scope": [ + "keyword.control", + "source.cpp keyword.operator.new", + "keyword.operator.delete", + "keyword.other.using", + "keyword.other.directive.using", + "keyword.other.operator", + "entity.name.operator" + ], + "settings": { + "foreground": "#C184C6" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable", + "constant.other.placeholder" + ], + "settings": { + "foreground": "#90D5FF" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#4CBDFF" + } + }, + { + "name": "Object keys, TS grammar specific", + "scope": [ + "meta.object-literal.key" + ], + "settings": { + "foreground": "#90D5FF" + } + }, + { + "name": "CSS property value", + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#C48F80" + } + }, + { + "name": "Regular expression groups", + "scope": [ + "punctuation.definition.group.regexp", + "punctuation.definition.group.assertion.regexp", + "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", + "keyword.operator.negation.regexp", + "support.other.parenthesis.regexp" + ], + "settings": { + "foreground": "#C49580" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#C86971" + } + }, + { + "scope": [ + "keyword.operator.or.regexp", + "keyword.control.anchor.regexp" + ], + "settings": { + "foreground": "#CBD6AE" + } + }, + { + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "#CCBD84" + } + }, + { + "scope": [ + "constant.character", + "constant.other.option" + ], + "settings": { + "foreground": "#4F9BDD" + } + }, + { + "scope": "constant.character.escape", + "settings": { + "foreground": "#CCB784" + } + }, + { + "scope": "entity.name.label", + "settings": { + "foreground": "#BAC2CC" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#A8CAAD" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#C586C0", + "stringLiteral": "#ce9178", + "customLiteral": "#DCDCAA", + "numberLiteral": "#b5cea8" + } +} diff --git a/extensions/theme-2026/themes/2026-light.json b/extensions/theme-2026/themes/2026-light.json new file mode 100644 index 00000000000..db48eaa6fdf --- /dev/null +++ b/extensions/theme-2026/themes/2026-light.json @@ -0,0 +1,558 @@ +{ + "$schema": "vscode://schemas/color-theme", + "name": "2026 Light", + "type": "light", + "colors": { + "foreground": "#202020", + "disabledForeground": "#BBBBBB", + "errorForeground": "#ad0707", + "descriptionForeground": "#555555", + "icon.foreground": "#666666", + "focusBorder": "#0069CCFF", + "textBlockQuote.background": "#EDEDED", + "textBlockQuote.border": "#ECEDEEFF", + "textCodeBlock.background": "#EDEDED", + "textLink.foreground": "#0069CC", + "textLink.activeForeground": "#0069CC", + "textPreformat.foreground": "#666666", + "textSeparator.foreground": "#EEEEEEFF", + "button.background": "#0069CC", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#0063C1", + "button.border": "#ECEDEEFF", + "button.secondaryBackground": "#EDEDED", + "button.secondaryForeground": "#202020", + "button.secondaryHoverBackground": "#E6E6E6", + "checkbox.background": "#EDEDED", + "checkbox.border": "#D8D8D8", + "checkbox.foreground": "#202020", + "dropdown.background": "#FFFFFF", + "dropdown.border": "#D8D8D8", + "dropdown.foreground": "#202020", + "dropdown.listBackground": "#FFFFFF", + "input.background": "#FFFFFF", + "input.border": "#D8D8D866", + "input.foreground": "#202020", + "input.placeholderForeground": "#999999", + "inputOption.activeBackground": "#0069CC26", + "inputOption.activeForeground": "#202020", + "inputOption.activeBorder": "#ECEDEEFF", + "inputValidation.errorBackground": "#FFFFFF", + "inputValidation.errorBorder": "#ECEDEEFF", + "inputValidation.errorForeground": "#202020", + "inputValidation.infoBackground": "#FFFFFF", + "inputValidation.infoBorder": "#ECEDEEFF", + "inputValidation.infoForeground": "#202020", + "inputValidation.warningBackground": "#FFFFFF", + "inputValidation.warningBorder": "#ECEDEEFF", + "inputValidation.warningForeground": "#202020", + "scrollbar.shadow": "#00000000", + "widget.shadow": "#00000000", + "editorStickyScroll.shadow": "#00000000", + "sideBarStickyScroll.shadow": "#00000000", + "panelStickyScroll.shadow": "#00000000", + "listFilterWidget.shadow": "#00000000", + "scrollbarSlider.background": "#99999926", + "scrollbarSlider.hoverBackground": "#99999940", + "scrollbarSlider.activeBackground": "#99999955", + "badge.background": "#0069CC", + "badge.foreground": "#FFFFFF", + "progressBar.background": "#0069CC", + "list.activeSelectionBackground": "#0069CC44", + "list.activeSelectionForeground": "#202020", + "list.inactiveSelectionBackground": "#E0E0E0", + "list.inactiveSelectionForeground": "#202020", + "list.hoverBackground": "#F7F7F7", + "list.hoverForeground": "#202020", + "list.dropBackground": "#0069CC15", + "list.focusBackground": "#0069CC1A", + "list.focusForeground": "#202020", + "list.focusOutline": "#0069CCFF", + "list.highlightForeground": "#0069CC", + "list.invalidItemForeground": "#BBBBBB", + "list.errorForeground": "#ad0707", + "list.warningForeground": "#667309", + "activityBar.background": "#FFFFFF", + "activityBar.foreground": "#202020", + "activityBar.inactiveForeground": "#666666", + "activityBar.border": "#ECEDEEFF", + "activityBar.activeBorder": "#000000", + "activityBar.activeFocusBorder": "#0069CCFF", + "activityBarBadge.background": "#0069CC", + "activityBarBadge.foreground": "#FFFFFF", + "activityBarTop.activeBorder": "#000000", + "sideBar.background": "#FFFFFF", + "sideBar.foreground": "#202020", + "sideBar.border": "#ECEDEEFF", + "sideBarTitle.foreground": "#202020", + "sideBarSectionHeader.background": "#FFFFFF", + "sideBarSectionHeader.foreground": "#202020", + "sideBarSectionHeader.border": "#ECEDEEFF", + "titleBar.activeBackground": "#FFFFFF", + "titleBar.activeForeground": "#424242", + "titleBar.inactiveBackground": "#FFFFFF", + "titleBar.inactiveForeground": "#666666", + "titleBar.border": "#ECEDEEFF", + "menubar.selectionBackground": "#EDEDED", + "menubar.selectionForeground": "#202020", + "menu.background": "#FFFFFF", + "menu.foreground": "#202020", + "menu.selectionBackground": "#0069CC1A", + "menu.selectionForeground": "#202020", + "menu.separatorBackground": "#F7F7F7", + "menu.border": "#ECEDEEFF", + "commandCenter.foreground": "#202020", + "commandCenter.activeForeground": "#202020", + "commandCenter.background": "#E8ECF2", + "commandCenter.activeBackground": "#E8ECF2B3", + "commandCenter.border": "#D8D8D866", + "editor.background": "#FFFFFF", + "editor.foreground": "#202020", + "editorLineNumber.foreground": "#666666", + "editorLineNumber.activeForeground": "#202020", + "editorCursor.foreground": "#202020", + "editor.selectionBackground": "#0069CC1A", + "editor.inactiveSelectionBackground": "#0069CC1A", + "editor.selectionHighlightBackground": "#0069CC15", + "editor.wordHighlightBackground": "#0069CC26", + "editor.wordHighlightStrongBackground": "#0069CC26", + "editor.findMatchBackground": "#0069CC40", + "editor.findMatchHighlightBackground": "#0069CC1A", + "editor.findRangeHighlightBackground": "#EDEDED", + "editor.hoverHighlightBackground": "#EDEDED", + "editor.lineHighlightBackground": "#EDEDED40", + "editor.rangeHighlightBackground": "#EDEDED", + "editorLink.activeForeground": "#0069CC", + "editorWhitespace.foreground": "#66666640", + "editorIndentGuide.background": "#F7F7F740", + "editorIndentGuide.activeBackground": "#F7F7F7", + "editorRuler.foreground": "#F7F7F7", + "editorCodeLens.foreground": "#666666", + "editorBracketMatch.background": "#0069CC40", + "editorBracketMatch.border": "#ECEDEEFF", + "editorWidget.background": "#E8ECF2E6", + "editorWidget.border": "#ECEDEEFF", + "editorWidget.foreground": "#202020", + "editorSuggestWidget.background": "#E8ECF2E6", + "editorSuggestWidget.border": "#ECEDEEFF", + "editorSuggestWidget.foreground": "#202020", + "editorSuggestWidget.highlightForeground": "#0069CC", + "editorSuggestWidget.selectedBackground": "#0069CC26", + "editorHoverWidget.background": "#E8ECF2E6", + "editorHoverWidget.border": "#ECEDEEFF", + "peekView.border": "#0069CC", + "peekViewEditor.background": "#E8ECF2E6", + "peekViewEditor.matchHighlightBackground": "#0069CC33", + "peekViewResult.background": "#E8ECF2E6", + "peekViewResult.fileForeground": "#202020", + "peekViewResult.lineForeground": "#666666", + "peekViewResult.matchHighlightBackground": "#0069CC33", + "peekViewResult.selectionBackground": "#0069CC26", + "peekViewResult.selectionForeground": "#202020", + "peekViewTitle.background": "#E8ECF2E6", + "peekViewTitleDescription.foreground": "#666666", + "peekViewTitleLabel.foreground": "#202020", + "editorGutter.addedBackground": "#587c0c", + "editorGutter.deletedBackground": "#ad0707", + "diffEditor.insertedTextBackground": "#587c0c26", + "diffEditor.removedTextBackground": "#ad070726", + "editorOverviewRuler.border": "#ECEDEEFF", + "editorOverviewRuler.findMatchForeground": "#0069CC99", + "editorOverviewRuler.modifiedForeground": "#0069CC", + "editorOverviewRuler.addedForeground": "#587c0c", + "editorOverviewRuler.deletedForeground": "#ad0707", + "editorOverviewRuler.errorForeground": "#ad0707", + "editorOverviewRuler.warningForeground": "#667309", + "editorGutter.background": "#FFFFFF", + "panel.background": "#FFFFFF", + "panel.border": "#00000000", + "panelTitle.activeBorder": "#000000", + "panelTitle.activeForeground": "#202020", + "panelTitle.inactiveForeground": "#666666", + "statusBar.background": "#E8ECF2", + "statusBar.foreground": "#666666", + "statusBar.border": "#00000000", + "statusBar.focusBorder": "#0069CCFF", + "statusBar.debuggingBackground": "#0069CC", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.noFolderBackground": "#E8ECF2", + "statusBar.noFolderForeground": "#666666", + "statusBarItem.activeBackground": "#E6E6E6", + "statusBarItem.hoverBackground": "#F7F7F7", + "statusBarItem.focusBorder": "#0069CCFF", + "statusBarItem.prominentBackground": "#0069CCDD", + "statusBarItem.prominentForeground": "#FFFFFF", + "statusBarItem.prominentHoverBackground": "#0069CC", + "tab.activeBackground": "#FFFFFF", + "tab.activeForeground": "#202020", + "tab.inactiveBackground": "#FFFFFF", + "tab.inactiveForeground": "#666666", + "tab.border": "#ECEDEEFF", + "tab.lastPinnedBorder": "#ECEDEEFF", + "tab.activeBorder": "#FFFFFF", + "tab.activeBorderTop": "#000000", + "tab.hoverBackground": "#F7F7F7", + "tab.hoverForeground": "#202020", + "tab.unfocusedActiveBackground": "#FFFFFF", + "tab.unfocusedActiveForeground": "#666666", + "tab.unfocusedInactiveBackground": "#FFFFFF", + "tab.unfocusedInactiveForeground": "#BBBBBB", + "editorGroupHeader.tabsBackground": "#FFFFFF", + "editorGroupHeader.tabsBorder": "#ECEDEEFF", + "breadcrumb.foreground": "#666666", + "breadcrumb.background": "#FFFFFF", + "breadcrumb.focusForeground": "#202020", + "breadcrumb.activeSelectionForeground": "#202020", + "breadcrumbPicker.background": "#E8ECF2E6", + "notificationCenter.border": "#ECEDEEFF", + "notificationCenterHeader.foreground": "#202020", + "notificationCenterHeader.background": "#E8ECF2E6", + "notificationToast.border": "#ECEDEEFF", + "notifications.foreground": "#202020", + "notifications.background": "#E8ECF2E6", + "notifications.border": "#ECEDEEFF", + "notificationLink.foreground": "#0069CC", + "notificationsWarningIcon.foreground": "#B69500", + "notificationsErrorIcon.foreground": "#ad0707", + "notificationsInfoIcon.foreground": "#0069CC", + "activityWarningBadge.foreground": "#202020", + "activityWarningBadge.background": "#F2C94C", + "activityErrorBadge.foreground": "#FFFFFF", + "activityErrorBadge.background": "#ad0707", + "extensionButton.prominentBackground": "#0069CC", + "extensionButton.prominentForeground": "#FFFFFF", + "extensionButton.prominentHoverBackground": "#0064CC", + "pickerGroup.border": "#ECEDEEFF", + "pickerGroup.foreground": "#202020", + "quickInput.background": "#E8ECF2E6", + "quickInput.foreground": "#202020", + "quickInputList.focusBackground": "#0069CC1A", + "quickInputList.focusForeground": "#202020", + "quickInputList.focusIconForeground": "#202020", + "quickInputList.hoverBackground": "#EDF0F5E6", + "terminal.selectionBackground": "#0069CC26", + "terminalCursor.foreground": "#202020", + "terminalCursor.background": "#FFFFFF", + "gitDecoration.addedResourceForeground": "#587c0c", + "gitDecoration.modifiedResourceForeground": "#667309", + "gitDecoration.deletedResourceForeground": "#ad0707", + "gitDecoration.untrackedResourceForeground": "#587c0c", + "gitDecoration.ignoredResourceForeground": "#8E8E90", + "gitDecoration.conflictingResourceForeground": "#ad0707", + "gitDecoration.stageModifiedResourceForeground": "#667309", + "gitDecoration.stageDeletedResourceForeground": "#ad0707", + "commandCenter.activeBorder": "#D8D8D8A6", + "quickInput.border": "#D8D8D8", + "gauge.foreground": "#0069CC", + "gauge.background": "#0069CC40", + "gauge.border": "#ECEDEEFF", + "gauge.warningForeground": "#B69500", + "gauge.warningBackground": "#B6950040", + "gauge.errorForeground": "#ad0707", + "gauge.errorBackground": "#ad070740", + "statusBarItem.prominentHoverForeground": "#FFFFFF", + "quickInputTitle.background": "#E8ECF2E6", + "chat.requestBubbleBackground": "#EEF4FB", + "chat.requestBubbleHoverBackground": "#E6EDFA", + "editorCommentsWidget.rangeBackground": "#EEF4FB", + "editorCommentsWidget.rangeActiveBackground": "#E6EDFA", + "charts.foreground": "#202020", + "charts.lines": "#20202066", + "charts.blue": "#1A5CFF", + "charts.red": "#ad0707", + "charts.yellow": "#667309", + "charts.orange": "#d18616", + "charts.green": "#388A34", + "charts.purple": "#652D90" + }, + "tokenColors": [ + { + "scope": [ + "comment" + ], + "settings": { + "foreground": "#60984D" + } + }, + { + "scope": [ + "keyword", + "storage.modifier", + "storage.type", + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.cast", + "keyword.operator.sizeof", + "keyword.operator.instanceof" + ], + "settings": { + "foreground": "#5460C1" + } + }, + { + "scope": [ + "string" + ], + "settings": { + "foreground": "#B86855" + } + }, + { + "name": "Language constants", + "scope": [ + "constant.language" + ], + "settings": { + "foreground": "#5460C1" + } + }, + { + "name": "HTML/XML tags", + "scope": [ + "entity.name.tag", + "meta.tag.sgml", + "markup.deleted.git_gutter" + ], + "settings": { + "foreground": "#5751DE" + } + }, + { + "name": "HTML/XML tag punctuation", + "scope": [ + "punctuation.definition.tag.html", + "punctuation.definition.tag.begin.html", + "punctuation.definition.tag.end.html" + ], + "settings": { + "foreground": "#93201A" + } + }, + { + "name": "HTML/XML attribute names", + "scope": [ + "entity.other.attribute-name" + ], + "settings": { + "foreground": "#E75854" + } + }, + { + "name": "Operators", + "scope": [ + "keyword.operator" + ], + "settings": { + "foreground": "#573F35" + } + }, + { + "name": "Function declarations", + "scope": [ + "entity.name.function", + "support.function", + "support.constant.handlebars", + "source.powershell variable.other.member", + "entity.name.operator.custom-literal" + ], + "settings": { + "foreground": "#98863B" + } + }, + { + "name": "Types declaration and references", + "scope": [ + "support.class", + "support.type", + "entity.name.type", + "entity.name.namespace", + "entity.other.attribute", + "entity.name.scope-resolution", + "entity.name.class", + "storage.type.numeric.go", + "storage.type.byte.go", + "storage.type.boolean.go", + "storage.type.string.go", + "storage.type.uintptr.go", + "storage.type.error.go", + "storage.type.rune.go", + "storage.type.cs", + "storage.type.generic.cs", + "storage.type.modifier.cs", + "storage.type.variable.cs", + "storage.type.annotation.java", + "storage.type.generic.java", + "storage.type.java", + "storage.type.object.array.java", + "storage.type.primitive.array.java", + "storage.type.primitive.java", + "storage.type.token.java", + "storage.type.groovy", + "storage.type.annotation.groovy", + "storage.type.parameters.groovy", + "storage.type.generic.groovy", + "storage.type.object.array.groovy", + "storage.type.primitive.array.groovy", + "storage.type.primitive.groovy" + ], + "settings": { + "foreground": "#46969A" + } + }, + { + "name": "Types declaration and references, TS grammar specific", + "scope": [ + "meta.type.cast.expr", + "meta.type.new.expr", + "support.constant.math", + "support.constant.dom", + "support.constant.json", + "entity.other.inherited-class", + "punctuation.separator.namespace.ruby" + ], + "settings": { + "foreground": "#419BB3" + } + }, + { + "name": "Control flow / Special keywords", + "scope": [ + "keyword.control", + "source.cpp keyword.operator.new", + "source.cpp keyword.operator.delete", + "keyword.other.using", + "keyword.other.directive.using", + "keyword.other.operator", + "entity.name.operator" + ], + "settings": { + "foreground": "#8F41AD" + } + }, + { + "name": "Variable and parameter name", + "scope": [ + "variable", + "meta.definition.variable.name", + "support.variable", + "entity.name.variable", + "constant.other.placeholder" + ], + "settings": { + "foreground": "#282D85" + } + }, + { + "name": "Constants and enums", + "scope": [ + "variable.other.constant", + "variable.other.enummember" + ], + "settings": { + "foreground": "#3086C5" + } + }, + { + "name": "Object keys, TS grammar specific", + "scope": [ + "meta.object-literal.key" + ], + "settings": { + "foreground": "#282D85" + } + }, + { + "name": "CSS property value", + "scope": [ + "support.constant.property-value", + "support.constant.font-name", + "support.constant.media-type", + "support.constant.media", + "constant.other.color.rgb-value", + "constant.other.rgb-value", + "support.constant.color" + ], + "settings": { + "foreground": "#2D6AAE" + } + }, + { + "name": "Regular expression groups", + "scope": [ + "punctuation.definition.group.regexp", + "punctuation.definition.group.assertion.regexp", + "punctuation.definition.character-class.regexp", + "punctuation.character.set.begin.regexp", + "punctuation.character.set.end.regexp", + "keyword.operator.negation.regexp", + "support.other.parenthesis.regexp" + ], + "settings": { + "foreground": "#D68490" + } + }, + { + "scope": [ + "constant.character.character-class.regexp", + "constant.other.character-class.set.regexp", + "constant.other.character-class.regexp", + "constant.character.set.regexp" + ], + "settings": { + "foreground": "#A63350" + } + }, + { + "scope": "keyword.operator.quantifier.regexp", + "settings": { + "foreground": "#573F35" + } + }, + { + "scope": [ + "keyword.operator.or.regexp", + "keyword.control.anchor.regexp" + ], + "settings": { + "foreground": "#C54C5B" + } + }, + { + "scope": [ + "constant.character", + "constant.other.option" + ], + "settings": { + "foreground": "#5751DE" + } + }, + { + "scope": "constant.character.escape", + "settings": { + "foreground": "#E14A46" + } + }, + { + "scope": "entity.name.label", + "settings": { + "foreground": "#5C3923" + } + }, + { + "name": "Numbers", + "scope": [ + "constant.numeric" + ], + "settings": { + "foreground": "#2B9A69" + } + } + ], + "semanticHighlighting": true, + "semanticTokenColors": { + "newOperator": "#AF00DB", + "stringLiteral": "#a31515", + "customLiteral": "#795E26", + "numberLiteral": "#098658" + } +} diff --git a/extensions/theme-2026/themes/styles.css b/extensions/theme-2026/themes/styles.css new file mode 100644 index 00000000000..f250ca966e1 --- /dev/null +++ b/extensions/theme-2026/themes/styles.css @@ -0,0 +1,505 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +:root { + --radius-sm: 4px; + --radius-md: 6px; + --radius-lg: 8px; + --radius-xl: 12px; + + --shadow-xs: 0 0 2px rgba(0, 0, 0, 0.06); + --shadow-sm: 0 0 4px rgba(0, 0, 0, 0.08); + --shadow-md: 0 0 6px rgba(0, 0, 0, 0.08); + --shadow-lg: 0 0 12px rgba(0, 0, 0, 0.14); + --shadow-xl: 0 0 20px rgba(0, 0, 0, 0.15); + --shadow-2xl: 0 0 20px rgba(0, 0, 0, 0.18); + --shadow-hover: 0 0 8px rgba(0, 0, 0, 0.12); + --shadow-sm-strong: 0 0 4px rgba(0, 0, 0, 0.18); + --shadow-button-active: inset 0 1px 2px rgba(0, 0, 0, 0.1); + --shadow-inset-white: inset 0 0 4px rgba(255, 255, 255, 0.1); + --shadow-active-tab: 0 8px 12px rgba(0, 0, 0, 0.02); + + --backdrop-blur-sm: blur(12px); + --backdrop-blur-md: blur(20px) saturate(180%); + --backdrop-blur-lg: blur(40px) saturate(180%); +} + +/* Stealth Shadows - shadow-based depth for UI elements, controlled by workbench.stealthShadows.enabled */ + +/* Activity Bar */ +.monaco-workbench .part.activitybar { box-shadow: var(--shadow-md); z-index: 50; position: relative;} +.monaco-workbench.activitybar-right .part.activitybar { box-shadow: var(--shadow-md); } + +/* Sidebar */ +.monaco-workbench .part.sidebar { box-shadow: var(--shadow-md); z-index: 40; position: relative; } +.monaco-workbench.sidebar-right .part.sidebar { box-shadow: var(--shadow-md); } +.monaco-workbench .part.auxiliarybar { box-shadow: var(--shadow-md); z-index: 40; position: relative; } + +/* Ensure iframe containers in pane-body render above sidebar z-index */ +.monaco-workbench > div[data-keybinding-context], +.monaco-workbench > div[data-keybinding-context] { + z-index: 50 !important; +} + + +/* Ensure webview containers render above sidebar z-index */ +.monaco-workbench .part.sidebar .webview, +.monaco-workbench .part.sidebar .webview-container, +.monaco-workbench .part.auxiliarybar .webview, +.monaco-workbench .part.auxiliarybar .webview-container { position: relative; z-index: 50; transform: translateZ(0); } + +/* Panel */ +.monaco-workbench .part.panel { box-shadow: var(--shadow-md); z-index: 35; position: relative; } +.monaco-workbench.panel-position-left .part.panel { box-shadow: var(--shadow-md); } +.monaco-workbench.panel-position-right .part.panel { box-shadow: var(--shadow-md); } + +.monaco-pane-view .split-view-view:first-of-type > .pane > .pane-header { + border-top: 1px solid var(--vscode-sideBarSectionHeader-border) !important; +} + +/* Sashes - ensure they extend full height and are above other panels */ +.monaco-workbench .monaco-sash { z-index: 45; } +.monaco-workbench .monaco-sash.vertical { z-index: 45; } +.monaco-workbench .monaco-sash.horizontal { z-index: 45; } + +.monaco-workbench.vs .activitybar.left.bordered::before, +.monaco-workbench.vs .activitybar.right.bordered::before { + border: none; +} + +/* Editor */ +.monaco-workbench .part.editor { position: relative; } +.monaco-workbench .part.editor > .content .editor-group-container > .title { box-shadow: none; position: relative; z-index: 10; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { + box-shadow: inset var(--shadow-active-tab); + position: relative; z-index: 5; + border-radius: 0; + border-top: none !important; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--vscode-focusBorder) 10%, transparent) 0%, + transparent 100% + ), var(--vscode-tab-activeBackground) !important; +} +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab:hover:not(.active) { + box-shadow: var(--shadow-sm); +} + +/* Tab border bottom - make transparent */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-and-actions-container { --tabs-border-bottom-color: transparent !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab { --tab-border-bottom-color: transparent !important; } + +/* Title Bar */ +.monaco-workbench.vs .part.titlebar { box-shadow: var(--shadow-md); } + +.monaco-workbench .part.titlebar { + position: relative; + overflow: visible !important; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--vscode-focusBorder) 10%, transparent) 0%, + transparent 100% + ), var(--vscode-titleBar-activeBackground) !important; +} + +.monaco-workbench .part.titlebar.inactive { + background: var(--vscode-titleBar-inactiveBackground) !important; + + .command-center .monaco-action-bar, + .command-center .actions-container, + .agent-status-pill, .agent-status-badge { + background-color: transparent !important; + } +} +.monaco-workbench .part.titlebar .titlebar-container, +.monaco-workbench .part.titlebar .titlebar-center, +.monaco-workbench .part.titlebar .titlebar-center .window-title, +.monaco-workbench .part.titlebar .command-center, +.monaco-workbench .part.titlebar .command-center .monaco-action-bar, +.monaco-workbench .part.titlebar .command-center .actions-container { overflow: visible !important; } + +/* Status Bar */ +.monaco-workbench .part.statusbar { box-shadow: var(--shadow-md); z-index: 55; position: relative; } + +/* Quick Input (Command Palette) */ +.monaco-workbench .quick-input-widget { + box-shadow: var(--shadow-xl) !important; + border-radius: 12px; overflow: hidden; + background-color: color-mix(in srgb, var(--vscode-quickInput-background) 30%, transparent) !important; + backdrop-filter: var(--backdrop-blur-lg); + -webkit-backdrop-filter: var(--backdrop-blur-lg); + -webkit-backdrop-filter: blur(40px) saturate(180%); +} +.monaco-workbench.vs-dark .quick-input-widget { + border: 1px solid var(--vscode-menu-border) !important; +} +.monaco-workbench .quick-input-widget .monaco-list-rows { background-color: transparent !important; } +.monaco-workbench .quick-input-widget .quick-input-header, +.monaco-workbench .quick-input-widget .quick-input-list, +.monaco-workbench .quick-input-widget .quick-input-titlebar, +.monaco-workbench .quick-input-widget .quick-input-title, +.monaco-workbench .quick-input-widget .quick-input-description, +.monaco-workbench .quick-input-widget .quick-input-filter, +.monaco-workbench .quick-input-widget .quick-input-action, +.monaco-workbench .quick-input-widget .quick-input-message, +.monaco-workbench .quick-input-widget .monaco-list, +.monaco-workbench .quick-input-widget .monaco-list-row { border-color: transparent !important; outline: none !important; } +.monaco-workbench .quick-input-widget .monaco-inputbox { box-shadow: none !important; background: transparent !important; } +.monaco-workbench .quick-input-widget .quick-input-filter .monaco-inputbox { + background: color-mix(in srgb, var(--vscode-input-background) 60%, transparent)!important; border-radius: 6px; +} + +.monaco-workbench .quick-input-widget .monaco-list.list_id_6:not(.drop-target):not(.dragging) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: color-mix(in srgb, var(--vscode-list-hoverBackground) 40%, transparent); } + +/* Chat Widget */ +.monaco-workbench .interactive-session .chat-input-container { box-shadow: inset var(--shadow-sm); border-radius: var(--radius-md); } +.monaco-workbench .interactive-session .interactive-input-part .chat-editor-container .interactive-input-editor .monaco-editor, +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { border-radius: var(--radius-sm) var(--radius-sm) 0 0; } +.monaco-workbench .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container { border-radius: 0 0 var(--radius-md) var(--radius-md); } +.monaco-workbench .part.panel .interactive-session, +.monaco-workbench .part.auxiliarybar .interactive-session { position: relative; } + +.monaco-workbench .interactive-session .chat-editing-session .chat-editing-session-container { + background-color: transparent !important; +} + +/* Notifications */ + +.monaco-workbench .notifications-toasts { + box-shadow: var(--shadow-lg); + border-radius: var(--radius-sm); +} +.monaco-workbench .notification-toast { box-shadow: none !important; margin: 0 !important;} + +.monaco-workbench .notification-toast-container { + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} + +.monaco-workbench .notification-toast-container .notification-toast { + background-color: transparent !important; +} + +.monaco-workbench .notifications-center { + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} + +.monaco-workbench .notifications-list-container, +.monaco-workbench > .notifications-center > .notifications-center-header, +.monaco-workbench .notifications-list-container .monaco-list-rows { + background: transparent !important; +} + +/* Context Menus */ +.monaco-workbench .monaco-menu .monaco-action-bar.vertical { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-xl); overflow: hidden; backdrop-filter: var(--backdrop-blur-md); -webkit-backdrop-filter: var(--backdrop-blur-md); } +.monaco-workbench .context-view .monaco-menu { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-xl); } + +.monaco-workbench .action-widget { + background: color-mix(in srgb, var(--vscode-menu-background) 50%, transparent) !important; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} +.monaco-workbench .action-widget .action-widget-action-bar {background: transparent;} + +/* Suggest Widget */ +.monaco-workbench .monaco-editor .suggest-widget { + box-shadow: var(--shadow-lg); + border: none; + border-radius: var(--radius-xl); + overflow: hidden; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} +.monaco-workbench.vs-dark .monaco-editor .suggest-widget { + background: color-mix(in srgb, var(--vscode-editorWidget-background) 40%, transparent) !important; + border: 1px solid var(--vscode-editorWidget-border); +} + +/* Find Widget */ +.monaco-workbench .monaco-editor .find-widget { + box-shadow: var(--shadow-hover); border: none; border-radius: var(--radius-lg); + backdrop-filter: var(--backdrop-blur-md); -webkit-backdrop-filter: var(--backdrop-blur-md); + margin-top: 4px !important; +} + +/* Dialog */ +.monaco-workbench .monaco-dialog-box { + box-shadow: var(--shadow-2xl); + border: none; + border-radius: var(--radius-xl); + backdrop-filter: var(--backdrop-blur-lg); -webkit-backdrop-filter: var(--backdrop-blur-lg); + background: color-mix(in srgb, var(--vscode-editor-background) 60%, transparent) !important; +} + +.monaco-workbench.vs-dark .monaco-dialog-box { + border: 1px solid var(--vscode-dialog-border); +} + +/* Peek View */ +.monaco-workbench .monaco-editor .peekview-widget { + box-shadow: var(--shadow-hover); + border: none; + background: var(--vscode-editor-background) !important; + backdrop-filter: var(--backdrop-blur-sm); + -webkit-backdrop-filter: var(--backdrop-blur-sm); + overflow: hidden; +} +.monaco-workbench .monaco-editor .peekview-widget .head, +.monaco-workbench .monaco-editor .peekview-widget .body { background: transparent !important; } + +.monaco-editor .monaco-hover { + background-color: color-mix(in srgb, var(--vscode-editorHoverWidget-background) 60%, transparent) !important; + box-shadow: var(--shadow-sm-strong); + border-radius: var(--radius-lg); + overflow: hidden; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} +.monaco-workbench .monaco-hover.workbench-hover, .monaco-hover.workbench-hover.compact { + background-color: color-mix(in srgb, var(--vscode-editorHoverWidget-background) 60%, transparent) !important; + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; +} + +.monaco-workbench .defineKeybindingWidget { + box-shadow: var(--shadow-lg); + border-radius: var(--radius-lg); + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} + +.monaco-workbench.vs-dark .defineKeybindingWidget { + border: 1px solid var(--vscode-editorWidget-border); +} + +.monaco-workbench .chat-editor-overlay-widget, .monaco-workbench .chat-diff-change-content-widget { + box-shadow: var(--shadow-md); + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); +} + +.monaco-workbench.vs-dark .chat-editor-overlay-widget, .monaco-workbench.vs-dark .chat-diff-change-content-widget +{ + border: 1px solid var(--vscode-editorWidget-border); +} +/* Settings */ +.monaco-workbench .settings-editor .settings-toc-container { box-shadow: var(--shadow-sm); } + +/* Welcome Tiles */ +.monaco-workbench .part.editor .welcomePageContainer .tile { box-shadow: var(--shadow-md); border: none; border-radius: var(--radius-lg); } +.monaco-workbench .part.editor .welcomePageContainer .tile:hover { box-shadow: var(--shadow-hover); } + +/* Extensions */ +.monaco-workbench .extensions-list .extension-list-item { box-shadow: var(--shadow-sm); border: none; } +.monaco-workbench .extensions-list .extension-list-item:hover { box-shadow: var(--shadow-md); } + +/* Breadcrumbs */ +.monaco-workbench.vs-dark .part.editor > .content .editor-group-container > .title .breadcrumbs-control, + +/* Input Boxes */ +.monaco-workbench .monaco-inputbox, +.monaco-workbench .suggest-input-container { box-shadow: inset var(--shadow-sm); border: none; } + +.monaco-inputbox .monaco-action-bar .action-item .codicon, +.monaco-workbench .search-container .input-box, +.monaco-custom-toggle { + color: var(--vscode-icon-foreground) !important; +} + +/* Chat input toolbar icons should use proper foreground color, not the muted icon.foreground */ +.monaco-workbench .interactive-session .chat-input-toolbars .monaco-action-bar .action-item .codicon, +.monaco-workbench .interactive-session .chat-input-toolbars .action-label .codicon { + color: var(--vscode-foreground) !important; +} + +/* Buttons */ +.monaco-workbench .monaco-button { box-shadow: var(--shadow-xs); } +.monaco-workbench .monaco-button:hover { box-shadow: var(--shadow-sm); } +.monaco-workbench .monaco-button:active { box-shadow: var(--shadow-button-active); } + +/* Todo List Widget - remove shadows from buttons */ +.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button, +.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:hover, +.monaco-workbench.vs .chat-todo-list-widget .todo-list-expand .monaco-button:active, +.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button, +.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:hover, +.monaco-workbench.vs .chat-todo-list-widget .todo-clear-button-container .monaco-button:active { + box-shadow: none; +} + +/* Link buttons and tool call buttons - remove shadows */ +.monaco-workbench .monaco-button.link-button, +.monaco-workbench .monaco-button.link-button:hover, +.monaco-workbench .monaco-button.link-button:active, +.monaco-workbench .chat-confirmation-widget-title.monaco-button, +.monaco-workbench .chat-confirmation-widget-title.monaco-button:hover, +.monaco-workbench .chat-confirmation-widget-title.monaco-button:active, +.monaco-workbench .chat-used-context-label .monaco-button, +.monaco-workbench .chat-used-context-label .monaco-button:hover, +.monaco-workbench .chat-used-context-label .monaco-button:active { + box-shadow: none; +} + +/* Dropdowns */ +.monaco-workbench .monaco-dropdown .dropdown-menu { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-lg); } + +/* Terminal */ +.monaco-workbench.vs .pane-body.integrated-terminal { box-shadow: var(--shadow-inset-white); } + +/* SCM */ +.monaco-workbench .scm-view .scm-provider { box-shadow: var(--shadow-sm); border-radius: var(--radius-md); } + +/* Debug Toolbar */ +.monaco-workbench .debug-toolbar { + box-shadow: var(--shadow-lg); + border: none; + border-radius: var(--radius-lg); + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; +} + +.monaco-workbench .debug-hover-widget { + box-shadow: var(--shadow-hover); + border-radius: var(--radius-lg); + overflow: hidden; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); + color: var(--vscode-editor-foreground) !important; +} + +.monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-rows .monaco-list-row:hover:not(.highlighted):not(.selected):not(.focused) { + background-color: var(--vscode-list-hoverBackground);; +} + +/* Action Widget */ +.monaco-workbench .action-widget { box-shadow: var(--shadow-lg) !important; border-radius: var(--radius-lg); } + +/* Parameter Hints */ +.monaco-workbench .monaco-editor .parameter-hints-widget { box-shadow: var(--shadow-hover); border: none; border-radius: var(--radius-xl); overflow: hidden; backdrop-filter: var(--backdrop-blur-md); -webkit-backdrop-filter: var(--backdrop-blur-md); } +.monaco-workbench.vs-dark .monaco-editor .parameter-hints-widget, +.monaco-workbench.vs .monaco-editor .parameter-hints-widget { background: color-mix(in srgb, var(--vscode-editorWidget-background) 70%, transparent) !important; } + +/* Minimap */ +.monaco-workbench .monaco-editor .minimap { backdrop-filter: var(--backdrop-blur-lg) !important; -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; } +.monaco-workbench .monaco-editor .minimap canvas { opacity: 0.85; } +.monaco-workbench.vs-dark .monaco-editor .minimap, +.monaco-workbench .monaco-editor .minimap-shadow-visible { box-shadow: var(--shadow-md); opacity: 0.85; background-color: var(--vscode-editor-background); left: 0;} + +/* Sticky Scroll */ +.monaco-workbench .monaco-editor .sticky-widget { + box-shadow: var(--shadow-md) !important; + border-bottom: none !important; + background: color-mix(in srgb, var(--vscode-editor-background) 75%, transparent) !important; + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; +} +.monaco-workbench.vs-dark .monaco-editor .sticky-widget { + border-bottom: none !important; +} +.monaco-workbench .monaco-editor .sticky-widget .sticky-widget-lines +{ background-color: transparent !important; background: transparent !important; } +.monaco-workbench.vs-dark .monaco-editor .sticky-widget, +.monaco-workbench .monaco-editor .sticky-widget-focus-preview, +.monaco-workbench .monaco-editor .sticky-scroll-focus-line, +.monaco-workbench .monaco-editor .focused .sticky-widget, +.monaco-workbench .monaco-editor:has(.sticky-widget:focus-within) .sticky-widget { + background: color-mix(in srgb, var(--vscode-editor-background) 75%, transparent) !important; + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; + box-shadow: var(--shadow-hover) !important; +} + +.monaco-editor .sticky-widget .sticky-line-content, +.monaco-workbench .monaco-editor .sticky-widget .sticky-line-number { + backdrop-filter: var(--backdrop-blur-lg) !important; -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; + background-color: color-mix(in srgb, var(--vscode-editor-background) 70%, transparent) +} + +.monaco-workbench.vs-dark .monaco-editor .sticky-widget .sticky-line-content, +.monaco-workbench.vs-dark .monaco-editor .sticky-widget .sticky-line-number { + background-color: color-mix(in srgb, var(--vscode-editor-background) 30%, transparent) +} + +.monaco-editor .rename-box.preview { + backdrop-filter: var(--backdrop-blur-lg) !important; + -webkit-backdrop-filter: var(--backdrop-blur-lg) !important; + box-shadow: var(--shadow-hover) !important; + border: 1px solid var(--vscode-editorWidget-border); +} + + +/* Notebook */ + +.monaco-workbench .notebookOverlay.notebook-editor { + z-index: 35 !important; +} + +.monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { + box-shadow: inset var(--shadow-sm); + border-radius: var(--radius-md); +} + +.notebookOverlay .monaco-list-row .cell-title-toolbar { + background-color: var(--vscode-editorWidget-background) !important; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); +} + +.notebookOverlay .cell-bottom-toolbar-container .action-item { + border-radius: var(--radius-sm); +} + +/* Inline Chat */ +.monaco-workbench .monaco-editor .inline-chat { box-shadow: var(--shadow-lg); border: none; border-radius: var(--radius-xl); } + +/* Command Center */ +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center { + box-shadow: inset var(--shadow-sm) !important; + border-radius: var(--radius-lg) !important; + backdrop-filter: var(--backdrop-blur-md); + -webkit-backdrop-filter: var(--backdrop-blur-md); + overflow: visible !important; +} +.monaco-workbench .part.titlebar > .titlebar-container > .titlebar-center > .window-title > .command-center .action-item.command-center-center:hover { + box-shadow: inset var(--shadow-sm) !important; +} + +.monaco-workbench .part.titlebar .command-center .agent-status-pill { + box-shadow: inset var(--shadow-sm); +} +.monaco-workbench .part.titlebar .command-center .agent-status-pill:hover { + box-shadow: none; + background-color: transparent; +} + +.monaco-dialog-modal-block .dialog-shadow { + border-radius: var(--radius-xl); +} + +.monaco-workbench .unified-quick-access-tabs { + background: transparent; +} + +.monaco-workbench .quick-input-list .quick-input-list-entry.quick-input-list-separator-border { + border-top-width: 0; +} + +/* Quick Input List - use descriptionForeground color for descriptions */ +.monaco-workbench .quick-input-list .monaco-icon-label .label-description { + opacity: 1; + color: var(--vscode-descriptionForeground); +} + +/* Remove Borders */ +.monaco-workbench.vs .part.sidebar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.auxiliarybar { border-right: none !important; border-left: none !important; } +.monaco-workbench .part.panel { border-top: none !important; } +.monaco-workbench.vs .part.activitybar { border-right: none !important; border-left: none !important; } +.monaco-workbench.vs .part.titlebar { border-bottom: none !important; } +.monaco-workbench.vs .part.statusbar { border-top: none !important; } +.monaco-workbench .pane-composite-part:not(.empty) > .header { border-bottom: none !important; } diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 51e0f371c27..574d89f9c4a 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -13,12 +13,12 @@ "badge.background": "#616161", "badge.foreground": "#F8F8F8", "button.background": "#0078D4", - "button.border": "#FFFFFF12", + "button.border": "#ffffff1a", "button.foreground": "#FFFFFF", "button.hoverBackground": "#026EC1", - "button.secondaryBackground": "#313131", + "button.secondaryBackground": "#00000000", "button.secondaryForeground": "#CCCCCC", - "button.secondaryHoverBackground": "#3C3C3C", + "button.secondaryHoverBackground": "#2B2B2B", "chat.slashCommandBackground": "#26477866", "chat.slashCommandForeground": "#85B6FF", "chat.editedFileForeground": "#E2C08D", diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index 29a82195861..8328860a9ff 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -83,7 +83,7 @@ "entity.name.operator" ], "settings": { - "foreground": "#CE92A4" + "foreground": "#C586C0" } }, { @@ -197,7 +197,7 @@ } ], "semanticTokenColors": { - "newOperator":"#CE92A4", + "newOperator":"#C586C0", "stringLiteral":"#ce9178", "customLiteral": "#DCDCAA", "numberLiteral": "#b5cea8", diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 299c728719f..2f71999b4b8 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -21,13 +21,25 @@ export const enum TunnelPrivacyId { */ const CLEANUP_TIMEOUT = 10_000; -const cliPath = process.env.VSCODE_FORWARDING_IS_DEV - ? path.join(__dirname, '../../../cli/target/debug/code') - : path.join( - vscode.env.appRoot, - process.platform === 'darwin' ? 'bin' : '../../bin', - vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders', - ) + (process.platform === 'win32' ? '.exe' : ''); +const versionFolder = vscode.env.appCommit?.substring(0, 10); +let cliPath: string; +if (process.env.VSCODE_FORWARDING_IS_DEV) { + cliPath = path.join(__dirname, '../../../cli/target/debug/code'); +} else { + let binPath: string; + if (process.platform === 'darwin') { + binPath = 'bin'; + } else if (process.platform === 'win32' && versionFolder && vscode.env.appRoot.includes(versionFolder)) { + binPath = '../../../bin'; + } else { + binPath = '../../bin'; + } + + const cliName = vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders'; + const extension = process.platform === 'win32' ? '.exe' : ''; + + cliPath = path.join(vscode.env.appRoot, binPath, cliName) + extension; +} class Tunnel implements vscode.Tunnel { private readonly disposeEmitter = new vscode.EventEmitter(); diff --git a/extensions/tunnel-forwarding/tsconfig.json b/extensions/tunnel-forwarding/tsconfig.json index a65d85d716d..0f6ad665692 100644 --- a/extensions/tunnel-forwarding/tsconfig.json +++ b/extensions/tunnel-forwarding/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/typescript-basics/language-configuration.json b/extensions/typescript-basics/language-configuration.json index 336daa91339..563d392beeb 100644 --- a/extensions/typescript-basics/language-configuration.json +++ b/extensions/typescript-basics/language-configuration.json @@ -26,6 +26,10 @@ ] ], "autoClosingPairs": [ + { + "open": "${", + "close": "}" + }, { "open": "{", "close": "}" @@ -70,6 +74,14 @@ } ], "surroundingPairs": [ + [ + "${", + "}" + ], + [ + "$", + "" + ], [ "{", "}" diff --git a/extensions/typescript-basics/package.json b/extensions/typescript-basics/package.json index d64e6df2147..830b32762e7 100644 --- a/extensions/typescript-basics/package.json +++ b/extensions/typescript-basics/package.json @@ -27,6 +27,7 @@ ".cts", ".mts" ], + "firstLine": "^#!.*\\b(deno|bun|ts-node)\\b", "configuration": "./language-configuration.json" }, { diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 7611b2fee34..b4706482fb6 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -53,7 +53,7 @@ "@types/semver": "^5.5.0" }, "scripts": { - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:typescript-language-features", + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:typescript-language-features", "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" }, @@ -1784,6 +1784,27 @@ ], "pattern": "$tsc" }, + { + "name": "tsgo-watch", + "label": "%typescript.problemMatchers.tsgo-watch.label%", + "owner": "typescript", + "source": "ts", + "applyTo": "closedDocuments", + "fileLocation": [ + "relative", + "${cwd}" + ], + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "^build starting at .*$" + }, + "endsPattern": { + "regexp": "^build finished in .*$" + } + } + }, { "name": "tsc-watch", "label": "%typescript.problemMatchers.tscWatch.label%", diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index 26b99390645..e01e4de605f 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -70,11 +70,12 @@ "typescript.tsc.autoDetect.build": "Only create single run compile tasks.", "typescript.tsc.autoDetect.watch": "Only create compile and watch tasks.", "typescript.problemMatchers.tsc.label": "TypeScript problems", + "typescript.problemMatchers.tsgo-watch.label": "TypeScript problems (watch mode)", "typescript.problemMatchers.tscWatch.label": "TypeScript problems (watch mode)", "configuration.suggest.paths": "Enable/disable suggestions for paths in import statements and require calls.", "configuration.tsserver.useSeparateSyntaxServer": "Enable/disable spawning a separate TypeScript server that can more quickly respond to syntax related operations, such as calculating folding or computing document symbols.", "configuration.tsserver.useSyntaxServer": "Controls if TypeScript launches a dedicated server to more quickly handle syntax related operations, such as computing code folding.", - "configuration.tsserver.useSyntaxServer.always": "Use a lighter weight syntax server to handle all IntelliSense operations. This syntax server can only provide IntelliSense for opened files.", + "configuration.tsserver.useSyntaxServer.always": "Use a lighter weight syntax server to handle all IntelliSense operations. This disables project-wide features including auto-imports, cross-file completions, and go to definition for symbols in other files. Only use this for very large projects where performance is critical.", "configuration.tsserver.useSyntaxServer.never": "Don't use a dedicated syntax server. Use a single server to handle all IntelliSense operations.", "configuration.tsserver.useSyntaxServer.auto": "Spawn both a full server and a lighter weight server dedicated to syntax operations. The syntax server is used to speed up syntax operations and provide IntelliSense while projects are loading.", "configuration.tsserver.maxTsServerMemory": "The maximum amount of memory (in MB) to allocate to the TypeScript server process. To use a memory limit greater than 4 GB, use `#typescript.tsserver.nodePath#` to run TS Server with a custom Node installation.", diff --git a/extensions/typescript-language-features/src/extension.ts b/extensions/typescript-language-features/src/extension.ts index 58e47a6f79d..6f51fe1b597 100644 --- a/extensions/typescript-language-features/src/extension.ts +++ b/extensions/typescript-language-features/src/extension.ts @@ -53,6 +53,12 @@ export function activate( new ExperimentationService(experimentTelemetryReporter, id, version, context.globalState); } + // Register features that work in both TSGO and non-TSGO modes + import('./languageFeatures/tsconfig').then(module => { + context.subscriptions.push(module.register()); + }); + + // Conditionally register features based on whether TSGO is enabled context.subscriptions.push(conditionalRegistration([ requireGlobalConfiguration('typescript', 'experimental.useTsgo'), requireHasVsCodeExtension(tsNativeExtensionId), @@ -95,10 +101,6 @@ export function activate( disposables.add(module.register(new Lazy(() => lazyClientHost.value.serviceClient))); }); - import('./languageFeatures/tsconfig').then(module => { - disposables.add(module.register()); - }); - disposables.add(lazilyActivateClient(lazyClientHost, pluginManager, activeJsTsEditorTracker)); return disposables; diff --git a/extensions/typescript-language-features/src/languageFeatures/quickFix.ts b/extensions/typescript-language-features/src/languageFeatures/quickFix.ts index c0e3221be2e..c0c89670d1d 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 { + disposables.forEach(d => d.dispose()); + }); } function isSupportedDocument( diff --git a/extensions/typescript-language-features/src/test/unit/textRendering.test.ts b/extensions/typescript-language-features/src/test/unit/textRendering.test.ts index f4245b59297..7bbbb0e7835 100644 --- a/extensions/typescript-language-features/src/test/unit/textRendering.test.ts +++ b/extensions/typescript-language-features/src/test/unit/textRendering.test.ts @@ -174,7 +174,7 @@ suite('typescript.previewer', () => { { 'text': '}', 'kind': 'link' }, { 'text': ' b', 'kind': 'text' } ], noopToResource), - 'a [`dog`](command:_typescript.openJsDocLink?%5B%7B%22file%22%3A%7B%22path%22%3A%22%2Fpath%2Ffile.ts%22%2C%22scheme%22%3A%22file%22%7D%2C%22position%22%3A%7B%22line%22%3A6%2C%22character%22%3A4%7D%7D%5D) b'); + 'a [`dog`](command:_typescript.openJsDocLink?%5B%7B%22file%22%3A%7B%22path%22%3A%22%2Fpath%2Ffile.ts%22%2C%22scheme%22%3A%22file%22%7D%2C%22position%22%3A%7B%22line%22%3A6%2C%22character%22%3A4%7D%7D%5D "Open symbol link") b'); }); test('Should render @linkcode text as code', () => { @@ -195,6 +195,6 @@ suite('typescript.previewer', () => { { 'text': '}', 'kind': 'link' }, { 'text': ' b', 'kind': 'text' } ], noopToResource), - 'a [`husky`](command:_typescript.openJsDocLink?%5B%7B%22file%22%3A%7B%22path%22%3A%22%2Fpath%2Ffile.ts%22%2C%22scheme%22%3A%22file%22%7D%2C%22position%22%3A%7B%22line%22%3A6%2C%22character%22%3A4%7D%7D%5D) b'); + 'a [`husky`](command:_typescript.openJsDocLink?%5B%7B%22file%22%3A%7B%22path%22%3A%22%2Fpath%2Ffile.ts%22%2C%22scheme%22%3A%22file%22%7D%2C%22position%22%3A%7B%22line%22%3A6%2C%22character%22%3A4%7D%7D%5D "Open symbol link") b'); }); }); diff --git a/extensions/typescript-language-features/src/tsServer/versionManager.ts b/extensions/typescript-language-features/src/tsServer/versionManager.ts index 43a2413e383..dcfee493f43 100644 --- a/extensions/typescript-language-features/src/tsServer/versionManager.ts +++ b/extensions/typescript-language-features/src/tsServer/versionManager.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import { TypeScriptServiceConfiguration } from '../configuration/configuration'; +import { tsNativeExtensionId } from '../commands/useTsgo'; import { setImmediate } from '../utils/async'; import { Disposable } from '../utils/dispose'; import { ITypeScriptVersionProvider, TypeScriptVersion } from './versionProvider'; @@ -77,16 +78,26 @@ export class TypeScriptVersionManager extends Disposable { } public async promptUserForVersion(): Promise { - const selected = await vscode.window.showQuickPick([ + const nativePreviewItem = this.getNativePreviewPickItem(); + const items: QuickPickItem[] = [ this.getBundledPickItem(), ...this.getLocalPickItems(), + ]; + + if (nativePreviewItem) { + items.push(nativePreviewItem); + } + + items.push( { kind: vscode.QuickPickItemKind.Separator, label: '', run: () => { /* noop */ }, }, LearnMorePickItem, - ], { + ); + + const selected = await vscode.window.showQuickPick(items, { placeHolder: vscode.l10n.t("Select the TypeScript version used for JavaScript and TypeScript language features"), }); @@ -129,6 +140,24 @@ export class TypeScriptVersionManager extends Disposable { }); } + private getNativePreviewPickItem(): QuickPickItem | undefined { + const nativePreviewExtension = vscode.extensions.getExtension(tsNativeExtensionId); + if (!nativePreviewExtension) { + return undefined; + } + + const tsConfig = vscode.workspace.getConfiguration('typescript'); + const isUsingTsgo = tsConfig.get('experimental.useTsgo', false); + + return { + label: (isUsingTsgo ? '• ' : '') + vscode.l10n.t("Use TypeScript Native Preview (Experimental)"), + description: nativePreviewExtension.packageJSON.version, + run: async () => { + await vscode.commands.executeCommand('typescript.native-preview.enable'); + }, + }; + } + private async promptUseWorkspaceTsdk(): Promise { const workspaceVersion = this.versionProvider.localVersion; diff --git a/extensions/typescript-language-features/tsconfig.json b/extensions/typescript-language-features/tsconfig.json index 7300af07c43..5663d5af904 100644 --- a/extensions/typescript-language-features/tsconfig.json +++ b/extensions/typescript-language-features/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/typescript-language-features/web/tsconfig.json b/extensions/typescript-language-features/web/tsconfig.json index 1cd59164023..2f7ea63d64b 100644 --- a/extensions/typescript-language-features/web/tsconfig.json +++ b/extensions/typescript-language-features/web/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "../../out", "esModuleInterop": true, "types": [ diff --git a/extensions/vb/language-configuration.json b/extensions/vb/language-configuration.json index 53f537617c5..2fefe78dbe7 100644 --- a/extensions/vb/language-configuration.json +++ b/extensions/vb/language-configuration.json @@ -25,5 +25,32 @@ "start": "^\\s*#Region\\b", "end": "^\\s*#End Region\\b" } - } + }, + "indentationRules": { + "decreaseIndentPattern": { + "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Case|Catch|Finally|Loop|Next|Wend|Until)\\b", + "flags": "i" + }, + "increaseIndentPattern": { + "pattern": "^\\s*((If|ElseIf)\\b.*\\bThen\\s*(('|REM).*)?|(Else|While|For|Do|Select\\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b(?!.*\\bEnd\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\\b).*(('|REM).*)?)$", + "flags": "i" + } + }, + "onEnterRules": [ + // Prevent indent after End statements and block terminators (Loop, Next, etc.) + { + "beforeText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, + "action": { + "indent": "none" + } + }, + // Prevent indent when pressing Enter on a blank line after End statements or block terminators + { + "beforeText": "^\\s*$", + "previousLineText": { "pattern": "^\\s*((End\\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\\b.*$", "flags": "i" }, + "action": { + "indent": "none" + } + } + ] } diff --git a/extensions/vscode-api-tests/package-lock.json b/extensions/vscode-api-tests/package-lock.json index 8e0b07da13a..644c4fdd017 100644 --- a/extensions/vscode-api-tests/package-lock.json +++ b/extensions/vscode-api-tests/package-lock.json @@ -12,7 +12,7 @@ "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/node-forge": "^1.3.11", - "node-forge": "^1.3.1", + "node-forge": "^1.3.2", "straightforward": "^4.2.2" }, "engines": { @@ -158,10 +158,11 @@ "dev": true }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 10eb7e68fe9..c3c08b17c5b 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -17,6 +17,7 @@ "documentFiltersExclusive", "editorInsets", "embeddings", + "envIsAppPortable", "extensionRuntime", "extensionsAny", "externalUriOpener", @@ -26,15 +27,14 @@ "fsChunks", "interactive", "languageStatusText", + "mcpServerDefinitions", "nativeWindowHandle", "notebookDeprecated", "notebookLiveShare", "notebookMessaging", "notebookMime", "portsAttributes", - "quickInputButtonLocation", "quickPickSortByLabel", - "quickPickItemResource", "resolvers", "scmActionButton", "scmSelectedProvider", @@ -54,7 +54,8 @@ "workspaceTrust", "inlineCompletionsAdditions", "devDeviceId", - "languageModelProxy" + "languageModelProxy", + "agentSessionsWorkspace" ], "private": true, "activationEvents": [], @@ -268,13 +269,13 @@ }, "scripts": { "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-api-tests ./tsconfig.json" + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:vscode-api-tests ./tsconfig.json" }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", "@types/node-forge": "^1.3.11", - "node-forge": "^1.3.1", + "node-forge": "^1.3.2", "straightforward": "^4.2.2" }, "repository": { diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index b00389d4376..ff5b49d9b69 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import * as fs from 'fs'; +import { join } from 'path'; import 'mocha'; -import { ChatContext, ChatRequest, ChatRequestTurn, ChatRequestTurn2, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; +import { ChatContext, ChatRequest, ChatRequestTurn, ChatRequestTurn2, ChatResult, Disposable, env, Event, EventEmitter, chat, commands, lm, UIKind } from 'vscode'; import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils'; suite('chat', () => { @@ -214,4 +216,28 @@ suite('chat', () => { // Title provider was not called again assert.strictEqual(calls, 1); }); + + test('can access node-pty module', async function () { + // Required for copilot cli in chat extension. + if (env.uiKind === UIKind.Web) { + this.skip(); + } + const nodePtyModules = [ + join(env.appRoot, 'node_modules.asar', 'node-pty'), + join(env.appRoot, 'node_modules', 'node-pty') + ]; + + for (const modulePath of nodePtyModules) { + // try to stat and require module + try { + await fs.promises.stat(modulePath); + const nodePty = require(modulePath); + assert.ok(nodePty, `Successfully required node-pty from ${modulePath}`); + return; + } catch (err) { + // failed to require, try next + } + } + assert.fail('Failed to find and require node-pty module'); + }); }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts index 7cc5e40a100..805c9446086 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/notebook.kernel.test.ts @@ -14,9 +14,7 @@ async function createRandomNotebookFile() { } async function openRandomNotebookDocument() { - console.log('Creating a random notebook file'); const uri = await createRandomNotebookFile(); - console.log('Created a random notebook file'); return vscode.workspace.openNotebookDocument(uri); } @@ -121,7 +119,6 @@ const apiTestSerializer: vscode.NotebookSerializer = { } ] }; - console.log('Returning NotebookData in deserializeNotebook'); return dto; } }; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index 29efe71d87c..2e445be00e9 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -18,8 +18,6 @@ import { assertNoRpc, poll } from '../utils'; extensionContext = global.testExtensionContext; const config = workspace.getConfiguration('terminal.integrated'); - // Disable conpty in integration tests because of https://github.com/microsoft/vscode/issues/76548 - await config.update('windowsEnableConpty', false, ConfigurationTarget.Global); // Disable exit alerts as tests may trigger then and we're not testing the notifications await config.update('showExitAlert', false, ConfigurationTarget.Global); // Canvas may cause problems when running in a container @@ -70,7 +68,7 @@ import { assertNoRpc, poll } from '../utils'; r(terminal); } })); - // Use a single character to avoid winpty/conpty issues with injected sequences + // Use a single character to avoid conpty issues with injected sequences const terminal = window.createTerminal({ env: { TEST: '`' } }); @@ -926,16 +924,16 @@ import { assertNoRpc, poll } from '../utils'; applyAtProcessCreation: true, applyAtShellIntegration: false }; - deepStrictEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions }); - deepStrictEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions }); - deepStrictEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions }); + deepStrictEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions, variable: 'A' }); + deepStrictEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions, variable: 'B' }); + deepStrictEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions, variable: 'C' }); // Verify forEach const entries: [string, EnvironmentVariableMutator][] = []; collection.forEach((v, m) => entries.push([v, m])); deepStrictEqual(entries, [ - ['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions }], - ['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions }], - ['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions }] + ['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions, variable: 'A' }], + ['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions, variable: 'B' }], + ['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions, variable: 'C' }] ]); }); @@ -956,17 +954,17 @@ import { assertNoRpc, poll } from '../utils'; applyAtShellIntegration: false }; const expectedScopedCollection = collection.getScoped(scope); - deepStrictEqual(expectedScopedCollection.get('A'), { value: 'scoped~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions }); - deepStrictEqual(expectedScopedCollection.get('B'), { value: 'scoped~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions }); - deepStrictEqual(expectedScopedCollection.get('C'), { value: 'scoped~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions }); + deepStrictEqual(expectedScopedCollection.get('A'), { value: 'scoped~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions, variable: 'A' }); + deepStrictEqual(expectedScopedCollection.get('B'), { value: 'scoped~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions, variable: 'B' }); + deepStrictEqual(expectedScopedCollection.get('C'), { value: 'scoped~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions, variable: 'C' }); // Verify forEach const entries: [string, EnvironmentVariableMutator][] = []; expectedScopedCollection.forEach((v, m) => entries.push([v, m])); deepStrictEqual(entries.map(v => v[1]), [ - { value: 'scoped~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions }, - { value: 'scoped~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions }, - { value: 'scoped~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions } + { value: 'scoped~a2~', type: EnvironmentVariableMutatorType.Replace, options: defaultOptions, variable: 'A' }, + { value: 'scoped~b2~', type: EnvironmentVariableMutatorType.Append, options: defaultOptions, variable: 'B' }, + { value: 'scoped~c2~', type: EnvironmentVariableMutatorType.Prepend, options: defaultOptions, variable: 'C' } ]); deepStrictEqual(entries.map(v => v[0]), ['A', 'B', 'C']); }); @@ -978,7 +976,7 @@ function sanitizeData(data: string): string { // Strip NL/CR so terminal dimensions don't impact tests data = data.replace(/[\r\n]/g, ''); - // Strip escape sequences so winpty/conpty doesn't cause flakiness, do for all platforms for + // Strip escape sequences so conpty doesn't cause flakiness, do for all platforms for // consistency const CSI_SEQUENCE = /(:?(:?\x1b\[|\x9B)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~])|(:?\x1b\].*?\x07)/g; data = data.replace(CSI_SEQUENCE, ''); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts new file mode 100644 index 00000000000..a9d9bc5aa34 --- /dev/null +++ b/extensions/vscode-api-tests/src/singlefolder-tests/tree.test.ts @@ -0,0 +1,353 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { asPromise, assertNoRpc, disposeAll, delay, DeferredPromise } from '../utils'; + +suite('vscode API - tree', () => { + + const disposables: vscode.Disposable[] = []; + + teardown(() => { + disposeAll(disposables); + disposables.length = 0; + assertNoRpc(); + }); + + test('TreeView - element already registered', async function () { + this.timeout(60_000); + + type TreeElement = { readonly kind: 'leaf' }; + + class QuickRefreshTreeDataProvider implements vscode.TreeDataProvider { + private readonly changeEmitter = new vscode.EventEmitter(); + private readonly requestEmitter = new vscode.EventEmitter(); + private readonly pendingRequests: DeferredPromise[] = []; + private readonly element: TreeElement = { kind: 'leaf' }; + + readonly onDidChangeTreeData = this.changeEmitter.event; + + getChildren(element?: TreeElement): Thenable { + if (!element) { + const deferred = new DeferredPromise(); + this.pendingRequests.push(deferred); + this.requestEmitter.fire(this.pendingRequests.length); + return deferred.p; + } + return Promise.resolve([]); + } + + getTreeItem(): vscode.TreeItem { + const item = new vscode.TreeItem('duplicate', vscode.TreeItemCollapsibleState.None); + item.id = 'dup'; + return item; + } + + getParent(): TreeElement | undefined { + return undefined; + } + + async waitForRequestCount(count: number): Promise { + while (this.pendingRequests.length < count) { + await asPromise(this.requestEmitter.event); + } + } + + async resolveNextRequest(): Promise { + const next = this.pendingRequests.shift(); + if (!next) { + return; + } + await next.complete([this.element]); + } + + dispose(): void { + this.changeEmitter.dispose(); + this.requestEmitter.dispose(); + while (this.pendingRequests.length) { + this.pendingRequests.shift()!.complete([]); + } + } + + getElement(): TreeElement { + return this.element; + } + } + + const provider = new QuickRefreshTreeDataProvider(); + disposables.push(provider); + + const treeView = vscode.window.createTreeView('test.treeId', { treeDataProvider: provider }); + disposables.push(treeView); + + const revealFirst = (treeView.reveal(provider.getElement(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + const revealSecond = (treeView.reveal(provider.getElement(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + await provider.waitForRequestCount(2); + + await provider.resolveNextRequest(); + await delay(0); + await provider.resolveNextRequest(); + + const [firstResult, secondResult] = await Promise.all([revealFirst, revealSecond]); + const error = firstResult.error ?? secondResult.error; + if (error && /Element with id .+ is already registered/.test(error.message)) { + assert.fail(error.message); + } + }); + + test('TreeView - element already registered after rapid root refresh', async function () { + this.timeout(60_000); + + // This test reproduces a race condition where rapid concurrent getChildren calls + // return different element object instances that have the same ID in their TreeItem, + // causing "Element with id ... is already registered" error. + // + // The bug: When _addChildrenToClear(undefined) is called, it clears _childrenFetchTokens. + // If two fetches are pending, both may reset the requestId counter to 1, so both think + // they are the current request. When both try to register elements with the same ID + // but different object instances, the error is thrown. + + type TreeElement = { readonly kind: 'leaf'; readonly instance: number }; + + class RapidRefreshTreeDataProvider implements vscode.TreeDataProvider { + private readonly changeEmitter = new vscode.EventEmitter(); + private readonly requestEmitter = new vscode.EventEmitter(); + private readonly pendingRequests: DeferredPromise[] = []; + // Return different element instance each time + private element1: TreeElement = { kind: 'leaf', instance: 1 }; + private element2: TreeElement = { kind: 'leaf', instance: 2 }; + + readonly onDidChangeTreeData = this.changeEmitter.event; + + getChildren(element?: TreeElement): Thenable { + if (!element) { + const deferred = new DeferredPromise(); + this.pendingRequests.push(deferred); + this.requestEmitter.fire(this.pendingRequests.length); + return deferred.p; + } + return Promise.resolve([]); + } + + getTreeItem(): vscode.TreeItem { + // Both element instances return the same id + const item = new vscode.TreeItem('test element', vscode.TreeItemCollapsibleState.None); + item.id = 'same-id-each-time'; + return item; + } + + getParent(): TreeElement | undefined { + return undefined; + } + + getElement1(): TreeElement { + return this.element1; + } + + getElement2(): TreeElement { + return this.element2; + } + + async waitForRequestCount(count: number): Promise { + while (this.pendingRequests.length < count) { + await asPromise(this.requestEmitter.event); + } + } + + resolveRequestWithElement(index: number, element: TreeElement): void { + const request = this.pendingRequests[index]; + if (request) { + request.complete([element]); + } + } + + dispose(): void { + this.changeEmitter.dispose(); + this.requestEmitter.dispose(); + while (this.pendingRequests.length) { + this.pendingRequests.shift()!.complete([]); + } + } + } + + const provider = new RapidRefreshTreeDataProvider(); + disposables.push(provider); + + const treeView = vscode.window.createTreeView('test.treeRapidRefresh', { treeDataProvider: provider }); + disposables.push(treeView); + + // Start two concurrent reveal operations - this should trigger two getChildren calls + // Similar to the first test + const firstReveal = (treeView.reveal(provider.getElement1(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + const secondReveal = (treeView.reveal(provider.getElement2(), { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + // Wait for both getChildren calls to be pending + await provider.waitForRequestCount(2); + + // Resolve requests returning DIFFERENT element instances with SAME id + // First request returns element1, second returns element2 + // Both elements have the same id 'same-id-each-time' in getTreeItem + provider.resolveRequestWithElement(0, provider.getElement1()); + await delay(0); + provider.resolveRequestWithElement(1, provider.getElement2()); + + const [firstResult, secondResult] = await Promise.all([firstReveal, secondReveal]); + const error = firstResult.error ?? secondResult.error; + if (error && /Element with id .+ is already registered/.test(error.message)) { + assert.fail(error.message); + } + }); + + test('TreeView - element already registered after refresh', async function () { + this.timeout(60_000); + + type ParentElement = { readonly kind: 'parent' }; + type ChildElement = { readonly kind: 'leaf'; readonly version: number }; + type TreeElement = ParentElement | ChildElement; + + class ParentRefreshTreeDataProvider implements vscode.TreeDataProvider { + private readonly changeEmitter = new vscode.EventEmitter(); + private readonly rootRequestEmitter = new vscode.EventEmitter(); + private readonly childRequestEmitter = new vscode.EventEmitter(); + private readonly rootRequests: DeferredPromise[] = []; + private readonly childRequests: DeferredPromise[] = []; + private readonly parentElement: ParentElement = { kind: 'parent' }; + private childVersion = 0; + private currentChild: ChildElement = { kind: 'leaf', version: 0 }; + + readonly onDidChangeTreeData = this.changeEmitter.event; + + getChildren(element?: TreeElement): Thenable { + if (!element) { + const deferred = new DeferredPromise(); + this.rootRequests.push(deferred); + this.rootRequestEmitter.fire(this.rootRequests.length); + return deferred.p; + } + if (element.kind === 'parent') { + const deferred = new DeferredPromise(); + this.childRequests.push(deferred); + this.childRequestEmitter.fire(this.childRequests.length); + return deferred.p; + } + return Promise.resolve([]); + } + + getTreeItem(element: TreeElement): vscode.TreeItem { + if (element.kind === 'parent') { + const item = new vscode.TreeItem('parent', vscode.TreeItemCollapsibleState.Collapsed); + item.id = 'parent'; + return item; + } + const item = new vscode.TreeItem('duplicate', vscode.TreeItemCollapsibleState.None); + item.id = 'dup'; + return item; + } + + getParent(element: TreeElement): TreeElement | undefined { + if (element.kind === 'leaf') { + return this.parentElement; + } + return undefined; + } + + getCurrentChild(): ChildElement { + return this.currentChild; + } + + replaceChild(): ChildElement { + this.childVersion++; + this.currentChild = { kind: 'leaf', version: this.childVersion }; + return this.currentChild; + } + + async waitForRootRequestCount(count: number): Promise { + while (this.rootRequests.length < count) { + await asPromise(this.rootRequestEmitter.event); + } + } + + async waitForChildRequestCount(count: number): Promise { + while (this.childRequests.length < count) { + await asPromise(this.childRequestEmitter.event); + } + } + + async resolveNextRootRequest(elements?: TreeElement[]): Promise { + const next = this.rootRequests.shift(); + if (!next) { + return; + } + await next.complete(elements ?? [this.parentElement]); + } + + async resolveChildRequestAt(index: number, elements?: TreeElement[]): Promise { + const request = this.childRequests[index]; + if (!request) { + return; + } + this.childRequests.splice(index, 1); + await request.complete(elements ?? [this.currentChild]); + } + + dispose(): void { + this.changeEmitter.dispose(); + this.rootRequestEmitter.dispose(); + this.childRequestEmitter.dispose(); + while (this.rootRequests.length) { + this.rootRequests.shift()!.complete([]); + } + while (this.childRequests.length) { + this.childRequests.shift()!.complete([]); + } + } + } + + const provider = new ParentRefreshTreeDataProvider(); + disposables.push(provider); + + const treeView = vscode.window.createTreeView('test.treeRefresh', { treeDataProvider: provider }); + disposables.push(treeView); + + const initialChild = provider.getCurrentChild(); + const firstReveal = (treeView.reveal(initialChild, { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + await provider.waitForRootRequestCount(1); + await provider.resolveNextRootRequest(); + + await provider.waitForChildRequestCount(1); + const staleChild = provider.getCurrentChild(); + const refreshedChild = provider.replaceChild(); + const secondReveal = (treeView.reveal(refreshedChild, { expand: true }) + .then(() => ({ error: undefined as Error | undefined })) as Promise<{ error: Error | undefined }>) + .catch(error => ({ error })); + + await provider.waitForChildRequestCount(2); + + await provider.resolveChildRequestAt(1, [refreshedChild]); + await delay(0); + await provider.resolveChildRequestAt(0, [staleChild]); + + const [firstResult, secondResult] = await Promise.all([firstReveal, secondReveal]); + const error = firstResult.error ?? secondResult.error; + if (error && /Element with id .+ is already registered/.test(error.message)) { + assert.fail(error.message); + } + }); +}); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index 44f8fca1c83..3d4de7af0cf 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -13,8 +13,6 @@ import { assertNoRpc } from '../utils'; suiteSetup(async () => { const config = workspace.getConfiguration('terminal.integrated'); - // Disable conpty in integration tests because of https://github.com/microsoft/vscode/issues/76548 - await config.update('windowsEnableConpty', false, ConfigurationTarget.Global); // Disable exit alerts as tests may trigger then and we're not testing the notifications await config.update('showExitAlert', false, ConfigurationTarget.Global); // Canvas may cause problems when running in a container diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 19f72931512..30d08c25b98 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -1149,12 +1149,12 @@ suite('vscode API - workspace', () => { const we = new vscode.WorkspaceEdit(); we.insert(file, new vscode.Position(0, 5), '2'); we.renameFile(file, newFile); - await vscode.workspace.applyEdit(we); + assert.ok(await vscode.workspace.applyEdit(we)); } // show the new document { - const document = await vscode.workspace.openTextDocument(newFile); + const document = await vscode.workspace.openTextDocument(newFile); // FAILS here await vscode.window.showTextDocument(document); assert.strictEqual(document.getText(), 'hello2'); assert.strictEqual(document.isDirty, true); diff --git a/extensions/vscode-api-tests/tsconfig.json b/extensions/vscode-api-tests/tsconfig.json index 6cfdb070b4e..88fdb5d4256 100644 --- a/extensions/vscode-api-tests/tsconfig.json +++ b/extensions/vscode-api-tests/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/vscode-colorize-perf-tests/package.json b/extensions/vscode-colorize-perf-tests/package.json index fb7ce45f613..2ac38c1a995 100644 --- a/extensions/vscode-colorize-perf-tests/package.json +++ b/extensions/vscode-colorize-perf-tests/package.json @@ -14,7 +14,7 @@ }, "icon": "media/icon.png", "scripts": { - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-perf-tests ./tsconfig.json", + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:vscode-colorize-perf-tests ./tsconfig.json", "watch": "gulp watch-extension:vscode-colorize-perf-tests", "compile": "gulp compile-extension:vscode-colorize-perf-tests" }, diff --git a/extensions/vscode-colorize-perf-tests/tsconfig.json b/extensions/vscode-colorize-perf-tests/tsconfig.json index 20f72eae51d..431bc8ed65c 100644 --- a/extensions/vscode-colorize-perf-tests/tsconfig.json +++ b/extensions/vscode-colorize-perf-tests/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index 49592763745..1abff3d9862 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -14,7 +14,7 @@ }, "icon": "media/icon.png", "scripts": { - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-colorize-tests ./tsconfig.json", + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:vscode-colorize-tests ./tsconfig.json", "watch": "gulp watch-extension:vscode-colorize-tests", "compile": "gulp compile-extension:vscode-colorize-tests" }, diff --git a/extensions/vscode-colorize-tests/test/colorize-results/basic_java.json b/extensions/vscode-colorize-tests/test/colorize-results/basic_java.json index 843740c186f..71a5a901280 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/basic_java.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/basic_java.json @@ -1697,12 +1697,12 @@ "c": "for", "t": "source.java meta.class.java meta.class.body.java meta.method.java meta.method.body.java keyword.control.java", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2243,12 +2243,12 @@ "c": "return", "t": "source.java meta.class.java meta.class.body.java meta.method.java meta.method.body.java keyword.control.java", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2817,12 +2817,12 @@ "c": "new", "t": "source.java meta.class.java meta.class.body.java meta.method.java meta.method.body.java keyword.control.new.java", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/issue-28354_php.json b/extensions/vscode-colorize-tests/test/colorize-results/issue-28354_php.json index 642bf6542ff..fa41ab9c049 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/issue-28354_php.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/issue-28354_php.json @@ -115,12 +115,12 @@ "c": "foreach", "t": "text.html.php meta.embedded.block.html source.js meta.embedded.block.php source.php keyword.control.foreach.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -183,7 +183,7 @@ }, { "c": "AS", - "t": "text.html.php meta.embedded.block.html source.js meta.embedded.block.php source.php keyword.operator.logical.php", + "t": "text.html.php meta.embedded.block.html source.js meta.embedded.block.php source.php keyword.operator.as.php", "r": { "dark_plus": "keyword.operator: #D4D4D4", "light_plus": "keyword.operator: #000000", diff --git a/extensions/vscode-colorize-tests/test/colorize-results/makefile.json b/extensions/vscode-colorize-tests/test/colorize-results/makefile.json index db94f1063e4..b03ac95a7f9 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/makefile.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/makefile.json @@ -1193,12 +1193,12 @@ "c": "@", "t": "source.makefile meta.scope.recipe.makefile keyword.control.@.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1235,12 +1235,12 @@ "c": "@-+-+", "t": "source.makefile meta.scope.recipe.makefile keyword.control.@-+-+.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1291,12 +1291,12 @@ "c": "@", "t": "source.makefile meta.scope.recipe.makefile keyword.control.@.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1487,12 +1487,12 @@ "c": "@-", "t": "source.makefile meta.scope.recipe.makefile keyword.control.@-.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1515,12 +1515,12 @@ "c": "define", "t": "source.makefile meta.scope.conditional.makefile keyword.control.define.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2075,12 +2075,12 @@ "c": "endef", "t": "source.makefile meta.scope.conditional.makefile keyword.control.override.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2089,12 +2089,12 @@ "c": "ifeq", "t": "source.makefile meta.scope.conditional.makefile keyword.control.ifeq.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2355,12 +2355,12 @@ "c": "endif", "t": "source.makefile meta.scope.conditional.makefile keyword.control.endif.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2369,12 +2369,12 @@ "c": "-include", "t": "source.makefile keyword.control.include.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2453,12 +2453,12 @@ "c": "ifeq", "t": "source.makefile meta.scope.conditional.makefile keyword.control.ifeq.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2775,12 +2775,12 @@ "c": "endif", "t": "source.makefile meta.scope.conditional.makefile keyword.control.endif.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3237,12 +3237,12 @@ "c": "ifeq", "t": "source.makefile meta.scope.conditional.makefile keyword.control.ifeq.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3419,12 +3419,12 @@ "c": "else ifeq", "t": "source.makefile meta.scope.conditional.makefile keyword.control.else.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3601,12 +3601,12 @@ "c": "else", "t": "source.makefile meta.scope.conditional.makefile keyword.control.else.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3755,12 +3755,12 @@ "c": "endif", "t": "source.makefile meta.scope.conditional.makefile keyword.control.endif.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4931,12 +4931,12 @@ "c": "export", "t": "source.makefile keyword.control.export.makefile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json index 720a867ca74..c1875bfa986 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-173336_sh.json @@ -213,12 +213,12 @@ "c": "if", "t": "source.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -437,12 +437,12 @@ "c": "then", "t": "source.shell meta.scope.if-block.shell keyword.control.then.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -815,12 +815,12 @@ "c": "fi", "t": "source.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-23630_cpp.json b/extensions/vscode-colorize-tests/test/colorize-results/test-23630_cpp.json index 222b60af2e0..79e4727a417 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-23630_cpp.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-23630_cpp.json @@ -3,12 +3,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.conditional.ifndef.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "ifndef", "t": "source.cpp keyword.control.directive.conditional.ifndef.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -73,12 +73,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -87,12 +87,12 @@ "c": "define", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -129,12 +129,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.endif.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -143,12 +143,12 @@ "c": "endif", "t": "source.cpp keyword.control.directive.endif.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-23850_cpp.json b/extensions/vscode-colorize-tests/test/colorize-results/test-23850_cpp.json index 61251a59c3c..a5e6addc3b1 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-23850_cpp.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-23850_cpp.json @@ -3,12 +3,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.conditional.ifndef.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "ifndef", "t": "source.cpp keyword.control.directive.conditional.ifndef.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -59,12 +59,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -73,12 +73,12 @@ "c": "define", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -115,12 +115,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.endif.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -129,12 +129,12 @@ "c": "endif", "t": "source.cpp keyword.control.directive.endif.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-241001_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-241001_ts.json index e2b376a059c..67b874115af 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-241001_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-241001_ts.json @@ -955,12 +955,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3027,12 +3027,12 @@ "c": "import", "t": "source.ts new.expr.ts keyword.control.import.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-6611_rs.json b/extensions/vscode-colorize-tests/test/colorize-results/test-6611_rs.json index a8dc0e0fd28..9897e09a654 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-6611_rs.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-6611_rs.json @@ -381,12 +381,12 @@ "c": "for", "t": "source.rust keyword.control.rust", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -689,12 +689,12 @@ "c": "for", "t": "source.rust keyword.control.rust", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-78769_cpp.json b/extensions/vscode-colorize-tests/test/colorize-results/test-78769_cpp.json index a1a55fd67db..16438692b78 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-78769_cpp.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-78769_cpp.json @@ -3,12 +3,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "define", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56377_py.json b/extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56377_py.json index 994f91b2ae1..432ecde8cce 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56377_py.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-freeze-56377_py.json @@ -269,12 +269,12 @@ "c": "for", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -325,12 +325,12 @@ "c": "in", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -493,12 +493,12 @@ "c": "if", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-issue11_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-issue11_ts.json index 957fe515fb7..68717cc4939 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-issue11_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-issue11_ts.json @@ -115,12 +115,12 @@ "c": "if", "t": "source.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -465,12 +465,12 @@ "c": "for", "t": "source.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "for", "t": "source.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1025,12 +1025,12 @@ "c": "for", "t": "source.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1305,12 +1305,12 @@ "c": "for", "t": "source.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1473,12 +1473,12 @@ "c": "for", "t": "source.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3055,12 +3055,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json index 9489f1d5b91..da9e674e46c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-issue241715_ts.json @@ -633,12 +633,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1081,12 +1081,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1711,12 +1711,12 @@ "c": "export", "t": "source.ts meta.interface.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2159,12 +2159,12 @@ "c": "return", "t": "source.ts meta.arrow.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2257,12 +2257,12 @@ "c": "export", "t": "source.ts meta.function.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2523,12 +2523,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3475,12 +3475,12 @@ "c": "export", "t": "source.ts meta.function.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3853,12 +3853,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-issue5431_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-issue5431_ts.json index e093fe01582..c1988500c97 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-issue5431_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-issue5431_ts.json @@ -591,12 +591,12 @@ "c": "return", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-issue5465_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-issue5465_ts.json index b238ed5c61a..4282511c5e8 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-issue5465_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-issue5465_ts.json @@ -129,12 +129,12 @@ "c": "yield", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -227,12 +227,12 @@ "c": "yield", "t": "source.ts meta.function.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json index 091238d6e12..a365ac3d098 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test-keywords_ts.json @@ -3,12 +3,12 @@ "c": "export", "t": "source.ts meta.var.expr.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test2_pl.json b/extensions/vscode-colorize-tests/test/colorize-results/test2_pl.json index dde3a9855c8..7d7b3bbe3e5 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test2_pl.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test2_pl.json @@ -3,12 +3,12 @@ "c": "die", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -129,12 +129,12 @@ "c": "unless", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -353,12 +353,12 @@ "c": "i", "t": "source.perl string.regexp.find.perl punctuation.definition.string.perl keyword.control.regexp-option.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -619,12 +619,12 @@ "c": "i", "t": "source.perl string.regexp.find.perl punctuation.definition.string.perl keyword.control.regexp-option.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "while", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1291,12 +1291,12 @@ "c": "next", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1319,12 +1319,12 @@ "c": "unless", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1529,12 +1529,12 @@ "c": "next", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1557,12 +1557,12 @@ "c": "unless", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1725,12 +1725,12 @@ "c": "$", "t": "source.perl string.regexp.find.perl keyword.control.anchor.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2131,12 +2131,12 @@ "c": "next", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2159,12 +2159,12 @@ "c": "unless", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2285,12 +2285,12 @@ "c": "$", "t": "source.perl string.regexp.find.perl keyword.control.anchor.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2439,12 +2439,12 @@ "c": "next", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2467,12 +2467,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2901,12 +2901,12 @@ "c": "for", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3419,12 +3419,12 @@ "c": "next", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3447,12 +3447,12 @@ "c": "unless", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3629,12 +3629,12 @@ "c": "for", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3965,12 +3965,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test6916_js.json b/extensions/vscode-colorize-tests/test/colorize-results/test6916_js.json index 5183b00921b..4fa10b042fa 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test6916_js.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test6916_js.json @@ -3,12 +3,12 @@ "c": "for", "t": "source.js keyword.control.loop.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -227,12 +227,12 @@ "c": "for", "t": "source.js meta.block.js keyword.control.loop.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -423,12 +423,12 @@ "c": "if", "t": "source.js meta.block.js meta.block.js keyword.control.conditional.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -535,12 +535,12 @@ "c": "return", "t": "source.js meta.block.js meta.block.js keyword.control.flow.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json b/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json index 4611be77db0..853018d8458 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_bat.json @@ -199,12 +199,12 @@ "c": "if", "t": "source.batchfile keyword.control.conditional.batchfile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -283,12 +283,12 @@ "c": "call", "t": "source.batchfile keyword.control.statement.batchfile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -381,12 +381,12 @@ "c": "if", "t": "source.batchfile keyword.control.conditional.batchfile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -563,12 +563,12 @@ "c": "call", "t": "source.batchfile keyword.control.statement.batchfile", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -685,4 +685,4 @@ "light_modern": "keyword: #0000FF" } } -] \ No newline at end of file +] diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_c.json b/extensions/vscode-colorize-tests/test/colorize-results/test_c.json index ecf56307bae..fbbf86a1c70 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_c.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_c.json @@ -87,12 +87,12 @@ "c": "#", "t": "source.c meta.preprocessor.include.c keyword.control.directive.include.c punctuation.definition.directive.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -101,12 +101,12 @@ "c": "include", "t": "source.c meta.preprocessor.include.c keyword.control.directive.include.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -171,12 +171,12 @@ "c": "#", "t": "source.c meta.preprocessor.include.c keyword.control.directive.include.c punctuation.definition.directive.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -185,12 +185,12 @@ "c": "include", "t": "source.c meta.preprocessor.include.c keyword.control.directive.include.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1179,12 +1179,12 @@ "c": "if", "t": "source.c meta.block.c keyword.control.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2089,12 +2089,12 @@ "c": "else", "t": "source.c meta.block.c keyword.control.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2117,12 +2117,12 @@ "c": "if", "t": "source.c meta.block.c keyword.control.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2691,12 +2691,12 @@ "c": "else", "t": "source.c meta.block.c keyword.control.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3489,12 +3489,12 @@ "c": "return", "t": "source.c meta.block.c keyword.control.c", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cc.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cc.json index 446f09c02c7..19e19cec621 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cc.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cc.json @@ -3,12 +3,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.conditional.if.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "if", "t": "source.cpp keyword.control.directive.conditional.if.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -269,12 +269,12 @@ "c": "for", "t": "source.cpp keyword.control.for.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -829,12 +829,12 @@ "c": "#", "t": "source.cpp keyword.control.directive.endif.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -843,12 +843,12 @@ "c": "endif", "t": "source.cpp keyword.control.directive.endif.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1655,12 +1655,12 @@ "c": "new", "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp keyword.operator.wordlike.cpp keyword.operator.new.cpp", "r": { - "dark_plus": "source.cpp keyword.operator.new: #CE92A4", + "dark_plus": "source.cpp keyword.operator.new: #C586C0", "light_plus": "source.cpp keyword.operator.new: #AF00DB", "dark_vs": "keyword.operator.new: #569CD6", "light_vs": "keyword.operator.new: #0000FF", "hc_black": "source.cpp keyword.operator.new: #C586C0", - "dark_modern": "source.cpp keyword.operator.new: #CE92A4", + "dark_modern": "source.cpp keyword.operator.new: #C586C0", "hc_light": "source.cpp keyword.operator.new: #B5200D", "light_modern": "source.cpp keyword.operator.new: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_clj.json b/extensions/vscode-colorize-tests/test/colorize-results/test_clj.json index 99786f9fec9..23597c44358 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_clj.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_clj.json @@ -45,12 +45,12 @@ "c": "require", "t": "source.clojure meta.expression.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -199,12 +199,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -885,12 +885,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2383,12 +2383,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2565,12 +2565,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3167,12 +3167,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3685,12 +3685,12 @@ "c": "def", "t": "source.clojure meta.expression.clojure meta.definition.global.clojure keyword.control.clojure", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_coffee.json b/extensions/vscode-colorize-tests/test/colorize-results/test_coffee.json index 7f79a5a6251..52efa92c373 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_coffee.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_coffee.json @@ -507,12 +507,12 @@ "c": "extends", "t": "source.coffee meta.class.coffee keyword.control.inheritance.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -941,12 +941,12 @@ "c": "while", "t": "source.coffee keyword.control.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1221,12 +1221,12 @@ "c": "for", "t": "source.coffee keyword.control.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1249,12 +1249,12 @@ "c": "in", "t": "source.coffee keyword.control.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1599,12 +1599,12 @@ "c": "for", "t": "source.coffee keyword.control.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1627,12 +1627,12 @@ "c": "in", "t": "source.coffee keyword.control.coffee", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cpp.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cpp.json index 7c95c4badca..652cc4824eb 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cpp.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cpp.json @@ -31,12 +31,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.include.cpp keyword.control.directive.include.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -45,12 +45,12 @@ "c": "include", "t": "source.cpp meta.preprocessor.include.cpp keyword.control.directive.include.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -115,12 +115,12 @@ "c": "using", "t": "source.cpp meta.using-namespace.cpp keyword.other.using.directive.cpp", "r": { - "dark_plus": "keyword.other.using: #CE92A4", + "dark_plus": "keyword.other.using: #C586C0", "light_plus": "keyword.other.using: #AF00DB", "dark_vs": "keyword: #569CD6", "light_vs": "keyword: #0000FF", "hc_black": "keyword.other.using: #C586C0", - "dark_modern": "keyword.other.using: #CE92A4", + "dark_modern": "keyword.other.using: #C586C0", "hc_light": "keyword.other.using: #B5200D", "light_modern": "keyword.other.using: #AF00DB" } @@ -199,12 +199,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -213,12 +213,12 @@ "c": "define", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -801,12 +801,12 @@ "c": "return", "t": "source.cpp meta.block.class.cpp meta.body.class.cpp meta.function.definition.cpp meta.body.function.definition.cpp keyword.control.return.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1347,12 +1347,12 @@ "c": "operator", "t": "source.cpp meta.function.definition.special.operator-overload.cpp meta.head.function.definition.special.operator-overload.cpp keyword.other.operator.overload.cpp", "r": { - "dark_plus": "keyword.other.operator: #CE92A4", + "dark_plus": "keyword.other.operator: #C586C0", "light_plus": "keyword.other.operator: #AF00DB", "dark_vs": "keyword: #569CD6", "light_vs": "keyword: #0000FF", "hc_black": "keyword.other.operator: #C586C0", - "dark_modern": "keyword.other.operator: #CE92A4", + "dark_modern": "keyword.other.operator: #C586C0", "hc_light": "keyword.other.operator: #B5200D", "light_modern": "keyword.other.operator: #AF00DB" } @@ -1501,12 +1501,12 @@ "c": "#", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp punctuation.definition.directive.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1515,12 +1515,12 @@ "c": "define", "t": "source.cpp meta.preprocessor.macro.cpp keyword.control.directive.define.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2775,12 +2775,12 @@ "c": "if", "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp keyword.control.if.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3027,12 +3027,12 @@ "c": "return", "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp keyword.control.return.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json index 293dfcbe2e7..7f4bbb7758b 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cs.json @@ -3,12 +3,12 @@ "c": "using", "t": "source.cs keyword.other.directive.using.cs", "r": { - "dark_plus": "keyword.other.directive.using: #CE92A4", + "dark_plus": "keyword.other.directive.using: #C586C0", "light_plus": "keyword.other.directive.using: #AF00DB", "dark_vs": "keyword: #569CD6", "light_vs": "keyword: #0000FF", "hc_black": "keyword.other.directive.using: #C586C0", - "dark_modern": "keyword.other.directive.using: #CE92A4", + "dark_modern": "keyword.other.directive.using: #C586C0", "hc_light": "keyword.other.directive.using: #B5200D", "light_modern": "keyword.other.directive.using: #AF00DB" } @@ -423,12 +423,12 @@ "c": "if", "t": "source.cs keyword.control.conditional.if.cs", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1487,12 +1487,12 @@ "c": "foreach", "t": "source.cs keyword.control.loop.foreach.cs", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1585,12 +1585,12 @@ "c": "in", "t": "source.cs keyword.control.loop.in.cs", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json index fbfec42db62..4307f03ed1e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cshtml.json @@ -3,12 +3,12 @@ "c": "@", "t": "text.html.cshtml meta.structure.razor.codeblock keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "{", "t": "text.html.cshtml meta.structure.razor.codeblock keyword.control.razor.directive.codeblock.open", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -311,12 +311,12 @@ "c": "@", "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.comment.razor keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -325,12 +325,12 @@ "c": "*", "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.comment.razor keyword.control.razor.comment.star", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -367,12 +367,12 @@ "c": "*", "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.comment.razor keyword.control.razor.comment.star", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -381,12 +381,12 @@ "c": "@", "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.comment.razor keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -409,12 +409,12 @@ "c": "if", "t": "text.html.cshtml meta.structure.razor.codeblock source.cs meta.statement.if.razor keyword.control.conditional.if.cs", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1473,12 +1473,12 @@ "c": "}", "t": "text.html.cshtml meta.structure.razor.codeblock keyword.control.razor.directive.codeblock.close", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3993,12 +3993,12 @@ "c": "@", "t": "text.html.cshtml meta.comment.razor keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4007,12 +4007,12 @@ "c": "*", "t": "text.html.cshtml meta.comment.razor keyword.control.razor.comment.star", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4049,12 +4049,12 @@ "c": "*", "t": "text.html.cshtml meta.comment.razor keyword.control.razor.comment.star", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4063,12 +4063,12 @@ "c": "@", "t": "text.html.cshtml meta.comment.razor keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4133,12 +4133,12 @@ "c": "@", "t": "text.html.cshtml meta.expression.implicit.cshtml keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4259,12 +4259,12 @@ "c": "@", "t": "text.html.cshtml meta.expression.explicit.cshtml keyword.control.cshtml.transition", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4273,12 +4273,12 @@ "c": "(", "t": "text.html.cshtml meta.expression.explicit.cshtml keyword.control.cshtml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4357,12 +4357,12 @@ "c": ")", "t": "text.html.cshtml meta.expression.explicit.cshtml keyword.control.cshtml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_css.json b/extensions/vscode-colorize-tests/test/colorize-results/test_css.json index 4bb1be19ba2..71722fd00db 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_css.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_css.json @@ -297,12 +297,12 @@ "c": "@", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css punctuation.definition.keyword.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -311,12 +311,12 @@ "c": "import", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -395,12 +395,12 @@ "c": "@", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css punctuation.definition.keyword.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -409,12 +409,12 @@ "c": "import", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -535,12 +535,12 @@ "c": "@", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css punctuation.definition.keyword.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -549,12 +549,12 @@ "c": "import", "t": "source.css meta.at-rule.import.css keyword.control.at-rule.import.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4917,12 +4917,12 @@ "c": "@", "t": "source.css meta.at-rule.header.css keyword.control.at-rule.css punctuation.definition.keyword.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4931,12 +4931,12 @@ "c": "property", "t": "source.css meta.at-rule.header.css keyword.control.at-rule.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json b/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json index 075453241e2..e926933337e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_cu.json @@ -3,12 +3,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "include", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -87,12 +87,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -101,12 +101,12 @@ "c": "include", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -171,12 +171,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -185,12 +185,12 @@ "c": "include", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -255,12 +255,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -269,12 +269,12 @@ "c": "include", "t": "source.cuda-cpp meta.preprocessor.include.cuda-cpp keyword.control.directive.include.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -339,12 +339,12 @@ "c": "#", "t": "source.cuda-cpp keyword.control.directive.conditional.if.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -353,12 +353,12 @@ "c": "if", "t": "source.cuda-cpp keyword.control.directive.conditional.if.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -381,12 +381,12 @@ "c": "defined", "t": "source.cuda-cpp meta.preprocessor.conditional.cuda-cpp keyword.control.directive.conditional.defined.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -437,12 +437,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.undef.cuda-cpp keyword.control.directive.undef.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -451,12 +451,12 @@ "c": "undef", "t": "source.cuda-cpp meta.preprocessor.undef.cuda-cpp keyword.control.directive.undef.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -493,12 +493,12 @@ "c": "#", "t": "source.cuda-cpp keyword.control.directive.endif.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -507,12 +507,12 @@ "c": "endif", "t": "source.cuda-cpp keyword.control.directive.endif.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -521,12 +521,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -535,12 +535,12 @@ "c": "define", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -661,12 +661,12 @@ "c": "do", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.do.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "if", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp keyword.control.if.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1571,12 +1571,12 @@ "c": "while", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.while.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1627,12 +1627,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1641,12 +1641,12 @@ "c": "define", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1767,12 +1767,12 @@ "c": "do", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.do.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1963,12 +1963,12 @@ "c": "if", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp meta.block.cpp keyword.control.if.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2873,12 +2873,12 @@ "c": "while", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.while.cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2929,12 +2929,12 @@ "c": "#", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp punctuation.definition.directive.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2943,12 +2943,12 @@ "c": "define", "t": "source.cuda-cpp meta.preprocessor.macro.cuda-cpp keyword.control.directive.define.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5799,12 +5799,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6345,12 +6345,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8221,12 +8221,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8991,12 +8991,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -9663,12 +9663,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13891,12 +13891,12 @@ "c": "for", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp keyword.control.for.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -14157,12 +14157,12 @@ "c": "if", "t": "source.cuda-cpp meta.function.definition.cuda-cpp meta.body.function.definition.cuda-cpp meta.block.cuda-cpp keyword.control.if.cuda-cpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json b/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json index 5f2fa73661c..dc43f74e3c8 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_dart.json @@ -129,12 +129,12 @@ "c": "async", "t": "source.dart keyword.control.dart", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_go.json b/extensions/vscode-colorize-tests/test/colorize-results/test_go.json index 7473403342c..d6b2ef38ebb 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_go.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_go.json @@ -45,12 +45,12 @@ "c": "import", "t": "source.go keyword.control.import.go", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -983,12 +983,12 @@ "c": "if", "t": "source.go keyword.control.go", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_groovy.json b/extensions/vscode-colorize-tests/test/colorize-results/test_groovy.json index 5bda16b6a10..2fb630ed130 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_groovy.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_groovy.json @@ -325,12 +325,12 @@ "c": "new", "t": "source.groovy keyword.control.new.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3153,12 +3153,12 @@ "c": "new", "t": "source.groovy meta.method-call.groovy keyword.control.new.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4665,12 +4665,12 @@ "c": "assert", "t": "source.groovy meta.declaration.assertion.groovy keyword.control.assert.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5827,12 +5827,12 @@ "c": "if", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5995,12 +5995,12 @@ "c": "else", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6023,12 +6023,12 @@ "c": "if", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6191,12 +6191,12 @@ "c": "else", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6737,12 +6737,12 @@ "c": "assert", "t": "source.groovy meta.declaration.assertion.groovy keyword.control.assert.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7409,12 +7409,12 @@ "c": "for", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7703,12 +7703,12 @@ "c": "for", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8207,12 +8207,12 @@ "c": "for", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8879,12 +8879,12 @@ "c": "for", "t": "source.groovy keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12617,12 +12617,12 @@ "c": "return", "t": "source.groovy meta.definition.method.groovy meta.method.body.java keyword.control.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13107,12 +13107,12 @@ "c": "assert", "t": "source.groovy meta.declaration.assertion.groovy keyword.control.assert.groovy", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_handlebars.json b/extensions/vscode-colorize-tests/test/colorize-results/test_handlebars.json index 82ca1b1508d..2ee4ccd1f3b 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_handlebars.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_handlebars.json @@ -297,12 +297,12 @@ "c": "#if", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -577,12 +577,12 @@ "c": "else", "t": "text.html.handlebars meta.function.inline.else.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "/if", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -885,12 +885,12 @@ "c": "#unless", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1151,12 +1151,12 @@ "c": "/unless", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1389,12 +1389,12 @@ "c": "#each", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1613,12 +1613,12 @@ "c": "/each", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1991,12 +1991,12 @@ "c": "#each", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2621,12 +2621,12 @@ "c": "/each", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_hbs.json b/extensions/vscode-colorize-tests/test/colorize-results/test_hbs.json index dd42bf5eb97..b79247facaa 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_hbs.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_hbs.json @@ -255,12 +255,12 @@ "c": "#each", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -885,12 +885,12 @@ "c": "/each", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1389,12 +1389,12 @@ "c": "#if", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1669,12 +1669,12 @@ "c": "/if", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2173,12 +2173,12 @@ "c": "#each", "t": "text.html.handlebars meta.function.block.start.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2425,12 +2425,12 @@ "c": "/each", "t": "text.html.handlebars meta.function.block.end.handlebars support.constant.handlebars keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_hlsl.json b/extensions/vscode-colorize-tests/test/colorize-results/test_hlsl.json index 481bb17d78a..5325987d5da 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_hlsl.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_hlsl.json @@ -311,12 +311,12 @@ "c": "return", "t": "source.hlsl keyword.control.hlsl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json b/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json index 8d8bb3c3dc0..deb5a66740a 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_jl.json @@ -143,12 +143,12 @@ "c": "end", "t": "source.julia keyword.control.end.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2103,12 +2103,12 @@ "c": "return", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2145,12 +2145,12 @@ "c": "for", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2257,12 +2257,12 @@ "c": "for", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2495,12 +2495,12 @@ "c": "if", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3083,12 +3083,12 @@ "c": "return", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3125,12 +3125,12 @@ "c": "end", "t": "source.julia keyword.control.end.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3153,12 +3153,12 @@ "c": "end", "t": "source.julia keyword.control.end.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3181,12 +3181,12 @@ "c": "end", "t": "source.julia keyword.control.end.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3209,12 +3209,12 @@ "c": "return", "t": "source.julia keyword.control.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3251,12 +3251,12 @@ "c": "end", "t": "source.julia keyword.control.end.julia", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_js.json b/extensions/vscode-colorize-tests/test/colorize-results/test_js.json index 5a3e62e3ac9..c4bb55a1e22 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_js.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_js.json @@ -1669,12 +1669,12 @@ "c": "return", "t": "source.js meta.function.expression.js meta.block.js keyword.control.flow.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2887,12 +2887,12 @@ "c": "return", "t": "source.js meta.function.expression.js meta.block.js keyword.control.flow.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4119,12 +4119,12 @@ "c": "for", "t": "source.js meta.function.js meta.block.js keyword.control.loop.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4847,12 +4847,12 @@ "c": "return", "t": "source.js meta.function.js meta.block.js keyword.control.flow.js", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_jsx.json b/extensions/vscode-colorize-tests/test/colorize-results/test_jsx.json index c1afc2165a9..ed8845d9816 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_jsx.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_jsx.json @@ -311,12 +311,12 @@ "c": "return", "t": "source.js.jsx meta.var.expr.js.jsx meta.objectliteral.js.jsx meta.object.member.js.jsx meta.function.expression.js.jsx meta.block.js.jsx keyword.control.flow.js.jsx", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1585,12 +1585,12 @@ "c": "if", "t": "source.js.jsx meta.var.expr.js.jsx meta.objectliteral.js.jsx meta.object.member.js.jsx meta.function.expression.js.jsx meta.block.js.jsx keyword.control.conditional.js.jsx", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1949,12 +1949,12 @@ "c": "return", "t": "source.js.jsx meta.var.expr.js.jsx meta.objectliteral.js.jsx meta.object.member.js.jsx meta.function.expression.js.jsx meta.block.js.jsx keyword.control.flow.js.jsx", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json index 073c538de82..a66224dd9e6 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_less.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_less.json @@ -3,12 +3,12 @@ "c": "@", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less punctuation.definition.keyword.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "import", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -101,12 +101,12 @@ "c": "@", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less punctuation.definition.keyword.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -115,12 +115,12 @@ "c": "import", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -241,12 +241,12 @@ "c": "@", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less punctuation.definition.keyword.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -255,12 +255,12 @@ "c": "import", "t": "source.css.less meta.at-rule.import.less keyword.control.at-rule.import.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -703,12 +703,12 @@ "c": "when", "t": "source.css.less meta.selector.less meta.conditional.guarded-namespace.less keyword.control.conditional.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1221,12 +1221,12 @@ "c": "when", "t": "source.css.less meta.selector.less meta.conditional.guarded-namespace.less keyword.control.conditional.less", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json b/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json index e56e3ebedc2..c7a93a0446f 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_lua.json @@ -59,12 +59,12 @@ "c": "function", "t": "source.lua meta.function.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -171,12 +171,12 @@ "c": "if", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -283,12 +283,12 @@ "c": "then", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -311,12 +311,12 @@ "c": "return", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -367,12 +367,12 @@ "c": "else", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -395,12 +395,12 @@ "c": "return", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -577,12 +577,12 @@ "c": "end", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -605,12 +605,12 @@ "c": "end", "t": "source.lua keyword.control.lua", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_m.json b/extensions/vscode-colorize-tests/test/colorize-results/test_m.json index 0c0e4587fd6..c6c7e2d6278 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_m.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_m.json @@ -59,12 +59,12 @@ "c": "#", "t": "source.objc meta.preprocessor.include.objc keyword.control.directive.import.objc punctuation.definition.directive.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -73,12 +73,12 @@ "c": "import", "t": "source.objc meta.preprocessor.include.objc keyword.control.directive.import.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -143,12 +143,12 @@ "c": "#", "t": "source.objc meta.preprocessor.include.objc keyword.control.directive.import.objc punctuation.definition.directive.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -157,12 +157,12 @@ "c": "import", "t": "source.objc meta.preprocessor.include.objc keyword.control.directive.import.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1809,12 +1809,12 @@ "c": "if", "t": "source.objc meta.implementation.objc meta.scope.implementation.objc meta.function-with-body.objc meta.block.objc meta.bracketed.objc meta.function-call.objc meta.block.objc keyword.control.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2159,12 +2159,12 @@ "c": "return", "t": "source.objc meta.implementation.objc meta.scope.implementation.objc meta.function-with-body.objc meta.block.objc keyword.control.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4021,12 +4021,12 @@ "c": "return", "t": "source.objc meta.implementation.objc meta.scope.implementation.objc meta.function-with-body.objc meta.block.objc keyword.control.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4077,12 +4077,12 @@ "c": "return", "t": "source.objc meta.implementation.objc meta.scope.implementation.objc meta.function-with-body.objc meta.block.objc keyword.control.objc", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_mm.json b/extensions/vscode-colorize-tests/test/colorize-results/test_mm.json index da24fb8688d..eeb16d5b737 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_mm.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_mm.json @@ -59,12 +59,12 @@ "c": "#", "t": "source.objcpp meta.preprocessor.include.objcpp keyword.control.directive.import.objcpp punctuation.definition.directive.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -73,12 +73,12 @@ "c": "import", "t": "source.objcpp meta.preprocessor.include.objcpp keyword.control.directive.import.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -143,12 +143,12 @@ "c": "#", "t": "source.objcpp meta.preprocessor.include.objcpp keyword.control.directive.import.objcpp punctuation.definition.directive.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -157,12 +157,12 @@ "c": "import", "t": "source.objcpp meta.preprocessor.include.objcpp keyword.control.directive.import.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1697,12 +1697,12 @@ "c": "if", "t": "source.objcpp meta.implementation.objcpp meta.scope.implementation.objcpp meta.function-with-body.objcpp meta.block.objcpp meta.bracket.square.access.objcpp keyword.control.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2075,12 +2075,12 @@ "c": "return", "t": "source.objcpp meta.implementation.objcpp meta.scope.implementation.objcpp meta.function-with-body.objcpp meta.block.objcpp keyword.control.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3811,12 +3811,12 @@ "c": "return", "t": "source.objcpp meta.implementation.objcpp meta.scope.implementation.objcpp meta.function-with-body.objcpp meta.block.objcpp keyword.control.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3867,12 +3867,12 @@ "c": "return", "t": "source.objcpp meta.implementation.objcpp meta.scope.implementation.objcpp meta.function-with-body.objcpp meta.block.objcpp keyword.control.objcpp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_php.json b/extensions/vscode-colorize-tests/test/colorize-results/test_php.json index 200544164a5..ea79b0e2006 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_php.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_php.json @@ -1347,12 +1347,12 @@ "c": "for", "t": "text.html.php meta.embedded.block.php source.php keyword.control.for.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2075,12 +2075,12 @@ "c": "if", "t": "text.html.php meta.embedded.block.php source.php keyword.control.if.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2411,12 +2411,12 @@ "c": "else", "t": "text.html.php meta.embedded.block.php source.php keyword.control.else.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3419,12 +3419,12 @@ "c": "for", "t": "text.html.php meta.embedded.block.php source.php keyword.control.for.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3783,12 +3783,12 @@ "c": "if", "t": "text.html.php meta.embedded.block.php source.php keyword.control.if.php", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_pl.json b/extensions/vscode-colorize-tests/test/colorize-results/test_pl.json index b10e49c8246..1a80edc135c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_pl.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_pl.json @@ -3,12 +3,12 @@ "c": "use", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -395,12 +395,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -689,12 +689,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1613,12 +1613,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1907,12 +1907,12 @@ "c": "return", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2061,12 +2061,12 @@ "c": "while", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2523,12 +2523,12 @@ "c": "while", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2775,12 +2775,12 @@ "c": "if", "t": "source.perl keyword.control.perl", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json b/extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json index b919dfdabda..7fee15950e9 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_ps1.json @@ -143,12 +143,12 @@ "c": "try", "t": "source.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -535,12 +535,12 @@ "c": "return", "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "catch", "t": "source.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -801,12 +801,12 @@ "c": "throw", "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1081,12 +1081,12 @@ "c": "param", "t": "source.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1361,12 +1361,12 @@ "c": "foreach", "t": "source.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1431,12 +1431,12 @@ "c": "in", "t": "source.powershell meta.scriptblock.powershell meta.group.simple.subexpression.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1613,12 +1613,12 @@ "c": "if", "t": "source.powershell meta.scriptblock.powershell meta.scriptblock.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2285,12 +2285,12 @@ "c": "if", "t": "source.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2817,12 +2817,12 @@ "c": "if", "t": "source.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3167,12 +3167,12 @@ "c": "else", "t": "source.powershell keyword.control.powershell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_py.json b/extensions/vscode-colorize-tests/test/colorize-results/test_py.json index 28d7010a42b..e8d718cad72 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_py.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_py.json @@ -3,12 +3,12 @@ "c": "from", "t": "source.python keyword.control.import.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -31,12 +31,12 @@ "c": "import", "t": "source.python keyword.control.import.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -759,12 +759,12 @@ "c": "return", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1109,12 +1109,12 @@ "c": "for", "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1165,12 +1165,12 @@ "c": "in", "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1221,12 +1221,12 @@ "c": "if", "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1333,12 +1333,12 @@ "c": "else", "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1501,12 +1501,12 @@ "c": "pass", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1515,12 +1515,12 @@ "c": "pass", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1683,12 +1683,12 @@ "c": "for", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1711,12 +1711,12 @@ "c": "in", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1823,12 +1823,12 @@ "c": "yield", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3237,12 +3237,12 @@ "c": "if", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3335,12 +3335,12 @@ "c": "return", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3391,12 +3391,12 @@ "c": "elif", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3629,12 +3629,12 @@ "c": "return", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3825,12 +3825,12 @@ "c": "else", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3867,12 +3867,12 @@ "c": "return", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5127,12 +5127,12 @@ "c": "if", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5519,12 +5519,12 @@ "c": "return", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6093,12 +6093,12 @@ "c": "while", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6163,12 +6163,12 @@ "c": "try", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6429,12 +6429,12 @@ "c": "break", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6457,12 +6457,12 @@ "c": "except", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6611,12 +6611,12 @@ "c": "async", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6639,12 +6639,12 @@ "c": "with", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6695,12 +6695,12 @@ "c": "as", "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7479,12 +7479,12 @@ "c": ">>> ", "t": "source.python string.quoted.docstring.raw.multi.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7521,12 +7521,12 @@ "c": "... ", "t": "source.python string.quoted.docstring.raw.multi.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7563,12 +7563,12 @@ "c": "... ", "t": "source.python string.quoted.docstring.raw.multi.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_r.json b/extensions/vscode-colorize-tests/test/colorize-results/test_r.json index 17fbf383edd..4d56b651660 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_r.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_r.json @@ -451,12 +451,12 @@ "c": "function", "t": "source.r meta.function.r keyword.control.r", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json b/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json index b93bf251b67..032617573e4 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_rb.json @@ -115,12 +115,12 @@ "c": "module", "t": "source.ruby meta.module.ruby keyword.control.module.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -297,12 +297,12 @@ "c": "class", "t": "source.ruby meta.class.ruby keyword.control.class.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1375,12 +1375,12 @@ "c": "def", "t": "source.ruby meta.function.method.with-arguments.ruby keyword.control.def.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1669,12 +1669,12 @@ "c": "super", "t": "source.ruby keyword.control.pseudo-method.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2047,12 +2047,12 @@ "c": "if", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2243,12 +2243,12 @@ "c": "unless", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3125,12 +3125,12 @@ "c": "if", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3433,12 +3433,12 @@ "c": "else", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3573,12 +3573,12 @@ "c": "end", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4091,12 +4091,12 @@ "c": "end", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4119,12 +4119,12 @@ "c": "end", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4133,12 +4133,12 @@ "c": "end", "t": "source.ruby keyword.control.ruby", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json index 825becadb30..aa8468c4d8a 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_rst.json @@ -87,12 +87,12 @@ "c": "1. ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -115,12 +115,12 @@ "c": "2. ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -143,12 +143,12 @@ "c": " - ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -171,12 +171,12 @@ "c": " - ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -199,12 +199,12 @@ "c": "3. ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -269,12 +269,12 @@ "c": "::", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -479,12 +479,12 @@ "c": "| ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -507,12 +507,12 @@ "c": "| ", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -535,12 +535,12 @@ "c": "+-------------+--------------+", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -549,12 +549,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -577,12 +577,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -605,12 +605,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -619,12 +619,12 @@ "c": "+=============+==============+", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -633,12 +633,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -661,12 +661,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -689,12 +689,12 @@ "c": "|", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -703,12 +703,12 @@ "c": "+-------------+--------------+", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -717,12 +717,12 @@ "c": "============ ============", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -745,12 +745,12 @@ "c": "============ ============", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -773,12 +773,12 @@ "c": "============ ============", "t": "source.rst keyword.control.table", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -829,12 +829,12 @@ "c": ">>>", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1221,12 +1221,12 @@ "c": ".. image::", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1277,12 +1277,12 @@ "c": ":sub:", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1305,12 +1305,12 @@ "c": ":sup:", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1431,12 +1431,12 @@ "c": "..", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1487,12 +1487,12 @@ "c": "replace::", "t": "source.rst keyword.control", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json b/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json index dfae042fd1b..933ebf2ba5c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_scss.json @@ -115,12 +115,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.charset.scss keyword.control.at-rule.charset.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -129,12 +129,12 @@ "c": "charset", "t": "source.css.scss meta.at-rule.charset.scss keyword.control.at-rule.charset.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7059,12 +7059,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.function.scss keyword.control.at-rule.function.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7073,12 +7073,12 @@ "c": "function", "t": "source.css.scss meta.at-rule.function.scss keyword.control.at-rule.function.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7199,12 +7199,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7213,12 +7213,12 @@ "c": "return", "t": "source.css.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7787,12 +7787,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7801,12 +7801,12 @@ "c": "import", "t": "source.css.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8025,12 +8025,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8039,12 +8039,12 @@ "c": "import", "t": "source.css.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8333,12 +8333,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8347,12 +8347,12 @@ "c": "import", "t": "source.css.scss meta.property-list.scss meta.at-rule.import.scss keyword.control.at-rule.import.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8655,12 +8655,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.media.scss keyword.control.at-rule.media.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8669,12 +8669,12 @@ "c": "media", "t": "source.css.scss meta.property-list.scss meta.at-rule.media.scss keyword.control.at-rule.media.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -9425,12 +9425,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -9439,12 +9439,12 @@ "c": "extend", "t": "source.css.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10069,12 +10069,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10083,12 +10083,12 @@ "c": "extend", "t": "source.css.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10237,12 +10237,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.warn.scss keyword.control.warn.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10251,12 +10251,12 @@ "c": "debug", "t": "source.css.scss meta.at-rule.warn.scss keyword.control.warn.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10293,12 +10293,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10307,12 +10307,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10461,12 +10461,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10475,12 +10475,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10601,12 +10601,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.property-list.scss meta.at-rule.warn.scss keyword.control.warn.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10615,12 +10615,12 @@ "c": "warn", "t": "source.css.scss meta.property-list.scss meta.property-list.scss meta.at-rule.warn.scss keyword.control.warn.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10951,12 +10951,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10965,12 +10965,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -11091,12 +11091,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.property-list.scss meta.at-rule.warn.scss keyword.control.warn.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -11105,12 +11105,12 @@ "c": "warn", "t": "source.css.scss meta.property-list.scss meta.property-list.scss meta.at-rule.warn.scss keyword.control.warn.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -11833,12 +11833,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -11847,12 +11847,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12197,12 +12197,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12211,12 +12211,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12505,12 +12505,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12519,12 +12519,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12883,12 +12883,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12897,12 +12897,12 @@ "c": "if", "t": "source.css.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13121,12 +13121,12 @@ "c": "@", "t": "source.css.scss meta.property-list.scss meta.at-rule.else.scss keyword.control.else.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13135,12 +13135,12 @@ "c": "else ", "t": "source.css.scss meta.property-list.scss meta.at-rule.else.scss keyword.control.else.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13331,12 +13331,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.for.scss keyword.control.for.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13345,12 +13345,12 @@ "c": "for", "t": "source.css.scss meta.at-rule.for.scss keyword.control.for.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13401,12 +13401,12 @@ "c": "from", "t": "source.css.scss meta.at-rule.for.scss keyword.control.operator", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13457,12 +13457,12 @@ "c": "through", "t": "source.css.scss meta.at-rule.for.scss keyword.control.operator", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13877,12 +13877,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss keyword.control.each.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13891,12 +13891,12 @@ "c": "each", "t": "source.css.scss meta.at-rule.each.scss keyword.control.each.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -13947,12 +13947,12 @@ "c": "in", "t": "source.css.scss meta.at-rule.each.scss keyword.control.operator", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -14479,12 +14479,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss keyword.control.while.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -14493,12 +14493,12 @@ "c": "while", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss keyword.control.while.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15109,12 +15109,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.function.scss keyword.control.at-rule.function.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15123,12 +15123,12 @@ "c": "function", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.function.scss keyword.control.at-rule.function.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15291,12 +15291,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.for.scss keyword.control.for.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15305,12 +15305,12 @@ "c": "for", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.for.scss keyword.control.for.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15361,12 +15361,12 @@ "c": "from", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.for.scss keyword.control.operator", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15417,12 +15417,12 @@ "c": "to", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.for.scss keyword.control.operator", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15501,12 +15501,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -15515,12 +15515,12 @@ "c": "if", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16005,12 +16005,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16019,12 +16019,12 @@ "c": "return", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16173,12 +16173,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16187,12 +16187,12 @@ "c": "return", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.return.scss keyword.control.return.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16299,12 +16299,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16313,12 +16313,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16915,12 +16915,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -16929,12 +16929,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17139,12 +17139,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17153,12 +17153,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17755,12 +17755,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17769,12 +17769,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17937,12 +17937,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17951,12 +17951,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -18413,12 +18413,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -18427,12 +18427,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -18889,12 +18889,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -18903,12 +18903,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19561,12 +19561,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19575,12 +19575,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19743,12 +19743,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19757,12 +19757,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19925,12 +19925,12 @@ "c": "@content", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.content.scss keyword.control.content.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -19995,12 +19995,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20009,12 +20009,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20331,12 +20331,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.if.scss keyword.control.if.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20345,12 +20345,12 @@ "c": "if", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.if.scss keyword.control.if.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20429,12 +20429,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20443,12 +20443,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20919,12 +20919,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.page.scss keyword.control.at-rule.page.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -20933,12 +20933,12 @@ "c": "page", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.at-rule.page.scss keyword.control.at-rule.page.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -21787,12 +21787,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -21801,12 +21801,12 @@ "c": "extend", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22473,12 +22473,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22487,12 +22487,12 @@ "c": "mixin", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.mixin.scss keyword.control.at-rule.mixin.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22641,12 +22641,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22655,12 +22655,12 @@ "c": "extend", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22767,12 +22767,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -22781,12 +22781,12 @@ "c": "extend", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.extend.scss keyword.control.at-rule.extend.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -23243,12 +23243,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -23257,12 +23257,12 @@ "c": "include", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.property-list.scss meta.at-rule.include.scss keyword.control.at-rule.include.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -23439,12 +23439,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.fontface.scss keyword.control.at-rule.fontface.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -23453,12 +23453,12 @@ "c": "font-face", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.fontface.scss keyword.control.at-rule.fontface.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -24531,12 +24531,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -24545,12 +24545,12 @@ "c": "-webkit-keyframes", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -24937,12 +24937,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -24951,12 +24951,12 @@ "c": "-moz-keyframes", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -25721,12 +25721,12 @@ "c": "@", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss punctuation.definition.keyword.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -25735,12 +25735,12 @@ "c": "keyframes", "t": "source.css.scss meta.at-rule.each.scss meta.at-rule.while.scss meta.property-list.scss meta.at-rule.keyframes.scss keyword.control.at-rule.keyframes.scss", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json index fc06d9f4d9a..0dde2e0748e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_sh.json @@ -31,12 +31,12 @@ "c": "if", "t": "source.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -297,12 +297,12 @@ "c": "then", "t": "source.shell meta.scope.if-block.shell keyword.control.then.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1235,12 +1235,12 @@ "c": "else", "t": "source.shell meta.scope.if-block.shell keyword.control.else.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1529,12 +1529,12 @@ "c": "fi", "t": "source.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2607,12 +2607,12 @@ "c": "if", "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.if.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2873,12 +2873,12 @@ "c": "then", "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.then.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3055,12 +3055,12 @@ "c": "else", "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.else.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3237,12 +3237,12 @@ "c": "fi", "t": "source.shell meta.function.shell meta.function.body.shell meta.scope.if-block.shell keyword.control.fi.shell", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3345,4 +3345,4 @@ "light_modern": "string: #A31515" } } -] \ No newline at end of file +] diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_swift.json b/extensions/vscode-colorize-tests/test/colorize-results/test_swift.json index c84a0185f31..8b1b64f71f5 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_swift.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_swift.json @@ -647,12 +647,12 @@ "c": "for", "t": "source.swift meta.definition.function.swift meta.definition.function.body.swift keyword.control.loop.swift", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -675,12 +675,12 @@ "c": "in", "t": "source.swift meta.definition.function.swift meta.definition.function.body.swift keyword.control.loop.swift", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -731,12 +731,12 @@ "c": "if", "t": "source.swift meta.definition.function.swift meta.definition.function.body.swift keyword.control.branch.swift", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -857,12 +857,12 @@ "c": "return", "t": "source.swift meta.definition.function.swift meta.definition.function.body.swift keyword.control.transfer.swift", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -969,12 +969,12 @@ "c": "return", "t": "source.swift meta.definition.function.swift meta.definition.function.body.swift keyword.control.transfer.swift", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_tex.json b/extensions/vscode-colorize-tests/test/colorize-results/test_tex.json index f2b21f30a78..7cc786089fd 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_tex.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_tex.json @@ -3,12 +3,12 @@ "c": "\\", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex punctuation.definition.function.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -17,12 +17,12 @@ "c": "documentclass", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -115,12 +115,12 @@ "c": "\\", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex punctuation.definition.function.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -129,12 +129,12 @@ "c": "usepackage", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -185,12 +185,12 @@ "c": "\\", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex punctuation.definition.function.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -199,12 +199,12 @@ "c": "usepackage", "t": "text.tex.latex meta.preamble.latex keyword.control.preamble.latex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -507,12 +507,12 @@ "c": "\\\\", "t": "text.tex.latex keyword.control.newline.tex", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json b/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json index c87cf0f039c..68504d08d8e 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_ts.json @@ -171,12 +171,12 @@ "c": "export", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1347,12 +1347,12 @@ "c": "export", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts keyword.control.export.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4021,12 +4021,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4539,12 +4539,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5533,12 +5533,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.arrow.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6807,12 +6807,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7171,12 +7171,12 @@ "c": "else", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7199,12 +7199,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7451,12 +7451,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7857,12 +7857,12 @@ "c": "for", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8193,12 +8193,12 @@ "c": "for", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.block.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8543,12 +8543,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.block.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8781,12 +8781,12 @@ "c": "continue", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.block.ts meta.block.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8823,12 +8823,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.block.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -9327,12 +9327,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -9705,12 +9705,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10223,12 +10223,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10293,12 +10293,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -10769,12 +10769,12 @@ "c": "for", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -11259,12 +11259,12 @@ "c": "for", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts meta.block.ts keyword.control.loop.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12085,12 +12085,12 @@ "c": "return", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.flow.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -12365,12 +12365,12 @@ "c": "if", "t": "source.ts meta.namespace.declaration.ts meta.block.ts meta.class.ts meta.method.declaration.ts meta.block.ts keyword.control.conditional.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_vb.json b/extensions/vscode-colorize-tests/test/colorize-results/test_vb.json index 6706deb203d..2d8b7a5fbf2 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_vb.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_vb.json @@ -1137,12 +1137,12 @@ "c": "Do", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1165,12 +1165,12 @@ "c": "While", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1585,12 +1585,12 @@ "c": "If", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1795,12 +1795,12 @@ "c": "Then", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2369,12 +2369,12 @@ "c": "If", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2397,12 +2397,12 @@ "c": "Then", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2425,12 +2425,12 @@ "c": "Exit Sub", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2691,12 +2691,12 @@ "c": "End If", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2747,12 +2747,12 @@ "c": "Loop", "t": "source.asp.vb.net keyword.control.asp", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json index 6ca362c7a18..0908e19e3ea 100644 --- a/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json +++ b/extensions/vscode-colorize-tests/test/colorize-results/test_yaml.json @@ -115,12 +115,12 @@ "c": "&", "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.anchor.yaml punctuation.definition.anchor.yaml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -507,12 +507,12 @@ "c": "*", "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -647,12 +647,12 @@ "c": "*", "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -829,12 +829,12 @@ "c": "*", "t": "source.yaml meta.stream.yaml meta.document.yaml meta.block.sequence.yaml meta.mapping.yaml meta.map.value.yaml keyword.control.flow.alias.yaml punctuation.definition.alias.yaml", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json index 2c4294c18a9..a5a8c33bf22 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-241001_ts.json @@ -577,12 +577,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2047,12 +2047,12 @@ "c": "import", "t": "new.expr.ts keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json index fef5b2f06f4..5e8481b6289 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue11_ts.json @@ -73,12 +73,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -367,12 +367,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -591,12 +591,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -815,12 +815,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1039,12 +1039,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1179,12 +1179,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2299,12 +2299,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json index 879e9e26c2c..c7c13ff004c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue241715_ts.json @@ -409,12 +409,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -703,12 +703,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1137,12 +1137,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1459,12 +1459,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1529,12 +1529,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -1711,12 +1711,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2313,12 +2313,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2579,12 +2579,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json index 067fee14bc1..2a122f89471 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5431_ts.json @@ -409,12 +409,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json index 1cfeec33c2e..100bf5c3d07 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-issue5465_ts.json @@ -87,12 +87,12 @@ "c": "yield", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -157,12 +157,12 @@ "c": "yield", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json index c6617c9f426..6753ae3fa4c 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test-keywords_ts.json @@ -3,12 +3,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_css.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_css.json index e7bf6aacd02..98adba8bf54 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_css.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_css.json @@ -101,12 +101,12 @@ "c": "@import", "t": "keyword.control.at-rule.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -125,6 +125,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "mystyle.css", + "t": "string.quoted.double.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "\"", "t": "string.quoted.double.css", @@ -157,12 +171,12 @@ "c": "@import", "t": "keyword.control.at-rule.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -209,6 +223,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "mystyle.css", + "t": "string.quoted.double.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "\"", "t": "string.quoted.double.css", @@ -255,12 +283,12 @@ "c": "@import", "t": "keyword.control.at-rule.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -307,6 +335,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "bluish.css", + "t": "string.quoted.double.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "\"", "t": "string.quoted.double.css", @@ -3307,12 +3349,12 @@ "c": "@property", "t": "keyword.control.at-rule.css", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3387,6 +3429,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "", + "t": "string.quoted.single.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "'", "t": "string.quoted.single.css", @@ -7307,6 +7363,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "#B3AE94", + "t": "string.quoted.single.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "'", "t": "string.quoted.single.css", @@ -8413,6 +8483,20 @@ "light_modern": "string: #A31515" } }, + { + "c": "codicon-", + "t": "meta.selector.css string.quoted.single.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178", + "dark_modern": "string: #CE9178", + "hc_light": "string: #0F4A85", + "light_modern": "string: #A31515" + } + }, { "c": "'", "t": "meta.selector.css string.quoted.single.css", diff --git a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json index d4c0361052c..b31e6daa154 100644 --- a/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json +++ b/extensions/vscode-colorize-tests/test/colorize-tree-sitter-results/test_ts.json @@ -59,12 +59,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -801,12 +801,12 @@ "c": "export", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2579,12 +2579,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -2943,12 +2943,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -3671,12 +3671,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4609,12 +4609,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4833,12 +4833,12 @@ "c": "else", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -4847,12 +4847,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5015,12 +5015,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5253,12 +5253,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5477,12 +5477,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5701,12 +5701,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5841,12 +5841,12 @@ "c": "continue", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -5869,12 +5869,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6233,12 +6233,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6457,12 +6457,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6765,12 +6765,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -6807,12 +6807,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7171,12 +7171,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -7493,12 +7493,12 @@ "c": "for", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8109,12 +8109,12 @@ "c": "return", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } @@ -8277,12 +8277,12 @@ "c": "if", "t": "keyword.control.ts", "r": { - "dark_plus": "keyword.control: #CE92A4", + "dark_plus": "keyword.control: #C586C0", "light_plus": "keyword.control: #AF00DB", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0", - "dark_modern": "keyword.control: #CE92A4", + "dark_modern": "keyword.control: #C586C0", "hc_light": "keyword.control: #B5200D", "light_modern": "keyword.control: #AF00DB" } diff --git a/extensions/vscode-colorize-tests/tsconfig.json b/extensions/vscode-colorize-tests/tsconfig.json index 20f72eae51d..431bc8ed65c 100644 --- a/extensions/vscode-colorize-tests/tsconfig.json +++ b/extensions/vscode-colorize-tests/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/extensions/vscode-test-resolver/package.json b/extensions/vscode-test-resolver/package.json index c96c1d5894f..0990d7c5036 100644 --- a/extensions/vscode-test-resolver/package.json +++ b/extensions/vscode-test-resolver/package.json @@ -18,7 +18,7 @@ ], "scripts": { "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-test-resolver" + "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.mjs compile-extension:vscode-test-resolver" }, "activationEvents": [ "onResolveRemoteAuthority:test", diff --git a/extensions/vscode-test-resolver/tsconfig.json b/extensions/vscode-test-resolver/tsconfig.json index 087654f228a..e5e48c3201d 100644 --- a/extensions/vscode-test-resolver/tsconfig.json +++ b/extensions/vscode-test-resolver/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./out", "typeRoots": [ "./node_modules/@types" diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 4dce0234239..00000000000 --- a/gulpfile.js +++ /dev/null @@ -1,9 +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 { createRequire } from 'node:module'; - -const require = createRequire(import.meta.url); -require('./build/gulpfile'); diff --git a/gulpfile.mjs b/gulpfile.mjs new file mode 100644 index 00000000000..5acdbee578a --- /dev/null +++ b/gulpfile.mjs @@ -0,0 +1,5 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import './build/gulpfile.ts'; diff --git a/package-lock.json b/package-lock.json index 327f538cf35..3172c2028d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,90 +1,69 @@ { "name": "code-oss-dev", - "version": "1.106.0", + "version": "1.110.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-oss-dev", - "version": "1.106.0", + "version": "1.110.0", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.40.0", - "@c4312/eventsource-umd": "^3.0.5", - "@floating-ui/react": "^0.27.8", - "@google/genai": "^0.13.0", + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.6.0", - "@modelcontextprotocol/sdk": "^1.11.2", - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", + "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", + "@vscode/codicons": "^0.0.45-4", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/native-watchdog": "^1.4.6", + "@vscode/policy-watcher": "^1.3.2", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.8-vscode", - "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/spdlog": "^0.15.7", + "@vscode/sqlite3": "5.1.12-vscode", + "@vscode/sudo-prompt": "9.3.2", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@vscodium/policy-watcher": "^1.3.0-2503300035", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/headless": "^5.6.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", - "ajv": "^8.17.1", - "cross-spawn": "^7.0.6", - "diff": "^7.0.0", - "eslint-plugin-react": "^7.37.5", - "google-auth-library": "^9.15.1", - "groq-sdk": "^0.20.1", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/headless": "^6.1.0-beta.109", + "@xterm/xterm": "^6.1.0-beta.109", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", "katex": "^0.16.22", "kerberos": "2.1.1", - "lucide-react": "^0.503.0", - "marked": "^15.0.11", "minimist": "^1.2.8", - "native-is-elevated": "0.7.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", - "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta35", - "ollama": "^0.5.15", + "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", - "openai": "^4.96.0", - "pdfjs-dist": "^5.4.394", - "posthog-node": "^4.14.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-tooltip": "^5.28.1", - "tas-client-umd": "0.2.0", - "undici": "^7.9.0", + "tas-client": "0.3.1", + "undici": "^7.18.2", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "^9.2.1", + "vscode-textmate": "^9.3.2", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, "devDependencies": { "@playwright/test": "^1.56.1", "@stylistic/eslint-plugin-ts": "^2.8.0", - "@tailwindcss/typography": "^0.5.16", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", - "@types/diff": "^7.0.2", "@types/eslint": "^9.6.1", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", @@ -92,8 +71,6 @@ "@types/minimist": "^1.2.1", "@types/mocha": "^10.0.10", "@types/node": "^22.18.10", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", @@ -105,31 +82,29 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20250812.1", + "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/gulp-electron": "^1.38.2", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", "@vscode/test-electron": "^2.4.0", - "@vscode/test-web": "^0.0.62", + "@vscode/test-web": "^0.0.76", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", - "@webgpu/types": "^0.1.44", + "@webgpu/types": "^0.1.66", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", - "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "37.7.0", + "electron": "39.3.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^50.3.1", - "eslint-plugin-local": "^6.0.0", "event-stream": "3.3.4", "fancy-log": "^1.3.3", "file-loader": "^6.2.0", @@ -148,7 +123,6 @@ "gulp-replace": "^0.5.4", "gulp-sourcemaps": "^3.0.0", "gulp-svgmin": "^4.1.0", - "gulp-untar": "^0.0.7", "husky": "^0.13.1", "innosetup": "^6.4.1", "istanbul-lib-coverage": "^3.2.0", @@ -163,93 +137,77 @@ "mocha": "^10.8.2", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", - "next": "^15.3.1", - "nodemon": "^3.1.10", - "npm-run-all": "^4.1.5", - "original-fs": "^1.2.0", + "npm-run-all2": "^8.0.4", "os-browserify": "^0.3.0", "p-all": "^1.0.0", "path-browserify": "^1.0.1", - "postcss": "^8.4.33", - "postcss-nesting": "^12.1.5", "pump": "^1.0.1", "rcedit": "^1.1.0", "rimraf": "^2.2.8", - "scope-tailwind": "^1.0.9", "sinon": "^12.0.1", "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", - "tailwindcss": "^3.4.17", + "tar": "^7.5.7", "ts-loader": "^9.5.1", - "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "tsup": "^8.4.0", - "typescript": "^6.0.0-dev.20250922", + "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", - "yaserver": "^0.4.0", - "zx": "^8.7.0" + "yaserver": "^0.4.0" }, "optionalDependencies": { - "windows-foreground-love": "0.5.0" + "windows-foreground-love": "0.6.1" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.40.1.tgz", - "integrity": "sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==", - "license": "MIT", + "node_modules/@anthropic-ai/sandbox-runtime": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.23.tgz", + "integrity": "sha512-Np0VRH6D71cGoJZvd8hCz1LMfwg9ERJovrOJSCz5aSQSQJPWPNIFPV1wfc8oAhJpStOuYkot+EmXOkRRxuGMCQ==", + "license": "Apache-2.0", "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" + "@pondwader/socks5-server": "^1.0.10", + "@types/lodash-es": "^4.17.12", + "commander": "^12.1.0", + "lodash-es": "^4.17.21", + "shell-quote": "^1.8.3", + "zod": "^3.24.1" + }, + "bin": { + "srt": "dist/cli.js" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" + "engines": { + "node": ">=18" } }, - "node_modules/@anthropic-ai/sdk/node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, "node_modules/@azure-rest/ai-translation-text": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/@azure-rest/ai-translation-text/-/ai-translation-text-1.0.0-beta.1.tgz", @@ -531,52 +489,50 @@ } }, "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==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.18.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz", + "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", + "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.18.10", + "@babel/helper-compilation-targets": "^7.18.9", + "@babel/helper-module-transforms": "^7.18.9", + "@babel/helpers": "^7.18.9", + "@babel/parser": "^7.18.10", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.18.10", + "@babel/types": "^7.18.10", + "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "json5": "^2.2.1", + "semver": "^6.3.0" }, "engines": { "node": ">=6.9.0" @@ -586,13 +542,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -603,93 +552,44 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" }, "engines": { - "node": ">=6.9.0" + "node": ">=6.0.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz", + "integrity": "sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg==", "dev": true, - "license": "ISC" - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz", - "integrity": "sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ==", - "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.5", - "semver": "^6.3.1" + "@babel/compat-data": "^7.18.8", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.20.2", + "semver": "^6.3.0" }, "engines": { "node": ">=6.9.0" @@ -698,131 +598,108 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/types": "^7.18.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz", + "integrity": "sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.6", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "node_modules/@babel/helper-simple-access": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz", + "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/types": "^7.18.6" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, "license": "MIT", "engines": { @@ -830,9 +707,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, "license": "MIT", "engines": { @@ -840,37 +717,36 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.27.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -879,138 +755,60 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz", - "integrity": "sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", - "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.28.5" + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, "engines": { - "node": ">=6.9.0" + "node": ">=4" } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1022,86 +820,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@c4312/eventsource-umd": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@c4312/eventsource-umd/-/eventsource-umd-3.0.5.tgz", - "integrity": "sha512-0QhLg51eFB+SS/a4Pv5tHaRSnjJBpdFsjT3WN/Vfh6qzeFXqvaE+evVIIToYvr2lRBLg1NIB635ip8ML+/84Sg==", - "license": "MIT", - "dependencies": { - "eventsource-parser": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@csstools/selector-resolve-nested": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-1.1.0.tgz", - "integrity": "sha512-uWvSaeRcHyeNenKg8tp17EVDRkpflmdyvbE0DHo6D/GdBb6PDnCYYU6gRpXhtICMGMcahQmj2zGxwFM/WC8hCg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.13" - } - }, - "node_modules/@csstools/selector-specificity": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", - "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^6.0.13" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz", @@ -1182,17 +900,6 @@ "node": ">= 4.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@es-joy/jsdoccomment": { "version": "0.48.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.48.0.tgz", @@ -1207,7972 +914,3674 @@ "node": ">=16" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/js": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", + "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", - "cpu": [ - "ia32" - ], + "node_modules/@gulp-sourcemaps/identity-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", + "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "acorn": "^6.4.1", + "normalize-path": "^3.0.0", + "postcss": "^7.0.16", + "source-map": "^0.6.0", + "through2": "^3.0.1" + }, "engines": { - "node": ">=18" + "node": ">= 0.10" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", - "cpu": [ - "loong64" - ], + "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=18" + "node": ">=0.4.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", - "cpu": [ - "mips64el" - ], + "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, "engines": { - "node": ">=18" + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", - "cpu": [ - "ppc64" - ], + "node_modules/@gulp-sourcemaps/map-sources": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", + "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o= sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "normalize-path": "^2.0.1", + "through2": "^2.0.3" + }, "engines": { - "node": ">=18" + "node": ">= 0.10" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", - "cpu": [ - "riscv64" - ], + "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, "engines": { - "node": ">=18" + "node": ">=0.10.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", - "cpu": [ - "s390x" - ], + "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", - "cpu": [ - "x64" - ], + "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", - "cpu": [ - "arm64" - ], + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", - "cpu": [ - "x64" - ], + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", - "cpu": [ - "arm64" - ], + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], "engines": { - "node": ">=18" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", - "cpu": [ - "x64" - ], + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "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": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui/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", - "optional": true, - "os": [ - "sunos" - ], "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", - "cpu": [ - "arm64" - ], + "node_modules/@isaacs/cliui/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", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", - "cpu": [ - "ia32" - ], + "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", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", - "cpu": [ - "x64" - ], + "node_modules/@isaacs/cliui/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", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "ansi-regex": "^6.0.1" + }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "node_modules/@isaacs/cliui/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": { - "eslint-visitor-keys": "^3.4.3" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "license": "MIT", "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=8" } }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, - "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=6.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", "dev": true, "license": "MIT" }, - "node_modules/@eslint/js": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz", - "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "node_modules/@koa/cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", + "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", "dev": true, - "license": "Apache-2.0", + "dependencies": { + "vary": "^1.1.2" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 14.0.0" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "node_modules/@koa/router": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-14.0.0.tgz", + "integrity": "sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" + "debug": "^4.4.1", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "path-to-regexp": "^8.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 20" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "license": "MIT", + "node_modules/@malept/cross-spawn-promise": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", + "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "cross-spawn": "^7.0.1" + }, + "engines": { + "node": ">= 10" } }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "license": "MIT", + "node_modules/@microsoft/1ds-core-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", + "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@microsoft/applicationinsights-core-js": "2.8.15", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" } }, - "node_modules/@floating-ui/react": { - "version": "0.27.16", - "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", - "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", - "license": "MIT", + "node_modules/@microsoft/1ds-post-js": { + "version": "3.2.13", + "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", + "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", "dependencies": { - "@floating-ui/react-dom": "^2.1.6", - "@floating-ui/utils": "^0.2.10", - "tabbable": "^6.0.0" - }, - "peerDependencies": { - "react": ">=17.0.0", - "react-dom": ">=17.0.0" + "@microsoft/1ds-core-js": "3.2.13", + "@microsoft/applicationinsights-shims": "^2.0.2", + "@microsoft/dynamicproto-js": "^1.1.7" } }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "license": "MIT", + "node_modules/@microsoft/applicationinsights-core-js": { + "version": "2.8.15", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", + "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@microsoft/applicationinsights-shims": "2.0.2", + "@microsoft/dynamicproto-js": "^1.1.9" }, "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "tslib": "*" } }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "license": "MIT" + "node_modules/@microsoft/applicationinsights-shims": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz", + "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" }, - "node_modules/@google/genai": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz", - "integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==", - "license": "Apache-2.0", + "node_modules/@microsoft/dynamicproto-js": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" + }, + "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, "dependencies": { - "google-auth-library": "^9.14.2", - "ws": "^8.18.0", - "zod": "^3.22.4", - "zod-to-json-schema": "^3.22.4" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">=18.0.0" + "node": ">= 8" } }, - "node_modules/@gulp-sourcemaps/identity-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz", - "integrity": "sha512-Tb+nSISZku+eQ4X1lAkevcQa+jknn/OVUgZ3XCxEKIsLsqYuPoJwJOPQeaOk75X3WPftb29GWY1eqE7GLsXb1Q==", + "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, - "dependencies": { - "acorn": "^6.4.1", - "normalize-path": "^3.0.0", - "postcss": "^7.0.16", - "source-map": "^0.6.0", - "through2": "^3.0.1" - }, "engines": { - "node": ">= 0.10" + "node": ">= 8" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "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, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, "engines": { - "node": ">=0.4.0" + "node": ">= 8" } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } }, - "node_modules/@gulp-sourcemaps/identity-map/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "node_modules/@octokit/core": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", + "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", "dev": true, + "license": "MIT", "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.1", + "@octokit/request": "^10.0.2", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">= 20" } }, - "node_modules/@gulp-sourcemaps/map-sources": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o= sha512-o/EatdaGt8+x2qpb0vFLC/2Gug/xYPRXb6a+ET1wGYKozKN3krDWC/zZFZAtrzxJHuDL12mwdfEFKcKMNvc55A==", + "node_modules/@octokit/endpoint": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", + "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { - "node": ">= 0.10" + "node": ">= 20" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "node_modules/@octokit/graphql": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", + "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", "dev": true, + "license": "MIT", "dependencies": { - "remove-trailing-separator": "^1.0.1" + "@octokit/request": "^10.0.2", + "@octokit/types": "^14.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 20" } }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "node_modules/@octokit/openapi-types": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", + "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", + "dev": true, + "license": "MIT" }, - "node_modules/@gulp-sourcemaps/map-sources/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", + "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", "dev": true, + "license": "MIT", "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "@octokit/types": "^14.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=18.18.0" + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", + "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" + "@octokit/types": "^14.1.0" }, "engines": { - "node": ">=18.18.0" + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@octokit/request": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", + "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", "dev": true, - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.0", + "@octokit/request-error": "^7.0.0", + "@octokit/types": "^14.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">= 20" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@octokit/request-error": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", + "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@octokit/types": "^14.0.0" + }, "engines": { - "node": ">=18.18" + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", + "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.2", + "@octokit/plugin-paginate-rest": "^13.0.1", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^16.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">= 20" } }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "node_modules/@octokit/types": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", + "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", "dev": true, "license": "MIT", - "optional": true, + "dependencies": { + "@octokit/openapi-types": "^25.1.0" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", + "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==", + "dev": true, "engines": { - "node": ">=18" + "node": ">=8.0.0" } }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "darwin" + "android" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ - "x64" + "arm64" ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ - "arm64" + "x64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "darwin" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ - "arm64" + "arm" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ - "ppc64" + "arm64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ - "riscv64" + "arm64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ - "s390x" + "x64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ "linux" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], - "dev": true, - "license": "LGPL-3.0-or-later", + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ - "arm" + "ia32" ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ - "arm64" + "x64" ], - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "node": ">=14" } }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "url": "https://opencollective.com/unts" } }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], + "node_modules/@playwright/browser-chromium": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.56.1.tgz", + "integrity": "sha512-n4xzZpOn4qOtZJylpIn8co2QDoWczfJ068sEeky3EE5Vvy+lHX2J3WAcC4MbXzcpfoBee1lJm8JtXuLZ9HBCBA==", "dev": true, + "hasInstallScript": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "playwright-core": "1.56.1" }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", "dev": true, "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "dependencies": { + "playwright": "1.56.1" }, - "funding": { - "url": "https://opencollective.com/libvips" + "bin": { + "playwright": "cli.js" }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "engines": { + "node": ">=18" } }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], + "node_modules/@pondwader/socks5-server": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", + "integrity": "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg==", + "license": "MIT" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } + "license": "MIT" }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/libvips" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], + "node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@sinonjs/commons": "^1.7.0" } }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], + "node_modules/@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", "dev": true, - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, - "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==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.8.0.tgz", + "integrity": "sha512-VukJqkRlC2psLKoIHJ+4R3ZxLJfWeizGGX+X5ZxunjXo4MbxRNtwu5UvXuerABg4s2RV6Z3LFTdm0WvI4+RAMQ==", "dev": true, "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" + "@typescript-eslint/utils": "^8.4.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@stylistic/eslint-plugin-ts/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", "dev": true, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "defer-to-connect": "^2.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=10" } }, - "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 + "node_modules/@tootallnate/once": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", + "integrity": "sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw==", + "engines": { + "node": ">= 10" + } }, - "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==", + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true, - "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": ">=10.13.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@ts-morph/common": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.20.0.tgz", + "integrity": "sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" } }, - "node_modules/@isaacs/cliui/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==", + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/cookie": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", + "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", + "dev": true + }, + "node_modules/@types/debug": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", + "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" + "@types/ms": "*" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, - "engines": { - "node": ">=6.0.0" + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", - "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "@types/eslint": "*", + "@types/estree": "*" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "node_modules/@types/expect": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", + "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", + "dev": true + }, + "node_modules/@types/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", "dev": true, - "license": "MIT", + "optional": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@types/minimatch": "*", + "@types/node": "*" } }, - "node_modules/@koa/cors": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@koa/cors/-/cors-5.0.0.tgz", - "integrity": "sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==", + "node_modules/@types/gulp-svgmin": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/gulp-svgmin/-/gulp-svgmin-1.2.1.tgz", + "integrity": "sha512-qT/Y+C2uWJZoGw4oAjuJGZk+ImmTrx+QZbMGSzf8a1absW3wztrmMPvCF64pdogATDVUSPQDLzPWAFeIxylJTA==", "dev": true, "dependencies": { - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 14.0.0" + "@types/node": "*", + "@types/svgo": "^1", + "@types/vinyl": "*" } }, - "node_modules/@koa/router": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/@koa/router/-/router-13.1.0.tgz", - "integrity": "sha512-mNVu1nvkpSd8Q8gMebGbCkDWJ51ODetrFvLKYusej+V0ByD4btqHYnPIzTBLXnQMVUlm/oxVwqmWBY3zQfZilw==", + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "dev": true, - "license": "MIT", - "dependencies": { - "http-errors": "^2.0.0", - "koa-compose": "^4.1.0", - "path-to-regexp": "^6.3.0" - }, - "engines": { - "node": ">= 18" - } + "license": "MIT" }, - "node_modules/@malept/cross-spawn-promise": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", - "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", + "node_modules/@types/http-proxy-agent": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-proxy-agent/-/http-proxy-agent-2.0.1.tgz", + "integrity": "sha512-dgsgbsgI3t+ZkdzF9H19uBaLsurIZJJjJsVpj4mCLp8B6YghQ7jVwyqhaL0PcVtuC3nOi0ZBhAi2Dd9jCUwdFA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 10" + "@types/node": "*" } }, - "node_modules/@microsoft/1ds-core-js": { - "version": "3.2.13", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", - "integrity": "sha512-CluYTRWcEk0ObG5EWFNWhs87e2qchJUn0p2D21ZUa3PWojPZfPSBs4//WIE0MYV8Qg1Hdif2ZTwlM7TbYUjfAg==", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/kerberos": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/kerberos/-/kerberos-1.1.2.tgz", + "integrity": "sha512-cLixfcXjdj7qohLasmC1G4fh+en4e4g7mFZiG38D+K9rS9BRKFlq1JH5dGkQzICckbu4wM+RcwSa4VRHlBg7Rg==", + "dev": true + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, "dependencies": { - "@microsoft/applicationinsights-core-js": "2.8.15", - "@microsoft/applicationinsights-shims": "^2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" + "@types/node": "*" } }, - "node_modules/@microsoft/1ds-post-js": { - "version": "3.2.13", - "resolved": "https://registry.npmjs.org/@microsoft/1ds-post-js/-/1ds-post-js-3.2.13.tgz", - "integrity": "sha512-HgS574fdD19Bo2vPguyznL4eDw7Pcm1cVNpvbvBLWiW3x4e1FCQ3VMXChWnAxCae8Hb0XqlA2sz332ZobBavTA==", - "dependencies": { - "@microsoft/1ds-core-js": "3.2.13", - "@microsoft/applicationinsights-shims": "^2.0.2", - "@microsoft/dynamicproto-js": "^1.1.7" - } - }, - "node_modules/@microsoft/applicationinsights-core-js": { - "version": "2.8.15", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-2.8.15.tgz", - "integrity": "sha512-yYAs9MyjGr2YijQdUSN9mVgT1ijI1FPMgcffpaPmYbHAVbQmF7bXudrBWHxmLzJlwl5rfep+Zgjli2e67lwUqQ==", - "dependencies": { - "@microsoft/applicationinsights-shims": "2.0.2", - "@microsoft/dynamicproto-js": "^1.1.9" - }, - "peerDependencies": { - "tslib": "*" - } - }, - "node_modules/@microsoft/applicationinsights-shims": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-2.0.2.tgz", - "integrity": "sha512-PoHEgsnmcqruLNHZ/amACqdJ6YYQpED0KSRe6J7gIJTtpZC1FfFU9b1fmDKDKtFoUSrPzEh1qzO3kmRZP0betg==" - }, - "node_modules/@microsoft/dynamicproto-js": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", - "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" - }, - "node_modules/@mistralai/mistralai": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.10.0.tgz", - "integrity": "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==", - "dependencies": { - "zod": "^3.20.0", - "zod-to-json-schema": "^3.24.1" - } - }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.22.0.tgz", - "integrity": "sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==", - "license": "MIT", - "dependencies": { - "ajv": "^8.17.1", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - } - } - }, - "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@napi-rs/canvas": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.82.tgz", - "integrity": "sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==", - "license": "MIT", - "optional": true, - "workspaces": [ - "e2e/*" - ], - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@napi-rs/canvas-android-arm64": "0.1.82", - "@napi-rs/canvas-darwin-arm64": "0.1.82", - "@napi-rs/canvas-darwin-x64": "0.1.82", - "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.82", - "@napi-rs/canvas-linux-arm64-gnu": "0.1.82", - "@napi-rs/canvas-linux-arm64-musl": "0.1.82", - "@napi-rs/canvas-linux-riscv64-gnu": "0.1.82", - "@napi-rs/canvas-linux-x64-gnu": "0.1.82", - "@napi-rs/canvas-linux-x64-musl": "0.1.82", - "@napi-rs/canvas-win32-x64-msvc": "0.1.82" - } - }, - "node_modules/@napi-rs/canvas-android-arm64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.82.tgz", - "integrity": "sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-darwin-arm64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.82.tgz", - "integrity": "sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-darwin-x64": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.82.tgz", - "integrity": "sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.82.tgz", - "integrity": "sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.82.tgz", - "integrity": "sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-arm64-musl": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.82.tgz", - "integrity": "sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.82.tgz", - "integrity": "sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-gnu": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.82.tgz", - "integrity": "sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-linux-x64-musl": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.82.tgz", - "integrity": "sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@napi-rs/canvas-win32-x64-msvc": { - "version": "0.1.82", - "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.82.tgz", - "integrity": "sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/env": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz", - "integrity": "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.6.tgz", - "integrity": "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.6.tgz", - "integrity": "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.6.tgz", - "integrity": "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.6.tgz", - "integrity": "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.6.tgz", - "integrity": "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.6.tgz", - "integrity": "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.6.tgz", - "integrity": "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.6.tgz", - "integrity": "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "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, - "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, - "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, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/core": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.3.tgz", - "integrity": "sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.1", - "@octokit/request": "^10.0.2", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/endpoint": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz", - "integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/graphql": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz", - "integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/request": "^10.0.2", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz", - "integrity": "sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.1.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-request-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", - "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz", - "integrity": "sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.1.0" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/request": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.3.tgz", - "integrity": "sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^11.0.0", - "@octokit/request-error": "^7.0.0", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/request-error": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz", - "integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/rest": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", - "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/core": "^7.0.2", - "@octokit/plugin-paginate-rest": "^13.0.1", - "@octokit/plugin-request-log": "^6.0.0", - "@octokit/plugin-rest-endpoint-methods": "^16.0.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.0.3.tgz", - "integrity": "sha512-puWxACExDe9nxbBB3lOymQFrLYml2dVOrd7USiVRnSbgXE+KwBu+HxFvxrzfqsiSda9IWsXJG1ef7C1O2/GmKQ==", - "dev": true, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "git+ssh://git@github.com/parcel-bundler/watcher.git#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", - "integrity": "sha512-Z0lk8pM5vwuOJU6pfheRXHrOpQYIIEnVl/z8DY6370D4+ZnrOTvFa5BUdf3pGxahT5ILbPWwQSm2Wthy4q1OTg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@playwright/browser-chromium": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.47.2.tgz", - "integrity": "sha512-tsk9bLcGzIu4k4xI2ixlwDrdJhMqCalUCsSj7TRI8VuvK7cLiJIa5SR0dprKbX+wkku/JMR4EN6g9DMHvfna+Q==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.47.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@playwright/test": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.1.tgz", - "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", - "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", - "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "node_modules/@stylistic/eslint-plugin-ts": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.8.0.tgz", - "integrity": "sha512-VukJqkRlC2psLKoIHJ+4R3ZxLJfWeizGGX+X5ZxunjXo4MbxRNtwu5UvXuerABg4s2RV6Z3LFTdm0WvI4+RAMQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^8.4.0", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": ">=8.40.0" - } - }, - "node_modules/@stylistic/eslint-plugin-ts/node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tailwindcss/typography": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", - "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "6.0.10" - }, - "peerDependencies": { - "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" - } - }, - "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { - "version": "6.0.10", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", - "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@thisismanta/pessimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@thisismanta/pessimist/-/pessimist-1.2.0.tgz", - "integrity": "sha512-rm8/zjNMuO9hPYhEMavVIIxmvawJJB8mthvbVXd74XUW7V/SbgmtDBQjICbCWKjluvA+gh+cqi7dv85/jexknA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lodash": "^4.17.21" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@tootallnate/once": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-3.0.0.tgz", - "integrity": "sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@ts-morph/common": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.20.0.tgz", - "integrity": "sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==", - "dev": true, - "dependencies": { - "fast-glob": "^3.2.12", - "minimatch": "^7.4.3", - "mkdirp": "^2.1.6", - "path-browserify": "^1.0.1" - } - }, - "node_modules/@ts-morph/common/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@ts-morph/common/node_modules/mkdirp": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", - "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", - "dev": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "node_modules/@types/cookie": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz", - "integrity": "sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==", - "dev": true - }, - "node_modules/@types/debug": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.9.tgz", - "integrity": "sha512-8Hz50m2eoS56ldRlepxSBa6PWEVCtzUo/92HgLc2qTMnotJNIm7xP+UZhyWoYsyOdd5dxZ+NZLb24rsKyFs2ow==", - "dev": true, - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/diff": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", - "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/expect": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/@types/expect/-/expect-1.20.4.tgz", - "integrity": "sha512-Q5Vn3yjTDyCMV50TB6VRIbQNxSE4OmZR86VSbGaNpfUolm0iePBB4KdEEHmxoY5sT2+2DIvXW0rvMDP2nHZ4Mg==", - "dev": true - }, - "node_modules/@types/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", - "dev": true, - "optional": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/gulp-svgmin": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/gulp-svgmin/-/gulp-svgmin-1.2.1.tgz", - "integrity": "sha512-qT/Y+C2uWJZoGw4oAjuJGZk+ImmTrx+QZbMGSzf8a1absW3wztrmMPvCF64pdogATDVUSPQDLzPWAFeIxylJTA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/svgo": "^1", - "@types/vinyl": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/http-proxy-agent": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/http-proxy-agent/-/http-proxy-agent-2.0.1.tgz", - "integrity": "sha512-dgsgbsgI3t+ZkdzF9H19uBaLsurIZJJjJsVpj4mCLp8B6YghQ7jVwyqhaL0PcVtuC3nOi0ZBhAi2Dd9jCUwdFA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true - }, - "node_modules/@types/kerberos": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/kerberos/-/kerberos-1.1.2.tgz", - "integrity": "sha512-cLixfcXjdj7qohLasmC1G4fh+en4e4g7mFZiG38D+K9rS9BRKFlq1JH5dGkQzICckbu4wM+RcwSa4VRHlBg7Rg==", - "dev": true - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true, - "optional": true - }, - "node_modules/@types/minimist": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", - "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", - "dev": true - }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.32.tgz", - "integrity": "sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.18.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", - "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/node-fetch": { - "version": "2.6.13", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", - "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "form-data": "^4.0.4" - } - }, - "node_modules/@types/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", - "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" - }, - "node_modules/@types/sinon": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", - "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", - "dev": true, - "dependencies": { - "@sinonjs/fake-timers": "^7.1.0" - } - }, - "node_modules/@types/sinon-test": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/sinon-test/-/sinon-test-2.4.2.tgz", - "integrity": "sha512-3BX9mk5+o//Xzs5N4bFYxPT+QlPLrqbyNfDWkIGtk9pVIp2Nl8ctsIGXsY3F01DsCd1Zlin3FqAk6V5XqkCyJA==", - "dev": true, - "dependencies": { - "@types/sinon": "*" - } - }, - "node_modules/@types/svgo": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@types/svgo/-/svgo-1.3.6.tgz", - "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==", - "dev": true - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tunnel": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", - "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/vinyl": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", - "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", - "dev": true, - "dependencies": { - "@types/expect": "^1.20.4", - "@types/node": "*" - } - }, - "node_modules/@types/vscode-notebook-renderer": { - "version": "1.72.0", - "resolved": "https://registry.npmjs.org/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.0.tgz", - "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", - "dev": true - }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, - "node_modules/@types/wicg-file-system-access": { - "version": "2023.10.7", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", - "integrity": "sha512-g49ijasEJvCd7ifmAY2D0wdEtt1xRjBbA33PJTiv8mKBr7DoMsPeISoJ8oQOTopSRi+FBWPpPW5ouDj2QPKtGA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/windows-foreground-love": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@types/windows-foreground-love/-/windows-foreground-love-0.3.0.tgz", - "integrity": "sha512-tFUVA/fiofNqOh6lZlymvQiQYPY+cZXZPR9mn9wN6/KS8uwx0zgH4Ij/jmFyRYr+x+DGZWEIeknS2BMi7FZJAQ==", - "dev": true - }, - "node_modules/@types/winreg": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", - "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= sha512-c4m/hnOI1j34i8hXlkZzelE6SXfOqaTWhBp0UgBuwmpiafh22OpsE261Rlg//agZtQHIY5cMgbkX8bnthUFrmA==", - "dev": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yazl": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz", - "integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", - "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.45.0", - "@typescript-eslint/type-utils": "8.45.0", - "@typescript-eslint/utils": "8.45.0", - "@typescript-eslint/visitor-keys": "8.45.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.45.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/parser": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", - "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.45.0", - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/typescript-estree": "8.45.0", - "@typescript-eslint/visitor-keys": "8.45.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", - "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.45.0", - "@typescript-eslint/types": "^8.45.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", - "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/visitor-keys": "8.45.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", - "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", - "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/typescript-estree": "8.45.0", - "@typescript-eslint/utils": "8.45.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", - "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", - "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.45.0", - "@typescript-eslint/tsconfig-utils": "8.45.0", - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/visitor-keys": "8.45.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", - "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.45.0", - "@typescript-eslint/types": "8.45.0", - "@typescript-eslint/typescript-estree": "8.45.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.45.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", - "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.45.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@typescript/native-preview": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-djbOSIm8Or967wMuO209ydMp2nq34hEulah1EhjUsLSqLplsbOk8RSOyVJJphU+CMP33rULDcnDAzvylU8Tq9Q==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsgo": "bin/tsgo.js" - }, - "optionalDependencies": { - "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251027.1", - "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251027.1", - "@typescript/native-preview-linux-arm": "7.0.0-dev.20251027.1", - "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251027.1", - "@typescript/native-preview-linux-x64": "7.0.0-dev.20251027.1", - "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251027.1", - "@typescript/native-preview-win32-x64": "7.0.0-dev.20251027.1" - } - }, - "node_modules/@typescript/native-preview-darwin-arm64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-4Nysrmep6Z4C722nQF07XkEk22qyI2/vCfvfPSlhOxpJJcIFAroxSkSH7Qy8EDZWhNer9D4CMTYX9q5I8B75lQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-darwin-x64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-WvHLb6Mry214ZTuhfvv6fP1FLgYZ4oTw55+B2hTAo/O6qq9KX3OW90dvFYSMJKPhgvWR5B9tIEcMkIXGjxfv1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@typescript/native-preview-linux-arm": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-epAynE0qbU9nuPwaOgr9N6WANoYAdwhyteNB+PG2qRWYoFDYPXSgParjO1FAkY0uMt88QaS6vQ6ZglInHsxvXQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-arm64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-CNbTvppx8wsoRS3g4RcpDapRp4tNYp1eu+94HmtKT7ch3RJOliKIhAa/8odXIrkqnT+kc0wrQCzFiICMW4YieQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-linux-x64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-lzSUTdWYfKvsQJPQF/BtYil1Xmzn0f3jpgk8/4uVg4NQeDtzW0J3ceWl2lw1TuGnhISq2dwyupjKJfLQhe4AVQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@typescript/native-preview-win32-arm64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-K9K8t3HW/35ejgVJALPW9Fqo0PHOxh1/ir01C8r5qbhIdPQqwGlBHAGwLzrfH0ZF1R2nR2X4T+z+gB8tLULsow==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@typescript/native-preview-win32-x64": { - "version": "7.0.0-dev.20251027.1", - "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20251027.1.tgz", - "integrity": "sha512-n7hb7ZjAEgoNBWYSt87+eMtSK2h6Xl9NWUd2ocw3Znz/tw8lwpUaG35FVd/Aj72kT1/5kiCBlM+7MxA214KGiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vscode/deviceid": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.1.tgz", - "integrity": "sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag==", - "hasInstallScript": true, - "dependencies": { - "fs-extra": "^11.2.0", - "uuid": "^9.0.1" - } - }, - "node_modules/@vscode/gulp-electron": { - "version": "1.38.2", - "resolved": "https://registry.npmjs.org/@vscode/gulp-electron/-/gulp-electron-1.38.2.tgz", - "integrity": "sha512-uFMp6Utz2kf62NMXVIht09FfIcuAFLuw7b9xhJNm2iGaaAI3b2BBHP05cKG3LYIPGvkWoC7UNk4EjyQDO7T/ZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^4.0.1", - "@octokit/rest": "^22.0.0", - "event-stream": "3.3.4", - "gulp-filter": "^5.1.0", - "gulp-rename": "1.2.2", - "gulp-symdest": "^1.2.0", - "gulp-vinyl-zip": "^2.1.2", - "mkdirp": "^0.5.1", - "plist": "^3.0.1", - "progress": "^1.1.8", - "rcedit": "^4.0.1", - "rimraf": "^2.4.2", - "semver": "^7.7.2", - "sumchecker": "^3.0.1", - "temp": "^0.8.3", - "vinyl": "^3.0.0", - "vinyl-fs": "^3.0.3" - }, - "engines": { - "node": ">=22" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/@electron/get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-4.0.1.tgz", - "integrity": "sha512-fTMFb/ZiK6xQace5YZlhT+vNR08ogat9SqpvwpaC9vD6hgx7ouz9cdcrSrFuNji4823Jmmy90/CDhJq0I4vRFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "got": "^14.4.5", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=22.12.0" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/@electron/get/node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/@sindresorhus/is": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", - "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/cacheable-request": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", - "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.4", - "get-stream": "^9.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.4", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.1", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/got": { - "version": "14.4.7", - "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", - "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^7.0.1", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^12.0.1", - "decompress-response": "^6.0.0", - "form-data-encoder": "^4.0.2", - "http2-wrapper": "^2.2.1", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", - "responselike": "^3.0.0", - "type-fest": "^4.26.1" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "dependencies": { - "minimist": "^1.2.5" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/normalize-url": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", - "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/rcedit": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", - "integrity": "sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn-windows-exe": "^1.1.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@vscode/gulp-electron/node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vscode/iconv-lite-umd": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", - "integrity": "sha512-tK6k0DXFHW7q5+GGuGZO+phpAqpxO4WXl+BLc/8/uOk3RsM2ssAL3CQUQDb1TGfwltjsauhN6S4ghYZzs4sPFw==", - "license": "MIT" - }, - "node_modules/@vscode/l10n-dev": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@vscode/l10n-dev/-/l10n-dev-0.0.35.tgz", - "integrity": "sha512-s6uzBXsVDSL69Z85HSqpc5dfKswQkeucY8L00t1TWzGalw7wkLQUKMRwuzqTq+AMwQKrRd7Po14cMoTcd11iDw==", - "dev": true, - "dependencies": { - "@azure-rest/ai-translation-text": "^1.0.0-beta.1", - "debug": "^4.3.4", - "deepmerge-json": "^1.5.0", - "glob": "^10.0.0", - "markdown-it": "^14.0.0", - "node-html-markdown": "^1.3.0", - "pseudo-localization": "^2.4.0", - "web-tree-sitter": "^0.20.8", - "xml2js": "^0.5.0", - "yargs": "^17.7.1" - }, - "bin": { - "vscode-l10n-dev": "dist/cli.js" - } - }, - "node_modules/@vscode/l10n-dev/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vscode/l10n-dev/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/l10n-dev/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/proxy-agent": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.36.0.tgz", - "integrity": "sha512-W4mls/+zErqTYcKC41utdmoYnBWZRH1dRF9U4cBAyKU5EhcnWfVsPBvUnXXw1CffI3djmMWnu9JrF/Ynw7lkcg==", - "license": "MIT", - "dependencies": { - "@tootallnate/once": "^3.0.0", - "agent-base": "^7.0.1", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.2", - "socks-proxy-agent": "^8.0.1", - "undici": "^7.2.0" - }, - "engines": { - "node": ">=22.15.0" - }, - "optionalDependencies": { - "@vscode/windows-ca-certs": "^0.3.1" - } - }, - "node_modules/@vscode/ripgrep": { - "version": "1.15.14", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.14.tgz", - "integrity": "sha512-/G1UJPYlm+trBWQ6cMO3sv6b8D1+G16WaJH1/DSqw32JOVlzgZbLkDxRyzIpTpv30AcYGMkCf5tUqGlW6HbDWw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "https-proxy-agent": "^7.0.2", - "proxy-from-env": "^1.1.0", - "yauzl": "^2.9.2" - } - }, - "node_modules/@vscode/ripgrep/node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/@vscode/spdlog": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@vscode/spdlog/-/spdlog-0.15.2.tgz", - "integrity": "sha512-8RQ7JEs81x5IFONYGtFhYtaF2a3IPtNtgMdp+MFLxTDokJQBAVittx0//EN38BYhlzeVqEPgusRsOA8Yulaysg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "mkdirp": "^1.0.4", - "node-addon-api": "7.1.0" - } - }, - "node_modules/@vscode/sqlite3": { - "version": "5.1.8-vscode", - "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.8-vscode.tgz", - "integrity": "sha512-9Ku18yZej1kxS7mh6dhCWxkCof043HljcLIdq+RRJr65QdOeAqPOUJ2i6qXRL63l1Kd72uXV/zLA2SBwhfgiOw==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "node-addon-api": "^8.2.0", - "tar": "^6.1.11" - } - }, - "node_modules/@vscode/sqlite3/node_modules/node-addon-api": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", - "integrity": "sha512-qnyuI2ROiCkye42n9Tj5aX1ns7rzj6n7zW1XReSnLSL9v/vbLeR6fJq6PU27YU/ICfYw6W7Ouk/N7cysWu/hlw==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/@vscode/sudo-prompt": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.1.tgz", - "integrity": "sha512-9ORTwwS74VaTn38tNbQhsA5U44zkJfcb0BdTSyyG6frP4e8KMtHuTXYmwefe5dpL8XB1aGSIVTaLjD3BbWb5iA==" - }, - "node_modules/@vscode/telemetry-extractor": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.10.2.tgz", - "integrity": "sha512-hn+KDSwIRj7LzDSFd9HALkc80UY1g16nQgWztHml+nxAZU3Hw/EoWEEDxOncvDYq9YcV+tX/cVHrVjbNL2Dg0g==", - "dev": true, - "dependencies": { - "@vscode/ripgrep": "^1.15.9", - "command-line-args": "^5.2.1", - "ts-morph": "^19.0.0" - }, - "bin": { - "vscode-telemetry-extractor": "out/extractor.js" - } - }, - "node_modules/@vscode/test-cli": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.6.tgz", - "integrity": "sha512-4i61OUv5PQr3GxhHOuUgHdgBDfIO/kXTPCsEyFiMaY4SOqQTgkTmyZLagHehjOgCfsXdcrJa3zgQ7zoc+Dh6hQ==", - "dev": true, - "dependencies": { - "@types/mocha": "^10.0.2", - "c8": "^9.1.0", - "chokidar": "^3.5.3", - "enhanced-resolve": "^5.15.0", - "glob": "^10.3.10", - "minimatch": "^9.0.3", - "mocha": "^10.2.0", - "supports-color": "^9.4.0", - "yargs": "^17.7.2" - }, - "bin": { - "vscode-test": "out/bin.mjs" - } - }, - "node_modules/@vscode/test-cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vscode/test-cli/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/test-cli/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/test-electron": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", - "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", - "dev": true, - "dependencies": { - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.4", - "jszip": "^3.10.1", - "ora": "^7.0.1", - "semver": "^7.6.2" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vscode/test-web": { - "version": "0.0.62", - "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.62.tgz", - "integrity": "sha512-Ypug5PvhPOPFbuHVilai7t23tm3Wm5geIpC2DB09Gy9o0jZCduramiSdPf+YN7yhkFy1usFYtN3Eaks1XoBrOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@koa/cors": "^5.0.0", - "@koa/router": "^13.1.0", - "@playwright/browser-chromium": "^1.47.2", - "glob": "^11.0.0", - "gunzip-maybe": "^1.4.2", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "koa": "^2.15.3", - "koa-morgan": "^1.0.1", - "koa-mount": "^4.0.0", - "koa-static": "^5.0.0", - "minimist": "^1.2.8", - "playwright": "^1.47.2", - "tar-fs": "^3.0.6", - "vscode-uri": "^3.0.8" - }, - "bin": { - "vscode-test-web": "out/server/index.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@vscode/test-web/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@vscode/test-web/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "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/test-web/node_modules/jackspeak": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", - "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/@vscode/test-web/node_modules/lru-cache": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", - "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", - "dev": true, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@vscode/test-web/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/test-web/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/@vscode/test-web/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, - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz", - "integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==", - "license": "MIT" - }, - "node_modules/@vscode/v8-heap-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@vscode/v8-heap-parser/-/v8-heap-parser-0.1.0.tgz", - "integrity": "sha512-3EvQak7EIOLyIGz+IP9qSwRmP08ZRWgTeoRgAXPVkkDXZ8riqJ7LDtkgx++uHBiJ3MUaSdlUYPZcLFFw7E6zGg==", - "dev": true - }, - "node_modules/@vscode/vscode-languagedetection": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz", - "integrity": "sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g==", - "bin": { - "vscode-languagedetection": "cli/index.js" - } - }, - "node_modules/@vscode/vscode-perf": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@vscode/vscode-perf/-/vscode-perf-0.0.19.tgz", - "integrity": "sha512-E/I0S+71K3Jo4kiMYbeKM8mUG3K8cHlj5MFVfPYVAvlp7KuIZTM914E7osp+jx8XgMLN6fChxnFmntm1GtVrKA==", - "dev": true, - "dependencies": { - "chalk": "^4.x", - "commander": "^9.4.0", - "cookie": "^0.7.2", - "js-base64": "^3.7.4", - "node-fetch": "2.6.8", - "playwright": "^1.29.2" - }, - "bin": { - "vscode-perf": "bin/vscode-perf" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", - "hasInstallScript": true, - "license": "BSD", - "optional": true, - "os": [ - "win32" - ], - "dependencies": { - "node-addon-api": "^8.2.0" - } - }, - "node_modules/@vscode/windows-ca-certs/node_modules/node-addon-api": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", - "integrity": "sha512-qnyuI2ROiCkye42n9Tj5aX1ns7rzj6n7zW1XReSnLSL9v/vbLeR6fJq6PU27YU/ICfYw6W7Ouk/N7cysWu/hlw==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/@vscode/windows-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-mutex/-/windows-mutex-0.5.0.tgz", - "integrity": "sha512-iD29L9AUscpn07aAvhP2QuhrXzuKc1iQpPF6u7ybtvRbR+o+RotfbuKqqF1RDlDDrJZkL+3AZTy4D01U4nEe5A==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "7.1.0" - } - }, - "node_modules/@vscode/windows-process-tree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.6.0.tgz", - "integrity": "sha512-7/DjBKKUtlmKNiAet2GRbdvfjgMKmfBeWVClIgONv8aqxGnaKca5N85eIDxh6rLMy2hKvFqIIsqgxs1Q26TWwg==", - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "7.1.0" - } - }, - "node_modules/@vscode/windows-registry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.1.0.tgz", - "integrity": "sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw==", - "hasInstallScript": true - }, - "node_modules/@vscodium/policy-watcher": { - "version": "1.3.0-2503300035", - "resolved": "https://registry.npmjs.org/@vscodium/policy-watcher/-/policy-watcher-1.3.0-2503300035.tgz", - "integrity": "sha512-Vf83Z2uKkq+SL2Kr+DHlE7Ezb+HBnspn5iMazUele1Mj/7WQv0Zq/fTQ3LFKKVC6hV5+G95BXwOc7oSAU0gWUw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^8.2.0" - } - }, - "node_modules/@vscodium/policy-watcher/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webgpu/types": { - "version": "0.1.44", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", - "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", - "dev": true - }, - "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", - "dev": true, - "engines": { - "node": ">=14.15.0" - }, - "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.119", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", - "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", - "license": "MIT", - "dependencies": { - "js-base64": "^3.7.5" - }, - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", - "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", - "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", - "license": "MIT", - "dependencies": { - "font-finder": "^1.1.0", - "font-ligatures": "^1.4.1" - }, - "engines": { - "node": ">8.0.0" - }, - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.42", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", - "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", - "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", - "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", - "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", - "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", - "license": "MIT", - "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" - } - }, - "node_modules/@xterm/headless": { - "version": "5.6.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.136.tgz", - "integrity": "sha512-3irueWS6Ei+XlTMCuh6ZWj1tBnVvjitDtD4PN+v81RKjaCNO/QN9abGTHQx+651GP291ESwY8ocKThSoQ9yklw==", - "license": "MIT" - }, - "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", - "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", - "license": "MIT" - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", - "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "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==", - "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/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", - "dev": true, - "engines": { - "node": ">=0.4.2" - } - }, - "node_modules/ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE= sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", - "dev": true, - "dependencies": { - "ansi-wrap": "0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "dependencies": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768= sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8= sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true - }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", - "dev": true, - "dependencies": { - "buffer-equal": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, - "node_modules/are-docs-informative": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", - "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "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 - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4= sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", - "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ= sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", - "dev": true, - "dependencies": { - "make-iterator": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-back": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", - "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8= sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U= sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", - "dev": true, - "dependencies": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-initial/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "dependencies": { - "is-number": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-last/node_modules/is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "dependencies": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asar": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/asar/-/asar-3.0.3.tgz", - "integrity": "sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw==", - "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", - "dev": true, - "dependencies": { - "chromium-pickle-js": "^0.2.0", - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - }, - "optionalDependencies": { - "@types/glob": "^7.1.1" - } - }, - "node_modules/asar/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/asar/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-done": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", - "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^2.0.0", - "stream-exhaust": "^1.0.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs= sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", - "dev": true, - "dependencies": { - "async-done": "^1.2.2" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", - "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", - "dev": true - }, - "node_modules/bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA= sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", - "dev": true, - "dependencies": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", - "dev": true, - "license": "Apache-2.0", - "optional": true - }, - "node_modules/bare-fs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz", - "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", - "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", - "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY= sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dev": true, - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/binaryextensions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U= sha512-xnG0l4K3ghM62rFzDi2jcNEuICl6uQ4NgvGpqQsY7HgW8gPDeAWGOxHI/k+qZfXfMANytzrArGNPXidaCwtbmA==", - "dev": true - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==", - "dev": true, - "dependencies": { - "inherits": "~2.0.0" - }, - "engines": { - "node": "0.4 || >=0.5.8" - } - }, - "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", - "license": "MIT", - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", - "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/body-parser/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24= sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/boolean": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz", - "integrity": "sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==", - "dev": true, - "optional": true + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "@types/lodash": "*" } }, - "node_modules/braces": { + "node_modules/@types/minimatch": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "node_modules/browserify-zlib": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", - "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", - "dev": true, - "dependencies": { - "pako": "~0.2.0" - } - }, - "node_modules/browserslist": { - "version": "4.28.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", - "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.25", - "caniuse-lite": "^1.0.30001754", - "electron-to-chromium": "^1.5.249", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74= sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" + "optional": true }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/@types/minimist": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/c8": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", - "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", - "dev": true, - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@istanbuljs/schema": "^0.1.3", - "find-up": "^5.0.0", - "foreground-child": "^3.1.1", - "istanbul-lib-coverage": "^3.2.0", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.1.6", - "test-exclude": "^6.0.0", - "v8-to-istanbul": "^9.0.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1" - }, - "bin": { - "c8": "bin/c8.js" - }, - "engines": { - "node": ">=14.14.0" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/cache-content-type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", - "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", - "dev": true, - "dependencies": { - "mime-types": "^2.1.18", - "ylru": "^1.2.0" - }, - "engines": { - "node": ">= 6.0.0" - } + "node_modules/@types/ms": { + "version": "0.7.32", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.32.tgz", + "integrity": "sha512-xPSg0jm4mqgEkNhowKgZFBNtwoEwF6gJ4Dhww+GFpm3IgtNseHQZ5IqdNwnquZEoANxyDAKDRAdVo4Z72VvD/g==", + "dev": true }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "node_modules/@types/node": { + "version": "22.18.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", + "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==", "dev": true, - "engines": { - "node": ">=10.6.0" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "node_modules/cacheable-request": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", - "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", + "node_modules/@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", "dev": true, "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" + "@types/node": "*", + "form-data": "^3.0.0" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "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==", - "license": "MIT", + "node_modules/@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "@types/node": "*" } }, - "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==", - "license": "MIT", + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==" + }, + "node_modules/@types/sinon": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", + "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", + "dev": true, "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "@sinonjs/fake-timers": "^7.1.0" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@types/sinon-test": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/sinon-test/-/sinon-test-2.4.2.tgz", + "integrity": "sha512-3BX9mk5+o//Xzs5N4bFYxPT+QlPLrqbyNfDWkIGtk9pVIp2Nl8ctsIGXsY3F01DsCd1Zlin3FqAk6V5XqkCyJA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" + "dependencies": { + "@types/sinon": "*" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/@types/svgo": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@types/svgo/-/svgo-1.3.6.tgz", + "integrity": "sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug==", + "dev": true + }, + "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==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" + "dependencies": { + "@types/node": "*" } }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "node_modules/@types/vinyl": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.12.tgz", + "integrity": "sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" + "@types/expect": "^1.20.4", + "@types/node": "*" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001755", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001755.tgz", - "integrity": "sha512-44V+Jm6ctPj7R52Na4TLi3Zri4dWUljJd+RDm+j8LtNCc/ihLCT+X1TzoOAkRETEWqjuLnh9581Tl80FvK7jVA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/@types/vscode-notebook-renderer": { + "version": "1.72.0", + "resolved": "https://registry.npmjs.org/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.72.0.tgz", + "integrity": "sha512-5iTjb39DpLn03ULUwrDR3L2Dy59RV4blSUHy0oLdQuIY11PhgWO4mXIcoFS0VxY1GZQ4IcjSf3ooT2Jrrcahnw==", + "dev": true }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@types/webpack": { + "version": "5.28.5", + "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", + "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" } }, - "node_modules/chalk/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==", + "node_modules/@types/wicg-file-system-access": { + "version": "2023.10.7", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2023.10.7.tgz", + "integrity": "sha512-g49ijasEJvCd7ifmAY2D0wdEtt1xRjBbA33PJTiv8mKBr7DoMsPeISoJ8oQOTopSRi+FBWPpPW5ouDj2QPKtGA==", "dev": true, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "node_modules/@types/windows-foreground-love": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@types/windows-foreground-love/-/windows-foreground-love-0.3.0.tgz", + "integrity": "sha512-tFUVA/fiofNqOh6lZlymvQiQYPY+cZXZPR9mn9wN6/KS8uwx0zgH4Ij/jmFyRYr+x+DGZWEIeknS2BMi7FZJAQ==", + "dev": true + }, + "node_modules/@types/winreg": { + "version": "1.2.30", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", + "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg= sha512-c4m/hnOI1j34i8hXlkZzelE6SXfOqaTWhBp0UgBuwmpiafh22OpsE261Rlg//agZtQHIY5cMgbkX8bnthUFrmA==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@types/node": "*" } }, - "node_modules/charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "node_modules/@types/yazl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@types/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==", "dev": true, - "engines": { - "node": "*" + "dependencies": { + "@types/node": "*" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", + "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/type-utils": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">= 8.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.45.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "node_modules/@typescript-eslint/eslint-plugin/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": ">=10" + "node": ">= 4" } }, - "node_modules/chrome-remote-interface": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.0.tgz", - "integrity": "sha512-tv/SgeBfShXk43fwFpQ9wnS7mOCPzETnzDXTNxCb6TqKOiOeIfbrJz+2NAp8GmzwizpKa058wnU1Te7apONaYg==", + "node_modules/@typescript-eslint/parser": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", + "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", + "dev": true, + "license": "MIT", "dependencies": { - "commander": "2.11.x", - "ws": "^7.2.0" + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4" }, - "bin": { - "chrome-remote-interface": "bin/client.js" - } - }, - "node_modules/chrome-remote-interface/node_modules/commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" - }, - "node_modules/chrome-remote-interface/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { - "node": ">=8.3.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", + "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "@typescript-eslint/tsconfig-utils": "^8.45.0", + "@typescript-eslint/types": "^8.45.0", + "debug": "^4.3.4" }, "engines": { - "node": ">=6.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/chrome-trace-event/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true - }, - "node_modules/ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", + "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", "dev": true, + "license": "MIT", "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", + "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", "dev": true, + "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/@typescript-eslint/types": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", + "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", + "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", "dev": true, + "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "@typescript-eslint/project-service": "8.45.0", + "@typescript-eslint/tsconfig-utils": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/@typescript-eslint/utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", + "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", "dev": true, + "license": "MIT", "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", + "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", "dev": true, + "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "@typescript-eslint/types": "8.45.0", + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "node_modules/@typescript/native-preview": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview/-/native-preview-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-lvt9sECmBkrABxl3rMNRAX2unzhYcoNhlTyR7rOvbyM//QTXKUctVD7ByWBvk02et2caUUwIWq2vnygaeW8Mew==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "bin": { + "tsgo": "bin/tsgo.js" + }, + "optionalDependencies": { + "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260130.1", + "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260130.1", + "@typescript/native-preview-linux-arm": "7.0.0-dev.20260130.1", + "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260130.1", + "@typescript/native-preview-linux-x64": "7.0.0-dev.20260130.1", + "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260130.1", + "@typescript/native-preview-win32-x64": "7.0.0-dev.20260130.1" + } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/@typescript/native-preview-darwin-arm64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-arm64/-/native-preview-darwin-arm64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-Jo5kVoxaewKPn/3bKWyUB/gPR+Tjhj6isLc8VshV4OyFX4n6pkvVyk3ANivl7Kwmiv3WGKGUotbZ71DKCZATwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-darwin-x64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-darwin-x64/-/native-preview-darwin-x64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-dR0fjdcLykfiDOIKjZMGqPBHVl9Dd/C+jFU43Wr3dcPFPFf1oVYsaWAZBSkTXnN9QP8i0/ZV+ZUr1gDjoi3x0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@typescript/native-preview-linux-arm": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm/-/native-preview-linux-arm-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-wnx4bY/1u006U67fEkPtPVZ65VYMLgkFqOadGyrUxhtveR5WbbgFUuUBES0mPxvzS4ToZzn94jhcnAvN8VOTcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-arm64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-arm64/-/native-preview-linux-arm64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-P/1YTpIiFd2pPtHt4sKEmUTaKf1xvuuiV0TvhQ7n2gDYskNjZ66iWCC9w7okjgsmWE9JLh/IRrNcb9FKVk3SHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-linux-x64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-linux-x64/-/native-preview-linux-x64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-OgHVjivuOS22WIZvIm+Pnm7yqFLwonkIrBOxRdew/pPwVGLQVSo+bQ+RocQDj2VFYxXcHs2yXwCk3PDmwLIYYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@typescript/native-preview-win32-arm64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-arm64/-/native-preview-win32-arm64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-f/DUxQtIWkZq0eUjZHFmaSxterO/ccu1NxFk0L/Oqj7AfjWVDCqrLVgZJKjvwcG5TEb5AVt7GMUpGEAYZQiUvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@typescript/native-preview-win32-x64": { + "version": "7.0.0-dev.20260130.1", + "resolved": "https://registry.npmjs.org/@typescript/native-preview-win32-x64/-/native-preview-win32-x64-7.0.0-dev.20260130.1.tgz", + "integrity": "sha512-Isr051Cq8RbXOUMYYmwLYw8yBGaEG/Zp0sp7HNeYhVVkc3/3KeveEqCk29q1QRwiBr7HnApdzJP7f+lSZk8gmg==", + "cpu": [ + "x64" + ], "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/codicons": { + "version": "0.0.45-4", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-4.tgz", + "integrity": "sha512-uuWqpry+FcHAw1JDkXwEW0YIuTtX3n6KqSshNlvLUjuP92PSrfq99jW52AWJ7qeunmPvgKCaZOeSSLUqHRHjmw==", + "license": "CC-BY-4.0" + }, + "node_modules/@vscode/deviceid": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.4.tgz", + "integrity": "sha512-3u705VptsQhKMcHvUMJzaOn9fBrKEQHsl7iibRRVQ8kUNV+cptki7bQXACPNsGtJ5Dh4/7A7W1uKtP3z39GUQg==", + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "fs-extra": "^11.2.0", + "uuid": "^9.0.1" + } + }, + "node_modules/@vscode/gulp-electron": { + "version": "1.38.2", + "resolved": "https://registry.npmjs.org/@vscode/gulp-electron/-/gulp-electron-1.38.2.tgz", + "integrity": "sha512-uFMp6Utz2kf62NMXVIht09FfIcuAFLuw7b9xhJNm2iGaaAI3b2BBHP05cKG3LYIPGvkWoC7UNk4EjyQDO7T/ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@electron/get": "^4.0.1", + "@octokit/rest": "^22.0.0", + "event-stream": "3.3.4", + "gulp-filter": "^5.1.0", + "gulp-rename": "1.2.2", + "gulp-symdest": "^1.2.0", + "gulp-vinyl-zip": "^2.1.2", + "mkdirp": "^0.5.1", + "plist": "^3.0.1", + "progress": "^1.1.8", + "rcedit": "^4.0.1", + "rimraf": "^2.4.2", + "semver": "^7.7.2", + "sumchecker": "^3.0.1", + "temp": "^0.8.3", + "vinyl": "^3.0.0", + "vinyl-fs": "^3.0.3" }, "engines": { - "node": ">=12" + "node": ">=22" } }, - "node_modules/cliui/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 - }, - "node_modules/cliui/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==", + "node_modules/@vscode/gulp-electron/node_modules/@electron/get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@electron/get/-/get-4.0.1.tgz", + "integrity": "sha512-fTMFb/ZiK6xQace5YZlhT+vNR08ogat9SqpvwpaC9vD6hgx7ouz9cdcrSrFuNji4823Jmmy90/CDhJq0I4vRFA==", "dev": true, + "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "got": "^14.4.5", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" }, "engines": { - "node": ">=8" + "node": ">=22.12.0" + }, + "optionalDependencies": { + "global-agent": "^3.0.0" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "node_modules/@vscode/gulp-electron/node_modules/@electron/get/node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=0.4.0" } }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg= sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "node_modules/@vscode/gulp-electron/node_modules/@sindresorhus/is": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "node_modules/@vscode/gulp-electron/node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, + "license": "MIT", "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" + "defer-to-connect": "^2.0.1" }, "engines": { - "node": ">=6" + "node": ">=14.16" + } + }, + "node_modules/@vscode/gulp-electron/node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" } }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/@vscode/gulp-electron/node_modules/cacheable-request": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", + "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", "dev": true, + "license": "MIT", "dependencies": { - "isobject": "^3.0.1" + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.4", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.1", + "responselike": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/clone-deep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/@vscode/gulp-electron/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", + "node_modules/@vscode/gulp-electron/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-response": "^1.0.0" + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "dev": true - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "node_modules/@vscode/gulp-electron/node_modules/got": { + "version": "14.4.7", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", + "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", "dev": true, + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" + "@sindresorhus/is": "^7.0.1", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^12.0.1", + "decompress-response": "^6.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^3.0.0", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/cloneable-readable/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/@vscode/gulp-electron/node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, + "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "node_modules/@vscode/gulp-electron/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, + "license": "MIT", "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "dev": true - }, - "node_modules/code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "node_modules/@vscode/gulp-electron/node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw= sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", + "node_modules/@vscode/gulp-electron/node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "dev": true, - "dependencies": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "node_modules/@vscode/gulp-electron/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "minimist": "^1.2.5" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "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==", + "node_modules/@vscode/gulp-electron/node_modules/normalize-url": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", + "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "license": "MIT", "engines": { - "node": ">=7.0.0" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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 - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "node_modules/@vscode/gulp-electron/node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", "dev": true, - "bin": { - "color-support": "bin.js" + "license": "MIT", + "engines": { + "node": ">=14.16" } }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "node_modules/@vscode/gulp-electron/node_modules/rcedit": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-4.0.1.tgz", + "integrity": "sha512-bZdaQi34krFWhrDn+O53ccBDw0MkAT2Vhu75SqhtvhQu4OPyFM4RoVheyYiVQYdjhUi6EJMVWQ0tR6bCIYVkUg==", "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", - "dev": true - }, - "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==", + "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "cross-spawn-windows-exe": "^1.1.0" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/combined-stream/node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" + "node": ">= 14.0.0" } }, - "node_modules/command-line-args": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", - "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "node_modules/@vscode/gulp-electron/node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dev": true, + "license": "MIT", "dependencies": { - "array-back": "^3.1.0", - "find-replace": "^3.0.0", - "lodash.camelcase": "^4.3.0", - "typical": "^4.0.0" + "lowercase-keys": "^3.0.0" }, "engines": { - "node": ">=4.0.0" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "node_modules/@vscode/iconv-lite-umd": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", + "integrity": "sha512-tK6k0DXFHW7q5+GGuGZO+phpAqpxO4WXl+BLc/8/uOk3RsM2ssAL3CQUQDb1TGfwltjsauhN6S4ghYZzs4sPFw==", + "license": "MIT" + }, + "node_modules/@vscode/l10n-dev": { + "version": "0.0.35", + "resolved": "https://registry.npmjs.org/@vscode/l10n-dev/-/l10n-dev-0.0.35.tgz", + "integrity": "sha512-s6uzBXsVDSL69Z85HSqpc5dfKswQkeucY8L00t1TWzGalw7wkLQUKMRwuzqTq+AMwQKrRd7Po14cMoTcd11iDw==", "dev": true, - "engines": { - "node": "^12.20.0 || >=14" + "dependencies": { + "@azure-rest/ai-translation-text": "^1.0.0-beta.1", + "debug": "^4.3.4", + "deepmerge-json": "^1.5.0", + "glob": "^10.0.0", + "markdown-it": "^14.0.0", + "node-html-markdown": "^1.3.0", + "pseudo-localization": "^2.4.0", + "web-tree-sitter": "^0.20.8", + "xml2js": "^0.5.0", + "yargs": "^17.7.1" + }, + "bin": { + "vscode-l10n-dev": "dist/cli.js" } }, - "node_modules/comment-parser": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", - "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "node_modules/@vscode/l10n-dev/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/@vscode/l10n-dev/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, - "engines": [ - "node >= 0.8" - ], + "license": "ISC", "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/@vscode/l10n-dev/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/@vscode/l10n-dev/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/@vscode/native-watchdog": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz", + "integrity": "sha512-C2hsQFVYF2hBv7sa7OztRBimrMsEofGNh/lYs7MIPpKdhyJpYSpDb5iu/bilgLqSO61PLBCJ5xw6iFI21LI+9Q==", + "hasInstallScript": true, "license": "MIT" }, - "node_modules/config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", - "dev": true, + "node_modules/@vscode/policy-watcher": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@vscode/policy-watcher/-/policy-watcher-1.3.7.tgz", + "integrity": "sha512-OvIczTbtGLZs7YU0ResbjM0KEB2ORBnlJ4ICxaB9fKHNVBwNVp4i2qIkDQGp3UBGtu7P8/+eg4/ZKk2oJGFcug==", + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "bindings": "^1.5.0", + "node-addon-api": "^8.2.0" } }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, + "node_modules/@vscode/policy-watcher/node_modules/node-addon-api": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", + "integrity": "sha512-qnyuI2ROiCkye42n9Tj5aX1ns7rzj6n7zW1XReSnLSL9v/vbLeR6fJq6PU27YU/ICfYw6W7Ouk/N7cysWu/hlw==", "license": "MIT", "engines": { - "node": "^14.18.0 || >=16.10.0" + "node": "^18 || ^20 || >= 21" } }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, + "node_modules/@vscode/proxy-agent": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz", + "integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==", + "license": "MIT", "dependencies": { - "safe-buffer": "5.2.1" + "@tootallnate/once": "^3.0.0", + "agent-base": "^7.0.1", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", + "socks-proxy-agent": "^8.0.1", + "undici": "^7.2.0" }, "engines": { - "node": ">= 0.6" + "node": ">=22.15.0" + }, + "optionalDependencies": { + "@vscode/windows-ca-certs": "^0.3.1" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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" - } - ] + "node_modules/@vscode/ripgrep": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz", + "integrity": "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "https-proxy-agent": "^7.0.2", + "proxy-from-env": "^1.1.0", + "yauzl": "^2.9.2" + } }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" + "node_modules/@vscode/ripgrep/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, - "node_modules/convert-source-map": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", - "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, + "node_modules/@vscode/spdlog": { + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@vscode/spdlog/-/spdlog-0.15.7.tgz", + "integrity": "sha512-xpHAtw0IESD6wmjqLr6LbpYAmr8ZYm8AT7hGE7oM7AojNeOBngXLOqmzpXbTNTAvXBq1KHy8PwbMmY24uYR/oQ==", + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "safe-buffer": "~5.1.1" + "bindings": "^1.5.0", + "mkdirp": "^1.0.4", + "node-addon-api": "7.1.0" } }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" + "node_modules/@vscode/sqlite3": { + "version": "5.1.12-vscode", + "resolved": "https://registry.npmjs.org/@vscode/sqlite3/-/sqlite3-5.1.12-vscode.tgz", + "integrity": "sha512-WLTftbMtK3Ni0s+q46qtKJ2CFtA3YrS5N4GcrETDCxqNTQAvk1LlYlG3RwGE6vZLcUqPt3TCHobijYeNUhEQ9Q==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "node-addon-api": "^8.2.0", + "tar": "^7.5.4" } }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "node_modules/@vscode/sqlite3/node_modules/node-addon-api": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", + "integrity": "sha512-qnyuI2ROiCkye42n9Tj5aX1ns7rzj6n7zW1XReSnLSL9v/vbLeR6fJq6PU27YU/ICfYw6W7Ouk/N7cysWu/hlw==", "license": "MIT", "engines": { - "node": ">=6.6.0" + "node": "^18 || ^20 || >= 21" } }, - "node_modules/cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "node_modules/@vscode/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw==", + "license": "MIT" + }, + "node_modules/@vscode/telemetry-extractor": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@vscode/telemetry-extractor/-/telemetry-extractor-1.10.2.tgz", + "integrity": "sha512-hn+KDSwIRj7LzDSFd9HALkc80UY1g16nQgWztHml+nxAZU3Hw/EoWEEDxOncvDYq9YcV+tX/cVHrVjbNL2Dg0g==", "dev": true, "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" + "@vscode/ripgrep": "^1.15.9", + "command-line-args": "^5.2.1", + "ts-morph": "^19.0.0" + }, + "bin": { + "vscode-telemetry-extractor": "out/extractor.js" + } + }, + "node_modules/@vscode/test-cli": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.6.tgz", + "integrity": "sha512-4i61OUv5PQr3GxhHOuUgHdgBDfIO/kXTPCsEyFiMaY4SOqQTgkTmyZLagHehjOgCfsXdcrJa3zgQ7zoc+Dh6hQ==", + "dev": true, + "dependencies": { + "@types/mocha": "^10.0.2", + "c8": "^9.1.0", + "chokidar": "^3.5.3", + "enhanced-resolve": "^5.15.0", + "glob": "^10.3.10", + "minimatch": "^9.0.3", + "mocha": "^10.2.0", + "supports-color": "^9.4.0", + "yargs": "^17.7.2" }, - "engines": { - "node": ">= 0.8" + "bin": { + "vscode-test": "out/bin.mjs" } }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "node_modules/@vscode/test-cli/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" } }, - "node_modules/copy-props": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", - "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", + "node_modules/@vscode/test-cli/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { - "each-props": "^1.3.2", - "is-plain-object": "^5.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "node_modules/@vscode/test-cli/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">= 14.15.0" + "node": ">=16 || 14 >=14.17" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/@vscode/test-cli/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, + "license": "ISC", "engines": { - "node": ">=10.13.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.1.3", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", - "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", + "node_modules/@vscode/test-electron": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.4.0.tgz", + "integrity": "sha512-yojuDFEjohx6Jb+x949JRNtSn6Wk2FAh4MldLE3ck9cfvCqzwxF32QsNy1T9Oe4oT+ZfFcg0uPUCajJzOmPlTA==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.11", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.4", + "jszip": "^3.10.1", + "ora": "^7.0.1", + "semver": "^7.6.2" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=16" } }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "node_modules/@vscode/test-web": { + "version": "0.0.76", + "resolved": "https://registry.npmjs.org/@vscode/test-web/-/test-web-0.0.76.tgz", + "integrity": "sha512-hB+GKNmxnaTKemNOOBUcqYsIa5a0uuccCRnNIdCMS+I3RhVlyCtLBl29ZN/RAB2+M+ujjI8L8qL6GLCPqNFIBg==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "@koa/cors": "^5.0.0", + "@koa/router": "^14.0.0", + "@playwright/browser-chromium": "^1.56.1", + "gunzip-maybe": "^1.4.2", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "koa": "^3.1.1", + "koa-morgan": "^1.0.1", + "koa-mount": "^4.2.0", + "koa-static": "^5.0.0", + "minimist": "^1.2.8", + "playwright": "^1.56.1", + "tar-fs": "^3.1.1", + "tinyglobby": "^0.2.15", + "vscode-uri": "^3.1.0" + }, + "bin": { + "vscode-test-web": "out/server/index.js" }, "engines": { - "node": ">= 0.10" + "node": ">=20" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "node_modules/@vscode/tree-sitter-wasm": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.0.tgz", + "integrity": "sha512-4kjB1jgLyG9VimGfyJb1F8/GFdrx55atsBCH/9r2D/iZHAUDCvZ5zhWXB7sRQ2z2WkkuNYm/0pgQtUm1jhdf7A==", + "license": "MIT" + }, + "node_modules/@vscode/v8-heap-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@vscode/v8-heap-parser/-/v8-heap-parser-0.1.0.tgz", + "integrity": "sha512-3EvQak7EIOLyIGz+IP9qSwRmP08ZRWgTeoRgAXPVkkDXZ8riqJ7LDtkgx++uHBiJ3MUaSdlUYPZcLFFw7E6zGg==", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/@vscode/vscode-languagedetection": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.21.tgz", + "integrity": "sha512-zSUH9HYCw5qsCtd7b31yqkpaCU6jhtkKLkvOOA8yTrIRfBSOFb8PPhgmMicD7B/m+t4PwOJXzU1XDtrM9Fd3/g==", + "bin": { + "vscode-languagedetection": "cli/index.js" + } + }, + "node_modules/@vscode/vscode-perf": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@vscode/vscode-perf/-/vscode-perf-0.0.19.tgz", + "integrity": "sha512-E/I0S+71K3Jo4kiMYbeKM8mUG3K8cHlj5MFVfPYVAvlp7KuIZTM914E7osp+jx8XgMLN6fChxnFmntm1GtVrKA==", + "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "chalk": "^4.x", + "commander": "^9.4.0", + "cookie": "^0.7.2", + "js-base64": "^3.7.4", + "node-fetch": "2.6.8", + "playwright": "^1.29.2" + }, + "bin": { + "vscode-perf": "bin/vscode-perf" }, "engines": { - "node": ">= 8" + "node": ">= 16" } }, - "node_modules/cross-spawn-windows-exe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", - "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" - } + "node_modules/@vscode/windows-ca-certs": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", + "hasInstallScript": true, + "license": "BSD", + "optional": true, + "os": [ + "win32" ], - "license": "Apache-2.0", "dependencies": { - "@malept/cross-spawn-promise": "^1.1.0", - "is-wsl": "^2.2.0", - "which": "^2.0.2" - }, + "node-addon-api": "^8.2.0" + } + }, + "node_modules/@vscode/windows-ca-certs/node_modules/node-addon-api": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.2.0.tgz", + "integrity": "sha512-qnyuI2ROiCkye42n9Tj5aX1ns7rzj6n7zW1XReSnLSL9v/vbLeR6fJq6PU27YU/ICfYw6W7Ouk/N7cysWu/hlw==", + "license": "MIT", + "optional": true, "engines": { - "node": ">= 10" + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/@vscode/windows-mutex": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@vscode/windows-mutex/-/windows-mutex-0.5.3.tgz", + "integrity": "sha512-hWNmD+AzINR57jWuc/iW53kA+BghI4iOuicxhAEeeJLPOeMm9X5IUD0ttDwJFEib+D8H/2T9pT/8FeB/xcqbRw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "7.1.0" + } + }, + "node_modules/@vscode/windows-process-tree": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.6.3.tgz", + "integrity": "sha512-mjirLbtgjv7P6fwD8gx7iaY961EfGqUExGvfzsKl3spLfScg57ejlMi+7O1jfJqpM2Zly9DTSxyY4cFsDN6c9Q==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "7.1.0" + } + }, + "node_modules/@vscode/windows-registry": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.1.3.tgz", + "integrity": "sha512-si8+b+2Wh0x2X6W2+kgDyLJD9hyGIrjUo1X/7RWlvsxyI5+Pg+bpdHJrVYtIW4cHOPVB0FYFaN1UZndbUbU5lQ==", + "hasInstallScript": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, - "node_modules/cross-spawn-windows-exe/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" } }, - "node_modules/cross-spawn-windows-exe/node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "node_modules/crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "engines": { - "node": "*" + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" } }, - "node_modules/css": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", - "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "inherits": "^2.0.4", - "source-map": "^0.6.1", - "source-map-resolve": "^0.6.0" + "@xtuc/long": "4.2.2" } }, - "node_modules/css-declaration-sorter": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", - "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "ISC", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } + "license": "MIT" }, - "node_modules/css-loader": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", - "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, + "license": "MIT", "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.4", - "postcss-modules-scope": "^3.1.1", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, - "node_modules/css-select": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", - "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, + "license": "MIT", "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, + "license": "MIT", "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" } }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", + "node_modules/@webgpu/types": { + "version": "0.1.66", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.66.tgz", + "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" + "node": ">=14.15.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" + "engines": { + "node": ">=14.15.0" }, "peerDependencies": { - "postcss": "^8.4.31" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=14.15.0" }, "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" + "webpack": "5.x.x", + "webpack-cli": "5.x.x" }, - "engines": { - "node": ">=8.0.0" + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } } - }, - "node_modules/csstype": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz", - "integrity": "sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==", - "dev": true, - "license": "MIT" - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "dev": true, - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "node_modules/@xterm/addon-clipboard": { + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.109.tgz", + "integrity": "sha512-iaKd86bsBYG2PgF6gAiYUPHxFW/LX8ERJfMBCiASQo9C7U/gPJgoiGEZikydf3AllnQFHku+4Kdf7lia6ci6tA==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "js-base64": "^3.7.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "node_modules/@xterm/addon-image": { + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.109.tgz", + "integrity": "sha512-O5A4tkxiT4D5yz+brgLlLR2he7NDVmLK+rl1e53nhaUXABHZUEonqqKMq3OPRc+/PeuEU6CH4dq+v49Pwmgfpw==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" + } + }, + "node_modules/@xterm/addon-ligatures": { + "version": "0.11.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.109.tgz", + "integrity": "sha512-nGiw2sAyGEeF72+92EHlwa3J8MOamuFVTTv7PYpJMZi75ejn1OtRF+/cWelBaHx4/aQr1nXsfsJPXu+g8FQrSg==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" + "lru-cache": "^6.0.0", + "opentype.js": "^0.8.0" }, "engines": { - "node": ">= 0.4" + "node": ">8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "license": "MIT", + "node_modules/@xterm/addon-ligatures/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "yallist": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/debounce": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", - "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", - "dev": true + "node_modules/@xterm/addon-ligatures/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "node_modules/@xterm/addon-progress": { + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.109.tgz", + "integrity": "sha512-yz/wfO7dNh+hYUP1d9Vry3EZOinJcQscdNPyfDFLe7AgL/me+BNRcYaAyYOv5ZN+vvXOZSmCa58grNnZlMVXjA==", "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/debug-fabulous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", - "dev": true, - "dependencies": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" + "node_modules/@xterm/addon-search": { + "version": "0.17.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.109.tgz", + "integrity": "sha512-RQ1bMQIWOXJ3rOEiTLidxjTnpHgFhgIj2a45W6VKo3c+ILBNcC5x/tUlcM+BgnyT62aMNgmGRUIlW5qv7aTxRA==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/debug-fabulous/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" + "node_modules/@xterm/addon-serialize": { + "version": "0.15.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.109.tgz", + "integrity": "sha512-qUY7mnGX7BnbfvgFirdUAUDIJyHPN+2U849w4Z8yLO9Q9t0eLcufHlwbXeVAOnNwM3pSI3Ohz5vXsWhmJrfSrQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/@xterm/addon-unicode11": { + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.109.tgz", + "integrity": "sha512-M++pTg4rF7OW7OhrY52m80pB2DRU8/bhmd4rIRlNWeuWlNmLOljjA5a7HrjNCV+PUBnK+6/BRIhO1Rt6uWFNvA==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "dev": true, - "engines": { - "node": ">=0.10" + "node_modules/@xterm/addon-webgl": { + "version": "0.20.0-beta.108", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.108.tgz", + "integrity": "sha512-d4wscQpbiTgrOGiL29QxG9ulUkV5UQiz6L20o53C2QQVAJqdqhXP716ihbKirnlYlXi+ndB2Ox4fyn0ohU/fug==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^6.1.0-beta.109" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/@xterm/headless": { + "version": "6.1.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.109.tgz", + "integrity": "sha512-oESLjJpJ5JsSyZpj10JJKYDPm+irdKAKogp6XKkks2RxU8YfH+pr99gR3kPi6ppz+MPmEqDh4bejMhXEe4/twQ==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/@xterm/xterm": { + "version": "6.1.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.109.tgz", + "integrity": "sha512-3MioB2ZPzf4Wli4W7rZNCvpSakCf/7FktoNqxrckKfeusTMtbFUgH1MZKVZ5yAy3DCv0ASOzDTGM0ELZDO7XWA==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/deemon": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.6.tgz", - "integrity": "sha512-+/fizwuGaBl+DwvFgP5I0+cYXCPHrjCBzFegm2xIcVmkC2sPTxK5KRwIVtyY0kIngoqwf9bENDkFEpfjMr0H2g==", + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "dependencies": { - "bl": "^4.0.2", - "tree-kill": "^1.2.2" - }, "bin": { - "deemon": "src/deemon.js" + "acorn": "bin/acorn" }, "engines": { - "node": ">=22" + "node": ">=0.4.0" } }, - "node_modules/deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", - "dev": true - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/acorn-import-phases": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.3.tgz", + "integrity": "sha512-jtKLnfoOzm28PazuQ4dVBcE9Jeo6ha1GAJvq3N0LlNOszmTfx+wSycBehn+FN0RnyeR77IBxN/qVYMw0Rlj0Xw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=4.0.0" + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.1.0.tgz", - "integrity": "sha512-/TnecbwXEdycfbsM2++O3eGiatEFHjjNciHEwJclM+T5Kd94qD1AP+2elP/Mq0L5b9VZJao5znR01Mz6eX8Seg==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 14" } }, - "node_modules/deepmerge-json": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/deepmerge-json/-/deepmerge-json-1.5.0.tgz", - "integrity": "sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "engines": { - "node": ">=4.0.0" + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/default-browser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "license": "MIT", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" + "ajv": "^8.0.0" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, - "node_modules/default-browser-id": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "license": "MIT", - "engines": { - "node": ">=18" + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/default-compare": { + "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "dependencies": { - "kind-of": "^5.0.2" - }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "ajv": "^6.9.1" } }, - "node_modules/default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", + "node_modules/amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">=0.4.2" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "node_modules/ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=6" } }, - "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==", + "node_modules/ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", + "dev": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "ansi-wrap": "0.1.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE= sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "dev": true, "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "ansi-wrap": "0.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "node_modules/ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", "dev": true, "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" + "ansi-wrap": "0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", - "integrity": "sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw= sha512-Si7mB08fdumvLNFddq3HQOoYf8BUxfITyZi+0RBn1sbojFm8c4gD1+3se7qVEji1uiVVLYE0Np0laaS9E+j6ag==", + "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, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "dev": true - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "node_modules/ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, + "dependencies": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768= sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50= sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", - "dev": true, - "engines": { - "node": ">=4" - } + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8= sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, "engines": { - "node": ">=8" + "node": ">= 8" } }, - "node_modules/detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", + "node_modules/append-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", + "integrity": "sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", "dev": true, + "dependencies": { + "buffer-equal": "^1.0.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", - "dev": true, - "optional": true - }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": ">=14" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" + "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 }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/arr-filter": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", + "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4= sha512-A2BETWCqhsecSvCkWAeVBFLH6sXEUGASuzkpjL3GR1SlL/PWL6M3J8EAAld2Uubmh39tvkJTqC9LeLHCUKmFXA==", "dev": true, "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "make-iterator": "^1.0.0" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "node_modules/arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ= sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", "dev": true, "dependencies": { - "domelementtype": "^2.3.0" + "make-iterator": "^1.0.0" }, "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/domutils": { + "node_modules/arr-union": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", "dev": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "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==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==", - "dev": true - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", "dev": true, - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "engines": { + "node": ">=6" } }, - "node_modules/duplexify/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8= sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", "dev": true, - "dependencies": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/each-props/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/array-initial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", + "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U= sha512-BC4Yl89vneCYfpLrs5JU2aAu9/a+xWbeKhvISg9PT7eWFB9UlRvI+rKEtk6mgxWr3dSkk9gQ8hCrdqt06NXPdw==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "array-slice": "^1.0.0", + "is-number": "^4.0.0" }, "engines": { "node": ">=0.10.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 - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" + "node_modules/array-initial/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/editorconfig": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", - "integrity": "sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ==", + "node_modules/array-last": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", + "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", "dev": true, "dependencies": { - "@types/node": "^10.11.7", - "@types/semver": "^5.5.0", - "commander": "^2.19.0", - "lru-cache": "^4.1.3", - "semver": "^5.6.0", - "sigmund": "^1.0.1" + "is-number": "^4.0.0" }, - "bin": { - "editorconfig": "bin/editorconfig" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/editorconfig/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "node_modules/array-last/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", "dev": true, - "license": "MIT" - }, - "node_modules/editorconfig/node_modules/@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", - "dev": true - }, - "node_modules/editorconfig/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/editorconfig/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true, - "bin": { - "semver": "bin/semver" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/electron": { - "version": "37.7.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-37.7.0.tgz", - "integrity": "sha512-LBzvfrS0aalynOsnC11AD7zeoU8eOois090mzLpQM3K8yZ2N04i2ZW9qmHOTFLrXlKvrwRc7EbyQf1u8XHMl6Q==", + "node_modules/array-sort": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", + "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", "dev": true, - "hasInstallScript": true, - "license": "MIT", "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" }, "engines": { - "node": ">= 12.20.55" + "node": ">=0.10.0" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.254", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", - "integrity": "sha512-DcUsWpVhv9svsKRxnSCZ86SjD+sp32SGidNB37KpqXJncp1mfUgKbHvBomE89WJDbfVKw1mdv5+ikrvd43r+Bg==", + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", "dev": true, "engines": { - "node": ">= 4" + "node": ">=0.10.0" } }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "dev": true, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "node_modules/asar": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/asar/-/asar-3.0.3.tgz", + "integrity": "sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw==", + "deprecated": "Please use @electron/asar moving forward. There is no API change, just a package name change", + "dev": true, "dependencies": { - "once": "^1.4.0" + "chromium-pickle-js": "^0.2.0", + "commander": "^5.0.0", + "glob": "^7.1.6", + "minimatch": "^3.0.4" + }, + "bin": { + "asar": "bin/asar.js" + }, + "engines": { + "node": ">=10.12.0" + }, + "optionalDependencies": { + "@types/glob": "^7.1.1" } }, - "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "node_modules/asar/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, "engines": { - "node": ">=10.13.0" + "node": ">= 6" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/asar/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=0.12" + "node": "*" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/env-paths": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", - "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "node_modules/async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", "dev": true, - "bin": { - "envinfo": "dist/cli.js" + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "node_modules/async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", + "dev": true + }, + "node_modules/async-settle": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", + "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs= sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==", "dev": true, "dependencies": { - "prr": "~1.0.1" + "async-done": "^1.2.2" }, - "bin": { - "errno": "cli.js" + "engines": { + "node": ">= 0.10" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" } }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -9180,699 +4589,692 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "engines": { - "node": ">= 0.4" - } + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", + "dev": true }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "license": "MIT", + "node_modules/bach": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", + "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA= sha512-bZOOfCb3gXBXbTFXq3OZtGR88LwGeJvzu6szttaIzymOTS4ZttBNOWSv7aLZja2EMycKtRYV0Oa8SNKH/zkxvg==", + "dev": true, "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" + "arr-filter": "^1.1.1", + "arr-flatten": "^1.0.1", + "arr-map": "^2.0.0", + "array-each": "^1.0.0", + "array-initial": "^1.0.0", + "array-last": "^1.1.1", + "async-done": "^1.2.2", + "async-settle": "^1.0.0", + "now-and-later": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">= 0.10" } }, - "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "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==", - "license": "MIT", + "node_modules/bare-events": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", + "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/bare-fs": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.0.2.tgz", + "integrity": "sha512-S5mmkMesiduMqnz51Bfh0Et9EX0aTCJxhsI4bvzFFLs8Z1AV8RDHadfY5CyLwdoLHgXbNBEN1gQcbEtGwuvixw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "dependencies": { - "es-errors": "^1.3.0" + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4" }, "engines": { - "node": ">= 0.4" + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, - "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==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, + "node_modules/bare-os": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz", + "integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "engines": { - "node": ">= 0.4" + "bare": ">=1.14.0" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "license": "MIT", + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "bare-os": "^3.0.1" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "license": "MIT", + "node_modules/bare-stream": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz", + "integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "streamx": "^2.21.0" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } } }, - "node_modules/es5-ext": { - "version": "0.10.63", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.63.tgz", - "integrity": "sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ==", + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dev": true, - "hasInstallScript": true, "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "esniff": "^2.0.1", - "next-tick": "^1.1.0" + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" }, "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "optional": true - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c= sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY= sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "dev": true, "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "dev": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } + "license": "Apache-2.0" }, - "node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "node": "*" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "node_modules/binaryextensions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", + "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U= sha512-xnG0l4K3ghM62rFzDi2jcNEuICl6uQ4NgvGpqQsY7HgW8gPDeAWGOxHI/k+qZfXfMANytzrArGNPXidaCwtbmA==", + "dev": true }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" } }, - "node_modules/eslint": { - "version": "9.36.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", - "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", - "dev": true, - "license": "MIT", + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.36.0", - "@eslint/plugin-kit": "^0.3.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/eslint-formatter-compact": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/eslint-formatter-compact/-/eslint-formatter-compact-8.40.0.tgz", - "integrity": "sha512-cwGUs113TgmTQXecx5kfRjB7m0y2wkDLSadPTE2pK6M/wO4N8PjmUaoWOFNCP9MHgsiZwgqd5bZFnDCnszC56Q==", + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24= sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/boolean": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.0.2.tgz", + "integrity": "sha512-RwywHlpCRc3/Wh81MiCKun4ydaIFyW5Ea6JbL6sRCVx5q5irDw7pMXBUFYF/jArQ6YrG36q0kpovc9P/Kd3I4g==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } + "optional": true }, - "node_modules/eslint-plugin-header": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", - "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "peerDependencies": { - "eslint": ">=7.7.0" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/eslint-plugin-jsdoc": { - "version": "50.3.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz", - "integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "@es-joy/jsdoccomment": "~0.48.0", - "are-docs-informative": "^0.0.2", - "comment-parser": "1.4.1", - "debug": "^4.3.6", - "escape-string-regexp": "^4.0.0", - "espree": "^10.1.0", - "esquery": "^1.6.0", - "parse-imports": "^2.1.1", - "semver": "^7.6.3", - "spdx-expression-parse": "^4.0.0", - "synckit": "^0.9.1" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + "node": ">=8" } }, - "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/browserify-zlib": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz", + "integrity": "sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ==", "dev": true, "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "pako": "~0.2.0" } }, - "node_modules/eslint-plugin-local": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-local/-/eslint-plugin-local-6.0.0.tgz", - "integrity": "sha512-pvy/pTTyanEKAqpYqy/SLfd4TdiAQ/yFO+GRXDGvGQa2vEUGtmlEjmWQXBDGSk790j4nrAB/7ipqPQY3nLduDg==", - "deprecated": "Since the coming of ESLint flat config file, you can specify local rules without the need of this package. For running ESLint rule unit tests, use eslint-rule-tester instead", + "node_modules/browserslist": { + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "@thisismanta/pessimist": "^1.2.0", - "chalk": "^4.0.0" + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { - "eslint-plugin-local": "executable.js" + "browserslist": "cli.js" }, - "peerDependencies": { - "eslint": ">=9.0.0" + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "license": "MIT", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + "node": "*" + } + }, + "node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74= sha512-tcBWO2Dl4e7Asr9hTGcpVrCe+F7DubpmqWCTbj4FHLmjqO2hIaC383acQubWtRJhdceqs5uBHs6Es+Sk//RKiQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "run-applescript": "^7.0.0" }, - "bin": { - "resolve": "bin/resolve" + "engines": { + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" } }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "node_modules/c8": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", + "integrity": "sha512-mBWcT5iqNir1zIkzSPyI3NCR9EZCVI3WUD+AVO17MVWTSFNyUueXE82qTeampNtTr+ilN/5Ua3j24LgbCKjDVg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=14.14.0" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=10.6.0" } }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/cacheable-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.2.tgz", + "integrity": "sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==", "dev": true, - "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" } }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "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==", "dev": true, - "license": "Apache-2.0", + "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" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 0.4" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "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": { - "is-glob": "^4.0.3" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" } }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/esniff": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", - "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.62", - "event-emitter": "^0.3.5", - "type": "^2.7.2" - }, + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=6" } }, - "node_modules/esniff/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "dev": true - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/caniuse-lite": { + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, - "license": "Apache-2.0", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">=10" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "node_modules/chalk/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, - "dependencies": { - "estraverse": "^5.1.0" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/chalk/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, "engines": { - "node": ">=4.0" + "node": "*" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">= 0.6" + "node": ">=18" } }, - "node_modules/event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", - "dev": true, + "node_modules/chrome-remote-interface": { + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.0.tgz", + "integrity": "sha512-tv/SgeBfShXk43fwFpQ9wnS7mOCPzETnzDXTNxCb6TqKOiOeIfbrJz+2NAp8GmzwizpKa058wnU1Te7apONaYg==", "dependencies": { - "d": "1", - "es5-ext": "~0.10.14" + "commander": "2.11.x", + "ws": "^7.2.0" + }, + "bin": { + "chrome-remote-interface": "bin/client.js" } }, - "node_modules/event-stream": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", - "dev": true, - "dependencies": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } + "node_modules/chrome-remote-interface/node_modules/commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==" }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", + "node_modules/chrome-remote-interface/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "engines": { - "node": ">=6" + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "node_modules/chrome-trace-event": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", + "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/eventsource": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", - "license": "MIT", "dependencies": { - "eventsource-parser": "^3.0.1" + "tslib": "^1.9.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=6.0" } }, - "node_modules/eventsource-parser": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } + "node_modules/chrome-trace-event/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/chromium-pickle-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", + "integrity": "sha1-BKEGZywYsIWrd02YPfo+oTjyIgU= sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "dev": true }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI= sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", + "node_modules/ci-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", + "dev": true + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, "dependencies": { - "debug": "^2.3.3", + "arr-union": "^3.1.0", "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { + "node_modules/class-utils/node_modules/define-property": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", @@ -9884,19 +5286,7 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "node_modules/class-utils/node_modules/is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", @@ -9909,7 +5299,7 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", @@ -9921,7 +5311,7 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "node_modules/class-utils/node_modules/is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", @@ -9934,7 +5324,7 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", @@ -9946,7 +5336,7 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-descriptor": { + "node_modules/class-utils/node_modules/is-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", @@ -9960,2725 +5350,2511 @@ "node": ">=0.10.0" } }, - "node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "dev": true, "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.0", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "restore-cursor": "^4.0.0" }, "engines": { - "node": ">= 18" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "license": "MIT", + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, "engines": { - "node": ">= 16" + "node": ">=6" }, "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "dependencies": { - "mime-db": "^1.54.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" + "node": ">=12" } }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "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/cliui/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 }, - "node_modules/express/node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", - "license": "MIT", + "node_modules/cliui/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, "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ext": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", - "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "dev": true, - "dependencies": { - "type": "^2.0.0" + "node": ">=8" } }, - "node_modules/ext/node_modules/type": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", - "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", - "dev": true - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=0.8" } }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg= sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY= sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "dependencies": { - "is-descriptor": "^1.0.0" + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "isobject": "^3.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/clone-deep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" + "mimic-response": "^1.0.0" } }, - "node_modules/extract-zip/node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", "dev": true, "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" } }, - "node_modules/fancy-log": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", - "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", + "node_modules/cloneable-readable/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "dependencies": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "parse-node-version": "^1.0.0", - "time-stamp": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "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==" - }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", "dev": true }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw= sha512-5D2XXSpkOnleOI21TG7p3T0bGAsZ/XknZpKBmGYyluO8pw4zA3K8ZlrBIbC4FXg3m6z/RNFiUFfT2sQK01+UHA==", "dev": true, "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.4" + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" }, "engines": { - "node": ">=8.6.0" + "node": ">=0.10.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==", + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "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==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" + "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, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "node_modules/fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "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 }, - "node_modules/fastq": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", - "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" + "bin": { + "color-support": "bin.js" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true + }, + "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, "dependencies": { - "pend": "~1.2.0" + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/combined-stream/node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "license": "MIT", "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "node": ">=0.4.0" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", "dev": true, "dependencies": { - "flat-cache": "^4.0.0" + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=4.0.0" } }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "node": "^12.20.0 || >=14" } }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 12.0.0" } }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, + "engines": [ + "node >= 0.8" + ], "dependencies": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "node_modules/config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" + "ini": "^1.3.4", + "proto-list": "~1.2.1" } }, - "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, - "node_modules/find-parent-dir": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", - "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", - "dev": true - }, - "node_modules/find-replace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", - "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "node_modules/convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", "dev": true, "dependencies": { - "array-back": "^3.0.1" - }, - "engines": { - "node": ">=4.0.0" + "safe-buffer": "~5.1.1" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.6" } }, - "node_modules/findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", "dev": true, + "license": "MIT", "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" + "depd": "~2.0.0", + "keygrip": "~1.1.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.8" + } + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/findup-sync/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/copy-props": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.5.tgz", + "integrity": "sha512-XBlx8HSqrT0ObQwmSzM7WE5k8FxTV75h1DX1Z3n6NhQ/UYYAvInWYmG06vFt7hQZArE2fuO62aihiWIVQwh1sw==", "dev": true, "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "each-props": "^1.3.2", + "is-plain-object": "^5.0.0" } }, - "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" } }, - "node_modules/findup-sync/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.3.tgz", + "integrity": "sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/findup-sync/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/findup-sync/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 8" } }, - "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/cross-spawn-windows-exe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz", + "integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==", "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/malept" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund" + } + ], + "license": "Apache-2.0", "dependencies": { - "is-buffer": "^1.1.5" + "@malept/cross-spawn-promise": "^1.1.0", + "is-wsl": "^2.2.0", + "which": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 10" } }, - "node_modules/findup-sync/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/cross-spawn-windows-exe/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/findup-sync/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/cross-spawn-windows-exe/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "is-docker": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/findup-sync/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", "dev": true, "dependencies": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-loader": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.9.1.tgz", + "integrity": "sha512-OzABOh0+26JKFdMzlK6PY1u5Zx8+Ck7CVRlcGNZoY9qwJjdfu2VWFuprTIpPW+Av5TZTVViYWcFQaEEQURLknQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">= 0.10" + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "node_modules/fined/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "dev": true, - "license": "MIT", "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "node_modules/flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, "bin": { - "flat": "cli.js" + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", "dev": true, "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "css-tree": "^1.1.2" }, "engines": { - "node": ">=16" + "node": ">=8.0.0" } }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", "dev": true, "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" + "es5-ext": "^0.10.50", + "type": "^1.0.1" } }, - "node_modules/flush-write-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "node_modules/debounce": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.1.0.tgz", + "integrity": "sha512-ZQVKfRVlwRfD150ndzEK8M90ABT+Y/JQKs4Y7U4MXdpuoUkkrr4DwKbVux3YjylA5bUMUj0Nc3pMxPJX6N2QQQ==", + "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "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": ">=4.0" + "node": ">=6.0" }, "peerDependenciesMeta": { - "debug": { + "supports-color": { "optional": true } } }, - "node_modules/font-finder": { + "node_modules/debug-fabulous": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz", - "integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==", - "license": "MIT", - "dependencies": { - "get-system-fonts": "^2.0.0", - "promise-stream-reader": "^1.0.1" - }, - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/font-ligatures": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz", - "integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", + "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", + "dev": true, "dependencies": { - "font-finder": "^1.0.3", - "lru-cache": "^6.0.0", - "opentype.js": "^0.8.0" - }, - "engines": { - "node": ">8.0.0" + "debug": "3.X", + "memoizee": "0.4.X", + "object-assign": "4.X" } }, - "node_modules/font-ligatures/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", + "node_modules/debug-fabulous/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "ms": "^2.1.1" } }, - "node_modules/font-ligatures/node_modules/yallist": { + "node_modules/decamelize": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.10" } }, - "node_modules/for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", - "dev": true, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "dependencies": { - "for-in": "^1.0.1" + "mimic-response": "^3.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "node_modules/deemon": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/deemon/-/deemon-1.13.6.tgz", + "integrity": "sha512-+/fizwuGaBl+DwvFgP5I0+cYXCPHrjCBzFegm2xIcVmkC2sPTxK5KRwIVtyY0kIngoqwf9bENDkFEpfjMr0H2g==", + "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" + "bl": "^4.0.2", + "tree-kill": "^1.2.2" + }, + "bin": { + "deemon": "src/deemon.js" }, "engines": { - "node": ">= 6" + "node": ">=22" } }, - "node_modules/form-data-encoder": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", - "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, - "node_modules/formdata-node": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", - "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", - "license": "MIT", - "dependencies": { - "node-domexception": "1.0.0", - "web-streams-polyfill": "4.0.0-beta.3" - }, - "engines": { - "node": ">= 12.20" - } + "license": "MIT" }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, - "node_modules/fraction.js": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", - "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/rawify" - } + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", + "node_modules/deepmerge": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.1.0.tgz", + "integrity": "sha512-/TnecbwXEdycfbsM2++O3eGiatEFHjjNciHEwJclM+T5Kd94qD1AP+2elP/Mq0L5b9VZJao5znR01Mz6eX8Seg==", "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "node_modules/deepmerge-json": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/deepmerge-json/-/deepmerge-json-1.5.0.tgz", + "integrity": "sha512-jZRrDmBKjmGcqMFEUJ14FjMJwm05Qaked+1vxaALRtF0UAl7lPU8OLWXFxvoeg3jbQM249VPFVn8g2znaQkEtA==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=4.0.0" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">=14.14" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fs-minipass/node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "node_modules/default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" + "kind-of": "^5.0.2" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/fs-minipass/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "node_modules/default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= sha512-2xaP6GiwVwOEbXCGoJ4ufgC76m8cj805jrghScewJC2ZDsb9U0b4BIrba+xt/Uytyd0HvQ6+WymSRTfnYj59GQ==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - }, "engines": { "node": ">= 0.10" } }, - "node_modules/fs-mkdirp-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=10" } }, - "node_modules/fs-mkdirp-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "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": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "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/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "deprecated": "This package is no longer supported.", + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" + "object-keys": "^1.0.12" }, "engines": { - "node": ">=0.6" + "node": ">= 0.4" } }, - "node_modules/fstream/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, + "node_modules/delayed-stream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", + "integrity": "sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw= sha512-Si7mB08fdumvLNFddq3HQOoYf8BUxfITyZi+0RBn1sbojFm8c4gD1+3se7qVEji1uiVVLYE0Np0laaS9E+j6ag==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.4.0" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" }, - "node_modules/gaxios": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", - "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "is-stream": "^2.0.0", - "node-fetch": "^2.6.9", - "uuid": "^9.0.1" - }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "engines": { - "node": ">=14" + "node": ">= 0.8" } }, - "node_modules/gaxios/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/gaxios/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/gcp-metadata": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", - "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^6.1.1", - "google-logging-utils": "^0.0.2", - "json-bigint": "^1.0.0" - }, + "node_modules/detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50= sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", + "dev": true, "engines": { - "node": ">=14" + "node": ">=4" } }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "license": "MIT", + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/detect-newline": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", + "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==", "dev": true, "engines": { - "node": ">=6.9.0" + "node": ">=0.10.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "node_modules/detect-node": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", + "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "dev": true, + "optional": true + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": "6.* || 8.* || >= 10.*" + "node": ">=0.3.1" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, "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", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" + "path-type": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "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==", - "license": "MIT", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/get-stdin": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", - "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, - "engines": { - "node": ">=8" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "dependencies": { - "pump": "^3.0.0" + "domelementtype": "^2.3.0" }, "engines": { - "node": ">=8" + "node": ">= 4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/get-stream/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "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-bound": "^1.0.3", + "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" + "gopd": "^1.2.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-system-fonts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.2.tgz", - "integrity": "sha512-zzlgaYnHMIEgHRrfC7x0Qp0Ylhw/sHpM6MHXeVBTYIsvGf5GpbnClB+Q6rAPdn+0gd2oZZIo6Tj3EaWrt4VhDQ==", - "license": "MIT", - "engines": { - "node": ">8.0.0" + "node_modules/duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==", + "dev": true + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "node_modules/each-props/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + "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 }, - "node_modules/glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/editorconfig": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz", + "integrity": "sha512-GWjSI19PVJAM9IZRGOS+YKI8LN+/sjkSjNyvxL5ucqP9/IqtYNXBaQ/6c/hkPNYQHyOHra2KoXZI/JVpuqwmcQ==", "dev": true, "dependencies": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "@types/node": "^10.11.7", + "@types/semver": "^5.5.0", + "commander": "^2.19.0", + "lru-cache": "^4.1.3", + "semver": "^5.6.0", + "sigmund": "^1.0.1" }, - "engines": { - "node": "*" + "bin": { + "editorconfig": "bin/editorconfig" + } + }, + "node_modules/editorconfig/node_modules/@types/node": { + "version": "10.17.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/editorconfig/node_modules/@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "node_modules/editorconfig/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron": { + "version": "39.3.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.3.0.tgz", + "integrity": "sha512-ZA2Cmu5Vs8zeuZBr71XWZ5vgm7lRDB9N50oV6ee7YocITyxRxx/apWFKY48Sxyn0gzVlX+6YQc3CS1PtYIkGUg==", "dev": true, + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" + "@electron/get": "^2.0.0", + "@types/node": "^22.7.7", + "extract-zip": "^2.0.1" + }, + "bin": { + "electron": "cli.js" }, "engines": { - "node": ">= 6" + "node": ">= 12.20.55" } }, - "node_modules/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "node_modules/electron-to-chromium": { + "version": "1.5.158", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.158.tgz", + "integrity": "sha512-9vcp2xHhkvraY6AHw2WMi+GDSLPX42qe2xjYaVoZqFRJiOcilVQFq9mZmpuHEQpzlgGDelKlV7ZiGcmMsc8WxQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, - "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, "engines": { - "node": ">= 0.10" + "node": ">= 4" } }, - "node_modules/glob-stream/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.8" } }, - "node_modules/glob-stream/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "once": "^1.4.0" } }, - "node_modules/glob-stream/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-extglob": "^2.1.0" + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/glob-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true - }, - "node_modules/glob-watcher": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", - "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", + "node_modules/env-paths": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", + "integrity": "sha512-6u0VYSCo/OW6IoD5WCLLy9JUGARbamfSavcNXry/eu8aHVFei6CD3Sw+VGX5alea1i9pgPHW0mbu6Xj0uBh7gA==", "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "is-negated-glob": "^1.0.0", - "just-debounce": "^1.0.0", - "normalize-path": "^3.0.0", - "object.defaults": "^1.1.0" - }, "engines": { - "node": ">= 0.10" + "node": ">=6" } }, - "node_modules/glob-watcher/node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" } }, - "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", "dev": true, "dependencies": { - "remove-trailing-separator": "^1.0.1" + "prr": "~1.0.1" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "errno": "cli.js" } }, - "node_modules/glob-watcher/node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "is-arrayish": "^0.2.1" } }, - "node_modules/glob-watcher/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "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, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/glob-watcher/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/glob-watcher/node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true }, - "node_modules/glob-watcher/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "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": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/glob-watcher/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "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": { - "is-extendable": "^0.1.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, - "node_modules/glob-watcher/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "node_modules/es5-ext": { + "version": "0.10.63", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.63.tgz", + "integrity": "sha512-hUCZd2Byj/mNKjfP9jXrdVZ62B8KuA/VoK7X8nUh5qT+AxDmcbvZz041oDVZdbIN1qW6XY9VDNwzkvKnZvK2TQ==", "dev": true, "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" }, "engines": { - "node": ">= 4.0" + "node": ">=0.10" } }, - "node_modules/glob-watcher/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "optional": true + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c= sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", "dev": true, "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, - "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" + "d": "^1.0.1", + "ext": "^1.1.2" } }, - "node_modules/glob-watcher/node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" } }, - "node_modules/glob-watcher/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/glob-watcher/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob-watcher/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/eslint": { + "version": "9.36.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz", + "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, + "license": "MIT", "dependencies": { - "is-buffer": "^1.1.5" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.36.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": ">=0.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/glob-watcher/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/eslint-formatter-compact": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/eslint-formatter-compact/-/eslint-formatter-compact-8.40.0.tgz", + "integrity": "sha512-cwGUs113TgmTQXecx5kfRjB7m0y2wkDLSadPTE2pK6M/wO4N8PjmUaoWOFNCP9MHgsiZwgqd5bZFnDCnszC56Q==", "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/eslint-plugin-header": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", + "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", "dev": true, + "peerDependencies": { + "eslint": ">=7.7.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "50.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.3.1.tgz", + "integrity": "sha512-SY9oUuTMr6aWoJggUS40LtMjsRzJPB5ZT7F432xZIHK3EfHF+8i48GbUBpwanrtlL9l1gILNTHK9o8gEhYLcKA==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.48.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.6", + "escape-string-regexp": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.6.0", + "parse-imports": "^2.1.1", + "semver": "^7.6.3", + "spdx-expression-parse": "^4.0.0", + "synckit": "^0.9.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, - "node_modules/glob-watcher/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "node_modules/glob-watcher/node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/glob-watcher/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, "engines": { - "node": ">=0.10.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" + "is-glob": "^4.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", "dev": true, "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.10" } }, - "node_modules/global-prefix/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/esniff/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "dev": true + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "isexe": "^2.0.0" + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/glogg": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", - "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "dependencies": { - "sparkles": "^1.0.0" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10" } }, - "node_modules/google-auth-library": { - "version": "9.15.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", - "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", - "license": "Apache-2.0", + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^6.1.1", - "gcp-metadata": "^6.1.0", - "gtoken": "^7.0.0", - "jws": "^4.0.0" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=14" + "node": ">=4.0" } }, - "node_modules/google-logging-utils": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", - "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", - "license": "Apache-2.0", + "node_modules/estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true, "engines": { - "node": ">=14" + "node": ">=4.0" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/got": { - "version": "11.8.5", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", - "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", "dev": true, "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "d": "1", + "es5-ext": "~0.10.14" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "node_modules/event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "node_modules/events": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", + "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI= sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", "dev": true, - "license": "MIT" - }, - "node_modules/groq-sdk": { - "version": "0.20.1", - "resolved": "https://registry.npmjs.org/groq-sdk/-/groq-sdk-0.20.1.tgz", - "integrity": "sha512-I/U7mHDcanKHR/P0oKSSS0M6oHR69G1QgtMplqmF3gSejJ5ihV7l+/0OqbNoqOzYoQKG4XH7O4zCqMoTKCztQQ==", - "license": "Apache-2.0", "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/groq-sdk/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "ms": "2.0.0" } }, - "node_modules/groq-sdk/node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", - "license": "MIT" - }, - "node_modules/groq-sdk/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "license": "MIT" - }, - "node_modules/gtoken": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", - "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", - "license": "MIT", + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, "dependencies": { - "gaxios": "^6.0.0", - "jws": "^4.0.0" + "is-descriptor": "^0.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=0.10.0" } }, - "node_modules/gulp": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", - "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "glob-watcher": "^5.0.3", - "gulp-cli": "^2.2.0", - "undertaker": "^1.2.1", - "vinyl-fs": "^3.0.0" - }, - "bin": { - "gulp": "bin/gulp.js" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/gulp-azure-storage/-/gulp-azure-storage-0.12.1.tgz", - "integrity": "sha512-n/hx8bbGsqrcizruqDTX6zy2FUdkTDGAz04IdopNxNTZivZmizf8u9WLYJreUE6/qCnSJnyjS1HP82+mLk7rjg==", + "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "deprecated": "Please upgrade to v0.1.7", "dev": true, "dependencies": { - "@azure/storage-blob": "^12.8.0", - "delayed-stream": "0.0.6", - "event-stream": "3.3.4", - "mime": "^1.3.4", - "progress": "^1.1.8", - "queue": "^3.0.10", - "streamifier": "^0.1.1", - "vinyl": "^2.2.0", - "vinyl-fs": "^3.0.3", - "yargs": "^15.3.0" + "kind-of": "^3.0.2" }, - "bin": { - "upload-to-azure": "bin/upload.js" - } - }, - "node_modules/gulp-azure-storage/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/expand-brackets/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "deprecated": "Please upgrade to v0.1.5", "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/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 - }, - "node_modules/gulp-azure-storage/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "is-buffer": "^1.1.5" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/expand-brackets/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/expand-brackets/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-azure-storage/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", "dev": true, "dependencies": { - "p-limit": "^2.2.0" + "homedir-polyfill": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "node_modules/ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", "dev": true, - "engines": { - "node": ">= 0.10" + "dependencies": { + "type": "^2.0.0" } }, - "node_modules/gulp-azure-storage/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==", + "node_modules/ext/node_modules/type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY= sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/gulp-azure-storage/node_modules/y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true + "node_modules/extglob/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/gulp-azure-storage/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", "dev": true, "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" }, "engines": { - "node": ">=8" + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" } }, - "node_modules/gulp-azure-storage/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "node_modules/extract-zip/node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", "dev": true, "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" } }, - "node_modules/gulp-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-bom/-/gulp-bom-3.0.0.tgz", - "integrity": "sha512-iw/J94F+MVlxG64Q17BSkHsyjpY17qHl3N3A/jDdrL77zQBkhKtTiKLqM4di9CUX/qFToyyeDsOWwH+rESBgmA==", + "node_modules/fancy-log": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.3.tgz", + "integrity": "sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw==", "dev": true, "dependencies": { - "plugin-error": "^1.0.1", - "through2": "^3.0.1" + "ansi-gray": "^0.1.1", + "color-support": "^1.1.3", + "parse-node-version": "^1.0.0", + "time-stamp": "^1.0.0" }, "engines": { - "node": ">=8" - }, - "peerDependencies": { - "gulp": ">=4" - }, - "peerDependenciesMeta": { - "gulp": { - "optional": true - } + "node": ">= 0.10" } }, - "node_modules/gulp-buffer": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/gulp-buffer/-/gulp-buffer-0.0.2.tgz", - "integrity": "sha1-r4G0NGEBc2tJlC7GyfqGf/5zcDY= sha512-EBkbjjTH2gRr2B8KBAcomdTemfZHqiKs8CxSYdaW0Hq3zxltQFrCg9BBmKVHC9cfxX/3l2BZK5oiGHYNJ/gcVw==", + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", "dev": true, - "dependencies": { - "through2": "~0.4.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" }, - "node_modules/gulp-buffer/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "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 }, - "node_modules/gulp-buffer/node_modules/object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "dev": true }, - "node_modules/gulp-buffer/node_modules/readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" } }, - "node_modules/gulp-buffer/node_modules/string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "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/gulp-buffer/node_modules/through2": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", - "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s= sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", - "dev": true, - "dependencies": { - "readable-stream": "~1.0.17", - "xtend": "~2.1.1" - } + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, - "node_modules/gulp-buffer/node_modules/xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os= sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", - "dev": true, - "dependencies": { - "object-keys": "~0.4.0" - }, - "engines": { - "node": ">=0.4" - } + "node_modules/fastest-levenshtein": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", + "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "dev": true }, - "node_modules/gulp-cli": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", - "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", + "node_modules/fastq": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.9.0.tgz", + "integrity": "sha512-i7FVWL8HhVY+CTkwFxkN2mk3h+787ixS5S63eb78diVRc1MCssarHq3W5cj0av7YDSwmaV928RNag+U1etRQ7w==", "dev": true, "dependencies": { - "ansi-colors": "^1.0.1", - "archy": "^1.0.0", - "array-sort": "^1.0.0", - "color-support": "^1.1.3", - "concat-stream": "^1.6.0", - "copy-props": "^2.0.1", - "fancy-log": "^1.3.2", - "gulplog": "^1.0.0", - "interpret": "^1.4.0", - "isobject": "^3.0.1", - "liftoff": "^3.1.0", - "matchdep": "^2.0.0", - "mute-stdout": "^1.0.0", - "pretty-hrtime": "^1.0.0", - "replace-homedir": "^1.0.0", - "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.2.0", - "yargs": "^7.1.0" - }, - "bin": { - "gulp": "bin/gulp.js" - }, - "engines": { - "node": ">= 0.10" + "reusify": "^1.0.4" } }, - "node_modules/gulp-cli/node_modules/ansi-colors": { + "node_modules/fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", "dependencies": { - "ansi-wrap": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" + "pend": "~1.2.0" } }, - "node_modules/gulp-cli/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/gulp-cli/node_modules/camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16.0.0" } }, - "node_modules/gulp-cli/node_modules/cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" } }, - "node_modules/gulp-cli/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/file-loader/node_modules/schema-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", + "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.6", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/gulp-cli/node_modules/get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { + "node_modules/file-uri-to-path": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs= sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { - "number-is-nan": "^1.0.0" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/gulp-cli/node_modules/require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", + "node_modules/find-parent-dir": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", + "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", "dev": true }, - "node_modules/gulp-cli/node_modules/string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", "dev": true, "dependencies": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "array-back": "^3.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=4.0.0" } }, - "node_modules/gulp-cli/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-cli/node_modules/which-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", - "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", - "dev": true - }, - "node_modules/gulp-cli/node_modules/wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", + "node_modules/findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, "dependencies": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-cli/node_modules/y18n": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", - "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", - "dev": true - }, - "node_modules/gulp-cli/node_modules/yargs": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", - "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "node_modules/findup-sync/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "dependencies": { - "camelcase": "^3.0.0", - "cliui": "^3.2.0", - "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.1" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-cli/node_modules/yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "node_modules/findup-sync/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-filter": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", - "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM= sha512-ZERu1ipbPmjrNQ2dQD6lL4BjrJQG66P/c5XiyMMBqV+tUAJ+fLOyYIL/qnXd2pHmw/G/r7CLQb9ttANvQWbpfQ==", + "node_modules/findup-sync/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "node_modules/findup-sync/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" + "is-extendable": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "node_modules/findup-sync/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "node_modules/findup-sync/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "node_modules/findup-sync/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "kind-of": "^1.1.0" + "is-buffer": "^1.1.5" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "node_modules/findup-sync/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-filter/node_modules/plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "node_modules/findup-sync/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "dependencies": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-flatmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/gulp-flatmap/-/gulp-flatmap-1.0.2.tgz", - "integrity": "sha512-xm+Ax2vPL/xiMBqLFI++wUyPtncm3b55ztGHewmRcoG/sYb0OUTatjSacOud3fee77rnk+jOgnDEHhwBtMHgFA==", + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { - "plugin-error": "0.1.2", - "through2": "2.0.3" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-flatmap/node_modules/arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", "dev": true, "dependencies": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-flatmap/node_modules/arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-flatmap/node_modules/array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-flatmap/node_modules/extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, - "dependencies": { - "kind-of": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" + "bin": { + "flat": "cli.js" } }, - "node_modules/gulp-flatmap/node_modules/kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16" } }, - "node_modules/gulp-flatmap/node_modules/plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/flush-write-stream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "dependencies": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" - }, - "engines": { - "node": ">=0.10.0" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, - "node_modules/gulp-flatmap/node_modules/readable-stream": { + "node_modules/flush-write-stream/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", @@ -12693,117 +7869,139 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-flatmap/node_modules/through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, "dependencies": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" + "is-callable": "^1.1.3" } }, - "node_modules/gulp-gunzip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", - "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", "dev": true, - "dependencies": { - "through2": "~2.0.3", - "vinyl": "~2.0.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-gunzip/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4= sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, "engines": { - "node": ">=0.8" + "node": ">=0.10.0" } }, - "node_modules/gulp-gunzip/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gulp-gunzip/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "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": ">= 0.10" + "node": ">= 6" } }, - "node_modules/gulp-gunzip/node_modules/through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", + "node_modules/form-data-encoder": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", "dev": true, - "dependencies": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" + "license": "MIT", + "engines": { + "node": ">= 18" } }, - "node_modules/gulp-gunzip/node_modules/vinyl": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", - "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw= sha512-ViPXqulxjb1yXxaf/kQZfLHkd2ppnVBWPq4XmvW377vcBTxHFtHR5NRfYsdXsiKpWndKRoCdn11DfEnoCz1Inw==", + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", "dev": true, "dependencies": { - "clone": "^1.0.0", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "is-stream": "^1.1.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "map-cache": "^0.2.2" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/gulp-gzip": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gulp-gzip/-/gulp-gzip-1.4.2.tgz", - "integrity": "sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ==", - "dev": true, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dependencies": { - "ansi-colors": "^1.0.1", - "bytes": "^3.0.0", - "fancy-log": "^1.3.2", - "plugin-error": "^1.0.0", - "stream-to-array": "^2.3.0", - "through2": "^2.0.3" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">=14.14" } }, - "node_modules/gulp-gzip/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", "dev": true, "dependencies": { - "ansi-wrap": "^0.1.0" + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-gzip/node_modules/readable-stream": { + "node_modules/fs-mkdirp-stream/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", @@ -12818,7 +8016,7 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-gzip/node_modules/through2": { + "node_modules/fs-mkdirp-stream/node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", @@ -12828,150 +8026,234 @@ "xtend": "~4.0.1" } }, - "node_modules/gulp-json-editor": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.5.0.tgz", - "integrity": "sha512-HyrBSaE+Di6oQbKsfNM6X7dPFowOuTTuVYjxratU8QAiW7LR7Rydm+/fSS3OehdnuP++A/07q/nksihuD5FZSA==", + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "dependencies": { - "deepmerge": "^3.0.0", - "detect-indent": "^5.0.0", - "js-beautify": "^1.8.9", - "plugin-error": "^1.0.1", - "through2": "^3.0.0" - }, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/gulp-plumber": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.0.tgz", - "integrity": "sha512-L/LJftsbKoHbVj6dN5pvMsyJn9jYI0wT0nMg3G6VZhDac4NesezecYTi8/48rHi+yEic3sUpw6jlSc7qNWh32A==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "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": { - "chalk": "^1.1.3", - "fancy-log": "^1.3.2", - "plugin-error": "^0.1.2", - "through2": "^2.0.3" + "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", + "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.10", - "npm": ">=1.2.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gulp-plumber/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "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.10.0" + "node": ">= 0.4" } }, - "node_modules/gulp-plumber/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/get-stdin": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-7.0.0.tgz", + "integrity": "sha512-zRKcywvrXlXsA0v0i9Io4KDRaAw7+a1ZpjRwl9Wox8PFlVCCHra7E9c4kqXCoCM9nR5tBkaTTZRBoCm60bFqTQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/gulp-plumber/node_modules/arr-diff": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", - "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "dependencies": { - "arr-flatten": "^1.0.1", - "array-slice": "^0.2.3" + "pump": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gulp-plumber/node_modules/arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "node_modules/get-stream/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-plumber/node_modules/array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E= sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "*" } }, - "node_modules/gulp-plumber/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 6" } }, - "node_modules/gulp-plumber/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.10" } }, - "node_modules/gulp-plumber/node_modules/extend-shallow": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", - "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "node_modules/glob-stream/node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "kind-of": "^1.1.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/gulp-plumber/node_modules/kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "node_modules/glob-stream/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "node_modules/gulp-plumber/node_modules/plugin-error": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", - "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", + "node_modules/glob-stream/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, "dependencies": { - "ansi-cyan": "^0.1.1", - "ansi-red": "^0.1.1", - "arr-diff": "^1.0.1", - "arr-union": "^2.0.1", - "extend-shallow": "^1.1.2" + "is-extglob": "^2.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/gulp-plumber/node_modules/readable-stream": { + "node_modules/glob-stream/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", @@ -12986,293 +8268,264 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-plumber/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob-watcher": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", + "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "anymatch": "^2.0.0", + "async-done": "^1.2.0", + "chokidar": "^2.0.0", + "is-negated-glob": "^1.0.0", + "just-debounce": "^1.0.0", + "normalize-path": "^3.0.0", + "object.defaults": "^1.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/gulp-plumber/node_modules/supports-color": { + "node_modules/glob-watcher/node_modules/anymatch": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/gulp-plumber/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-rename": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.2.2.tgz", - "integrity": "sha1-OtRCh2PwXidk3sHGfYaNsnVoeBc= sha512-qhfUlYwq5zIP4cpRjx+np2vZVnr0xCRQrF3RsGel8uqL47Gu3yjmllSfnvJyl/39zYuxS68e1nnxImbm7388vw==", - "dev": true, - "engines": { - "node": ">=0.10.0", - "npm": ">=1.2.10" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" } }, - "node_modules/gulp-replace": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", - "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk= sha512-lHL+zKJN8uV95UkONnfRkoj2yJxPPupt2SahxA4vo5c+Ee3+WaIiMdWbOyUhg8BhAROQrWKnnxKOWPdVrnBwGw==", + "node_modules/glob-watcher/node_modules/anymatch/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", "dev": true, "dependencies": { - "istextorbinary": "1.0.2", - "readable-stream": "^2.0.1", - "replacestream": "^4.0.0" + "remove-trailing-separator": "^1.0.1" }, "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, - "node_modules/gulp-replace/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/glob-watcher/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", - "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "node_modules/glob-watcher/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "dependencies": { - "@gulp-sourcemaps/identity-map": "^2.0.1", - "@gulp-sourcemaps/map-sources": "^1.0.0", - "acorn": "^6.4.1", - "convert-source-map": "^1.0.0", - "css": "^3.0.0", - "debug-fabulous": "^1.0.0", - "detect-newline": "^2.0.0", - "graceful-fs": "^4.0.0", - "source-map": "^0.6.0", - "strip-bom-string": "^1.0.0", - "through2": "^2.0.0" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "node_modules/gulp-sourcemaps/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "node_modules/glob-watcher/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/gulp-sourcemaps/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/gulp-sourcemaps/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "node": ">=0.10.0" } }, - "node_modules/gulp-svgmin": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/gulp-svgmin/-/gulp-svgmin-4.1.0.tgz", - "integrity": "sha512-WKpif+yu3+oIlp1e11CQi5F64YddP699l2mFmxpz8swv8/P8dhxVcMKdCPFWouArlVyn7Ma1eWCJHw5gx4NMtw==", + "node_modules/glob-watcher/node_modules/chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", "dev": true, "dependencies": { - "lodash.clonedeep": "^4.5.0", - "plugin-error": "^1.0.1", - "svgo": "^2.7.0" + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "optionalDependencies": { + "fsevents": "^1.2.7" } }, - "node_modules/gulp-symdest": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/gulp-symdest/-/gulp-symdest-1.3.0.tgz", - "integrity": "sha512-n1VaNYMpyOq4GfyQyIwRZhXOOsQVdEy56BCFxL4hu+stKwYeSQcZxLX5FOZL6jZUlBYXCWlXL+E5JU13ZMldIw==", + "node_modules/glob-watcher/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "event-stream": "3.3.4", - "mkdirp": "^0.5.1", - "queue": "^3.1.0", - "vinyl-fs": "^3.0.3" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-symdest/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "node_modules/glob-watcher/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "is-extendable": "^0.1.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-untar": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.7.tgz", - "integrity": "sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw==", + "node_modules/glob-watcher/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], "dependencies": { - "event-stream": "~3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" - } - }, - "node_modules/gulp-untar/node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4= sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, "engines": { - "node": ">=0.8" + "node": ">= 4.0" } }, - "node_modules/gulp-untar/node_modules/clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", - "dev": true - }, - "node_modules/gulp-untar/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/glob-watcher/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" } }, - "node_modules/gulp-untar/node_modules/replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", + "node_modules/glob-watcher/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", "dev": true, + "dependencies": { + "is-extglob": "^2.1.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" } }, - "node_modules/gulp-untar/node_modules/tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.", + "node_modules/glob-watcher/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", "dev": true, "dependencies": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-untar/node_modules/through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", + "node_modules/glob-watcher/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", "dev": true, - "dependencies": { - "readable-stream": "^2.1.5", - "xtend": "~4.0.1" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-untar/node_modules/vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= sha512-Ci3wnR2uuSAWFMSglZuB8Z2apBdtOyz8CV7dC6/U1XbltXBC+IuutUkXQISz01P+US2ouBuesSbV6zILZ6BuzQ==", + "node_modules/glob-watcher/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "kind-of": "^3.0.2" }, "engines": { - "node": ">= 0.9" + "node": ">=0.10.0" } }, - "node_modules/gulp-vinyl-zip": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", - "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "node_modules/glob-watcher/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "event-stream": "3.3.4", - "queue": "^4.2.1", - "through2": "^2.0.3", - "vinyl": "^2.0.2", - "vinyl-fs": "^3.0.3", - "yauzl": "^2.2.1", - "yazl": "^2.2.1" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-vinyl-zip/node_modules/fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= sha512-MX1ZLPIuKED51hrI4++K+1B0VX87Cs4EkybD2q12Ysuf5p4vkmHqMvQJRlDwROqFr4D2Pzyit5wGQxf30grIcw==", + "node_modules/glob-watcher/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "dependencies": { - "pend": "~1.2.0" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-vinyl-zip/node_modules/queue": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.0.tgz", - "integrity": "sha512-DwxpAnqJuoQa+wyDgQuwkSshkhlqIlWEvwvdAY27fDPunZ2cVJzXU4JyjY+5l7zs7oGLaYAQm4MbLOVFAHFBzA==", + "node_modules/glob-watcher/node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "dependencies": { - "inherits": "~2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/gulp-vinyl-zip/node_modules/readable-stream": { + "node_modules/glob-watcher/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", @@ -13287,178 +8540,139 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/gulp-vinyl-zip/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", + "node_modules/glob-watcher/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" }, "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8= sha512-tOFjaiYI4cNrDuqujDv5G1KdCmGtuIULZqLv263CCADNQlNInl8sJPD+Gf3neEVecFQ0sw6D4oJTI/dqlunkSw==", - "dev": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.0.1" + "node": ">=0.10" } }, - "node_modules/gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U= sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", + "node_modules/glob-watcher/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, "dependencies": { - "glogg": "^1.0.0" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/gunzip-maybe": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", - "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", "dev": true, + "optional": true, "dependencies": { - "browserify-zlib": "^0.1.4", - "is-deflate": "^1.0.0", - "is-gzip": "^1.0.0", - "peek-stream": "^1.1.0", - "pumpify": "^1.3.3", - "through2": "^2.0.3" + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" }, - "bin": { - "gunzip-maybe": "bin.js" - } - }, - "node_modules/gunzip-maybe/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">=10.0" } }, - "node_modules/gunzip-maybe/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } - }, - "node_modules/has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", "dev": true, "dependencies": { - "ansi-regex": "^2.0.0" + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/has-ansi/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0= sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/globalthis": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", + "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", "dev": true, - "engines": { - "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==", + "optional": true, "dependencies": { - "es-define-property": "^1.0.0" + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "license": "MIT", + "node_modules/glogg": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", + "dev": true, "dependencies": { - "dunder-proto": "^1.0.0" + "sparkles": "^1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.10" } }, - "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==", + "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" @@ -13467,2307 +8681,2228 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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==", - "license": "MIT", + "node_modules/got": { + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", + "dev": true, "dependencies": { - "has-symbols": "^1.0.3" + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=10.19.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sindresorhus/got?sponsor=1" } }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/gulp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", + "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", "dev": true, "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "glob-watcher": "^5.0.3", + "gulp-cli": "^2.2.0", + "undertaker": "^1.2.1", + "vinyl-fs": "^3.0.0" + }, + "bin": { + "gulp": "bin/gulp.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "node_modules/gulp-azure-storage": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/gulp-azure-storage/-/gulp-azure-storage-0.12.1.tgz", + "integrity": "sha512-n/hx8bbGsqrcizruqDTX6zy2FUdkTDGAz04IdopNxNTZivZmizf8u9WLYJreUE6/qCnSJnyjS1HP82+mLk7rjg==", "dev": true, "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" + "@azure/storage-blob": "^12.8.0", + "delayed-stream": "0.0.6", + "event-stream": "3.3.4", + "mime": "^1.3.4", + "progress": "^1.1.8", + "queue": "^3.0.10", + "streamifier": "^0.1.1", + "vinyl": "^2.2.0", + "vinyl-fs": "^3.0.3", + "yargs": "^15.3.0" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "upload-to-azure": "bin/upload.js" } }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/gulp-azure-storage/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/gulp-azure-storage/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" } }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc= sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "node_modules/gulp-azure-storage/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/gulp-azure-storage/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 + }, + "node_modules/gulp-azure-storage/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { - "function-bind": "^1.1.2" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "node_modules/gulp-azure-storage/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "bin": { - "he": "bin/he" + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/gulp-azure-storage/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "dependencies": { - "parse-passwd": "^1.0.0" + "p-try": "^2.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/html-escaper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", - "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", - "dev": true + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/http-assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", - "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "node_modules/gulp-azure-storage/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "dependencies": { - "deep-equal": "~1.0.1", - "http-errors": "~1.8.0" + "p-limit": "^2.2.0" }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/http-assert/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/gulp-azure-storage/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">= 0.10" } }, - "node_modules/http-assert/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "node_modules/gulp-azure-storage/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, "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=8" } }, - "node_modules/http-assert/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "node_modules/gulp-azure-storage/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", "dev": true, + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">= 0.10" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", + "node_modules/gulp-azure-storage/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">=8" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/gulp-azure-storage/node_modules/y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "dev": true + }, + "node_modules/gulp-azure-storage/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" }, "engines": { - "node": ">= 14" + "node": ">=8" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "node_modules/gulp-azure-storage/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" }, "engines": { - "node": ">=10.19.0" + "node": ">=6" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "node_modules/gulp-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-bom/-/gulp-bom-3.0.0.tgz", + "integrity": "sha512-iw/J94F+MVlxG64Q17BSkHsyjpY17qHl3N3A/jDdrL77zQBkhKtTiKLqM4di9CUX/qFToyyeDsOWwH+rESBgmA==", + "dev": true, "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" + "plugin-error": "^1.0.1", + "through2": "^3.0.1" }, "engines": { - "node": ">= 14" + "node": ">=8" + }, + "peerDependencies": { + "gulp": ">=4" + }, + "peerDependenciesMeta": { + "gulp": { + "optional": true + } } }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", + "node_modules/gulp-buffer": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/gulp-buffer/-/gulp-buffer-0.0.2.tgz", + "integrity": "sha1-r4G0NGEBc2tJlC7GyfqGf/5zcDY= sha512-EBkbjjTH2gRr2B8KBAcomdTemfZHqiKs8CxSYdaW0Hq3zxltQFrCg9BBmKVHC9cfxX/3l2BZK5oiGHYNJ/gcVw==", + "dev": true, "dependencies": { - "ms": "^2.0.0" + "through2": "~0.4.0" } }, - "node_modules/husky": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/husky/-/husky-0.13.4.tgz", - "integrity": "sha512-kafsK/82ndSVKJe1IoR/z7NKkiI2LYM6H+VNI/YlKOeoOXEJTpD65TNu05Zx7pzSZzLuAdMt4fHgpUsnd6HJ7A==", + "node_modules/gulp-buffer/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/gulp-buffer/node_modules/object-keys": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", + "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY= sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==", + "dev": true + }, + "node_modules/gulp-buffer/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", "dev": true, - "hasInstallScript": true, "dependencies": { - "chalk": "^1.1.3", - "find-parent-dir": "^0.3.0", - "is-ci": "^1.0.9", - "normalize-path": "^1.0.0" + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" } }, - "node_modules/husky/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/gulp-buffer/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true }, - "node_modules/husky/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "node_modules/gulp-buffer/node_modules/through2": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz", + "integrity": "sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s= sha512-45Llu+EwHKtAZYTPPVn3XZHBgakWMN3rokhEv5hu596XP+cNgplMg+Gj+1nmAvj+L0K7+N49zBKx5rah5u0QIQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "readable-stream": "~1.0.17", + "xtend": "~2.1.1" } }, - "node_modules/husky/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "node_modules/gulp-buffer/node_modules/xtend": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", + "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os= sha512-vMNKzr2rHP9Dp/e1NQFnLQlwlhp9L/LfvnsVdHxN1f+uggyVI3i08uD14GPvCToPkdsRfyPqIyYGmIk58V98ZQ==", "dev": true, "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" + "object-keys": "~0.4.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4" } }, - "node_modules/husky/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/gulp-cli": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dev": true, + "dependencies": { + "ansi-colors": "^1.0.1", + "archy": "^1.0.0", + "array-sort": "^1.0.0", + "color-support": "^1.1.3", + "concat-stream": "^1.6.0", + "copy-props": "^2.0.1", + "fancy-log": "^1.3.2", + "gulplog": "^1.0.0", + "interpret": "^1.4.0", + "isobject": "^3.0.1", + "liftoff": "^3.1.0", + "matchdep": "^2.0.0", + "mute-stdout": "^1.0.0", + "pretty-hrtime": "^1.0.0", + "replace-homedir": "^1.0.0", + "semver-greatest-satisfied-range": "^1.1.0", + "v8flags": "^3.2.0", + "yargs": "^7.1.0" + }, + "bin": { + "gulp": "bin/gulp.js" + }, "engines": { - "node": ">=0.8.0" + "node": ">= 0.10" } }, - "node_modules/husky/node_modules/normalize-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", - "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", + "node_modules/gulp-cli/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/husky/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "node_modules/gulp-cli/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/husky/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "node_modules/gulp-cli/node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, "engines": { - "node": ">=0.8.0" + "node": ">=0.10.0" } }, - "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", + "node_modules/gulp-cli/node_modules/cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", + "dev": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "node_modules/gulp-cli/node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "node_modules/gulp-cli/node_modules/get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "node_modules/gulp-cli/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs= sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "dev": true, + "dependencies": { + "number-is-nan": "^1.0.0" + }, "engines": { - "node": ">= 4" + "node": ">=0.10.0" } }, - "node_modules/ignore-by-default": { + "node_modules/gulp-cli/node_modules/require-main-filename": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==", "dev": true }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "node_modules/gulp-cli/node_modules/string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "dev": true, - "license": "MIT", "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "node_modules/gulp-cli/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "ansi-regex": "^2.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o= sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } + "node_modules/gulp-cli/node_modules/which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==", + "dev": true }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "node_modules/gulp-cli/node_modules/wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, - "node_modules/innosetup": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/innosetup/-/innosetup-6.4.1.tgz", - "integrity": "sha512-Bh/dNlyyFsFKBGoTf19n1msPnm4Njfb0CWVirw2CV1MSJXYy0ajRI+xjo/Yr9DtZFLLafhnwm1ZsxTu309F8lQ==", - "dev": true, - "bin": { - "innosetup-compiler": "lib/iscc" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "license": "MIT", + "node_modules/gulp-cli/node_modules/y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, + "node_modules/gulp-cli/node_modules/yargs": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", + "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.1" } }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "node_modules/gulp-cli/node_modules/yargs-parser": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, - "engines": { - "node": ">= 0.10" + "dependencies": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" } }, - "node_modules/invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY= sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", + "node_modules/gulp-filter": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", + "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM= sha512-ZERu1ipbPmjrNQ2dQD6lL4BjrJQG66P/c5XiyMMBqV+tUAJ+fLOyYIL/qnXd2pHmw/G/r7CLQb9ttANvQWbpfQ==", "dev": true, + "dependencies": { + "multimatch": "^2.0.0", + "plugin-error": "^0.1.2", + "streamfilter": "^1.0.5" + }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", - "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "node_modules/gulp-filter/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" }, "engines": { - "node": ">= 12" + "node": ">=0.10.0" } }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", + "node_modules/gulp-filter/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "node_modules/gulp-filter/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", "dev": true, - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "deprecated": "Please upgrade to v1.0.1", + "node_modules/gulp-filter/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" + "kind-of": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/gulp-filter/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/gulp-filter/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "license": "MIT", + "node_modules/gulp-flatmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/gulp-flatmap/-/gulp-flatmap-1.0.2.tgz", + "integrity": "sha512-xm+Ax2vPL/xiMBqLFI++wUyPtncm3b55ztGHewmRcoG/sYb0OUTatjSacOud3fee77rnk+jOgnDEHhwBtMHgFA==", + "dev": true, "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" + "plugin-error": "0.1.2", + "through2": "2.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "license": "MIT", + "node_modules/gulp-flatmap/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, + "node_modules/gulp-flatmap/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/gulp-flatmap/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "license": "MIT", + "node_modules/gulp-flatmap/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "kind-of": "^1.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", + "node_modules/gulp-flatmap/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", + "node_modules/gulp-flatmap/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", "dev": true, "dependencies": { - "ci-info": "^1.5.0" + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" }, - "bin": { - "is-ci": "bin.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", + "node_modules/gulp-flatmap/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "deprecated": "Please upgrade to v1.0.1", + "node_modules/gulp-flatmap/node_modules/through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", "dev": true, "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" } }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/gulp-gunzip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", + "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", + "dev": true, + "dependencies": { + "through2": "~2.0.3", + "vinyl": "~2.0.1" + } + }, + "node_modules/gulp-gunzip/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4= sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.8" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "license": "MIT", + "node_modules/gulp-gunzip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, + "node_modules/gulp-gunzip/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.10" } }, - "node_modules/is-deflate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", - "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", - "dev": true - }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "node_modules/gulp-gunzip/node_modules/through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4= sha512-tmNYYHFqXmaKSSlOU4ZbQ82cxmFQa5LRWKFtWCNkGIiZ3/VHmOffCeWfBRZZRyXAhNP9itVMR+cuvomBOPlm8g==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" } }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/gulp-gunzip/node_modules/vinyl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", + "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw= sha512-ViPXqulxjb1yXxaf/kQZfLHkd2ppnVBWPq4XmvW377vcBTxHFtHR5NRfYsdXsiKpWndKRoCdn11DfEnoCz1Inw==", "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" + "dependencies": { + "clone": "^1.0.0", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "is-stream": "^1.1.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.10" } }, - "node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "node_modules/gulp-gzip": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gulp-gzip/-/gulp-gzip-1.4.2.tgz", + "integrity": "sha512-ZIxfkUwk2XmZPTT9pPHrHUQlZMyp9nPhg2sfoeN27mBGpi7OaHnOD+WCN41NXjfJQ69lV1nQ9LLm1hYxx4h3UQ==", "dev": true, "dependencies": { - "is-plain-object": "^2.0.4" + "ansi-colors": "^1.0.1", + "bytes": "^3.0.0", + "fancy-log": "^1.3.2", + "plugin-error": "^1.0.0", + "stream-to-array": "^2.3.0", + "through2": "^2.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10.0" } }, - "node_modules/is-extendable/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/gulp-gzip/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, "dependencies": { - "isobject": "^3.0.1" + "ansi-wrap": "^0.1.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" + "node_modules/gulp-gzip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "license": "MIT", + "node_modules/gulp-gzip/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "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==", + "node_modules/gulp-json-editor": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/gulp-json-editor/-/gulp-json-editor-2.5.0.tgz", + "integrity": "sha512-HyrBSaE+Di6oQbKsfNM6X7dPFowOuTTuVYjxratU8QAiW7LR7Rydm+/fSS3OehdnuP++A/07q/nksihuD5FZSA==", "dev": true, + "dependencies": { + "deepmerge": "^3.0.0", + "detect-indent": "^5.0.0", + "js-beautify": "^1.8.9", + "plugin-error": "^1.0.1", + "through2": "^3.0.0" + }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "license": "MIT", + "node_modules/gulp-plumber": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gulp-plumber/-/gulp-plumber-1.2.0.tgz", + "integrity": "sha512-L/LJftsbKoHbVj6dN5pvMsyJn9jYI0wT0nMg3G6VZhDac4NesezecYTi8/48rHi+yEic3sUpw6jlSc7qNWh32A==", + "dev": true, "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2", + "through2": "^2.0.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10", + "npm": ">=1.2.10" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, + "node_modules/gulp-plumber/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-gzip": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", - "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", + "node_modules/gulp-plumber/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", + "node_modules/gulp-plumber/node_modules/arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo= sha512-OQwDZUqYaQwyyhDJHThmzId8daf4/RFNLaeh3AevmSeZ5Y7ug4Ga/yKc6l6kTZOBW781rCj103ZuTh8GAsB3+Q==", + "dev": true, "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" }, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "node_modules/gulp-plumber/node_modules/arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= sha512-t5db90jq+qdgk8aFnxEkjqta0B/GHrM1pxzuuZz2zWsOXc5nKu3t+76s/PQBA8FTcM/ipspIH9jWG4OxCBc2eA==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "node_modules/gulp-plumber/node_modules/array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU= sha512-rlVfZW/1Ph2SNySXwR9QYkChp8EkOEiTMO5Vwx60usw04i4nWemkm9RXmQqgkQFaLHsqLuADvjp6IfgL9l2M8Q==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node_modules/gulp-plumber/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/gulp-plumber/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { - "node": ">=0.12.0" + "node": ">=0.8.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "license": "MIT", + "node_modules/gulp-plumber/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "kind-of": "^1.1.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/is-plain-obj": { + "node_modules/gulp-plumber/node_modules/kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "node_modules/gulp-plumber/node_modules/plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= sha512-WzZHcm4+GO34sjFMxQMqZbsz3xiNEgonCskQ9v+IroMmYgk/tas8dG+Hr2D6IbRPybZ12oWpzE/w3cGJ6FJzOw==", "dev": true, + "dependencies": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", - "dev": true - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", + "node_modules/gulp-plumber/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/gulp-plumber/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, "dependencies": { - "is-unc-path": "^1.0.0" + "ansi-regex": "^2.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "license": "MIT", + "node_modules/gulp-plumber/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.8.0" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "license": "MIT", + "node_modules/gulp-plumber/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ= sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "node_modules/gulp-rename": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.2.2.tgz", + "integrity": "sha1-OtRCh2PwXidk3sHGfYaNsnVoeBc= sha512-qhfUlYwq5zIP4cpRjx+np2vZVnr0xCRQrF3RsGel8uqL47Gu3yjmllSfnvJyl/39zYuxS68e1nnxImbm7388vw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=0.10.0", + "npm": ">=1.2.10" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "license": "MIT", + "node_modules/gulp-replace": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", + "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk= sha512-lHL+zKJN8uV95UkONnfRkoj2yJxPPupt2SahxA4vo5c+Ee3+WaIiMdWbOyUhg8BhAROQrWKnnxKOWPdVrnBwGw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" + "istextorbinary": "1.0.2", + "readable-stream": "^2.0.1", + "replacestream": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10" } }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "license": "MIT", + "node_modules/gulp-replace/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", + "node_modules/gulp-sourcemaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-3.0.0.tgz", + "integrity": "sha512-RqvUckJkuYqy4VaIH60RMal4ZtG0IbQ6PXMNkNsshEGJ9cldUPRb/YCgboYae+CLAs1HQNb4ADTKCx65HInquQ==", + "dev": true, "dependencies": { - "which-typed-array": "^1.1.16" + "@gulp-sourcemaps/identity-map": "^2.0.1", + "@gulp-sourcemaps/map-sources": "^1.0.0", + "acorn": "^6.4.1", + "convert-source-map": "^1.0.0", + "css": "^3.0.0", + "debug-fabulous": "^1.0.0", + "detect-newline": "^2.0.0", + "graceful-fs": "^4.0.0", + "source-map": "^0.6.0", + "strip-bom-string": "^1.0.0", + "through2": "^2.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 6" } }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "node_modules/gulp-sourcemaps/node_modules/acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", "dev": true, - "dependencies": { - "unc-path-regex": "^0.1.2" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4.0" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "node_modules/gulp-sourcemaps/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true + "node_modules/gulp-sourcemaps/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "node_modules/gulp-svgmin": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gulp-svgmin/-/gulp-svgmin-4.1.0.tgz", + "integrity": "sha512-WKpif+yu3+oIlp1e11CQi5F64YddP699l2mFmxpz8swv8/P8dhxVcMKdCPFWouArlVyn7Ma1eWCJHw5gx4NMtw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "plugin-error": "^1.0.1", + "svgo": "^2.7.0" } }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node_modules/gulp-symdest": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/gulp-symdest/-/gulp-symdest-1.3.0.tgz", + "integrity": "sha512-n1VaNYMpyOq4GfyQyIwRZhXOOsQVdEy56BCFxL4hu+stKwYeSQcZxLX5FOZL6jZUlBYXCWlXL+E5JU13ZMldIw==", + "dev": true, + "dependencies": { + "event-stream": "3.3.4", + "mkdirp": "^0.5.1", + "queue": "^3.1.0", + "vinyl-fs": "^3.0.3" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "license": "MIT", + "node_modules/gulp-symdest/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" + "minimist": "^1.2.5" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "license": "MIT", + "node_modules/gulp-vinyl-zip": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", + "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "event-stream": "3.3.4", + "queue": "^4.2.1", + "through2": "^2.0.3", + "vinyl": "^2.0.2", + "vinyl-fs": "^3.0.3", + "yauzl": "^2.2.1", + "yazl": "^2.2.1" } }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "node_modules/gulp-vinyl-zip/node_modules/fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU= sha512-MX1ZLPIuKED51hrI4++K+1B0VX87Cs4EkybD2q12Ysuf5p4vkmHqMvQJRlDwROqFr4D2Pzyit5wGQxf30grIcw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "pend": "~1.2.0" } }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "license": "MIT", + "node_modules/gulp-vinyl-zip/node_modules/queue": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.0.tgz", + "integrity": "sha512-DwxpAnqJuoQa+wyDgQuwkSshkhlqIlWEvwvdAY27fDPunZ2cVJzXU4JyjY+5l7zs7oGLaYAQm4MbLOVFAHFBzA==", + "dev": true, "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "inherits": "~2.0.0" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8= sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/gulp-vinyl-zip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "node_modules/gulp-vinyl-zip/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", - "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", + "node_modules/gulp-vinyl-zip/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=10" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/gulp-vinyl-zip/node_modules/vinyl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", "dev": true, "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-report/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, - "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/istanbul-lib-report/node_modules/supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "node_modules/gulp-vinyl-zip/node_modules/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8= sha512-tOFjaiYI4cNrDuqujDv5G1KdCmGtuIULZqLv263CCADNQlNInl8sJPD+Gf3neEVecFQ0sw6D4oJTI/dqlunkSw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.0.1" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "node_modules/gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U= sha512-hm6N8nrm3Y08jXie48jsC55eCZz9mnb4OirAStEk2deqeyhXU3C1otDVh+ccttMuc1sBi6RX6ZJ720hs9RCvgw==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" + "glogg": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">= 0.10" } }, - "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "node_modules/gunzip-maybe": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", + "integrity": "sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw==", "dev": true, "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "browserify-zlib": "^0.1.4", + "is-deflate": "^1.0.0", + "is-gzip": "^1.0.0", + "peek-stream": "^1.1.0", + "pumpify": "^1.3.3", + "through2": "^2.0.3" }, - "engines": { - "node": ">=8" + "bin": { + "gunzip-maybe": "bin.js" } }, - "node_modules/istextorbinary": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8= sha512-qZ5ptUDuni2pdCngFTraYa5kalQ0mX47Mhn08tT0DZZv/7yhX1eMb9lFtXVbWhFtgRtpLG/UdqVAjh9teO5x+w==", + "node_modules/gunzip-maybe/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "dependencies": { - "binaryextensions": "~1.0.0", - "textextensions": "~1.0.0" - }, - "engines": { - "node": ">=0.4" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "license": "MIT", + "node_modules/gunzip-maybe/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "dependencies": { - "@isaacs/cliui": "^8.0.2" + "function-bind": "^1.1.1" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node": ">= 0.4.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "dev": true, - "license": "MIT", "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "ansi-regex": "^2.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=0.10.0" } }, - "node_modules/jest-worker/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==", + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "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, - "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" + "es-define-property": "^1.0.0" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "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", - "bin": { - "jiti": "bin/jiti.js" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "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": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" - }, - "node_modules/js-beautify": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.8.9.tgz", - "integrity": "sha512-MwPmLywK9RSX0SPsUJjN7i+RQY9w/yC17Lbrq9ViEefpLRgqAR2BgrMN2AbifkUuhDV8tRauLhLda/9+bE0YQA==", + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", "dev": true, "dependencies": { - "config-chain": "^1.1.12", - "editorconfig": "^0.15.2", - "glob": "^7.1.3", - "mkdirp": "~0.5.0", - "nopt": "~4.0.1" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" }, - "bin": { - "css-beautify": "js/bin/css-beautify.js", - "html-beautify": "js/bin/html-beautify.js", - "js-beautify": "js/bin/js-beautify.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/js-beautify/node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=0.10.0" } }, - "node_modules/js-beautify/node_modules/mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "node_modules/has-values/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "minimist": "^1.2.5" + "kind-of": "^3.0.2" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": ">=0.10.0" } }, - "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==", - "license": "MIT" - }, - "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==", + "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "argparse": "^2.0.1" + "is-buffer": "^1.1.5" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", - "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" - }, - "node_modules/jschardet": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", - "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", - "license": "LGPL-2.1+", + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc= sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "dependencies": { + "is-buffer": "^1.1.5" + }, "engines": { - "node": ">=0.1.90" + "node": ">=0.10.0" } }, - "node_modules/jsdoc-type-pratt-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", - "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, "engines": { - "node": ">=12.0.0" + "node": ">= 0.4" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, - "license": "MIT", "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" + "he": "bin/he" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, "dependencies": { - "bignumber.js": "^9.0.0" + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "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==", - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/html-escaper": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.0.tgz", + "integrity": "sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig==", "dev": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">= 0.8" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, + "license": "MIT", "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/jszip/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= sha512-/QLqfspz7WJ+TPmzDp5WJOlm2r3j+/12rGo7dG5uwD9vGM5sWg8p251b7Us0p19JqjddJzcYOK2v6FN92nREmg==", - "dev": true - }, - "node_modules/just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, - "node_modules/jwa": { + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" } }, - "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", + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, "dependencies": { - "commander": "^8.3.0" + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" }, - "bin": { - "katex": "cli.js" + "engines": { + "node": ">=10.19.0" } }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, "engines": { - "node": ">= 12" + "node": ">= 14" } }, - "node_modules/kerberos": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz", - "integrity": "sha512-414s1G/qgK2T60cXnZsHbtRj8Ynjg0DBlQWeY99tkyqQ2e8vGgFHvxRdvjTlLHg/SxBA0zLQcGE6Pk6Dfq/BCA==", + "node_modules/husky": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/husky/-/husky-0.13.4.tgz", + "integrity": "sha512-kafsK/82ndSVKJe1IoR/z7NKkiI2LYM6H+VNI/YlKOeoOXEJTpD65TNu05Zx7pzSZzLuAdMt4fHgpUsnd6HJ7A==", + "dev": true, "hasInstallScript": true, "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.2" - }, + "chalk": "^1.1.3", + "find-parent-dir": "^0.3.0", + "is-ci": "^1.0.9", + "normalize-path": "^1.0.0" + } + }, + "node_modules/husky/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, "engines": { - "node": ">=12.9.0" + "node": ">=0.10.0" } }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "node_modules/husky/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/husky/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, "dependencies": { - "tsscmp": "1.0.6" + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/husky/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, - "dependencies": { - "json-buffer": "3.0.1" + "engines": { + "node": ">=0.8.0" } }, - "node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "node_modules/husky/node_modules/normalize-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz", + "integrity": "sha512-7WyT0w8jhpDStXRq5836AMmihQwq2nrUVQrgjvUo/p/NZf9uy/MeJ246lBJVmWuYXMlJuG9BNZHF0hWjfTbQUA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/koa": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", - "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", + "node_modules/husky/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "dev": true, - "license": "MIT", "dependencies": { - "accepts": "^1.3.5", - "cache-content-type": "^1.0.0", - "content-disposition": "~0.5.2", - "content-type": "^1.0.4", - "cookies": "~0.9.0", - "debug": "^4.3.2", - "delegates": "^1.0.0", - "depd": "^2.0.0", - "destroy": "^1.0.4", - "encodeurl": "^1.0.2", - "escape-html": "^1.0.3", - "fresh": "~0.5.2", - "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", - "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", - "on-finished": "^2.3.0", - "only": "~0.0.2", - "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", - "vary": "^1.1.2" + "ansi-regex": "^2.0.0" }, "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + "node": ">=0.10.0" } }, - "node_modules/koa-compose": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", - "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", - "dev": true - }, - "node_modules/koa-convert": { + "node_modules/husky/node_modules/supports-color": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, "engines": { - "node": ">= 10" + "node": ">= 4" } }, - "node_modules/koa-morgan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", - "integrity": "sha1-CAUuDODYOdPEMXi5CluzQkvvH5k= sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", - "dev": true, - "dependencies": { - "morgan": "^1.6.1" - } + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true }, - "node_modules/koa-mount": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.0.0.tgz", - "integrity": "sha512-rm71jaA/P+6HeCpoRhmCv8KVBIi0tfGuO/dMKicbQnQW/YJntJ6MnnspkodoA4QstMVEZArsCphmd0bJEtoMjQ==", + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, + "license": "MIT", "dependencies": { - "debug": "^4.0.1", - "koa-compose": "^4.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">= 7.6.0" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/koa-send": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", - "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "node_modules/import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "http-errors": "^1.7.3", - "resolve-path": "^1.4.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">= 8" + "node": ">=8" } }, - "node_modules/koa-send/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o= sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=0.8.19" } }, - "node_modules/koa-send/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/innosetup": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/innosetup/-/innosetup-6.4.1.tgz", + "integrity": "sha512-Bh/dNlyyFsFKBGoTf19n1msPnm4Njfb0CWVirw2CV1MSJXYy0ajRI+xjo/Yr9DtZFLLafhnwm1ZsxTu309F8lQ==", + "dev": true, + "bin": { + "innosetup-compiler": "lib/iscc" }, "engines": { - "node": ">= 0.6" + "node": ">= 0.8.0" } }, - "node_modules/koa-send/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">= 0.10" } }, - "node_modules/koa-static": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", - "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "node_modules/invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY= sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", "dev": true, - "dependencies": { - "debug": "^3.1.0", - "koa-send": "^5.0.0" - }, "engines": { - "node": ">= 7.6.0" + "node": ">=0.10.0" } }, - "node_modules/koa-static/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", "dependencies": { - "ms": "^2.1.1" + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" } }, - "node_modules/koa/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/koa/node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "node_modules/is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "deprecated": "Please upgrade to v1.0.1", "dev": true, + "dependencies": { + "kind-of": "^6.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/koa/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "node_modules/is-accessor-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/last-run": { + "node_modules/is-arguments": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls= sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, "dependencies": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lazy.js": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/lazy.js/-/lazy.js-0.4.3.tgz", - "integrity": "sha1-h/Z6B602VVEh5P/xUg3zG+Znhtg= sha512-kHcnVaCZzhv6P+YgC4iRZFw62+biYIcBYU8qqKzJysC7cdKwPgb3WRtcBPyINTSLZwsjyFdBtd97sHbkseTZKw==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "node_modules/lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= sha512-/330KFbmC/zKdtZoVDRwvkJ8snrJyBPfoZ39zsJl2O24HOE1CTNiEbeZmHXmjBVxTSSv7JlJEXPYhU83DhA2yg==", + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "dependencies": { - "readable-stream": "^2.0.5" + "binary-extensions": "^2.0.0" }, "engines": { - "node": ">= 0.6.3" + "node": ">=8" } }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "node_modules/is-ci": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "dev": true, "dependencies": { - "invert-kv": "^1.0.0" + "ci-info": "^1.5.0" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "is-ci": "bin.js" } }, - "node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "dependencies": { - "flush-write-stream": "^1.0.2" + "has": "^1.0.3" }, - "engines": { - "node": ">= 0.10" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "deprecated": "Please upgrade to v1.0.1", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "kind-of": "^6.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "node_modules/is-data-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "dependencies": { - "immediate": "~3.0.5" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "node_modules/is-deflate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-deflate/-/is-deflate-1.0.0.tgz", + "integrity": "sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ==", + "dev": true + }, + "node_modules/is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "dependencies": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/liftoff/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/is-descriptor/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, "engines": { - "node": ">=14" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/linkify-it": { - "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==", + "node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "dependencies": { - "uc.micro": "^2.0.0" + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs= sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "node_modules/is-extendable/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" + "isobject": "^3.0.1" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=0.10.0" } }, - "node_modules/loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "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, "engines": { - "node": ">=6.11.5" + "node": ">=8" } }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "node_modules/is-generator-function": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.9.tgz", + "integrity": "sha512-ZJ34p1uvIfptHCN7sFTjGibB9/oBg17sHqzDLfuwhvmN/qLVvIQXRQ8licZQ35WJ8KuEQt/etnnzQFI9C9Ue/A==", "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": ">=8.9.0" + "node": ">=0.10.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/is-gzip": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-gzip/-/is-gzip-1.0.0.tgz", + "integrity": "sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ==", "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" }, "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY= sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true - }, - "node_modules/lodash.clone": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", - "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", - "dev": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true - }, - "node_modules/lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", - "dev": true - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" } }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, "dependencies": { - "es5-ext": "~0.10.2" + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/lucide-react": { - "version": "0.503.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.503.0.tgz", - "integrity": "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ= sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, "dependencies": { - "semver": "^7.5.3" + "unc-path-regex": "^0.1.2" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, "engines": { "node": ">=10" }, @@ -15775,356 +10910,408 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", "dev": true }, - "node_modules/make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", "dev": true, - "dependencies": { - "kind-of": "^6.0.2" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/make-iterator/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8= sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", - "dev": true + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "node_modules/istanbul-lib-instrument": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz", + "integrity": "sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA==", "dev": true, "dependencies": { - "object-visit": "^1.0.0" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" + "engines": { + "node": ">=10" } }, - "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" - }, + "node_modules/istanbul-lib-report/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, "engines": { - "node": ">= 18" + "node": ">=8" } }, - "node_modules/matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, - "license": "MIT", "dependencies": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" + "has-flag": "^4.0.0" }, "engines": { - "node": ">= 0.10.0" + "node": ">=8" } }, - "node_modules/matchdep/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, "dependencies": { - "is-extendable": "^0.1.0" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/matchdep/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "node_modules/istextorbinary": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", + "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8= sha512-qZ5ptUDuni2pdCngFTraYa5kalQ0mX47Mhn08tT0DZZv/7yhX1eMb9lFtXVbWhFtgRtpLG/UdqVAjh9teO5x+w==", "dev": true, "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" + "binaryextensions": "~1.0.0", + "textextensions": "~1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=0.4" } }, - "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "is-extendable": "^0.1.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/matchdep/node_modules/findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 10.13.0" } }, - "node_modules/matchdep/node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "node_modules/jest-worker/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": ">=0.10.0" + "node": ">=8" } }, - "node_modules/matchdep/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { - "is-extglob": "^2.1.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/matchdep/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" + }, + "node_modules/js-beautify": { + "version": "1.8.9", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.8.9.tgz", + "integrity": "sha512-MwPmLywK9RSX0SPsUJjN7i+RQY9w/yC17Lbrq9ViEefpLRgqAR2BgrMN2AbifkUuhDV8tRauLhLda/9+bE0YQA==", "dev": true, "dependencies": { - "kind-of": "^3.0.2" + "config-chain": "^1.1.12", + "editorconfig": "^0.15.2", + "glob": "^7.1.3", + "mkdirp": "~0.5.0", + "nopt": "~4.0.1" }, - "engines": { - "node": ">=0.10.0" + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" } }, - "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/js-beautify/node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=0.10.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/matchdep/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/js-beautify/node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/matchdep/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "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": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" + "argparse": "^2.0.1" }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==" + }, + "node_modules/jschardet": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-3.1.4.tgz", + "integrity": "sha512-/kmVISmrwVwtyYU40iQUOp3SUPk2dhNCMsZBQX0R1/jZ8maaXJ/oZIzUOiyOqcgtLnETFKYChbJ5iDC/eWmFHg==", + "license": "LGPL-2.1+", "engines": { - "node": ">=0.10.0" + "node": ">=0.1.90" } }, - "node_modules/matchdep/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz", + "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==", "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=10" + "node": ">=4" } }, - "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==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, - "node_modules/md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "dev": true, - "dependencies": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" - } + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, - "node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/memoizee": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", - "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", - "dev": true, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.53", - "es6-weak-map": "^2.0.3", - "event-emitter": "^0.3.5", - "is-promise": "^2.2.2", - "lru-queue": "^0.1.0", - "next-tick": "^1.1.0", - "timers-ext": "^0.1.7" + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "dev": true, "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" } }, - "node_modules/memory-fs/node_modules/readable-stream": { + "node_modules/jszip/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", @@ -16139,591 +11326,537 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/memorystream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI= sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-options": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz", - "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==", - "dev": true, - "dependencies": { - "is-plain-obj": "^1.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" + "node_modules/just-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", + "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo= sha512-/QLqfspz7WJ+TPmzDp5WJOlm2r3j+/12rGo7dG5uwD9vGM5sWg8p251b7Us0p19JqjddJzcYOK2v6FN92nREmg==", + "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, - "engines": { - "node": ">= 8" - } + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "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": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "commander": "^8.3.0" }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" + "katex": "cli.js" } }, - "node_modules/mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==", + "node_modules/katex/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": ">= 0.6" + "node": ">= 12" } }, - "node_modules/mime-types": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", - "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", + "node_modules/kerberos": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz", + "integrity": "sha512-414s1G/qgK2T60cXnZsHbtRj8Ynjg0DBlQWeY99tkyqQ2e8vGgFHvxRdvjTlLHg/SxBA0zLQcGE6Pk6Dfq/BCA==", + "hasInstallScript": true, "dependencies": { - "mime-db": "1.45.0" + "bindings": "^1.5.0", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.2" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" + "node": ">=12.9.0" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "tsscmp": "1.0.6" }, "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/minizlib/node_modules/minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" + "json-buffer": "3.0.1" } }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" + "node_modules/koa": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.1.1.tgz", + "integrity": "sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^1.3.8", + "content-disposition": "~0.5.4", + "content-type": "^1.0.5", + "cookies": "~0.9.1", + "delegates": "^1.0.0", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=10" + "node": ">= 18" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true, + "license": "MIT" }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "node_modules/koa-morgan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/koa-morgan/-/koa-morgan-1.0.1.tgz", + "integrity": "sha1-CAUuDODYOdPEMXi5CluzQkvvH5k= sha512-JOUdCNlc21G50afBXfErUrr1RKymbgzlrO5KURY+wmDG1Uvd2jmxUJcHgylb/mYXy2SjiNZyYim/ptUBGsIi3A==", "dev": true, - "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" + "morgan": "^1.6.1" } }, - "node_modules/mocha": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", - "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "node_modules/koa-mount": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/koa-mount/-/koa-mount-4.2.0.tgz", + "integrity": "sha512-2iHQc7vbA9qLeVq5gKAYh3m5DOMMlMfIKjW/REPAS18Mf63daCJHHVXY9nbu7ivrnYn5PiPC4CE523Tf5qvjeQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^8.1.0", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^16.2.0", - "yargs-parser": "^20.2.9", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" + "debug": "^4.0.1", + "koa-compose": "^4.1.0" }, "engines": { - "node": ">= 14.0.0" + "node": ">= 7.6.0" } }, - "node_modules/mocha-junit-reporter": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", - "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "md5": "^2.3.0", - "mkdirp": "^3.0.0", - "strip-ansi": "^6.0.1", - "xml": "^1.0.1" + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" }, - "peerDependencies": { - "mocha": ">=2.2.5" + "engines": { + "node": ">= 8" } }, - "node_modules/mocha-junit-reporter/node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "node_modules/koa-send/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.6" } }, - "node_modules/mocha-multi-reporters": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", - "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", + "node_modules/koa-send/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "dependencies": { - "debug": "^4.1.1", - "lodash": "^4.17.15" + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { - "node": ">=6.0.0" - }, - "peerDependencies": { - "mocha": ">=3.1.2" + "node": ">= 0.6" } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/koa-send/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "license": "MIT", "engines": { - "node": ">=6" + "node": ">= 0.6" } }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", "dev": true, - "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" } }, - "node_modules/mocha/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "ms": "^2.1.1" } }, - "node_modules/mocha/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "node_modules/koa/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">=0.3.1" + "node": ">= 0.6" } }, - "node_modules/mocha/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 - }, - "node_modules/mocha/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "node_modules/koa/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/mocha/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==", + "node_modules/last-run": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", + "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls= sha512-U/VxvpX4N/rFvPzr3qG5EtLKEnNI0emvIQB3/ecEwv+8GHaUKbIB8vxv1Oai5FAF0d0r7LXHhLLe5K/yChm5GQ==", "dev": true, + "dependencies": { + "default-resolution": "^2.0.0", + "es6-weak-map": "^2.0.1" + }, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/lazy.js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/lazy.js/-/lazy.js-0.4.3.tgz", + "integrity": "sha1-h/Z6B602VVEh5P/xUg3zG+Znhtg= sha512-kHcnVaCZzhv6P+YgC4iRZFw62+biYIcBYU8qqKzJysC7cdKwPgb3WRtcBPyINTSLZwsjyFdBtd97sHbkseTZKw==", + "dev": true + }, + "node_modules/lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= sha512-/330KFbmC/zKdtZoVDRwvkJ8snrJyBPfoZ39zsJl2O24HOE1CTNiEbeZmHXmjBVxTSSv7JlJEXPYhU83DhA2yg==", "dev": true, - "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "readable-stream": "^2.0.5" }, "engines": { - "node": ">=10" + "node": ">= 0.6.3" } }, - "node_modules/mocha/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==", + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", + "dev": true, + "dependencies": { + "invert-kv": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "flush-write-stream": "^1.0.2" }, "engines": { - "node": ">=8" + "node": ">= 0.10" } }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">= 0.8.0" } }, - "node_modules/mocha/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "dev": true, "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" + "immediate": "~3.0.5" } }, - "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "node_modules/liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", "dev": true, - "license": "ISC", + "dependencies": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "node_modules/liftoff/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "license": "MIT", "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.1.0" + "isobject": "^3.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">=0.10.0" } }, - "node_modules/morgan/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/linkify-it": { + "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, "dependencies": { - "ms": "2.0.0" + "uc.micro": "^2.0.0" } }, - "node_modules/morgan/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { - "ee-first": "1.1.1" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" }, "engines": { - "node": ">= 0.8" + "node": ">=8.9.0" } }, - "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/multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis= sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA==", + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "dependencies": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/multimatch/node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY= sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, + "node_modules/lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= sha512-GhrVeweiTD6uTmmn5hV/lzgCQhccwReIVRLHp7LT4SopOjqEZ5BbX8b5WWEtAKasjmy8hR7ZPwsYlxRCku5odg==", + "dev": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.some": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", + "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0= sha512-j7MJE+TuT51q9ggt4fSgVqro163BEFjAt3u97IqU+JA2DkWl80nFTrowzLpZ/BnpN7rrl0JA/593NAdd8p/scQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, "dependencies": { - "array-uniq": "^1.0.1" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "dev": true, "engines": { - "node": ">= 0.10" + "node": ">=8" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "dev": true, - "license": "MIT", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, - "node_modules/nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", "dev": true, - "optional": true + "dependencies": { + "es5-ext": "~0.10.2" + } }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "semver": "^7.5.3" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", "dev": true, "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "kind-of": "^6.0.2" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/nanomatch/node_modules/kind-of": { + "node_modules/make-iterator/node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", @@ -16732,2559 +11865,2272 @@ "node": ">=0.10.0" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "node_modules/native-is-elevated": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.7.0.tgz", - "integrity": "sha512-tp8hUqK7vexBiyIWKMvmRxdG6kqUtO+3eay9iB0i16NYgvCqE5wMe1Y0guHilpkmRgvVXEWNW4et1+qqcwpLBA==", - "hasInstallScript": true - }, - "node_modules/native-keymap": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/native-keymap/-/native-keymap-3.3.5.tgz", - "integrity": "sha512-7XDOLPNX1FnUFC/cX3cioBz2M+dO212ai9DuwpfKFzkPu3xTmEzOm5xewOMLXE4V9YoRhNPxvq1H2YpPWDgSsg==", - "hasInstallScript": true - }, - "node_modules/native-watchdog": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/native-watchdog/-/native-watchdog-1.4.2.tgz", - "integrity": "sha512-iT3Uj6FFdrW5vHbQ/ybiznLus9oiUoMJ8A8nyugXv9rV3EBhIodmGs+mztrwQyyBc+PB5/CrskAH/WxaUVRRSQ==", - "hasInstallScript": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", "dev": true, "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "node_modules/map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, - "node_modules/next": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", - "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", "dev": true, - "license": "MIT", "dependencies": { - "@next/env": "15.5.6", - "@swc/helpers": "0.5.15", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" + "object-visit": "^1.0.0" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.6", - "@next/swc-darwin-x64": "15.5.6", - "@next/swc-linux-arm64-gnu": "15.5.6", - "@next/swc-linux-arm64-musl": "15.5.6", - "@next/swc-linux-x64-gnu": "15.5.6", - "@next/swc-linux-x64-musl": "15.5.6", - "@next/swc-win32-arm64-msvc": "15.5.6", - "@next/swc-win32-x64-msvc": "15.5.6", - "sharp": "^0.34.3" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "dev": true + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "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.mjs" + } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "node_modules/matchdep": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", + "integrity": "sha512-LFgVbaHIHMqCRuCZyfCtUOq9/Lnzhi7Z0KFUE2fhD54+JN2jLh3hC02RLkqauJ3U4soU6H1J3tfj/Byk7GoEjA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "findup-sync": "^2.0.0", + "micromatch": "^3.0.4", + "resolve": "^1.4.0", + "stack-trace": "0.0.10" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.10.0" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/nise": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", - "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "node_modules/matchdep/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "dependencies": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^7.0.4", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/nise/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true + "node_modules/matchdep/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "node_modules/matchdep/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", "dev": true, "dependencies": { - "isarray": "0.0.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/node-abi": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", - "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "node_modules/matchdep/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, "dependencies": { - "semver": "^7.3.5" + "is-extendable": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "node_modules/matchdep/node_modules/findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha512-vs+3unmJT45eczmcAZ6zMJtxN3l/QXeccaXQx5cu/MeJMhewVfoWZqibRkOxPnmoR59+Zy5hjabfQc6JLSah4g==", + "dev": true, "license": "MIT", + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + }, "engines": { - "node": "^16 || ^18 || >= 20" + "node": ">= 0.10" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", + "node_modules/matchdep/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, "engines": { - "node": ">=10.5.0" + "node": ">=0.10.0" } }, - "node_modules/node-fetch": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", - "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", + "node_modules/matchdep/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, "dependencies": { - "whatwg-url": "^5.0.0" + "is-extglob": "^2.1.0" }, "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/node-html-markdown": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-1.3.0.tgz", - "integrity": "sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==", + "node_modules/matchdep/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", "dev": true, "dependencies": { - "node-html-parser": "^6.1.1" + "kind-of": "^3.0.2" }, "engines": { - "node": ">=10.0.0" + "node": ">=0.10.0" } }, - "node_modules/node-html-parser": { - "version": "6.1.13", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", - "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", + "node_modules/matchdep/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, - "node_modules/node-pty": { - "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": { - "node-addon-api": "^7.1.0" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "node_modules/matchdep/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/nodemon": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", - "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "node_modules/matchdep/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, - "license": "MIT", "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" + "node": ">=0.10.0" } }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/matchdep/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, - "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= sha512-+5XZFpQZEY0cg5JaxLwGxDlKNKYxuXwGt8/Oi3UXm5/4ymrJve9d2CURituxv3rSrVCGZj4m1U1JlHTdcKt2Ng==", + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", "dev": true, + "optional": true, "dependencies": { - "abbrev": "1", - "osenv": "^0.1.4" + "escape-string-regexp": "^4.0.0" }, - "bin": { - "nopt": "bin/nopt.js" + "engines": { + "node": ">=10" } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "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, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", "dev": true, - "bin": { - "semver": "bin/semver" + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" } }, - "node_modules/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "node_modules/memory-fs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", + "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, "dependencies": { - "once": "^1.3.2" + "errno": "^0.1.3", + "readable-stream": "^2.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=4.3.0 <5.0.0 || >=5.10" } }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "node_modules/memory-fs/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/npm-run-all/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI= sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, "engines": { - "node": ">=4" + "node": ">= 0.10.0" } }, - "node_modules/npm-run-all/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/merge-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-1.0.1.tgz", + "integrity": "sha512-iuPV41VWKWBIOpBsjoxjDZw8/GbSfZ2mk7N1453bwMrfzdrIk7EzBd+8UVR6rkw67th7xnk9Dytl3J+lHPdxvg==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "is-plain-obj": "^1.1" }, "engines": { "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/npm-run-all/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "license": "MIT" }, - "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "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, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, "engines": { - "node": ">=4.8" + "node": ">= 8" } }, - "node_modules/npm-run-all/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "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": ">=0.8.0" + "node": ">=8.6" } }, - "node_modules/npm-run-all/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "bin": { + "mime": "cli.js" + }, "engines": { "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "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, - "bin": { - "semver": "bin/semver" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "node_modules/npm-run-all/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "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": { - "shebang-regex": "^1.0.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.6" } }, - "node_modules/npm-run-all/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, - "node_modules/npm-run-all/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, "engines": { "node": ">=4" } }, - "node_modules/npm-run-all/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "isexe": "^2.0.0" + "brace-expansion": "^1.1.7" }, - "bin": { - "which": "bin/which" + "engines": { + "node": "*" } }, - "node_modules/nth-check": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", - "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 18" } }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw= sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, + "node_modules/minizlib/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { - "node": ">=0.10.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "dependencies": { - "is-descriptor": "^0.1.0" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "deprecated": "Please upgrade to v0.1.7", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "deprecated": "Please upgrade to v0.1.5", + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", "dev": true, + "license": "MIT", "dependencies": { - "kind-of": "^3.0.2" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" }, "engines": { - "node": ">=0.10.0" + "node": ">= 14.0.0" } }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/mocha-junit-reporter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", + "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", "dev": true, "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" + "debug": "^4.3.4", + "md5": "^2.3.0", + "mkdirp": "^3.0.0", + "strip-ansi": "^6.0.1", + "xml": "^1.0.1" }, - "engines": { - "node": ">=0.10.0" + "peerDependencies": { + "mocha": ">=2.2.5" } }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "node_modules/mocha-junit-reporter/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "node_modules/mocha-multi-reporters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", + "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", "dev": true, "dependencies": { - "is-buffer": "^1.1.5" + "debug": "^4.1.1", + "lodash": "^4.17.15" }, "engines": { - "node": ">=0.10.0" + "node": ">=6.0.0" + }, + "peerDependencies": { + "mocha": ">=3.1.2" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/mocha/node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 6" + "node": ">=6" } }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/mocha/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 + }, + "node_modules/mocha/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "node_modules/mocha/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, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { - "isobject": "^3.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", + "node_modules/mocha/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, "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "dependencies": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "license": "MIT", + "node_modules/mocha/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "dev": true, + "license": "MIT", "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8.0" } }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" + "ms": "2.0.0" } }, - "node_modules/object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60= sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, "dependencies": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" + "ee-first": "1.1.1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "license": "MIT", + "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/multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis= sha512-0mzK8ymiWdehTBiJh0vClAzGyQbdtyWqzSVx//EK4N/D+599RFlGfTAsKw2zMSABtDG9C6Ul2+t8f2Lbdjf5mA==", + "dev": true, "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/ollama": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.5.18.tgz", - "integrity": "sha512-lTFqTf9bo7Cd3hpF6CviBe/DEhewjoZYd9N/uCe7O20qYTvGqrNOFOBDj3lbZgFWHUgDv5EeyusYxsZSLS8nvg==", - "license": "MIT", - "dependencies": { - "whatwg-fetch": "^3.6.20" + "node": ">=0.10.0" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/multimatch/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", + "dev": true, "dependencies": { - "ee-first": "1.1.1" + "array-uniq": "^1.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">=0.10.0" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "node_modules/mute-stdout": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", + "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } + "node_modules/nan": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "dev": true, + "optional": true }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, - "node_modules/open": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", - "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", - "license": "MIT", + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/openai": { - "version": "4.104.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", - "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", - "license": "Apache-2.0", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.23.8" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.130", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", - "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", - "license": "MIT", - "dependencies": { - "undici-types": "~5.26.4" - } + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, - "node_modules/openai/node_modules/form-data-encoder": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", - "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "node_modules/native-is-elevated": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/native-is-elevated/-/native-is-elevated-0.9.0.tgz", + "integrity": "sha512-DTPZixwPTtWHU9g46hXMyK6eW77e8L69uZNkB/cjY00BF8NCXQ5cyJEDim91WmxHruJoIQzLdZBb4TEjrI+PQw==", + "hasInstallScript": true, "license": "MIT" }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "node_modules/native-keymap": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/native-keymap/-/native-keymap-3.3.9.tgz", + "integrity": "sha512-d/ydQ5x+GM5W0dyAjFPwexhtc9CDH1g/xWZESS5CXk16ThyFzSBLvlBJq1+FyzUIFf/F2g1MaHdOpa6G9150YQ==", + "hasInstallScript": true, "license": "MIT" }, - "node_modules/opentype.js": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz", - "integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==", - "license": "MIT", - "dependencies": { - "tiny-inflate": "^1.0.2" - }, - "bin": { - "ot": "bin/ot" - } + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, + "license": "MIT", "engines": { - "node": ">= 0.8.0" + "node": ">= 0.6" } }, - "node_modules/ora": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", - "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true + }, + "node_modules/nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", "dev": true, "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^4.0.0", - "cli-spinners": "^2.9.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^1.3.0", - "log-symbols": "^5.1.0", - "stdin-discarder": "^0.1.0", - "string-width": "^6.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" } }, - "node_modules/ora/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/nise/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/nise/node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "dependencies": { + "isarray": "0.0.1" } }, - "node_modules/ora/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node_modules/node-abi": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", + "integrity": "sha512-tzua9qWWi7iW4I42vUPKM+SfaF0vQSLAm4yO5J83mSwB7GeoWrDKC/K+8YCnYNwqP5duwazbw2X9l4m8SC2cUw==", + "dependencies": { + "semver": "^7.3.5" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=10" } }, - "node_modules/ora/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, + "node_modules/node-addon-api": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", + "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^16 || ^18 || >= 20" } }, - "node_modules/ora/node_modules/log-symbols": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", - "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "node_modules/node-fetch": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.8.tgz", + "integrity": "sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==", "dev": true, "dependencies": { - "chalk": "^5.0.0", - "is-unicode-supported": "^1.1.0" + "whatwg-url": "^5.0.0" }, "engines": { - "node": ">=12" + "node": "4.x || >=6.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, - "node_modules/ora/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/node-html-markdown": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-1.3.0.tgz", + "integrity": "sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==", "dev": true, "dependencies": { - "ansi-regex": "^6.0.1" + "node-html-parser": "^6.1.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=10.0.0" } }, - "node_modules/ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", + "node_modules/node-html-parser": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", + "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", "dev": true, "dependencies": { - "readable-stream": "^2.0.1" + "css-select": "^5.1.0", + "he": "1.2.0" } }, - "node_modules/ordered-read-streams/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, + "node_modules/node-pty": { + "version": "1.2.0-beta.10", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.10.tgz", + "integrity": "sha512-vONwSCtAiOVNxeaP/lzDdRw733Q6uB/ELOCFM8DUfKMw6rTFovwFCuvqr9usya7JXV2pfaers3EwuzZfv0QtwA==", + "hasInstallScript": true, + "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node-addon-api": "^7.1.0" } }, - "node_modules/original-fs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/original-fs/-/original-fs-1.2.0.tgz", - "integrity": "sha512-IGo+qFumpIV65oDchJrqL0BOk9kr82fObnTesNJt8t3YgP6vfqcmRs0ofPzg3D9PKMeBHt7lrg1k/6L+oFdS8g==", + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, - "license": "Unlicense" + "license": "MIT" }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true + "node_modules/nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= sha512-+5XZFpQZEY0cg5JaxLwGxDlKNKYxuXwGt8/Oi3UXm5/4ymrJve9d2CURituxv3rSrVCGZj4m1U1JlHTdcKt2Ng==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M= sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, - "engines": { - "node": ">=0.10.0" + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "node_modules/os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "dependencies": { - "lcid": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" + "bin": { + "semver": "bin/semver" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "deprecated": "This package is no longer supported.", + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "dev": true, - "dependencies": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, "engines": { - "node": ">= 0.4" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-all": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-all/-/p-all-1.0.0.tgz", - "integrity": "sha1-k731OlWiOCH9+pi0F0qZv38x340= sha512-OtbznqfGjQT+i89LK9C9YPh1G8d6n8xgsJ8yRVXrx6PRXrlOthNJhP+dHxrPopty8fugYb1DodpwrzP7z0Mtvw==", + "node_modules/now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", "dev": true, "dependencies": { - "p-map": "^1.0.0" + "once": "^1.3.2" }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, + "license": "ISC", "engines": { - "node": ">=8" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/npm-run-all2": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz", + "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "picomatch": "^4.0.2", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" }, - "engines": { - "node": ">=10" + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.5.0 || >=22.0.0", + "npm": ">= 10" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/npm-run-all2/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, - "dependencies": { - "p-limit": "^3.0.2" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=16" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/npm-run-all2/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true - }, - "node_modules/pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", - "dev": true - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/npm-run-all2/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" + "bin": { + "pidtree": "bin/pidtree.js" }, "engines": { - "node": ">=6" + "node": ">=0.10" } }, - "node_modules/parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "isexe": "^3.1.1" }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/parse-imports": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", - "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", - "dev": true, - "dependencies": { - "es-module-lexer": "^1.5.3", - "slashes": "^3.0.12" + "bin": { + "node-which": "bin/which.js" }, "engines": { - "node": ">= 18" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", "dev": true, "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "boolbase": "^1.0.0" }, - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/parse-node-version": { + "node_modules/number-is-nan": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18= sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw= sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", "dev": true, "dependencies": { - "path-root-regex": "^0.1.0" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, + "dependencies": { + "is-descriptor": "^0.1.0" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/object-copy/node_modules/is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", + "deprecated": "Please upgrade to v0.1.7", "dev": true, "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "kind-of": "^3.0.2" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=0.10.0" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", - "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", + "node_modules/object-copy/node_modules/is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", + "deprecated": "Please upgrade to v0.1.5", "dev": true, + "dependencies": { + "kind-of": "^3.0.2" + }, "engines": { - "node": "14 || >=16.14" + "node": ">=0.10.0" } }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/object-copy/node_modules/is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, + "dependencies": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", "dev": true, - "dependencies": { - "through": "~2.3" - } - }, - "node_modules/pdfjs-dist": { - "version": "5.4.394", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.394.tgz", - "integrity": "sha512-9ariAYGqUJzx+V/1W4jHyiyCep6IZALmDzoaTLZ6VNu8q9LWi1/ukhzHgE2Xsx96AZi0mbZuK4/ttIbqSbLypg==", - "license": "Apache-2.0", "engines": { - "node": ">=20.16.0 || >=22.3.0" - }, - "optionalDependencies": { - "@napi-rs/canvas": "^0.1.81" + "node": ">=0.10.0" } }, - "node_modules/peek-stream": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", - "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", + "node_modules/object-copy/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "dev": true, "dependencies": { - "buffer-from": "^1.0.0", - "duplexify": "^3.5.0", - "through2": "^2.0.3" + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" } }, - "node_modules/peek-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "engines": { + "node": ">= 0.4" } }, - "node_modules/peek-stream/node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", "dev": true, "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "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==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, "engines": { - "node": ">=8.6" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/pidtree": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", - "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=0.10.0" } }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA= sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, "engines": { "node": ">=0.10.0" } }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o= sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "node_modules/object.reduce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", + "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60= sha512-naLhxxpUESbNkRqc35oQ2scZSJueHGQNUfMW/0U37IgN6tE2dgDWg3whf+NEliy3F/QysrO48XKUz/nGPe+AQw==", "dev": true, "dependencies": { - "pinkie": "^2.0.0" + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, "engines": { - "node": ">= 6" + "node": ">= 0.8" } }, - "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, "license": "MIT", "engines": { - "node": ">=16.20.0" + "node": ">= 0.8" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E= sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "node_modules/opentype.js": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/opentype.js/-/opentype.js-0.8.0.tgz", + "integrity": "sha512-FQHR4oGP+a0m/f6yHoRpBOIbn/5ZWxKd4D/djHVJu8+KpBTYrJda0b7mLcgDEMWXE9xBCJm+qb0yv6FcvPjukg==", + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "tiny-inflate": "^1.0.2" }, - "engines": { - "node": ">=8" + "bin": { + "ot": "bin/ot" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "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==", + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/playwright": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", - "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.56.1" - }, - "bin": { - "playwright": "cli.js" - }, "engines": { - "node": ">=18" + "node": ">=12" }, - "optionalDependencies": { - "fsevents": "2.3.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/playwright-core": { - "version": "1.47.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", - "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", + "node_modules/ora/node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/playwright/node_modules/playwright-core": { - "version": "1.56.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", - "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" + "dependencies": { + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-83vX4eYdQp3vP9SxuYgEM/G/pJQqLUz/V/xzPrzruLs7fz7jxGQ1msZ/mg1nwZxUSuOp4sb+/bEIbRrbzZRxDA==", + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", "dev": true, "dependencies": { - "base64-js": "^1.5.1", - "xmlbuilder": "^9.0.7" - }, - "engines": { - "node": ">=6" + "readable-stream": "^2.0.1" } }, - "node_modules/plist/node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==", + "node_modules/ordered-read-streams/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "engines": { - "node": ">=4.0" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/plugin-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", - "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M= sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", "dev": true, - "dependencies": { - "ansi-colors": "^1.0.1", - "arr-diff": "^4.0.0", - "arr-union": "^3.1.0", - "extend-shallow": "^3.0.2" - }, "engines": { - "node": ">= 0.10" + "node": ">=0.10.0" } }, - "node_modules/plugin-error/node_modules/ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "node_modules/os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", "dev": true, "dependencies": { - "ansi-wrap": "^0.1.0" + "lcid": "^1.0.0" }, "engines": { "node": ">=0.10.0" } }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" } }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "node_modules/p-all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-all/-/p-all-1.0.0.tgz", + "integrity": "sha1-k731OlWiOCH9+pi0F0qZv38x340= sha512-OtbznqfGjQT+i89LK9C9YPh1G8d6n8xgsJ8yRVXrx6PRXrlOthNJhP+dHxrPopty8fugYb1DodpwrzP7z0Mtvw==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "p-map": "^1.0.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=4" } }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" - }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" + "node": ">=8" } }, - "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=10" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=10" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "node_modules/p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", "dev": true, - "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, - "license": "MIT", "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=6" } }, - "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", + "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": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } + "license": "BlueOak-1.0.0" }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "dev": true }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" + "callsites": "^3.0.0" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" + "node": ">=6" } }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", "dependencies": { - "camelcase-css": "^2.0.1" + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" }, "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" + "node": ">=0.8" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "node_modules/parse-imports": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/parse-imports/-/parse-imports-2.2.1.tgz", + "integrity": "sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", "dependencies": { - "lilconfig": "^3.1.1" + "es-module-lexer": "^1.5.3", + "slashes": "^3.0.12" }, "engines": { "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } } }, - "node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", - "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">= 0.10" } }, - "node_modules/postcss-merge-rules": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", - "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", - "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">= 0.8" } }, - "node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", - "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", "dev": true, - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-minify-params": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", - "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", - "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18= sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=8" } }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", - "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", "dev": true, "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "path-root-regex": "^0.1.0" }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": ">=0.10.0" } }, - "node_modules/postcss-modules-scope": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", - "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { - "postcss-selector-parser": "^6.0.4" + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": "^10 || ^12 || >= 14" + "node": ">=16 || 14 >=14.18" }, - "peerDependencies": { - "postcss": "^8.1.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.3.0.tgz", + "integrity": "sha512-CQl19J/g+Hbjbv4Y3mFNNXFEL/5t/KCg8POCuUqd4rMKjGG+j1ybER83hxV58zL+dFI1PTkt3GNFSHRt+d8qEQ==", "dev": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "node": "14 || >=16.14" } }, - "node_modules/postcss-nested": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", - "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" + "node": ">=8" } }, - "node_modules/postcss-nested/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", "dev": true, - "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" + "through": "~2.3" } }, - "node_modules/postcss-nesting": { - "version": "12.1.5", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz", - "integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==", + "node_modules/peek-stream": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz", + "integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", "dependencies": { - "@csstools/selector-resolve-nested": "^1.1.0", - "@csstools/selector-specificity": "^3.1.1", - "postcss-selector-parser": "^6.1.0" - }, - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.4" + "buffer-from": "^1.0.0", + "duplexify": "^3.5.0", + "through2": "^2.0.3" } }, - "node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", - "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "node_modules/peek-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", - "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "node_modules/peek-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" - }, + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "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==" + }, + "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.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=8.6" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/postcss-normalize-positions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", - "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA= sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", - "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o= sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "pinkie": "^2.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-normalize-string": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", - "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "find-up": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", - "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", - "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" + "p-locate": "^4.1.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-normalize-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", - "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "p-try": "^2.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=6" }, - "peerDependencies": { - "postcss": "^8.4.31" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", - "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "p-limit": "^2.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=8" } }, - "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, - "peerDependencies": { - "postcss": "^8.4.31" + "optionalDependencies": { + "fsevents": "2.3.2" } }, - "node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", - "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=18" } }, - "node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", - "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", "dev": true, "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0" + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=10.4.0" } }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "node_modules/plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", "dev": true, - "license": "MIT", "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" + "ansi-colors": "^1.0.1", + "arr-diff": "^4.0.0", + "arr-union": "^3.1.0", + "extend-shallow": "^3.0.2" }, "engines": { - "node": ">=4" + "node": ">= 0.10" } }, - "node_modules/postcss-svgo": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", - "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", + "node_modules/plugin-error/node_modules/ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", "dev": true, - "license": "MIT", "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" + "ansi-wrap": "^0.1.0" }, "engines": { - "node": "^14 || ^16 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=0.10.0" } }, - "node_modules/postcss-svgo/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==", + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", "dev": true, - "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=0.10.0" } }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-svgo/node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postcss-svgo/node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz", + "integrity": "sha512-L4QzMnOdVwRm1Qb8m4x8jsZzKAaPAgrUF1r/hjDR2Xj7R+8Zsf97jAlSQzWtKx5YNiNGN8QxmPFIc/sh+RQl+Q==", "dev": true, - "license": "MIT", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postcss-svgo/node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "node_modules/postcss-modules-scope": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz", + "integrity": "sha512-uZgqzdTleelWjzJY+Fhti6F3C9iF1JR/dODLs/JDefozYcKTBCdD8BIl6nNPbTbcLnGrk56hzwZC2DaGNvYjzA==", "dev": true, - "license": "CC0-1.0" + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dev": true, - "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" + "icss-utils": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": "^10 || ^12 || >= 14" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/postcss-unique-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", - "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.16" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" + "node": ">=4" } }, "node_modules/postcss-value-parser": { @@ -19293,18 +14139,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/posthog-node": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.18.0.tgz", - "integrity": "sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==", - "license": "MIT", - "dependencies": { - "axios": "^1.8.2" - }, - "engines": { - "node": ">=15.0.0" - } - }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -19413,45 +14247,12 @@ "node": ">=0.4.0" } }, - "node_modules/promise-stream-reader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", - "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", - "license": "MIT", - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", "dev": true }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -19503,13 +14304,6 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.2.tgz", @@ -19541,16 +14335,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -19560,21 +14344,6 @@ "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==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -19611,46 +14380,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -19679,79 +14408,28 @@ "integrity": "sha512-JkXJ0IrUcdupLoIx6gE4YcFaMVSGtu7kQf4NJoDJUnfBZGuATmJ2Yal2v55KTltp+WV8dGr7A0RtOzx6jmtM6Q==", "dev": true }, - "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-tooltip": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.30.0.tgz", - "integrity": "sha512-Yn8PfbgQ/wmqnL7oBpz1QiDaLKrzZMdSUUdk7nVeGTwzbxCAJiJzR4VSYW+eIO42F1INt57sPUmpgKv0KwJKtg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.6.1", - "classnames": "^2.3.0" - }, - "peerDependencies": { - "react": ">=16.14.0", - "react-dom": ">=16.14.0" - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-cache/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/read-pkg-up": { @@ -19869,18 +14547,6 @@ "node": ">=0.10.0" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -19918,28 +14584,6 @@ "node": ">= 0.10" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/regex-not": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", @@ -19949,28 +14593,8 @@ "extend-shallow": "^3.0.2", "safe-regex": "^1.1.0" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, "node_modules/remove-bom-buffer": { @@ -20120,6 +14744,7 @@ "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, "engines": { "node": ">=0.10.0" } @@ -20137,22 +14762,18 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", "dev": true, - "license": "MIT", "dependencies": { - "is-core-module": "^2.16.1", + "is-core-module": "^2.9.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, - "engines": { - "node": ">= 0.4" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -20389,80 +15010,6 @@ "node": ">=8.0" } }, - "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -20495,58 +15042,11 @@ } ] }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, "node_modules/safe-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", @@ -20556,41 +15056,12 @@ "ret": "~0.1.10" } }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, "node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -20611,6 +15082,22 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/schema-utils/node_modules/ajv-keywords": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", @@ -20623,39 +15110,16 @@ "ajv": "^8.8.2" } }, - "node_modules/scope-tailwind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz", - "integrity": "sha512-sxtAKxJq143lYK/RCE36YGq13ficBZ9/9Z0TZa78k0AEiKNT5nH4kfhD8YAfEXR/qPR+G7tl9KL4UoHh+Cs93g==", - "dev": true, - "license": "AGPL-3.0-only", - "dependencies": { - "@babel/core": "^7.26.0", - "@babel/preset-typescript": "^7.26.0", - "@babel/types": "^7.26.0", - "autoprefixer": "^10.4.20", - "commander": "^12.1.0", - "postcss": "^8.4.48", - "postcss-nested": "^7.0.2" - }, - "bin": { - "scope-tailwind": "dist/main.js" - } - }, - "node_modules/scope-tailwind/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/schema-utils/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 }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -20683,67 +15147,6 @@ "node": ">= 0.10" } }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/send/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serialize-error": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", @@ -20783,30 +15186,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", - "license": "MIT", - "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serve-static/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -20817,6 +15196,7 @@ "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", @@ -20829,35 +15209,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -20915,7 +15266,8 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true }, "node_modules/shallow-clone": { "version": "3.0.1", @@ -20938,142 +15290,32 @@ "node": ">=0.10.0" } }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "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" - }, - "funding": { - "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==", - "license": "MIT", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "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==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "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==", + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", "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" }, @@ -21142,19 +15384,6 @@ "simple-concat": "^1.0.0" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sinon": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", @@ -21192,16 +15421,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/sinon/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -21702,9 +15921,10 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -21760,19 +15980,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-combiner": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", @@ -21881,6 +16088,7 @@ "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", @@ -21894,7 +16102,8 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { "version": "6.0.1", @@ -21923,116 +16132,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.padend": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz", - "integrity": "sha512-eCzTASPnoCr5Ht+Vn1YXgm8SB015hHKgEIMu9Nr9bQmLhRBxKRfmzSj/IQsxDFc8JInJDDFA0qXwK+xxI7wDkg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -22051,6 +16150,7 @@ "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" }, @@ -22058,15 +16158,6 @@ "node": ">=8" } }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -22094,161 +16185,14 @@ "integrity": "sha512-RHs/vcrKdQK8wZliteNK4NKzxvLBzpuHMqYmUVWeKa6MkaIQ97ZTOS0b+zapZhy6GcrgWnvWYCMHRirC3FsUmw==", "dev": true, "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/styled-jsx": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", - "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "client-only": "0.0.1" - }, - "engines": { - "node": ">= 12.0.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/stylehacks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", - "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "glob": "^10.3.10", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/sucrase/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/sucrase/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": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, "node_modules/sumchecker": { @@ -22279,6 +16223,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -22422,134 +16367,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", - "license": "MIT" - }, - "node_modules/tailwindcss": { - "version": "3.4.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", - "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.6.0", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.21.7", - "lilconfig": "^3.1.3", - "micromatch": "^4.0.8", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.1.1", - "postcss": "^8.4.47", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", - "postcss-nested": "^6.2.0", - "postcss-selector-parser": "^6.1.2", - "resolve": "^1.22.8", - "sucrase": "^3.35.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tailwindcss/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, "node_modules/tapable": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz", @@ -22560,19 +16377,19 @@ } }, "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", + "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -22611,15 +16428,32 @@ "streamx": "^2.15.0" } }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } }, - "node_modules/tas-client-umd": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tas-client-umd/-/tas-client-umd-0.2.0.tgz", - "integrity": "sha512-oezN7mJVm5qZDVEby7OzxCLKUpUN5of0rY4dvOWaDF2JZBlGpd3BXceFN8B53qlTaIkVSzP65aAMT0Vc+/N25Q==" + "node_modules/tas-client": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.3.1.tgz", + "integrity": "sha512-Mn4+4t/KXEf8aIENeI1TkzpKIImzmG+FjPZ2dlaoGNFgxJqBE/pp3MT7nc2032EG4aS73E4OEcr2WiNaWW8mdA==", + "license": "MIT", + "engines": { + "node": ">=22" + } }, "node_modules/teex": { "version": "1.0.1", @@ -22764,29 +16598,6 @@ "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI= sha512-jm9KjEWiDmtGLBrTqXEduGzlYTTlPaoDKdq5YRQhD0rYjo61ZNTYKZ/x5J4ajPSBH9wIYY5qm9GNG5otIKjtOA==", "dev": true }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -22863,13 +16674,6 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -22956,6 +16760,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -23004,20 +16809,11 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "engines": { "node": ">=0.6" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", @@ -23033,6 +16829,15 @@ "node": ">=6" } }, + "node_modules/tough-cookie/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie/node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -23045,7 +16850,8 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true }, "node_modules/tree-kill": { "version": "1.2.2", @@ -23069,13 +16875,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ts-loader": { "version": "9.5.1", "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", @@ -23115,214 +16914,61 @@ "code-block-writer": "^12.0.0" } }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/tsec": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tsec/-/tsec-0.2.7.tgz", "integrity": "sha512-Pj9DuBBWLEo8p7QsbrEdXzW/u6QJBcib0ZGOTXkeSDx+PLXFY7hwyZE9Tfhp3TA3LQNpYouyT0WmzXRyUW4otQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "glob": "^7.1.1", - "minimatch": "^3.0.3" - }, - "bin": { - "tsec": "bin/tsec" - }, - "peerDependencies": { - "@bazel/bazelisk": ">=1.7.5", - "@bazel/concatjs": ">=5.3.0", - "typescript": ">=3.9.2" - } - }, - "node_modules/tsec/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "dev": true, - "engines": { - "node": ">=0.6.x" - } - }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" + "glob": "^7.1.1", + "minimatch": "^3.0.3" }, - "engines": { - "node": ">= 14.16.0" + "bin": { + "tsec": "bin/tsec" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "@bazel/bazelisk": ">=1.7.5", + "@bazel/concatjs": ">=5.3.0", + "typescript": ">=3.9.2" } }, - "node_modules/tsup/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/tsec/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "license": "MIT", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, "engines": { - "node": ">= 14.18.0" + "node": "*" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/tsup/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true }, - "node_modules/tsup/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "engines": { - "node": ">= 12" + "node": ">=0.6.x" } }, "node_modules/tunnel": { @@ -23386,90 +17032,45 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.6" } }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.4" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typedarray": { @@ -23479,9 +17080,9 @@ "dev": true }, "node_modules/typescript": { - "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==", + "version": "6.0.0-dev.20260130", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20260130.tgz", + "integrity": "sha512-flWwLX5Xzh7to9d46u3LXfVDq9F0L0FtgnsYcx/SksqP05uHBIPnWfB6wWOZphTkb7GRSRKU13X/zBHmbzhXXg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -23531,31 +17132,6 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "dev": true }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -23565,13 +17141,6 @@ "node": ">=0.10.0" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undertaker": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.3.0.tgz", @@ -23609,9 +17178,9 @@ "dev": true }, "node_modules/undici": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.9.0.tgz", - "integrity": "sha512-e696y354tf5cFZPXsF26Yg+5M63+5H3oE6Vtkh2oqbvsE2Oe7s2nIbcQh5lmG7Lp/eS29vJtTpw9+p6PX0qNSg==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz", + "integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -23621,6 +17190,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/union-value": { @@ -23672,15 +17242,6 @@ "node": ">= 10.0.0" } }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -23740,9 +17301,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dev": true, "funding": [ { @@ -23775,11 +17336,19 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -23836,12 +17405,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/v8-inspect-profiler": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/v8-inspect-profiler/-/v8-inspect-profiler-0.1.1.tgz", @@ -23905,6 +17468,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "engines": { "node": ">= 0.8" } @@ -24077,16 +17641,17 @@ } }, "node_modules/vscode-textmate": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.1.tgz", - "integrity": "sha512-eXiUi2yYFv9bdvgrYtJynA7UemCEkpVNE50S9iBBA08LYG5t9+/TB+8IRS/YoYOubCez2OkSyZ1Q12eQMwzbrw==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", "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==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" }, "node_modules/watchpack": { "version": "2.4.1", @@ -24101,15 +17666,6 @@ "node": ">=10.13.0" } }, - "node_modules/web-streams-polyfill": { - "version": "4.0.0-beta.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", - "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/web-tree-sitter": { "version": "0.20.8", "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz", @@ -24119,7 +17675,8 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true }, "node_modules/webpack": { "version": "5.100.0", @@ -24362,16 +17919,11 @@ "node": ">=4.0" } }, - "node_modules/whatwg-fetch": { - "version": "3.6.20", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", - "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", - "license": "MIT" - }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0= sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -24381,6 +17933,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -24391,76 +17944,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "license": "MIT" - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -24468,18 +17951,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -24495,10 +17976,11 @@ "dev": true }, "node_modules/windows-foreground-love": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.5.0.tgz", - "integrity": "sha512-yjBwmKEmQBDk3Z7yg/U9hizGWat8C6Pe4MQWl5bN6mvPU81Bt6HV2k/6mGlK3ETJLW1hCLhYx2wcGh+ykUUCyA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/windows-foreground-love/-/windows-foreground-love-0.6.1.tgz", + "integrity": "sha512-uusaXCkDfifZkV/EJMnfJ/i9hsY1BGS9YIQs7LGGRrg1+au7er4ONADtMp6svQWlwgztTgWu31g0nT5OPyYLwg==", "hasInstallScript": true, + "license": "MIT", "optional": true }, "node_modules/word-wrap": { @@ -24540,6 +18022,7 @@ "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", @@ -24556,13 +18039,15 @@ "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 + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -24597,27 +18082,6 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -24646,6 +18110,16 @@ "node": ">=4.0" } }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -24770,24 +18244,6 @@ "buffer-crc32": "~0.2.3" } }, - "node_modules/ylru": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", - "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -24808,28 +18264,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } - }, - "node_modules/zx": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/zx/-/zx-8.7.0.tgz", - "integrity": "sha512-pArftqj5JV/er8p+czFZwF+k6SbCldl7kcfCR+rIiDIh3gUsLB0F3Xh05diP8PzToZ39D/GWeFoVFimjHQkbAg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "zx": "build/cli.js" - }, - "engines": { - "node": ">= 12.17.0" - } } } } diff --git a/package.json b/package.json index a80847ceb23..449d28fd1e1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.106.0", - "distro": "21c8d8ea1e46d97c5639a7cabda6c0e063cc8dd5", + "version": "1.110.0", + "distro": "6a427d4e06fa83b1b299fde50735094cb6562065", "author": { "name": "Microsoft Corporation" }, @@ -10,149 +10,122 @@ "type": "module", "private": true, "scripts": { - "buildreact": "cd ./src/vs/workbench/contrib/cortexide/browser/react/ && node build.js && cd ../../../../../../../", - "watchreact": "cd ./src/vs/workbench/contrib/cortexide/browser/react/ && node build.js --watch && cd ../../../../../../../", - "watchreactd": "deemon npm run watchreact", "test": "echo Please run any of the test scripts from the scripts folder.", - "test:ci": "npm run test-node && npm run test-browser-no-install", "test-browser": "npx playwright install && node test/unit/browser/index.js", "test-browser-no-install": "node test/unit/browser/index.js", "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --timeout=5000 --exit", "test-extension": "vscode-test", "test-build-scripts": "cd build && npm run test", - "lint:ci": "npm run eslint", - "coverage:report": "npm run test-node -- --reporter json --reporter-options output=coverage/test-results.json && nyc report --reporter=cobertura --reporter=text --reporter=html", - "preinstall": "node build/npm/preinstall.js", - "postinstall": "node build/npm/postinstall.js", - "compile": "node --max-old-space-size=12288 ./node_modules/gulp/bin/gulp.js compile", + "preinstall": "node build/npm/preinstall.ts", + "postinstall": "node build/npm/postinstall.ts", + "compile": "npm run gulp compile", "compile-check-ts-native": "tsgo --project ./src/tsconfig.json --noEmit --skipLibCheck", - "watch": "npm-run-all -lp watch-client watch-extensions", + "watch": "npm-run-all2 -lp watch-client watch-extensions", "watchd": "deemon npm run watch", "watch-webd": "deemon npm run watch-web", "kill-watchd": "deemon --kill npm run watch", "kill-watch-webd": "deemon --kill npm run watch-web", "restart-watchd": "deemon --restart npm run watch", "restart-watch-webd": "deemon --restart npm run watch-web", - "watch-client": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client", + "watch-client": "npm run gulp watch-client", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", - "watch-extensions": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", + "watch-extensions": "npm run gulp watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", - "precommit": "node build/hygiene.js", + "precommit": "node --experimental-strip-types build/hygiene.ts", "gulp": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js", - "electron": "node build/lib/electron", + "electron": "node build/lib/electron.ts", "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", + "update-grammars": "node build/npm/update-all-grammars.ts", + "update-localization-extension": "node build/npm/update-localization-extension.ts", + "mixin-telemetry-docs": "node build/npm/mixin-telemetry-docs.ts", + "smoketest": "node build/lib/preLaunch.ts && 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", - "download-builtin-extensions-cg": "node build/lib/builtInExtensionsCG.js", + "download-builtin-extensions": "node build/lib/builtInExtensions.ts", + "download-builtin-extensions-cg": "node build/lib/builtInExtensionsCG.ts", "monaco-compile-check": "tsgo --project src/tsconfig.monaco.json --noEmit", "tsec-compile-check": "node node_modules/tsec/bin/tsec -p src/tsconfig.tsec.json", "vscode-dts-compile-check": "tsgo --project src/tsconfig.vscode-dts.json && tsgo --project src/tsconfig.vscode-proposed-dts.json", - "valid-layers-check": "node build/checker/layersChecker.js && tsgo --project build/checker/tsconfig.browser.json && tsgo --project build/checker/tsconfig.worker.json && tsgo --project build/checker/tsconfig.node.json && tsgo --project build/checker/tsconfig.electron-browser.json && tsgo --project build/checker/tsconfig.electron-main.json && tsgo --project build/checker/tsconfig.electron-utility.json", - "define-class-fields-check": "node build/lib/propertyInitOrderChecker.js && tsgo --project src/tsconfig.defineClassFields.json", - "update-distro": "node build/npm/update-distro.mjs", + "valid-layers-check": "node build/checker/layersChecker.ts && tsgo --project build/checker/tsconfig.browser.json && tsgo --project build/checker/tsconfig.worker.json && tsgo --project build/checker/tsconfig.node.json && tsgo --project build/checker/tsconfig.electron-browser.json && tsgo --project build/checker/tsconfig.electron-main.json && tsgo --project build/checker/tsconfig.electron-utility.json", + "define-class-fields-check": "node build/lib/propertyInitOrderChecker.ts && tsgo --project src/tsconfig.defineClassFields.json", + "update-distro": "node build/npm/update-distro.ts", "web": "echo 'npm run web' is replaced by './scripts/code-server' or './scripts/code-web'", - "compile-cli": "gulp compile-cli", - "compile-web": "node ./node_modules/gulp/bin/gulp.js compile-web", - "watch-web": "node ./node_modules/gulp/bin/gulp.js watch-web", - "watch-cli": "node ./node_modules/gulp/bin/gulp.js watch-cli", - "eslint": "node build/eslint", - "stylelint": "node build/stylelint", + "compile-cli": "npm run gulp compile-cli", + "compile-web": "npm run gulp compile-web", + "watch-web": "npm run gulp watch-web", + "watch-cli": "npm run gulp watch-cli", + "eslint": "node build/eslint.ts", + "stylelint": "node build/stylelint.ts", "playwright-install": "npm exec playwright install", - "compile-build": "node ./node_modules/gulp/bin/gulp.js compile-build-with-mangling", - "compile-extensions-build": "node ./node_modules/gulp/bin/gulp.js compile-extensions-build", - "minify-vscode": "node ./node_modules/gulp/bin/gulp.js minify-vscode", - "minify-vscode-reh": "node ./node_modules/gulp/bin/gulp.js minify-vscode-reh", - "minify-vscode-reh-web": "node ./node_modules/gulp/bin/gulp.js minify-vscode-reh-web", - "hygiene": "node ./node_modules/gulp/bin/gulp.js hygiene", - "core-ci": "node ./node_modules/gulp/bin/gulp.js core-ci", - "core-ci-pr": "node ./node_modules/gulp/bin/gulp.js core-ci-pr", - "extensions-ci": "node ./node_modules/gulp/bin/gulp.js extensions-ci", - "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", + "compile-build": "npm run gulp compile-build-with-mangling", + "compile-extensions-build": "npm run gulp compile-extensions-build", + "minify-vscode": "npm run gulp minify-vscode", + "minify-vscode-reh": "npm run gulp minify-vscode-reh", + "minify-vscode-reh-web": "npm run gulp minify-vscode-reh-web", + "hygiene": "npm run gulp hygiene", + "core-ci": "npm run gulp core-ci", + "core-ci-pr": "npm run gulp core-ci-pr", + "extensions-ci": "npm run gulp extensions-ci", + "extensions-ci-pr": "npm run gulp extensions-ci-pr", "perf": "node scripts/code-perf.js", - "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run compile)" + "update-build-ts-version": "npm install -D typescript@next && npm install -D @typescript/native-preview && (cd build && npm run typecheck)" }, "dependencies": { - "@anthropic-ai/sdk": "^0.40.0", - "@c4312/eventsource-umd": "^3.0.5", - "@floating-ui/react": "^0.27.8", - "@google/genai": "^0.13.0", - "@mistralai/mistralai": "^1.6.0", - "@modelcontextprotocol/sdk": "^1.11.2", + "@anthropic-ai/sandbox-runtime": "0.0.23", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", + "@parcel/watcher": "^2.5.6", "@types/semver": "^7.5.8", + "@vscode/codicons": "^0.0.45-4", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", - "@vscodium/policy-watcher": "^1.3.0-2503300035", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/native-watchdog": "^1.4.6", + "@vscode/policy-watcher": "^1.3.2", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/spdlog": "^0.15.2", - "@vscode/sqlite3": "5.1.8-vscode", - "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/spdlog": "^0.15.7", + "@vscode/sqlite3": "5.1.12-vscode", + "@vscode/sudo-prompt": "9.3.2", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/headless": "^5.6.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", - "ajv": "^8.17.1", - "cross-spawn": "^7.0.6", - "diff": "^7.0.0", - "eslint-plugin-react": "^7.37.5", - "google-auth-library": "^9.15.1", - "groq-sdk": "^0.20.1", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/headless": "^6.1.0-beta.109", + "@xterm/xterm": "^6.1.0-beta.109", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.4", "katex": "^0.16.22", "kerberos": "2.1.1", - "lucide-react": "^0.503.0", - "marked": "^15.0.11", "minimist": "^1.2.8", - "native-is-elevated": "0.7.0", + "native-is-elevated": "0.9.0", "native-keymap": "^3.3.5", - "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta35", - "ollama": "^0.5.15", + "node-pty": "^1.2.0-beta.10", "open": "^10.1.2", - "openai": "^4.96.0", - "pdfjs-dist": "^5.4.394", - "posthog-node": "^4.14.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "react-tooltip": "^5.28.1", - "tas-client-umd": "0.2.0", - "undici": "^7.9.0", + "tas-client": "0.3.1", + "undici": "^7.18.2", "v8-inspect-profiler": "^0.1.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "^9.2.1", + "vscode-textmate": "^9.3.2", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, "devDependencies": { "@playwright/test": "^1.56.1", "@stylistic/eslint-plugin-ts": "^2.8.0", - "@tailwindcss/typography": "^0.5.16", "@types/cookie": "^0.3.3", "@types/debug": "^4.1.5", - "@types/diff": "^7.0.2", "@types/eslint": "^9.6.1", "@types/gulp-svgmin": "^1.2.1", "@types/http-proxy-agent": "^2.0.1", @@ -160,8 +133,6 @@ "@types/minimist": "^1.2.1", "@types/mocha": "^10.0.10", "@types/node": "^22.18.10", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.2", "@types/sinon": "^10.0.2", "@types/sinon-test": "^2.4.2", "@types/trusted-types": "^2.0.7", @@ -173,31 +144,29 @@ "@types/yauzl": "^2.10.0", "@types/yazl": "^2.4.2", "@typescript-eslint/utils": "^8.45.0", - "@typescript/native-preview": "^7.0.0-dev.20250812.1", + "@typescript/native-preview": "^7.0.0-dev.20260130", "@vscode/gulp-electron": "^1.38.2", "@vscode/l10n-dev": "0.0.35", "@vscode/telemetry-extractor": "^1.10.2", "@vscode/test-cli": "^0.0.6", "@vscode/test-electron": "^2.4.0", - "@vscode/test-web": "^0.0.62", + "@vscode/test-web": "^0.0.76", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.19", - "@webgpu/types": "^0.1.44", + "@webgpu/types": "^0.1.66", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "cookie": "^0.7.2", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.9.1", - "cssnano": "^6.0.3", "debounce": "^1.0.0", "deemon": "^1.13.6", - "electron": "37.7.0", + "electron": "39.3.0", "eslint": "^9.36.0", "eslint-formatter-compact": "^8.40.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^50.3.1", - "eslint-plugin-local": "^6.0.0", "event-stream": "3.3.4", "fancy-log": "^1.3.3", "file-loader": "^6.2.0", @@ -216,7 +185,6 @@ "gulp-replace": "^0.5.4", "gulp-sourcemaps": "^3.0.0", "gulp-svgmin": "^4.1.0", - "gulp-untar": "^0.0.7", "husky": "^0.13.1", "innosetup": "^6.4.1", "istanbul-lib-coverage": "^3.2.0", @@ -228,42 +196,33 @@ "merge-options": "^1.0.1", "mime": "^1.4.1", "minimatch": "^3.0.4", - "next": "^15.3.1", - "nodemon": "^3.1.10", "mocha": "^10.8.2", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", - "npm-run-all": "^4.1.5", - "original-fs": "^1.2.0", + "npm-run-all2": "^8.0.4", "os-browserify": "^0.3.0", "p-all": "^1.0.0", "path-browserify": "^1.0.1", - "postcss": "^8.4.33", - "postcss-nesting": "^12.1.5", "pump": "^1.0.1", "rcedit": "^1.1.0", "rimraf": "^2.2.8", - "scope-tailwind": "^1.0.9", "sinon": "^12.0.1", "sinon-test": "^3.1.3", "source-map": "0.6.1", "source-map-support": "^0.3.2", "style-loader": "^3.3.2", - "tailwindcss": "^3.4.17", + "tar": "^7.5.7", "ts-loader": "^9.5.1", - "ts-node": "^10.9.1", "tsec": "0.2.7", "tslib": "^2.6.3", - "tsup": "^8.4.0", - "typescript": "^6.0.0-dev.20250922", + "typescript": "^6.0.0-dev.20260130", "typescript-eslint": "^8.45.0", "util": "^0.12.4", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", "xml2js": "^0.5.0", - "yaserver": "^0.4.0", - "zx": "^8.7.0" + "yaserver": "^0.4.0" }, "overrides": { "node-gyp-build": "4.8.1", @@ -279,6 +238,6 @@ "url": "https://github.com/microsoft/vscode/issues" }, "optionalDependencies": { - "windows-foreground-love": "0.5.0" + "windows-foreground-love": "0.6.1" } } diff --git a/product.json b/product.json index f590d285658..4303c3c6641 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "CortexIDE", "nameLong": "CortexIDE", - "cortexVersion": "0.0.10", + "cortexVersion": "0.0.11", "cortexRelease": "0001", "applicationName": "cortexide", "dataFolderName": ".cortexide", diff --git a/remote/.npmrc b/remote/.npmrc index 40367a0138b..326fb9fd0f6 100644 --- a/remote/.npmrc +++ b/remote/.npmrc @@ -1,6 +1,6 @@ disturl="https://nodejs.org/dist" -target="22.20.0" -ms_build_id="365661" +target="22.21.1" +ms_build_id="374314" runtime="node" build_from_source="true" legacy-peer-deps="true" diff --git a/remote/package-lock.json b/remote/package-lock.json index 4fd02152f92..60fc79b26fa 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -10,26 +10,27 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", + "@parcel/watcher": "^2.5.6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/native-watchdog": "^1.4.6", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/spdlog": "^0.15.2", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/spdlog": "^0.15.7", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/headless": "^5.6.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/headless": "^6.1.0-beta.109", + "@xterm/xterm": "^6.1.0-beta.109", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -37,12 +38,11 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta35", - "tas-client-umd": "0.2.0", + "node-pty": "^1.2.0-beta.10", + "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "^9.2.1", + "vscode-textmate": "^9.3.2", "yauzl": "^3.0.0", "yazl": "^2.4.3" } @@ -90,20 +90,295 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "git+ssh://git@github.com/parcel-bundler/watcher.git#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", - "integrity": "sha512-Z0lk8pM5vwuOJU6pfheRXHrOpQYIIEnVl/z8DY6370D4+ZnrOTvFa5BUdf3pGxahT5ILbPWwQSm2Wthy4q1OTg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" @@ -118,10 +393,11 @@ } }, "node_modules/@vscode/deviceid": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.1.tgz", - "integrity": "sha512-ErpoMeKKNYAkR1IT3zxB5RtiTqEECdh8fxggupWvzuxpTAX77hwOI2NdJ7um+vupnXRBZVx4ugo0+dVHJWUkag==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.4.tgz", + "integrity": "sha512-3u705VptsQhKMcHvUMJzaOn9fBrKEQHsl7iibRRVQ8kUNV+cptki7bQXACPNsGtJ5Dh4/7A7W1uKtP3z39GUQg==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "fs-extra": "^11.2.0", "uuid": "^9.0.1" @@ -133,10 +409,17 @@ "integrity": "sha512-tK6k0DXFHW7q5+GGuGZO+phpAqpxO4WXl+BLc/8/uOk3RsM2ssAL3CQUQDb1TGfwltjsauhN6S4ghYZzs4sPFw==", "license": "MIT" }, + "node_modules/@vscode/native-watchdog": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@vscode/native-watchdog/-/native-watchdog-1.4.6.tgz", + "integrity": "sha512-C2hsQFVYF2hBv7sa7OztRBimrMsEofGNh/lYs7MIPpKdhyJpYSpDb5iu/bilgLqSO61PLBCJ5xw6iFI21LI+9Q==", + "hasInstallScript": true, + "license": "MIT" + }, "node_modules/@vscode/proxy-agent": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.36.0.tgz", - "integrity": "sha512-W4mls/+zErqTYcKC41utdmoYnBWZRH1dRF9U4cBAyKU5EhcnWfVsPBvUnXXw1CffI3djmMWnu9JrF/Ynw7lkcg==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@vscode/proxy-agent/-/proxy-agent-0.37.0.tgz", + "integrity": "sha512-FDBc/3qf7fLMp4fmdRBav2dy3UZ/Vao4PN6a5IeTYvcgh9erd9HfOcVoU3ogy2uwCii6vZNvmEeF9+gr64spVQ==", "license": "MIT", "dependencies": { "@tootallnate/once": "^3.0.0", @@ -155,9 +438,9 @@ } }, "node_modules/@vscode/ripgrep": { - "version": "1.15.14", - "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.15.14.tgz", - "integrity": "sha512-/G1UJPYlm+trBWQ6cMO3sv6b8D1+G16WaJH1/DSqw32JOVlzgZbLkDxRyzIpTpv30AcYGMkCf5tUqGlW6HbDWw==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@vscode/ripgrep/-/ripgrep-1.17.0.tgz", + "integrity": "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -176,9 +459,9 @@ } }, "node_modules/@vscode/spdlog": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@vscode/spdlog/-/spdlog-0.15.2.tgz", - "integrity": "sha512-8RQ7JEs81x5IFONYGtFhYtaF2a3IPtNtgMdp+MFLxTDokJQBAVittx0//EN38BYhlzeVqEPgusRsOA8Yulaysg==", + "version": "0.15.7", + "resolved": "https://registry.npmjs.org/@vscode/spdlog/-/spdlog-0.15.7.tgz", + "integrity": "sha512-xpHAtw0IESD6wmjqLr6LbpYAmr8ZYm8AT7hGE7oM7AojNeOBngXLOqmzpXbTNTAvXBq1KHy8PwbMmY24uYR/oQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -188,9 +471,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz", - "integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.0.tgz", + "integrity": "sha512-4kjB1jgLyG9VimGfyJb1F8/GFdrx55atsBCH/9r2D/iZHAUDCvZ5zhWXB7sRQ2z2WkkuNYm/0pgQtUm1jhdf7A==", "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { @@ -202,9 +485,9 @@ } }, "node_modules/@vscode/windows-ca-certs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.3.tgz", - "integrity": "sha512-C0Iq5RcH+H31GUZ8bsMORsX3LySVkGAqe4kQfUSVcCqJ0QOhXkhgwUMU7oCiqYLXaQWyXFp6Fj6eMdt05uK7VA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@vscode/windows-ca-certs/-/windows-ca-certs-0.3.4.tgz", + "integrity": "sha512-DcDLjBpu8srh6wUiZqEMyhXHzNDO81ecZOttL3+1u3Iht4CS6Qtxy5WkTPX/aDgbheASO/MK8yg6uLq58RzEWg==", "hasInstallScript": true, "license": "BSD", "optional": true, @@ -226,113 +509,121 @@ } }, "node_modules/@vscode/windows-process-tree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.6.0.tgz", - "integrity": "sha512-7/DjBKKUtlmKNiAet2GRbdvfjgMKmfBeWVClIgONv8aqxGnaKca5N85eIDxh6rLMy2hKvFqIIsqgxs1Q26TWwg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@vscode/windows-process-tree/-/windows-process-tree-0.6.3.tgz", + "integrity": "sha512-mjirLbtgjv7P6fwD8gx7iaY961EfGqUExGvfzsKl3spLfScg57ejlMi+7O1jfJqpM2Zly9DTSxyY4cFsDN6c9Q==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "node-addon-api": "7.1.0" } }, "node_modules/@vscode/windows-registry": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.1.0.tgz", - "integrity": "sha512-5AZzuWJpGscyiMOed0IuyEwt6iKmV5Us7zuwCDCFYMIq7tsvooO9BUiciywsvuthGz6UG4LSpeDeCxvgMVhnIw==", - "hasInstallScript": true + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vscode/windows-registry/-/windows-registry-1.1.3.tgz", + "integrity": "sha512-si8+b+2Wh0x2X6W2+kgDyLJD9hyGIrjUo1X/7RWlvsxyI5+Pg+bpdHJrVYtIW4cHOPVB0FYFaN1UZndbUbU5lQ==", + "hasInstallScript": true, + "license": "MIT" }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.119", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", - "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.109.tgz", + "integrity": "sha512-iaKd86bsBYG2PgF6gAiYUPHxFW/LX8ERJfMBCiASQo9C7U/gPJgoiGEZikydf3AllnQFHku+4Kdf7lia6ci6tA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", - "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.109.tgz", + "integrity": "sha512-O5A4tkxiT4D5yz+brgLlLR2he7NDVmLK+rl1e53nhaUXABHZUEonqqKMq3OPRc+/PeuEU6CH4dq+v49Pwmgfpw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", - "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", + "version": "0.11.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.109.tgz", + "integrity": "sha512-nGiw2sAyGEeF72+92EHlwa3J8MOamuFVTTv7PYpJMZi75ejn1OtRF+/cWelBaHx4/aQr1nXsfsJPXu+g8FQrSg==", "license": "MIT", "dependencies": { - "font-finder": "^1.1.0", - "font-ligatures": "^1.4.1" + "lru-cache": "^6.0.0", + "opentype.js": "^0.8.0" }, "engines": { "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.42", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", - "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.109.tgz", + "integrity": "sha512-yz/wfO7dNh+hYUP1d9Vry3EZOinJcQscdNPyfDFLe7AgL/me+BNRcYaAyYOv5ZN+vvXOZSmCa58grNnZlMVXjA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", - "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", + "version": "0.17.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.109.tgz", + "integrity": "sha512-RQ1bMQIWOXJ3rOEiTLidxjTnpHgFhgIj2a45W6VKo3c+ILBNcC5x/tUlcM+BgnyT62aMNgmGRUIlW5qv7aTxRA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", - "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", + "version": "0.15.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.109.tgz", + "integrity": "sha512-qUY7mnGX7BnbfvgFirdUAUDIJyHPN+2U849w4Z8yLO9Q9t0eLcufHlwbXeVAOnNwM3pSI3Ohz5vXsWhmJrfSrQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", - "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.109.tgz", + "integrity": "sha512-M++pTg4rF7OW7OhrY52m80pB2DRU8/bhmd4rIRlNWeuWlNmLOljjA5a7HrjNCV+PUBnK+6/BRIhO1Rt6uWFNvA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", - "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", + "version": "0.20.0-beta.108", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.108.tgz", + "integrity": "sha512-d4wscQpbiTgrOGiL29QxG9ulUkV5UQiz6L20o53C2QQVAJqdqhXP716ihbKirnlYlXi+ndB2Ox4fyn0ohU/fug==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/headless": { - "version": "5.6.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-5.6.0-beta.136.tgz", - "integrity": "sha512-3irueWS6Ei+XlTMCuh6ZWj1tBnVvjitDtD4PN+v81RKjaCNO/QN9abGTHQx+651GP291ESwY8ocKThSoQ9yklw==", - "license": "MIT" + "version": "6.1.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/headless/-/headless-6.1.0-beta.109.tgz", + "integrity": "sha512-oESLjJpJ5JsSyZpj10JJKYDPm+irdKAKogp6XKkks2RxU8YfH+pr99gR3kPi6ppz+MPmEqDh4bejMhXEe4/twQ==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", - "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", - "license": "MIT" + "version": "6.1.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.109.tgz", + "integrity": "sha512-3MioB2ZPzf4Wli4W7rZNCvpSakCf/7FktoNqxrckKfeusTMtbFUgH1MZKVZ5yAy3DCv0ASOzDTGM0ELZDO7XWA==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/agent-base": { "version": "7.1.1", @@ -382,17 +673,6 @@ "readable-stream": "^3.4.0" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -523,44 +803,6 @@ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/font-finder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz", - "integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==", - "license": "MIT", - "dependencies": { - "get-system-fonts": "^2.0.0", - "promise-stream-reader": "^1.0.1" - }, - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/font-ligatures": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz", - "integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==", - "license": "MIT", - "dependencies": { - "font-finder": "^1.0.3", - "lru-cache": "^6.0.0", - "opentype.js": "^0.8.0" - }, - "engines": { - "node": ">8.0.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -579,15 +821,6 @@ "node": ">=14.14" } }, - "node_modules/get-system-fonts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.2.tgz", - "integrity": "sha512-zzlgaYnHMIEgHRrfC7x0Qp0Ylhw/sHpM6MHXeVBTYIsvGf5GpbnClB+Q6rAPdn+0gd2oZZIo6Tj3EaWrt4VhDQ==", - "license": "MIT", - "engines": { - "node": ">8.0.0" - } - }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -682,14 +915,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -761,18 +986,6 @@ "node": ">=10" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -819,12 +1032,6 @@ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" }, - "node_modules/native-watchdog": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/native-watchdog/-/native-watchdog-1.4.2.tgz", - "integrity": "sha512-iT3Uj6FFdrW5vHbQ/ybiznLus9oiUoMJ8A8nyugXv9rV3EBhIodmGs+mztrwQyyBc+PB5/CrskAH/WxaUVRRSQ==", - "hasInstallScript": true - }, "node_modules/node-abi": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.8.0.tgz", @@ -846,9 +1053,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta35", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta35.tgz", - "integrity": "sha512-dGKw3PtLj/+uiFWUODNjr3QMyNjxRB2JY372AN4uzonfb6ri23d4PMr4s6UoibiqsXOQ3elXRCdq1qDLd86J8Q==", + "version": "1.2.0-beta.10", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.2.0-beta.10.tgz", + "integrity": "sha512-vONwSCtAiOVNxeaP/lzDdRw733Q6uB/ELOCFM8DUfKMw6rTFovwFCuvqr9usya7JXV2pfaers3EwuzZfv0QtwA==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -881,11 +1088,12 @@ "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA= sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -916,15 +1124,6 @@ "node": ">=10" } }, - "node_modules/promise-stream-reader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", - "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", - "license": "MIT", - "engines": { - "node": ">8.0.0" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -1125,10 +1324,14 @@ "node": ">=6" } }, - "node_modules/tas-client-umd": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tas-client-umd/-/tas-client-umd-0.2.0.tgz", - "integrity": "sha512-oezN7mJVm5qZDVEby7OzxCLKUpUN5of0rY4dvOWaDF2JZBlGpd3BXceFN8B53qlTaIkVSzP65aAMT0Vc+/N25Q==" + "node_modules/tas-client": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.3.1.tgz", + "integrity": "sha512-Mn4+4t/KXEf8aIENeI1TkzpKIImzmG+FjPZ2dlaoGNFgxJqBE/pp3MT7nc2032EG4aS73E4OEcr2WiNaWW8mdA==", + "license": "MIT", + "engines": { + "node": ">=22" + } }, "node_modules/tiny-inflate": { "version": "1.0.3", @@ -1136,17 +1339,6 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1159,9 +1351,9 @@ } }, "node_modules/undici": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.9.0.tgz", - "integrity": "sha512-e696y354tf5cFZPXsF26Yg+5M63+5H3oE6Vtkh2oqbvsE2Oe7s2nIbcQh5lmG7Lp/eS29vJtTpw9+p6PX0qNSg==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.19.0.tgz", + "integrity": "sha512-Heho1hJD81YChi+uS2RkSjcVO+EQLmLSyUlHyp7Y/wFbxQaGb4WXVKD073JytrjXJVkSZVzoE2MCSOKugFGtOQ==", "license": "MIT", "engines": { "node": ">=20.18.1" @@ -1209,9 +1401,9 @@ } }, "node_modules/vscode-textmate": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.1.tgz", - "integrity": "sha512-eXiUi2yYFv9bdvgrYtJynA7UemCEkpVNE50S9iBBA08LYG5t9+/TB+8IRS/YoYOubCez2OkSyZ1Q12eQMwzbrw==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", "license": "MIT" }, "node_modules/wrappy": { diff --git a/remote/package.json b/remote/package.json index d991ee5d8e1..7f9b6f8b80d 100644 --- a/remote/package.json +++ b/remote/package.json @@ -5,26 +5,27 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@parcel/watcher": "parcel-bundler/watcher#1ca032aa8339260a8a3bcf825c3a1a71e3e43542", + "@parcel/watcher": "^2.5.6", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/proxy-agent": "^0.36.0", + "@vscode/native-watchdog": "^1.4.6", + "@vscode/proxy-agent": "^0.37.0", "@vscode/ripgrep": "^1.15.13", - "@vscode/spdlog": "^0.15.2", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/spdlog": "^0.15.7", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/headless": "^5.6.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/headless": "^6.1.0-beta.109", + "@xterm/xterm": "^6.1.0-beta.109", "cookie": "^0.7.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", @@ -32,12 +33,11 @@ "katex": "^0.16.22", "kerberos": "2.1.1", "minimist": "^1.2.8", - "native-watchdog": "^1.4.1", - "node-pty": "1.1.0-beta35", - "tas-client-umd": "0.2.0", + "node-pty": "^1.2.0-beta.10", + "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", "vscode-regexpp": "^3.1.0", - "vscode-textmate": "^9.2.1", + "vscode-textmate": "^9.3.2", "yauzl": "^3.0.0", "yazl": "^2.4.3" }, diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index 68d70b572c6..9ebe85bde28 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -10,23 +10,24 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@vscode/codicons": "^0.0.45-4", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/xterm": "^6.1.0-beta.109", "jschardet": "3.1.4", "katex": "^0.16.22", - "tas-client-umd": "0.2.0", + "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "^9.2.1" + "vscode-textmate": "^9.3.2" } }, "node_modules/@microsoft/1ds-core-js": { @@ -71,6 +72,12 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@vscode/codicons": { + "version": "0.0.45-4", + "resolved": "https://registry.npmjs.org/@vscode/codicons/-/codicons-0.0.45-4.tgz", + "integrity": "sha512-uuWqpry+FcHAw1JDkXwEW0YIuTtX3n6KqSshNlvLUjuP92PSrfq99jW52AWJ7qeunmPvgKCaZOeSSLUqHRHjmw==", + "license": "CC-BY-4.0" + }, "node_modules/@vscode/iconv-lite-umd": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.1.tgz", @@ -78,9 +85,9 @@ "license": "MIT" }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.2.0.tgz", - "integrity": "sha512-abvLfKwmriqgdS4WrIzFK7mzdPUVqIIW1UWarp2lA8lpOZ1EDPL1snRBKe7g+5R5ri173mNJEuPLnG/NlpMp4w==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.0.tgz", + "integrity": "sha512-4kjB1jgLyG9VimGfyJb1F8/GFdrx55atsBCH/9r2D/iZHAUDCvZ5zhWXB7sRQ2z2WkkuNYm/0pgQtUm1jhdf7A==", "license": "MIT" }, "node_modules/@vscode/vscode-languagedetection": { @@ -92,92 +99,95 @@ } }, "node_modules/@xterm/addon-clipboard": { - "version": "0.2.0-beta.119", - "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.2.0-beta.119.tgz", - "integrity": "sha512-yWmCpGuTvSaIeEfdSijdf8K8qRAYuEGnKkaJZ6er+cOzdmGHBNzyBDKKeyins0aV2j4CGKPDiWHQF5+qGzZDGw==", + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-clipboard/-/addon-clipboard-0.3.0-beta.109.tgz", + "integrity": "sha512-iaKd86bsBYG2PgF6gAiYUPHxFW/LX8ERJfMBCiASQo9C7U/gPJgoiGEZikydf3AllnQFHku+4Kdf7lia6ci6tA==", "license": "MIT", "dependencies": { "js-base64": "^3.7.5" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-image": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.9.0-beta.136.tgz", - "integrity": "sha512-syWhqpFMAcQ1+US0JjFzj0ORokj8hkz2VgXcCCbTfO0cDtpSYYxMNLaY2fpL459rnOFB4olI9Nf9PZdonmBPDw==", + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-image/-/addon-image-0.10.0-beta.109.tgz", + "integrity": "sha512-O5A4tkxiT4D5yz+brgLlLR2he7NDVmLK+rl1e53nhaUXABHZUEonqqKMq3OPRc+/PeuEU6CH4dq+v49Pwmgfpw==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-ligatures": { - "version": "0.10.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.10.0-beta.136.tgz", - "integrity": "sha512-WkvL7BVdoqpNf8QsH4n37Pu7jEZTiJ+OD4FmLMVavw0euhgG18zzJKNKIYRuKcddR52dT/Q8TrspVJofpL98GQ==", + "version": "0.11.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-ligatures/-/addon-ligatures-0.11.0-beta.109.tgz", + "integrity": "sha512-nGiw2sAyGEeF72+92EHlwa3J8MOamuFVTTv7PYpJMZi75ejn1OtRF+/cWelBaHx4/aQr1nXsfsJPXu+g8FQrSg==", "license": "MIT", "dependencies": { - "font-finder": "^1.1.0", - "font-ligatures": "^1.4.1" + "lru-cache": "^6.0.0", + "opentype.js": "^0.8.0" }, "engines": { "node": ">8.0.0" }, "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-progress": { - "version": "0.2.0-beta.42", - "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.2.0-beta.42.tgz", - "integrity": "sha512-C5w7y6rwSUdRcEiJHFnB2qJI/6DBOi/fJAvTmIpmNZE60cVnrLUuyLmXh6aKbSQ44J6W3PrD5xthb8re3UVUOw==", + "version": "0.3.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-progress/-/addon-progress-0.3.0-beta.109.tgz", + "integrity": "sha512-yz/wfO7dNh+hYUP1d9Vry3EZOinJcQscdNPyfDFLe7AgL/me+BNRcYaAyYOv5ZN+vvXOZSmCa58grNnZlMVXjA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-search": { - "version": "0.16.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.16.0-beta.136.tgz", - "integrity": "sha512-Y2T/ShQBelmOGy7lup3VEfFF/yXeNkkMXqhGftmjzmwSA+eylFW+92vczMSrckTW++EFvVLR/L5jMXiSw0qOWQ==", + "version": "0.17.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.17.0-beta.109.tgz", + "integrity": "sha512-RQ1bMQIWOXJ3rOEiTLidxjTnpHgFhgIj2a45W6VKo3c+ILBNcC5x/tUlcM+BgnyT62aMNgmGRUIlW5qv7aTxRA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-serialize": { - "version": "0.14.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0-beta.136.tgz", - "integrity": "sha512-ursvqITzhZrBQT8XsbOyAQJJKohv33NEm6ToLtMZUmPurBG6KXlVZ9LAPs2YpCBqkifLktSE1GdsofJCpADWuA==", + "version": "0.15.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.15.0-beta.109.tgz", + "integrity": "sha512-qUY7mnGX7BnbfvgFirdUAUDIJyHPN+2U849w4Z8yLO9Q9t0eLcufHlwbXeVAOnNwM3pSI3Ohz5vXsWhmJrfSrQ==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-unicode11": { - "version": "0.9.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.9.0-beta.136.tgz", - "integrity": "sha512-RwtNbON1uNndrtPCM6qMMElTTpxs7ZLRQVbSm4/BMW6GAt6AbW1RAqwoxMRhbz7VVTux/c3HcKfj3SI1MhqSOw==", + "version": "0.10.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode11/-/addon-unicode11-0.10.0-beta.109.tgz", + "integrity": "sha512-M++pTg4rF7OW7OhrY52m80pB2DRU8/bhmd4rIRlNWeuWlNmLOljjA5a7HrjNCV+PUBnK+6/BRIhO1Rt6uWFNvA==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/addon-webgl": { - "version": "0.19.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0-beta.136.tgz", - "integrity": "sha512-MzVlFKrlgJjKQ6T4/TuamvlvR2FFDvxAPY90lo9u4899k7NNif+M8bBdNea3+bsPMU3fKLhGHoTp0+8MjskaeA==", + "version": "0.20.0-beta.108", + "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.20.0-beta.108.tgz", + "integrity": "sha512-d4wscQpbiTgrOGiL29QxG9ulUkV5UQiz6L20o53C2QQVAJqdqhXP716ihbKirnlYlXi+ndB2Ox4fyn0ohU/fug==", "license": "MIT", "peerDependencies": { - "@xterm/xterm": "^5.6.0-beta.136" + "@xterm/xterm": "^6.1.0-beta.109" } }, "node_modules/@xterm/xterm": { - "version": "5.6.0-beta.136", - "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.6.0-beta.136.tgz", - "integrity": "sha512-cOWfdbPUYjV8qJY0yg/HdJBiq/hl8J2NRma563crQbSveDpuiiKV+T+ZVeGKQ2YZztLCz6h+kox6J7LQcPtpiQ==", - "license": "MIT" + "version": "6.1.0-beta.109", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.109.tgz", + "integrity": "sha512-3MioB2ZPzf4Wli4W7rZNCvpSakCf/7FktoNqxrckKfeusTMtbFUgH1MZKVZ5yAy3DCv0ASOzDTGM0ELZDO7XWA==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] }, "node_modules/commander": { "version": "8.3.0", @@ -188,42 +198,6 @@ "node": ">= 12" } }, - "node_modules/font-finder": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz", - "integrity": "sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==", - "license": "MIT", - "dependencies": { - "get-system-fonts": "^2.0.0", - "promise-stream-reader": "^1.0.1" - }, - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/font-ligatures": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz", - "integrity": "sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==", - "license": "MIT", - "dependencies": { - "font-finder": "^1.0.3", - "lru-cache": "^6.0.0", - "opentype.js": "^0.8.0" - }, - "engines": { - "node": ">8.0.0" - } - }, - "node_modules/get-system-fonts": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-system-fonts/-/get-system-fonts-2.0.2.tgz", - "integrity": "sha512-zzlgaYnHMIEgHRrfC7x0Qp0Ylhw/sHpM6MHXeVBTYIsvGf5GpbnClB+Q6rAPdn+0gd2oZZIo6Tj3EaWrt4VhDQ==", - "license": "MIT", - "engines": { - "node": ">8.0.0" - } - }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -278,20 +252,15 @@ "ot": "bin/ot" } }, - "node_modules/promise-stream-reader": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz", - "integrity": "sha512-Tnxit5trUjBAqqZCGWwjyxhmgMN4hGrtpW3Oc/tRI4bpm/O2+ej72BB08l6JBnGQgVDGCLvHFGjGgQS6vzhwXg==", + "node_modules/tas-client": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tas-client/-/tas-client-0.3.1.tgz", + "integrity": "sha512-Mn4+4t/KXEf8aIENeI1TkzpKIImzmG+FjPZ2dlaoGNFgxJqBE/pp3MT7nc2032EG4aS73E4OEcr2WiNaWW8mdA==", "license": "MIT", "engines": { - "node": ">8.0.0" + "node": ">=22" } }, - "node_modules/tas-client-umd": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/tas-client-umd/-/tas-client-umd-0.2.0.tgz", - "integrity": "sha512-oezN7mJVm5qZDVEby7OzxCLKUpUN5of0rY4dvOWaDF2JZBlGpd3BXceFN8B53qlTaIkVSzP65aAMT0Vc+/N25Q==" - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -304,9 +273,9 @@ "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==" }, "node_modules/vscode-textmate": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.2.1.tgz", - "integrity": "sha512-eXiUi2yYFv9bdvgrYtJynA7UemCEkpVNE50S9iBBA08LYG5t9+/TB+8IRS/YoYOubCez2OkSyZ1Q12eQMwzbrw==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-9.3.2.tgz", + "integrity": "sha512-n2uGbUcrjhUEBH16uGA0TvUfhWwliFZ1e3+pTjrkim1Mt7ydB41lV08aUvsi70OlzDWp6X7Bx3w/x3fAXIsN0Q==", "license": "MIT" }, "node_modules/yallist": { diff --git a/remote/web/package.json b/remote/web/package.json index e69c409d24e..f940ea309ad 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,22 +5,23 @@ "dependencies": { "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@vscode/codicons": "^0.0.45-4", "@vscode/iconv-lite-umd": "0.7.1", - "@vscode/tree-sitter-wasm": "^0.2.0", + "@vscode/tree-sitter-wasm": "^0.3.0", "@vscode/vscode-languagedetection": "1.0.21", - "@xterm/addon-clipboard": "^0.2.0-beta.119", - "@xterm/addon-image": "^0.9.0-beta.136", - "@xterm/addon-ligatures": "^0.10.0-beta.136", - "@xterm/addon-progress": "^0.2.0-beta.42", - "@xterm/addon-search": "^0.16.0-beta.136", - "@xterm/addon-serialize": "^0.14.0-beta.136", - "@xterm/addon-unicode11": "^0.9.0-beta.136", - "@xterm/addon-webgl": "^0.19.0-beta.136", - "@xterm/xterm": "^5.6.0-beta.136", + "@xterm/addon-clipboard": "^0.3.0-beta.109", + "@xterm/addon-image": "^0.10.0-beta.109", + "@xterm/addon-ligatures": "^0.11.0-beta.109", + "@xterm/addon-progress": "^0.3.0-beta.109", + "@xterm/addon-search": "^0.17.0-beta.109", + "@xterm/addon-serialize": "^0.15.0-beta.109", + "@xterm/addon-unicode11": "^0.10.0-beta.109", + "@xterm/addon-webgl": "^0.20.0-beta.108", + "@xterm/xterm": "^6.1.0-beta.109", "jschardet": "3.1.4", "katex": "^0.16.22", - "tas-client-umd": "0.2.0", + "tas-client": "0.3.1", "vscode-oniguruma": "1.7.0", - "vscode-textmate": "^9.2.1" + "vscode-textmate": "^9.3.2" } } diff --git a/resources/linux/snap/electron-launch b/resources/linux/snap/electron-launch index bbd8e76588e..57d025ccef1 100755 --- a/resources/linux/snap/electron-launch +++ b/resources/linux/snap/electron-launch @@ -276,4 +276,4 @@ fi wait_for_async_execs -exec "$@" +exec "$@" --ozone-platform=x11 diff --git a/resources/win32/versioned/bin/code.cmd b/resources/win32/versioned/bin/code.cmd new file mode 100644 index 00000000000..1298c72ee0e --- /dev/null +++ b/resources/win32/versioned/bin/code.cmd @@ -0,0 +1,7 @@ +@echo off +setlocal +set VSCODE_DEV= +set ELECTRON_RUN_AS_NODE=1 +"%~dp0..\@@NAME@@.exe" "%~dp0..\@@VERSIONFOLDER@@\resources\app\out\cli.js" %* +IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL% +endlocal diff --git a/resources/win32/versioned/bin/code.sh b/resources/win32/versioned/bin/code.sh new file mode 100644 index 00000000000..2443d965ca7 --- /dev/null +++ b/resources/win32/versioned/bin/code.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env sh +# +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +if [ "$VSCODE_WSL_DEBUG_INFO" = true ]; then + set -x +fi + +COMMIT="@@COMMIT@@" +APP_NAME="@@APPNAME@@" +QUALITY="@@QUALITY@@" +NAME="@@NAME@@" +SERVERDATAFOLDER="@@SERVERDATAFOLDER@@" +VERSIONFOLDER="@@VERSIONFOLDER@@" +VSCODE_PATH="$(dirname "$(dirname "$(realpath "$0")")")" +ELECTRON="$VSCODE_PATH/$NAME.exe" + +IN_WSL=false +if [ -n "$WSL_DISTRO_NAME" ]; then + # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 + IN_WSL=true +else + WSL_BUILD=$(uname -r | sed -E 's/^[0-9.]+-([0-9]+)-Microsoft.*|.*/\1/') + if [ -n "$WSL_BUILD" ]; then + if [ "$WSL_BUILD" -ge 17063 ]; then + # WSLPATH is available since WSL build 17046 + # WSLENV is available since WSL build 17063 + IN_WSL=true + else + # If running under older WSL, don't pass cli.js to Electron as + # environment vars cannot be transferred from WSL to Windows + # See: https://github.com/microsoft/BashOnWindows/issues/1363 + # https://github.com/microsoft/BashOnWindows/issues/1494 + "$ELECTRON" "$@" + exit $? + fi + fi +fi +if [ $IN_WSL = true ]; then + + export WSLENV="ELECTRON_RUN_AS_NODE/w:$WSLENV" + CLI=$(wslpath -m "$VSCODE_PATH/$VERSIONFOLDER/resources/app/out/cli.js") + + # use the Remote WSL extension if installed + WSL_EXT_ID="ms-vscode-remote.remote-wsl" + + ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --locate-extension $WSL_EXT_ID >/tmp/remote-wsl-loc.txt 2>/dev/null /dev/null 2>&1; then + echo -e "${RED}Error: Not in a git repository${NC}" + exit 1 +fi + +# Ensure vscode remote exists +if ! git remote get-url vscode > /dev/null 2>&1; then + echo -e "${YELLOW}Adding vscode remote...${NC}" + git remote add vscode git@github.com:microsoft/vscode.git +fi + +# Fetch latest from vscode +echo -e "${GREEN}Fetching latest from vscode/main...${NC}" +git fetch vscode main 2>&1 | grep -v "^$" || true + +# Get current state +CURRENT_BRANCH=$(git branch --show-current) +CURRENT_COMMIT=$(git rev-parse HEAD) +VSCODE_COMMIT=$(git rev-parse vscode/main) +MERGE_BASE=$(git merge-base HEAD vscode/main) + +echo "" | tee -a "$REPORT_FILE" +echo "═══════════════════════════════════════════════════════════" | tee -a "$REPORT_FILE" +echo " VS Code Sync Comparison Report" | tee -a "$REPORT_FILE" +echo " Generated: $(date)" | tee -a "$REPORT_FILE" +echo "═══════════════════════════════════════════════════════════" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Current state +echo -e "${BLUE}Current State:${NC}" | tee -a "$REPORT_FILE" +echo " Branch: $CURRENT_BRANCH" | tee -a "$REPORT_FILE" +echo " Commit: $CURRENT_COMMIT" | tee -a "$REPORT_FILE" +echo " $(git log -1 --format='%s' HEAD)" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# VS Code state +echo -e "${BLUE}VS Code State:${NC}" | tee -a "$REPORT_FILE" +echo " Commit: $VSCODE_COMMIT" | tee -a "$REPORT_FILE" +echo " $(git log -1 --format='%s' vscode/main)" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Merge base +echo -e "${BLUE}Common Ancestor (Merge Base):${NC}" | tee -a "$REPORT_FILE" +echo " Commit: $MERGE_BASE" | tee -a "$REPORT_FILE" +echo " $(git log -1 --format='%s' $MERGE_BASE)" | tee -a "$REPORT_FILE" +MERGE_BASE_DATE=$(git log -1 --format='%ci' $MERGE_BASE) +echo " Date: $MERGE_BASE_DATE" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Count commits +COMMITS_AHEAD=$(git rev-list --count $MERGE_BASE..HEAD) +COMMITS_BEHIND=$(git rev-list --count $MERGE_BASE..vscode/main) +COMMITS_VSCODE=$(git rev-list --count $MERGE_BASE..vscode/main) + +echo -e "${BLUE}Divergence Analysis:${NC}" | tee -a "$REPORT_FILE" +echo " Your commits ahead of merge base: $COMMITS_AHEAD" | tee -a "$REPORT_FILE" +echo " VS Code commits ahead of merge base: $COMMITS_BEHIND" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Check for potential conflicts +echo -e "${YELLOW}Analyzing potential conflicts...${NC}" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Get list of files changed in both branches +echo -e "${BLUE}Files changed in your branch (since merge base):${NC}" | tee -a "$REPORT_FILE" +YOUR_FILES=$(git diff --name-only $MERGE_BASE..HEAD | wc -l | tr -d ' ') +echo " Total files modified: $YOUR_FILES" | tee -a "$REPORT_FILE" +if [ "$YOUR_FILES" -lt 50 ]; then + git diff --name-only $MERGE_BASE..HEAD | head -20 | sed 's/^/ /' | tee -a "$REPORT_FILE" + if [ "$YOUR_FILES" -gt 20 ]; then + echo " ... and $((YOUR_FILES - 20)) more files" | tee -a "$REPORT_FILE" + fi +fi +echo "" | tee -a "$REPORT_FILE" + +echo -e "${BLUE}Files changed in VS Code (since merge base):${NC}" | tee -a "$REPORT_FILE" +VSCODE_FILES=$(git diff --name-only $MERGE_BASE..vscode/main | wc -l | tr -d ' ') +echo " Total files modified: $VSCODE_FILES" | tee -a "$REPORT_FILE" +if [ "$VSCODE_FILES" -lt 50 ]; then + git diff --name-only $MERGE_BASE..vscode/main | head -20 | sed 's/^/ /' | tee -a "$REPORT_FILE" + if [ "$VSCODE_FILES" -gt 20 ]; then + echo " ... and $((VSCODE_FILES - 20)) more files" | tee -a "$REPORT_FILE" + fi +fi +echo "" | tee -a "$REPORT_FILE" + +# Find overlapping files (potential conflicts) +echo -e "${BLUE}Potential Conflict Analysis:${NC}" | tee -a "$REPORT_FILE" +YOUR_FILE_LIST=$(mktemp) +VSCODE_FILE_LIST=$(mktemp) +git diff --name-only $MERGE_BASE..HEAD | sort > "$YOUR_FILE_LIST" +git diff --name-only $MERGE_BASE..vscode/main | sort > "$VSCODE_FILE_LIST" + +OVERLAPPING_FILES=$(comm -12 "$YOUR_FILE_LIST" "$VSCODE_FILE_LIST" | wc -l | tr -d ' ') +echo " Files modified in both branches: $OVERLAPPING_FILES" | tee -a "$REPORT_FILE" + +if [ "$OVERLAPPING_FILES" -gt 0 ]; then + echo -e "${YELLOW} ⚠️ These files may have conflicts:${NC}" | tee -a "$REPORT_FILE" + comm -12 "$YOUR_FILE_LIST" "$VSCODE_FILE_LIST" | head -20 | sed 's/^/ /' | tee -a "$REPORT_FILE" + if [ "$OVERLAPPING_FILES" -gt 20 ]; then + echo " ... and $((OVERLAPPING_FILES - 20)) more files" | tee -a "$REPORT_FILE" + fi +else + echo -e "${GREEN} ✓ No overlapping files - merge should be clean!${NC}" | tee -a "$REPORT_FILE" +fi + +rm -f "$YOUR_FILE_LIST" "$VSCODE_FILE_LIST" +echo "" | tee -a "$REPORT_FILE" + +# Check CortexIDE-specific files +echo -e "${BLUE}CortexIDE-Specific Files (should be safe):${NC}" | tee -a "$REPORT_FILE" +CORTEXIDE_FILES=$(git diff --name-only $MERGE_BASE..HEAD | grep -E "(cortexide|void)" | wc -l | tr -d ' ') +echo " CortexIDE-specific files modified: $CORTEXIDE_FILES" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Recent commits summary +echo -e "${BLUE}Your Recent Commits (last 10):${NC}" | tee -a "$REPORT_FILE" +git log --oneline $MERGE_BASE..HEAD | head -10 | sed 's/^/ /' | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +echo -e "${BLUE}VS Code Recent Commits (last 10):${NC}" | tee -a "$REPORT_FILE" +git log --oneline $MERGE_BASE..vscode/main | head -10 | sed 's/^/ /' | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +# Sync recommendations +echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" | tee -a "$REPORT_FILE" +echo -e "${CYAN} Sync Recommendations${NC}" | tee -a "$REPORT_FILE" +echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" | tee -a "$REPORT_FILE" +echo "" | tee -a "$REPORT_FILE" + +if [ "$OVERLAPPING_FILES" -eq 0 ]; then + echo -e "${GREEN}✓ RECOMMENDED: Safe to merge/rebase${NC}" | tee -a "$REPORT_FILE" + echo " No file conflicts detected. You can safely sync." | tee -a "$REPORT_FILE" + echo "" | tee -a "$REPORT_FILE" + echo " To merge:" | tee -a "$REPORT_FILE" + echo " git merge vscode/main" | tee -a "$REPORT_FILE" + echo "" | tee -a "$REPORT_FILE" + echo " To rebase:" | tee -a "$REPORT_FILE" + echo " git rebase vscode/main" | tee -a "$REPORT_FILE" +else + echo -e "${YELLOW}⚠ CAUTION: Potential conflicts detected${NC}" | tee -a "$REPORT_FILE" + echo " $OVERLAPPING_FILES files were modified in both branches." | tee -a "$REPORT_FILE" + echo " Review the files listed above before syncing." | tee -a "$REPORT_FILE" + echo "" | tee -a "$REPORT_FILE" + echo " RECOMMENDED APPROACH:" | tee -a "$REPORT_FILE" + echo " 1. Create a test branch: git checkout -b test-sync-vscode" | tee -a "$REPORT_FILE" + echo " 2. Try merging: git merge vscode/main" | tee -a "$REPORT_FILE" + echo " 3. Resolve any conflicts" | tee -a "$REPORT_FILE" + echo " 4. Test thoroughly" | tee -a "$REPORT_FILE" + echo " 5. If successful, merge test branch back to main" | tee -a "$REPORT_FILE" +fi + +echo "" | tee -a "$REPORT_FILE" +echo -e "${BLUE}Full report saved to: ${REPORT_FILE}${NC}" +echo "" + +# Ask if user wants to create a test sync branch +if [ "$OVERLAPPING_FILES" -gt 0 ]; then + read -p "Create a test branch to try syncing? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + TEST_BRANCH="test-sync-vscode-$(date +%Y%m%d)" + echo -e "${GREEN}Creating test branch: $TEST_BRANCH${NC}" + git checkout -b "$TEST_BRANCH" 2>&1 || { + echo -e "${YELLOW}Branch might already exist or git write not allowed${NC}" + echo "You can create it manually: git checkout -b $TEST_BRANCH" + } + echo -e "${GREEN}Test branch created. You can now try: git merge vscode/main${NC}" + fi +fi diff --git a/scripts/playground-server.ts b/scripts/playground-server.ts deleted file mode 100644 index e28a20488d9..00000000000 --- a/scripts/playground-server.ts +++ /dev/null @@ -1,899 +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 * as fsPromise from 'fs/promises'; -import path from 'path'; -import * as http from 'http'; -import * as parcelWatcher from '@parcel/watcher'; - -/** - * Launches the server for the monaco editor playground - */ -function main() { - const server = new HttpServer({ host: 'localhost', port: 5001, cors: true }); - server.use('/', redirectToMonacoEditorPlayground()); - - const rootDir = path.join(__dirname, '..'); - const fileServer = new FileServer(rootDir); - server.use(fileServer.handleRequest); - - const moduleIdMapper = new SimpleModuleIdPathMapper(path.join(rootDir, 'out')); - const editorMainBundle = new CachedBundle('vs/editor/editor.main', moduleIdMapper); - fileServer.overrideFileContent(editorMainBundle.entryModulePath, () => editorMainBundle.bundle()); - - const loaderPath = path.join(rootDir, 'out/vs/loader.js'); - fileServer.overrideFileContent(loaderPath, async () => - Buffer.from(new TextEncoder().encode(makeLoaderJsHotReloadable(await fsPromise.readFile(loaderPath, 'utf8'), new URL('/file-changes', server.url)))) - ); - - const watcher = DirWatcher.watchRecursively(moduleIdMapper.rootDir); - watcher.onDidChange((path, newContent) => { - editorMainBundle.setModuleContent(path, newContent); - editorMainBundle.bundle(); - console.log(`${new Date().toLocaleTimeString()}, file change: ${path}`); - }); - server.use('/file-changes', handleGetFileChangesRequest(watcher, fileServer, moduleIdMapper)); - - console.log(`Server listening on ${server.url}`); -} -setTimeout(main, 0); - -// #region Http/File Server - -type RequestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => Promise; -type ChainableRequestHandler = (req: http.IncomingMessage, res: http.ServerResponse, next: RequestHandler) => Promise; - -class HttpServer { - private readonly server: http.Server; - public readonly url: URL; - - private handler: ChainableRequestHandler[] = []; - - constructor(options: { host: string; port: number; cors: boolean }) { - this.server = http.createServer(async (req, res) => { - if (options.cors) { - res.setHeader('Access-Control-Allow-Origin', '*'); - } - - let i = 0; - const next = async (req: http.IncomingMessage, res: http.ServerResponse) => { - if (i >= this.handler.length) { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('404 Not Found'); - return; - } - const handler = this.handler[i]; - i++; - await handler(req, res, next); - }; - await next(req, res); - }); - this.server.listen(options.port, options.host); - this.url = new URL(`http://${options.host}:${options.port}`); - } - - use(handler: ChainableRequestHandler); - use(path: string, handler: ChainableRequestHandler); - use(...args: [path: string, handler: ChainableRequestHandler] | [handler: ChainableRequestHandler]) { - const handler = args.length === 1 ? args[0] : (req, res, next) => { - const path = args[0]; - const requestedUrl = new URL(req.url, this.url); - if (requestedUrl.pathname === path) { - return args[1](req, res, next); - } else { - return next(req, res); - } - }; - - this.handler.push(handler); - } -} - -function redirectToMonacoEditorPlayground(): ChainableRequestHandler { - return async (req, res) => { - const url = new URL('https://microsoft.github.io/monaco-editor/playground.html'); - url.searchParams.append('source', `http://${req.headers.host}/out/vs`); - res.writeHead(302, { Location: url.toString() }); - res.end(); - }; -} - -class FileServer { - private readonly overrides = new Map Promise>(); - - constructor(public readonly publicDir: string) { } - - public readonly handleRequest: ChainableRequestHandler = async (req, res, next) => { - const requestedUrl = new URL(req.url!, `http://${req.headers.host}`); - - const pathName = requestedUrl.pathname; - - const filePath = path.join(this.publicDir, pathName); - if (!filePath.startsWith(this.publicDir)) { - res.writeHead(403, { 'Content-Type': 'text/plain' }); - res.end('403 Forbidden'); - return; - } - - try { - const override = this.overrides.get(filePath); - let content: Buffer; - if (override) { - content = await override(); - } else { - content = await fsPromise.readFile(filePath); - } - - const contentType = getContentType(filePath); - res.writeHead(200, { 'Content-Type': contentType }); - res.end(content); - } catch (err) { - if (err.code === 'ENOENT') { - next(req, res); - } else { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('500 Internal Server Error'); - } - } - }; - - public filePathToUrlPath(filePath: string): string | undefined { - const relative = path.relative(this.publicDir, filePath); - const isSubPath = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); - - if (!isSubPath) { - return undefined; - } - const relativePath = relative.replace(/\\/g, '/'); - return `/${relativePath}`; - } - - public overrideFileContent(filePath: string, content: () => Promise): void { - this.overrides.set(filePath, content); - } -} - -function getContentType(filePath: string): string { - const extname = path.extname(filePath); - switch (extname) { - case '.js': - return 'text/javascript'; - case '.css': - return 'text/css'; - case '.json': - return 'application/json'; - case '.png': - return 'image/png'; - case '.jpg': - return 'image/jpg'; - case '.svg': - return 'image/svg+xml'; - case '.html': - return 'text/html'; - case '.wasm': - return 'application/wasm'; - default: - return 'text/plain'; - } -} - -// #endregion - -// #region File Watching - -interface IDisposable { - dispose(): void; -} - -class DirWatcher { - public static watchRecursively(dir: string): DirWatcher { - const listeners: ((path: string, newContent: string) => void)[] = []; - const fileContents = new Map(); - const event = (handler: (path: string, newContent: string) => void) => { - listeners.push(handler); - return { - dispose: () => { - const idx = listeners.indexOf(handler); - if (idx >= 0) { - listeners.splice(idx, 1); - } - } - }; - }; - parcelWatcher.subscribe(dir, async (err, events) => { - for (const e of events) { - if (e.type === 'update') { - const newContent = await fsPromise.readFile(e.path, 'utf8'); - if (fileContents.get(e.path) !== newContent) { - fileContents.set(e.path, newContent); - listeners.forEach(l => l(e.path, newContent)); - } - } - } - }); - return new DirWatcher(event); - } - - constructor(public readonly onDidChange: (handler: (path: string, newContent: string) => void) => IDisposable) { - } -} - -function handleGetFileChangesRequest(watcher: DirWatcher, fileServer: FileServer, moduleIdMapper: SimpleModuleIdPathMapper): ChainableRequestHandler { - return async (req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - const d = watcher.onDidChange((fsPath, newContent) => { - const path = fileServer.filePathToUrlPath(fsPath); - if (path) { - res.write(JSON.stringify({ changedPath: path, moduleId: moduleIdMapper.getModuleId(fsPath), newContent }) + '\n'); - } - }); - res.on('close', () => d.dispose()); - }; -} -function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): string { - loaderJsCode = loaderJsCode.replace( - /constructor\(env, scriptLoader, defineFunc, requireFunc, loaderAvailableTimestamp = 0\) {/, - '$&globalThis.___globalModuleManager = this; globalThis.vscode = { process: { env: { VSCODE_DEV: true } } }' - ); - - const ___globalModuleManager: any = undefined; - - // This code will be appended to loader.js - function $watchChanges(fileChangesUrl: string) { - interface HotReloadConfig { } - - let reloadFn; - if (globalThis.$sendMessageToParent) { - reloadFn = () => globalThis.$sendMessageToParent({ kind: 'reload' }); - } else if (typeof window !== 'undefined') { - reloadFn = () => window.location.reload(); - } else { - reloadFn = () => { }; - } - - console.log('Connecting to server to watch for changes...'); - // eslint-disable-next-line local/code-no-any-casts - (fetch as any)(fileChangesUrl) - .then(async request => { - const reader = request.body.getReader(); - let buffer = ''; - while (true) { - const { done, value } = await reader.read(); - if (done) { break; } - buffer += new TextDecoder().decode(value); - const lines = buffer.split('\n'); - buffer = lines.pop()!; - - const changes: { relativePath: string; config: HotReloadConfig | undefined; path: string; newContent: string }[] = []; - - for (const line of lines) { - const data = JSON.parse(line); - const relativePath = data.changedPath.replace(/\\/g, '/').split('/out/')[1]; - changes.push({ config: {}, path: data.changedPath, relativePath, newContent: data.newContent }); - } - - const result = handleChanges(changes, 'playground-server'); - if (result.reloadFailedJsFiles.length > 0) { - reloadFn(); - } - } - }).catch(err => { - console.error(err); - setTimeout(() => $watchChanges(fileChangesUrl), 1000); - }); - - - function handleChanges(changes: { - relativePath: string; - config: HotReloadConfig | undefined; - path: string; - newContent: string; - }[], debugSessionName: string) { - // This function is stringified and injected into the debuggee. - - const hotReloadData: { count: number; originalWindowTitle: any; timeout: any; shouldReload: boolean } = globalThis.$hotReloadData || (globalThis.$hotReloadData = { count: 0, messageHideTimeout: undefined, shouldReload: false }); - - const reloadFailedJsFiles: { relativePath: string; path: string }[] = []; - - for (const change of changes) { - handleChange(change.relativePath, change.path, change.newContent, change.config); - } - - return { reloadFailedJsFiles }; - - function handleChange(relativePath: string, path: string, newSrc: string, config: any) { - if (relativePath.endsWith('.css')) { - handleCssChange(relativePath); - } else if (relativePath.endsWith('.js')) { - handleJsChange(relativePath, path, newSrc, config); - } - } - - function handleCssChange(relativePath: string) { - if (typeof document === 'undefined') { - return; - } - - const styleSheet = (([...document.querySelectorAll(`link[rel='stylesheet']`)] as HTMLLinkElement[])) - .find(l => new URL(l.href, document.location.href).pathname.endsWith(relativePath)); - if (styleSheet) { - setMessage(`reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'css reloaded', relativePath); - styleSheet.href = styleSheet.href.replace(/\?.*/, '') + '?' + Date.now(); - } else { - setMessage(`could not reload ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'ignoring css change, as stylesheet is not loaded', relativePath); - } - } - - - function handleJsChange(relativePath: string, path: string, newSrc: string, config: any) { - const moduleIdStr = trimEnd(relativePath, '.js'); - - const requireFn: any = globalThis.require; - // eslint-disable-next-line local/code-no-any-casts - const moduleManager = (requireFn as any).moduleManager; - if (!moduleManager) { - console.log(debugSessionName, 'ignoring js change, as moduleManager is not available', relativePath); - return; - } - - const moduleId = moduleManager._moduleIdProvider.getModuleId(moduleIdStr); - const oldModule = moduleManager._modules2[moduleId]; - - if (!oldModule) { - console.log(debugSessionName, 'ignoring js change, as module is not loaded', relativePath); - return; - } - - // Check if we can reload - // eslint-disable-next-line local/code-no-any-casts - const g = globalThis as any; - - // A frozen copy of the previous exports - const oldExports = Object.freeze({ ...oldModule.exports }); - const reloadFn = g.$hotReload_applyNewExports?.({ oldExports, newSrc, config }); - - if (!reloadFn) { - console.log(debugSessionName, 'ignoring js change, as module does not support hot-reload', relativePath); - hotReloadData.shouldReload = true; - - reloadFailedJsFiles.push({ relativePath, path }); - - setMessage(`hot reload not supported for ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - return; - } - - // Eval maintains source maps - function newScript(/* this parameter is used by newSrc */ define) { - // eslint-disable-next-line no-eval - eval(newSrc); // CodeQL [SM01632] This code is only executed during development. It is required for the hot-reload functionality. - } - - newScript(/* define */ function (deps, callback) { - // Evaluating the new code was successful. - - // Redefine the module - delete moduleManager._modules2[moduleId]; - moduleManager.defineModule(moduleIdStr, deps, callback); - const newModule = moduleManager._modules2[moduleId]; - - - // Patch the exports of the old module, so that modules using the old module get the new exports - Object.assign(oldModule.exports, newModule.exports); - // We override the exports so that future reloads still patch the initial exports. - newModule.exports = oldModule.exports; - - const successful = reloadFn(newModule.exports); - if (!successful) { - hotReloadData.shouldReload = true; - setMessage(`hot reload failed ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - console.log(debugSessionName, 'hot reload was not successful', relativePath); - return; - } - - console.log(debugSessionName, 'hot reloaded', moduleIdStr); - setMessage(`successfully reloaded ${formatPath(relativePath)} - ${new Date().toLocaleTimeString()}`); - }); - } - - function setMessage(message: string) { - const domElem = (document.querySelector('.titlebar-center .window-title')) as HTMLDivElement | undefined; - if (!domElem) { return; } - if (!hotReloadData.timeout) { - hotReloadData.originalWindowTitle = domElem.innerText; - } else { - clearTimeout(hotReloadData.timeout); - } - if (hotReloadData.shouldReload) { - message += ' (manual reload required)'; - } - - domElem.innerText = message; - hotReloadData.timeout = setTimeout(() => { - hotReloadData.timeout = undefined; - // If wanted, we can restore the previous title message - // domElem.replaceChildren(hotReloadData.originalWindowTitle); - }, 5000); - } - - function formatPath(path: string): string { - const parts = path.split('/'); - parts.reverse(); - let result = parts[0]; - parts.shift(); - for (const p of parts) { - if (result.length + p.length > 40) { - break; - } - result = p + '/' + result; - if (result.length > 20) { - break; - } - } - return result; - } - - function trimEnd(str, suffix) { - if (str.endsWith(suffix)) { - return str.substring(0, str.length - suffix.length); - } - return str; - } - } - } - - const additionalJsCode = ` -(${(function () { - globalThis.$hotReload_deprecateExports = new Set<(oldExports: any, newExports: any) => void>(); - }).toString()})(); -${$watchChanges.toString()} -$watchChanges(${JSON.stringify(fileChangesUrl)}); -`; - - return `${loaderJsCode}\n${additionalJsCode}`; -} - -// #endregion - -// #region Bundling - -class CachedBundle { - public readonly entryModulePath = this.mapper.resolveRequestToPath(this.moduleId)!; - - constructor( - private readonly moduleId: string, - private readonly mapper: SimpleModuleIdPathMapper, - ) { - } - - private loader: ModuleLoader | undefined = undefined; - - private bundlePromise: Promise | undefined = undefined; - public async bundle(): Promise { - if (!this.bundlePromise) { - this.bundlePromise = (async () => { - if (!this.loader) { - this.loader = new ModuleLoader(this.mapper); - await this.loader.addModuleAndDependencies(this.entryModulePath); - } - const editorEntryPoint = await this.loader.getModule(this.entryModulePath); - const content = bundleWithDependencies(editorEntryPoint!); - return content; - })(); - } - return this.bundlePromise; - } - - public async setModuleContent(path: string, newContent: string): Promise { - if (!this.loader) { - return; - } - const module = await this.loader!.getModule(path); - if (module) { - if (!this.loader.updateContent(module, newContent)) { - this.loader = undefined; - } - } - this.bundlePromise = undefined; - } -} - -function bundleWithDependencies(module: IModule): Buffer { - const visited = new Set(); - const builder = new SourceMapBuilder(); - - function visit(module: IModule) { - if (visited.has(module)) { - return; - } - visited.add(module); - for (const dep of module.dependencies) { - visit(dep); - } - builder.addSource(module.source); - } - - visit(module); - - const sourceMap = builder.toSourceMap(); - sourceMap.sourceRoot = module.source.sourceMap.sourceRoot; - const sourceMapBase64Str = Buffer.from(JSON.stringify(sourceMap)).toString('base64'); - - builder.addLine(`//# sourceMappingURL=data:application/json;base64,${sourceMapBase64Str}`); - - return builder.toContent(); -} - -class ModuleLoader { - private readonly modules = new Map>(); - - constructor(private readonly mapper: SimpleModuleIdPathMapper) { } - - public getModule(path: string): Promise { - return Promise.resolve(this.modules.get(path)); - } - - public updateContent(module: IModule, newContent: string): boolean { - const parsedModule = parseModule(newContent, module.path, this.mapper); - if (!parsedModule) { - return false; - } - if (!arrayEquals(parsedModule.dependencyRequests, module.dependencyRequests)) { - return false; - } - - module.dependencyRequests = parsedModule.dependencyRequests; - module.source = parsedModule.source; - - return true; - } - - async addModuleAndDependencies(path: string): Promise { - if (this.modules.has(path)) { - return this.modules.get(path)!; - } - - const promise = (async () => { - const content = await fsPromise.readFile(path, { encoding: 'utf-8' }); - - const parsedModule = parseModule(content, path, this.mapper); - if (!parsedModule) { - return undefined; - } - - const dependencies = (await Promise.all(parsedModule.dependencyRequests.map(async r => { - if (r === 'require' || r === 'exports' || r === 'module') { - return null; - } - - const depPath = this.mapper.resolveRequestToPath(r, path); - if (!depPath) { - return null; - } - return await this.addModuleAndDependencies(depPath); - }))).filter((d): d is IModule => !!d); - - const module: IModule = { - id: this.mapper.getModuleId(path)!, - dependencyRequests: parsedModule.dependencyRequests, - dependencies, - path, - source: parsedModule.source, - }; - return module; - })(); - - this.modules.set(path, promise); - return promise; - } -} - -function arrayEquals(a: T[], b: T[]): boolean { - if (a.length !== b.length) { - return false; - } - for (let i = 0; i < a.length; i++) { - if (a[i] !== b[i]) { - return false; - } - } - return true; -} - -const encoder = new TextEncoder(); - -function parseModule(content: string, path: string, mapper: SimpleModuleIdPathMapper): { source: Source; dependencyRequests: string[] } | undefined { - const m = content.match(/define\((\[.*?\])/); - if (!m) { - return undefined; - } - - const dependencyRequests = JSON.parse(m[1].replace(/'/g, '"')) as string[]; - - const sourceMapHeader = '//# sourceMappingURL=data:application/json;base64,'; - const idx = content.indexOf(sourceMapHeader); - - let sourceMap: any = null; - if (idx !== -1) { - const sourceMapJsonStr = Buffer.from(content.substring(idx + sourceMapHeader.length), 'base64').toString('utf-8'); - sourceMap = JSON.parse(sourceMapJsonStr); - content = content.substring(0, idx); - } - - content = content.replace('define([', `define("${mapper.getModuleId(path)}", [`); - - const contentBuffer = Buffer.from(encoder.encode(content)); - const source = new Source(contentBuffer, sourceMap); - - return { dependencyRequests, source }; -} - -class SimpleModuleIdPathMapper { - constructor(public readonly rootDir: string) { } - - public getModuleId(path: string): string | null { - if (!path.startsWith(this.rootDir) || !path.endsWith('.js')) { - return null; - } - const moduleId = path.substring(this.rootDir.length + 1); - - - return moduleId.replace(/\\/g, '/').substring(0, moduleId.length - 3); - } - - public resolveRequestToPath(request: string, requestingModulePath?: string): string | null { - if (request.indexOf('css!') !== -1) { - return null; - } - - if (request.startsWith('.')) { - return path.join(path.dirname(requestingModulePath!), request + '.js'); - } else { - return path.join(this.rootDir, request + '.js'); - } - } -} - -interface IModule { - id: string; - dependencyRequests: string[]; - dependencies: IModule[]; - path: string; - source: Source; -} - -// #endregion - -// #region SourceMapBuilder - -// From https://stackoverflow.com/questions/29905373/how-to-create-sourcemaps-for-concatenated-files with modifications - -class Source { - // Ends with \n - public readonly content: Buffer; - public readonly sourceMap: SourceMap; - public readonly sourceLines: number; - - public readonly sourceMapMappings: Buffer; - - - constructor(content: Buffer, sourceMap: SourceMap | undefined) { - if (!sourceMap) { - sourceMap = SourceMapBuilder.emptySourceMap; - } - - let sourceLines = countNL(content); - if (content.length > 0 && content[content.length - 1] !== 10) { - sourceLines++; - content = Buffer.concat([content, Buffer.from([10])]); - } - - this.content = content; - this.sourceMap = sourceMap; - this.sourceLines = sourceLines; - this.sourceMapMappings = typeof this.sourceMap.mappings === 'string' - ? Buffer.from(encoder.encode(sourceMap.mappings as string)) - : this.sourceMap.mappings; - } -} - -class SourceMapBuilder { - public static emptySourceMap: SourceMap = { version: 3, sources: [], mappings: Buffer.alloc(0) }; - - private readonly outputBuffer = new DynamicBuffer(); - private readonly sources: string[] = []; - private readonly mappings = new DynamicBuffer(); - private lastSourceIndex = 0; - private lastSourceLine = 0; - private lastSourceCol = 0; - - addLine(text: string) { - this.outputBuffer.addString(text); - this.outputBuffer.addByte(10); - this.mappings.addByte(59); // ; - } - - addSource(source: Source) { - const sourceMap = source.sourceMap; - this.outputBuffer.addBuffer(source.content); - - const sourceRemap: number[] = []; - for (const v of sourceMap.sources) { - let pos = this.sources.indexOf(v); - if (pos < 0) { - pos = this.sources.length; - this.sources.push(v); - } - sourceRemap.push(pos); - } - let lastOutputCol = 0; - - const inputMappings = source.sourceMapMappings; - let outputLine = 0; - let ip = 0; - let inOutputCol = 0; - let inSourceIndex = 0; - let inSourceLine = 0; - let inSourceCol = 0; - let shift = 0; - let value = 0; - let valpos = 0; - const commit = () => { - if (valpos === 0) { return; } - this.mappings.addVLQ(inOutputCol - lastOutputCol); - lastOutputCol = inOutputCol; - if (valpos === 1) { - valpos = 0; - return; - } - const outSourceIndex = sourceRemap[inSourceIndex]; - this.mappings.addVLQ(outSourceIndex - this.lastSourceIndex); - this.lastSourceIndex = outSourceIndex; - this.mappings.addVLQ(inSourceLine - this.lastSourceLine); - this.lastSourceLine = inSourceLine; - this.mappings.addVLQ(inSourceCol - this.lastSourceCol); - this.lastSourceCol = inSourceCol; - valpos = 0; - }; - while (ip < inputMappings.length) { - let b = inputMappings[ip++]; - if (b === 59) { // ; - commit(); - this.mappings.addByte(59); - inOutputCol = 0; - lastOutputCol = 0; - outputLine++; - } else if (b === 44) { // , - commit(); - this.mappings.addByte(44); - } else { - b = charToInteger[b]; - if (b === 255) { throw new Error('Invalid sourceMap'); } - value += (b & 31) << shift; - if (b & 32) { - shift += 5; - } else { - const shouldNegate = value & 1; - value >>= 1; - if (shouldNegate) { value = -value; } - switch (valpos) { - case 0: inOutputCol += value; break; - case 1: inSourceIndex += value; break; - case 2: inSourceLine += value; break; - case 3: inSourceCol += value; break; - } - valpos++; - value = shift = 0; - } - } - } - commit(); - while (outputLine < source.sourceLines) { - this.mappings.addByte(59); - outputLine++; - } - } - - toContent(): Buffer { - return this.outputBuffer.toBuffer(); - } - - toSourceMap(sourceRoot?: string): SourceMap { - return { version: 3, sourceRoot, sources: this.sources, mappings: this.mappings.toBuffer().toString() }; - } -} - -export interface SourceMap { - version: number; // always 3 - file?: string; - sourceRoot?: string; - sources: string[]; - sourcesContent?: string[]; - names?: string[]; - mappings: string | Buffer; -} - -const charToInteger = Buffer.alloc(256); -const integerToChar = Buffer.alloc(64); - -charToInteger.fill(255); - -'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split('').forEach((char, i) => { - charToInteger[char.charCodeAt(0)] = i; - integerToChar[i] = char.charCodeAt(0); -}); - -class DynamicBuffer { - private buffer: Buffer; - private size: number; - - constructor() { - this.buffer = Buffer.alloc(512); - this.size = 0; - } - - ensureCapacity(capacity: number) { - if (this.buffer.length >= capacity) { - return; - } - const oldBuffer = this.buffer; - this.buffer = Buffer.alloc(Math.max(oldBuffer.length * 2, capacity)); - oldBuffer.copy(this.buffer); - } - - addByte(b: number) { - this.ensureCapacity(this.size + 1); - this.buffer[this.size++] = b; - } - - addVLQ(num: number) { - let clamped: number; - - if (num < 0) { - num = (-num << 1) | 1; - } else { - num <<= 1; - } - - do { - clamped = num & 31; - num >>= 5; - - if (num > 0) { - clamped |= 32; - } - - this.addByte(integerToChar[clamped]); - } while (num > 0); - } - - addString(s: string) { - const l = Buffer.byteLength(s); - this.ensureCapacity(this.size + l); - this.buffer.write(s, this.size); - this.size += l; - } - - addBuffer(b: Buffer) { - this.ensureCapacity(this.size + b.length); - b.copy(this.buffer, this.size); - this.size += b.length; - } - - toBuffer(): Buffer { - return this.buffer.slice(0, this.size); - } -} - -function countNL(b: Buffer): number { - let res = 0; - for (let i = 0; i < b.length; i++) { - if (b[i] === 10) { res++; } - } - return res; -} - -// #endregion diff --git a/scripts/safe-sync-vscode.sh b/scripts/safe-sync-vscode.sh new file mode 100755 index 00000000000..fbbd079ed10 --- /dev/null +++ b/scripts/safe-sync-vscode.sh @@ -0,0 +1,189 @@ +#!/bin/bash +# Safe sync workflow - creates a test branch, attempts merge, and provides conflict resolution guide + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN} Safe VS Code Sync Workflow${NC}" +echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" +echo "" + +# Check if we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo -e "${RED}Error: Not in a git repository${NC}" + exit 1 +fi + +# Check for uncommitted changes +if ! git diff-index --quiet HEAD --; then + echo -e "${YELLOW}Warning: You have uncommitted changes${NC}" + read -p "Do you want to stash them? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + git stash push -m "Auto-stash before safe sync with vscode" + STASHED=true + echo -e "${GREEN}Changes stashed${NC}" + else + echo -e "${RED}Aborting. Please commit or stash your changes first.${NC}" + exit 1 + fi +fi + +# Ensure vscode remote exists and fetch +if ! git remote get-url vscode > /dev/null 2>&1; then + echo -e "${YELLOW}Adding vscode remote...${NC}" + git remote add vscode git@github.com:microsoft/vscode.git +fi + +echo -e "${GREEN}Fetching latest from vscode/main...${NC}" +git fetch vscode main + +# Get current branch +CURRENT_BRANCH=$(git branch --show-current) +TEST_BRANCH="test-sync-vscode-$(date +%Y%m%d-%H%M%S)" + +echo -e "${BLUE}Current branch: ${CURRENT_BRANCH}${NC}" +echo -e "${BLUE}Creating test branch: ${TEST_BRANCH}${NC}" + +# Create test branch +git checkout -b "$TEST_BRANCH" 2>&1 || { + echo -e "${RED}Failed to create test branch. Branch might already exist.${NC}" + exit 1 +} + +echo -e "${GREEN}Test branch created successfully${NC}" +echo "" + +# Run comparison first +echo -e "${CYAN}Running comparison analysis...${NC}" +if [ -f "./scripts/compare-and-sync-vscode.sh" ]; then + ./scripts/compare-and-sync-vscode.sh +fi + +echo "" +echo -e "${YELLOW}Ready to attempt merge. This will help identify conflicts.${NC}" +read -p "Proceed with merge attempt? (y/n) " -n 1 -r +echo + +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${YELLOW}Merge cancelled. You're on test branch ${TEST_BRANCH}${NC}" + echo "You can manually run: git merge vscode/main" + exit 0 +fi + +# Attempt merge +echo -e "${GREEN}Attempting to merge vscode/main...${NC}" +echo "" + +MERGE_OUTPUT=$(git merge vscode/main 2>&1) || MERGE_EXIT=$? + +if [ -z "$MERGE_EXIT" ]; then + # Merge succeeded! + echo -e "${GREEN}✓ Merge successful! No conflicts detected.${NC}" + echo "" + echo -e "${BLUE}Next steps:${NC}" + echo " 1. Test your application thoroughly" + echo " 2. If everything works, merge this branch back to main:" + echo " git checkout $CURRENT_BRANCH" + echo " git merge $TEST_BRANCH" + echo " 3. Push to origin: git push origin $CURRENT_BRANCH" + echo "" + echo -e "${YELLOW}If you want to abort and try rebase instead:${NC}" + echo " git merge --abort" + echo " git rebase vscode/main" + + # Restore stashed changes if any + if [ "$STASHED" = true ]; then + echo "" + echo -e "${GREEN}Restoring stashed changes...${NC}" + git stash pop || true + fi +else + # Merge had conflicts + echo -e "${YELLOW}⚠ Merge conflicts detected!${NC}" + echo "" + echo -e "${BLUE}Conflict Resolution Guide:${NC}" + echo "" + + # Show conflicted files + CONFLICTED_FILES=$(git diff --name-only --diff-filter=U) + CONFLICT_COUNT=$(echo "$CONFLICTED_FILES" | grep -c . || echo "0") + + echo "Conflicted files ($CONFLICT_COUNT):" + echo "$CONFLICTED_FILES" | sed 's/^/ - /' + echo "" + + echo -e "${BLUE}Options:${NC}" + echo "" + echo "1. ${GREEN}Resolve conflicts manually:${NC}" + echo " - Edit each conflicted file" + echo " - Look for <<<<<<< HEAD markers" + echo " - Choose which changes to keep" + echo " - Remove conflict markers" + echo " - Stage resolved files: git add " + echo " - Complete merge: git commit" + echo "" + echo "2. ${GREEN}Use VS Code merge tool:${NC}" + echo " code ." + echo " # VS Code will show merge conflicts in the UI" + echo "" + echo "3. ${GREEN}Abort and try rebase instead:${NC}" + echo " git merge --abort" + echo " git rebase vscode/main" + echo "" + echo "4. ${GREEN}Accept VS Code's version for specific files:${NC}" + echo " git checkout --theirs # Use VS Code version" + echo " git add " + echo "" + echo "5. ${GREEN}Accept your version for specific files:${NC}" + echo " git checkout --ours # Use your version" + echo " git add " + echo "" + + # Create conflict resolution helper script + CONFLICT_HELPER="resolve-conflicts-$(date +%Y%m%d-%H%M%S).sh" + cat > "$CONFLICT_HELPER" << 'EOF' +#!/bin/bash +# Conflict resolution helper - run this after resolving conflicts + +echo "Checking conflict status..." +if git diff --check > /dev/null 2>&1; then + echo "✓ No conflict markers found" + + CONFLICTED=$(git diff --name-only --diff-filter=U) + if [ -z "$CONFLICTED" ]; then + echo "✓ All conflicts resolved!" + echo "" + echo "Stage all resolved files:" + echo " git add ." + echo "" + echo "Complete the merge:" + echo " git commit" + else + echo "⚠ Still have unmerged files:" + echo "$CONFLICTED" | sed 's/^/ - /' + fi +else + echo "⚠ Conflict markers still found in files" + echo "Run: git diff --check" +fi +EOF + chmod +x "$CONFLICT_HELPER" + echo -e "${GREEN}Created conflict resolution helper: ${CONFLICT_HELPER}${NC}" + echo "" + + echo -e "${YELLOW}You're currently on test branch: ${TEST_BRANCH}${NC}" + echo "Take your time to resolve conflicts. When done, run:" + echo " git add ." + echo " git commit" + echo "" + echo "Or abort: git merge --abort" +fi diff --git a/scripts/sync-vscode.sh b/scripts/sync-vscode.sh new file mode 100755 index 00000000000..6905439e632 --- /dev/null +++ b/scripts/sync-vscode.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# Sync with microsoft/vscode upstream instead of voideditor/void +# This script fetches from vscode remote and merges into your current branch + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Syncing with microsoft/vscode:main...${NC}" + +# Check if we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo -e "${RED}Error: Not in a git repository${NC}" + exit 1 +fi + +# Check if vscode remote exists +if ! git remote get-url vscode > /dev/null 2>&1; then + echo -e "${YELLOW}Adding vscode remote...${NC}" + git remote add vscode git@github.com:microsoft/vscode.git +fi + +# Get current branch +CURRENT_BRANCH=$(git branch --show-current) +echo -e "${GREEN}Current branch: ${CURRENT_BRANCH}${NC}" + +# Check for uncommitted changes +if ! git diff-index --quiet HEAD --; then + echo -e "${YELLOW}Warning: You have uncommitted changes${NC}" + read -p "Do you want to stash them? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + git stash push -m "Auto-stash before syncing with vscode" + STASHED=true + else + echo -e "${RED}Aborting. Please commit or stash your changes first.${NC}" + exit 1 + fi +fi + +# Fetch from vscode +echo -e "${GREEN}Fetching from vscode remote...${NC}" +git fetch vscode main + +# Ask user if they want to merge or rebase +echo -e "${YELLOW}Choose sync method:${NC}" +echo "1) Merge (creates a merge commit)" +echo "2) Rebase (replays your commits on top of vscode/main)" +read -p "Enter choice (1 or 2, default: 1): " -n 1 -r +echo + +if [[ $REPLY =~ ^[2]$ ]]; then + echo -e "${GREEN}Rebasing onto vscode/main...${NC}" + git rebase vscode/main +else + echo -e "${GREEN}Merging vscode/main...${NC}" + git merge vscode/main --no-edit +fi + +# Restore stashed changes if any +if [ "$STASHED" = true ]; then + echo -e "${GREEN}Restoring stashed changes...${NC}" + git stash pop +fi + +echo -e "${GREEN}✓ Sync complete!${NC}" +echo -e "${YELLOW}Don't forget to push: git push origin ${CURRENT_BRANCH}${NC}" diff --git a/scripts/test.bat b/scripts/test.bat index d45505db8a7..79d939a589c 100644 --- a/scripts/test.bat +++ b/scripts/test.bat @@ -12,7 +12,7 @@ set NAMESHORT=%NAMESHORT:"=%.exe set CODE=".build\electron\%NAMESHORT%" :: Download Electron if needed -call node build\lib\electron.js +call node build\lib\electron.ts if %errorlevel% neq 0 node .\node_modules\gulp\bin\gulp.js electron :: Run tests diff --git a/scripts/xterm-update.js b/scripts/xterm-update.js index 35c2084f794..1a36e6ac41a 100644 --- a/scripts/xterm-update.js +++ b/scripts/xterm-update.js @@ -23,8 +23,8 @@ const backendOnlyModuleNames = [ ]; const vscodeDir = process.argv.length >= 3 ? process.argv[2] : process.cwd(); -if (path.basename(vscodeDir) !== 'vscode') { - console.error('The cwd is not named "vscode"'); +if (!path.basename(vscodeDir).match(/.*vscode.*/)) { + console.error('The cwd is not "vscode" root'); return; } diff --git a/src/bootstrap-node.ts b/src/bootstrap-node.ts index 8cb580e738b..e0aa65a7589 100644 --- a/src/bootstrap-node.ts +++ b/src/bootstrap-node.ts @@ -142,6 +142,11 @@ export function configurePortable(product: Partial): { po return path.dirname(path.dirname(path.dirname(appRoot))); } + // appRoot = ..\Microsoft VS Code Insiders\\resources\app + if (process.platform === 'win32' && product.win32VersionedUpdate) { + return path.dirname(path.dirname(path.dirname(appRoot))); + } + return path.dirname(path.dirname(appRoot)); } diff --git a/src/main.ts b/src/main.ts index 7b7e1da509e..ecbbb165479 100644 --- a/src/main.ts +++ b/src/main.ts @@ -330,9 +330,8 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // Following features are disabled from the runtime: // `CalculateNativeWinOcclusion` - Disable native window occlusion tracker (https://groups.google.com/a/chromium.org/g/embedder-dev/c/ZF3uHHyWLKw/m/VDN2hDXMAAAJ) - // `FontationsLinuxSystemFonts` - Revert to FreeType for system fonts on Linux Refs https://github.com/microsoft/vscode/issues/260391 const featuresToDisable = - `CalculateNativeWinOcclusion,FontationsLinuxSystemFonts,${app.commandLine.getSwitchValue('disable-features')}`; + `CalculateNativeWinOcclusion,${app.commandLine.getSwitchValue('disable-features')}`; app.commandLine.appendSwitch('disable-features', featuresToDisable); // Blink features to configure. @@ -353,6 +352,14 @@ function configureCommandlineSwitchesSync(cliArgs: NativeParsedArgs) { // Runtime sets the default version to 3, refs https://github.com/electron/electron/pull/44426 app.commandLine.appendSwitch('xdg-portal-required-version', '4'); + // Increase the maximum number of active WebGL contexts as each terminal may + // use up to 2 + app.commandLine.appendSwitch('max-active-webgl-contexts', '32'); + + // Disable Skia Graphite backend. + // Refs https://github.com/microsoft/vscode/issues/284162 + app.commandLine.appendSwitch('disable-skia-graphite'); + return argvConfig; } @@ -529,7 +536,8 @@ function configureCrashReporter(): void { productName: process.env['VSCODE_DEV'] ? `${productName} Dev` : productName, submitURL, uploadToServer, - compress: true + compress: true, + ignoreSystemCrashHandler: true }); } diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 732da287a10..b1c66907abf 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -20,7 +20,6 @@ "DOM", "DOM.Iterable", "WebWorker.ImportScripts" - ], - "allowSyntheticDefaultImports": true + ] } } diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index cd3d0d860b9..6293f59ba2b 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -13,7 +13,8 @@ "preserveConstEnums": true, "target": "ES2022", "sourceMap": false, - "declaration": true + "declaration": true, + "skipLibCheck": true }, "include": [ "typings/css.d.ts", diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json index f913df5e7da..83691e2de5a 100644 --- a/src/tsec.exemptions.json +++ b/src/tsec.exemptions.json @@ -18,7 +18,7 @@ "vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts" ], "ban-worker-calls": [ - "vs/base/browser/webWorkerFactory.ts", + "vs/platform/webWorker/browser/webWorkerServiceImpl.ts", "vs/workbench/services/extensions/browser/webWorkerExtensionHost.ts" ], "ban-worker-importscripts": [ diff --git a/src/typings/vscode-globals-product.d.ts b/src/typings/vscode-globals-product.d.ts index 2cd632e77a0..ab169bd82d0 100644 --- a/src/typings/vscode-globals-product.d.ts +++ b/src/typings/vscode-globals-product.d.ts @@ -27,6 +27,20 @@ declare global { */ var _VSCODE_PACKAGE_JSON: Record; + /** + * Used to disable CSS import map loading during development. Needed + * when a bundler is used that loads the css directly. + * @deprecated Avoid using this variable. + */ + var _VSCODE_DISABLE_CSS_IMPORT_MAP: boolean | undefined; + + /** + * If this variable is set, and the source code references another module + * via import, the (relative) module should be referenced (instead of the + * JS module in the out folder). + * @deprecated Avoid using this variable. + */ + var _VSCODE_USE_RELATIVE_IMPORTS: boolean | undefined; } // fake export to make global work diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index be401204e06..df985c36c16 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -11,14 +11,14 @@ import { AbstractIdleValue, IntervalTimer, TimeoutTimer, _runWhenIdle, IdleDeadl import { BugIndicatingError, onUnexpectedError } from '../common/errors.js'; import * as event from '../common/event.js'; import { KeyCode } from '../common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from '../common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../common/lifecycle.js'; import { RemoteAuthorities } from '../common/network.js'; import * as platform from '../common/platform.js'; import { URI } from '../common/uri.js'; import { hash } from '../common/hash.js'; import { CodeWindow, ensureCodeWindow, mainWindow } from './window.js'; import { isPointWithinTriangle } from '../common/numbers.js'; -import { IObservable, derived, derivedOpts, IReader, observableValue } from '../common/observable.js'; +import { IObservable, derived, derivedOpts, IReader, observableValue, isObservable } from '../common/observable.js'; export interface IRegisteredCodeWindow { readonly window: CodeWindow; @@ -123,6 +123,98 @@ export const { //#endregion +//#region External Focus Tracking + +/** + * Information about external focus state, including the associated window. + */ +export interface IExternalFocusInfo { + readonly hasFocus: boolean; + readonly window?: CodeWindow; +} + +/** + * A function that checks if a component outside the normal DOM tree has focus. + * Returns focus info including which window the component is associated with. + */ +export type ExternalFocusChecker = () => IExternalFocusInfo; + +/** + * A registry for functions that check if a component outside the normal DOM tree has focus. + * This is used to extend the concept of "window has focus" to include things like + * Electron WebContentsViews (browser views) that exist outside the workbench DOM. + */ +const externalFocusCheckers = new Set(); + +/** + * Register a function that checks if a component outside the DOM has focus. + * This allows `hasExternalFocus` to detect when focus is in components like browser views, + * and `getExternalFocusWindow` to determine which window the focused component belongs to. + * + * @param checker A function that returns focus info for the component + * @returns A disposable to unregister the checker + */ +export function registerExternalFocusChecker(checker: ExternalFocusChecker): IDisposable { + externalFocusCheckers.add(checker); + + return toDisposable(() => { + externalFocusCheckers.delete(checker); + }); +} + +/** + * Check if any registered external component has focus. + * This is used to extend focus detection beyond the normal DOM to include + * components like Electron WebContentsViews. + * + * @returns true if any registered external component has focus + */ +export function hasExternalFocus(): boolean { + for (const checker of externalFocusCheckers) { + if (checker().hasFocus) { + return true; + } + } + return false; +} + +/** + * Get the window associated with a focused external component. + * This is used to determine which window should receive UI like dialogs + * when an external component (like a browser view) has focus. + * + * @returns The window of the focused external component, or undefined if none + */ +export function getExternalFocusWindow(): CodeWindow | undefined { + for (const checker of externalFocusCheckers) { + const info = checker(); + if (info.hasFocus && info.window) { + return info.window; + } + } + return undefined; +} + +/** + * Check if the application has focus in any window, either via the normal DOM or via an + * external component like a browser view (which exists outside the document tree). + * + * @returns true if the application owns the current focus + */ +export function hasAppFocus(): boolean { + for (const { window } of getWindows()) { + if (window.document.hasFocus()) { + return true; + } + } + if (hasExternalFocus()) { + return true; + } + return false; +} + +//#endregion + export function clearNode(node: HTMLElement): void { while (node.firstChild) { node.firstChild.remove(); @@ -413,6 +505,57 @@ export function modify(targetWindow: Window, callback: () => void): IDisposable return scheduleAtNextAnimationFrame(targetWindow, callback, -10000 /* must be late */); } +/** + * A scheduler that coalesces multiple `schedule()` calls into a single callback + * at the next animation frame. Similar to `RunOnceScheduler` but uses animation frames + * instead of timeouts. + */ +export class AnimationFrameScheduler implements IDisposable { + + private readonly runner: () => void; + private readonly node: Node; + private readonly pendingRunner = new MutableDisposable(); + + constructor(node: Node, runner: () => void) { + this.node = node; + this.runner = runner; + } + + dispose(): void { + this.pendingRunner.dispose(); + } + + /** + * Cancel the currently scheduled runner (if any). + */ + cancel(): void { + this.pendingRunner.clear(); + } + + /** + * Schedule the runner to execute at the next animation frame. + * If already scheduled, this is a no-op (the existing schedule is kept). + * If currently in an animation frame, the runner will execute immediately. + */ + schedule(): void { + if (this.pendingRunner.value) { + return; // Already scheduled + } + + this.pendingRunner.value = runAtThisOrScheduleAtNextAnimationFrame(getWindow(this.node), () => { + this.pendingRunner.clear(); + this.runner(); + }); + } + + /** + * Returns true if a runner is scheduled. + */ + isScheduled(): boolean { + return this.pendingRunner.value !== undefined; + } +} + /** * Add a throttled listener. `handler` is fired at most every 8.33333ms or with the next animation frame (if browser supports it). */ @@ -920,8 +1063,8 @@ export function isActiveDocument(element: Element): boolean { /** * Returns the active document across main and child windows. - * Prefers the window with focus, otherwise falls back to - * the main windows document. + * Prefers the window with focus (including external components like browser views), + * otherwise falls back to the main windows document. */ export function getActiveDocument(): Document { if (getWindowsCount() <= 1) { @@ -929,7 +1072,18 @@ export function getActiveDocument(): Document { } const documents = Array.from(getWindows()).map(({ window }) => window.document); - return documents.find(doc => doc.hasFocus()) ?? mainWindow.document; + const focusedDoc = documents.find(doc => doc.hasFocus()); + if (focusedDoc) { + return focusedDoc; + } + + // Check if an external component (like browser view) has focus + const externalWindow = getExternalFocusWindow(); + if (externalWindow) { + return externalWindow.document; + } + + return mainWindow.document; } /** @@ -1940,6 +2094,25 @@ export class DragAndDropObserver extends Disposable { } } +/** + * A wrapper around ResizeObserver that is disposable. + */ +export class DisposableResizeObserver extends Disposable { + + private readonly observer: ResizeObserver; + + constructor(callback: ResizeObserverCallback) { + super(); + this.observer = new ResizeObserver(callback); + this._register(toDisposable(() => this.observer.disconnect())); + } + + observe(target: Element, options?: ResizeObserverOptions): IDisposable { + this.observer.observe(target, options); + return toDisposable(() => this.observer.unobserve(target)); + } +} + type HTMLElementAttributeKeys = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? HTMLElementAttributeKeys : T[K] }>; type ElementAttributes = HTMLElementAttributeKeys & Record; type RemoveHTMLElement = T extends HTMLElement ? never : T; @@ -2507,7 +2680,43 @@ export abstract class ObserverNode | undefined = undefined; + + get isHovered(): IObservable { + if (!this._isHovered) { + const hovered = observableValue('hovered', false); + this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined)); + this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined)); + this._isHovered = hovered; + } + return this._isHovered; + } + + private _didMouseMoveDuringHover: IObservable | undefined = undefined; + + get didMouseMoveDuringHover(): IObservable { + if (!this._didMouseMoveDuringHover) { + let _hovering = false; + const hovered = observableValue('didMouseMoveDuringHover', false); + this._element.addEventListener('mouseenter', (_e) => { + _hovering = true; + }); + this._element.addEventListener('mousemove', (_e) => { + if (_hovering) { + hovered.set(true, undefined); + } + }); + this._element.addEventListener('mouseleave', (_e) => { + _hovering = false; + hovered.set(false, undefined); + }); + this._didMouseMoveDuringHover = hovered; + } + return this._didMouseMoveDuringHover; + } } + function setClassName(domNode: HTMLOrSVGElement, className: string) { if (isSVGElement(domNode)) { domNode.setAttribute('class', className); @@ -2515,6 +2724,7 @@ function setClassName(domNode: HTMLOrSVGElement, className: string) { domNode.className = className; } } + function resolve(value: ValueOrList, reader: IReader | undefined, cb: (val: T) => void): void { if (isObservable(value)) { cb(value.read(reader)); @@ -2582,41 +2792,6 @@ export class ObserverNodeWithElement | undefined = undefined; - - get isHovered(): IObservable { - if (!this._isHovered) { - const hovered = observableValue('hovered', false); - this._element.addEventListener('mouseenter', (_e) => hovered.set(true, undefined)); - this._element.addEventListener('mouseleave', (_e) => hovered.set(false, undefined)); - this._isHovered = hovered; - } - return this._isHovered; - } - - private _didMouseMoveDuringHover: IObservable | undefined = undefined; - - get didMouseMoveDuringHover(): IObservable { - if (!this._didMouseMoveDuringHover) { - let _hovering = false; - const hovered = observableValue('didMouseMoveDuringHover', false); - this._element.addEventListener('mouseenter', (_e) => { - _hovering = true; - }); - this._element.addEventListener('mousemove', (_e) => { - if (_hovering) { - hovered.set(true, undefined); - } - }); - this._element.addEventListener('mouseleave', (_e) => { - _hovering = false; - hovered.set(false, undefined); - }); - this._didMouseMoveDuringHover = hovered; - } - return this._didMouseMoveDuringHover; - } } function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unknown) { if (value === null || value === undefined) { @@ -2626,9 +2801,36 @@ function setOrRemoveAttribute(element: HTMLOrSVGElement, key: string, value: unk } } -function isObservable(obj: unknown): obj is IObservable { - return !!obj && (>obj).read !== undefined && (>obj).reportChanges !== undefined; -} type ElementAttributeKeys = Partial<{ [K in keyof T]: T[K] extends Function ? never : T[K] extends object ? ElementAttributeKeys : Value; }>; + +/** + * A custom element that fires callbacks when connected to or disconnected from the DOM. + * Useful for tracking whether a template or component is currently mounted, especially + * with iframes/webviews that are sensitive to movement. + * + * @example + * ```ts + * const observer = document.createElement('connection-observer') as ConnectionObserverElement; + * observer.onDidConnect = () => console.log('mounted'); + * observer.onDidDisconnect = () => console.log('unmounted'); + * container.appendChild(observer); + * ``` + */ +export class ConnectionObserverElement extends HTMLElement { + public onDidConnect?: () => void; + public onDidDisconnect?: () => void; + + disconnectedCallback() { + this.onDidDisconnect?.(); + } + + connectedCallback() { + this.onDidConnect?.(); + } +} + +if (!customElements.get('connection-observer')) { + customElements.define('connection-observer', ConnectionObserverElement); +} diff --git a/src/vs/base/browser/domStylesheets.ts b/src/vs/base/browser/domStylesheets.ts index 1e34173680e..c338502d541 100644 --- a/src/vs/base/browser/domStylesheets.ts +++ b/src/vs/base/browser/domStylesheets.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore, toDisposable, IDisposable } from '../common/lifecycle.js'; +import { DisposableStore, toDisposable, IDisposable, Disposable } from '../common/lifecycle.js'; import { autorun, IObservable } from '../common/observable.js'; import { isFirefox } from './browser.js'; import { getWindows, sharedMutationObserver } from './dom.js'; @@ -15,35 +15,27 @@ export function isGlobalStylesheet(node: Node): boolean { return globalStylesheets.has(node as HTMLStyleElement); } -/** - * A version of createStyleSheet which has a unified API to initialize/set the style content. - */ -export function createStyleSheet2(): WrappedStyleElement { - return new WrappedStyleElement(); -} - -class WrappedStyleElement { +class WrappedStyleElement extends Disposable { private _currentCssStyle = ''; private _styleSheet: HTMLStyleElement | undefined = undefined; - public setStyle(cssStyle: string): void { + setStyle(cssStyle: string): void { if (cssStyle === this._currentCssStyle) { return; } this._currentCssStyle = cssStyle; if (!this._styleSheet) { - this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.textContent = cssStyle); + this._styleSheet = createStyleSheet(mainWindow.document.head, s => s.textContent = cssStyle, this._store); } else { this._styleSheet.textContent = cssStyle; } } - public dispose(): void { - if (this._styleSheet) { - this._styleSheet.remove(); - this._styleSheet = undefined; - } + override dispose(): void { + super.dispose(); + + this._styleSheet = undefined; } } @@ -121,12 +113,10 @@ function getSharedStyleSheet(): HTMLStyleElement { function getDynamicStyleSheetRules(style: HTMLStyleElement) { if (style?.sheet?.rules) { - // Chrome, IE - return style.sheet.rules; + return style.sheet.rules; // Chrome, IE } if (style?.sheet?.cssRules) { - // FF - return style.sheet.cssRules; + return style.sheet.cssRules; // FF } return []; } @@ -174,7 +164,7 @@ function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule { export function createStyleSheetFromObservable(css: IObservable): IDisposable { const store = new DisposableStore(); - const w = store.add(createStyleSheet2()); + const w = store.add(new WrappedStyleElement()); store.add(autorun(reader => { w.setStyle(css.read(reader)); })); diff --git a/src/vs/base/browser/dompurify/dompurify.js b/src/vs/base/browser/dompurify/dompurify.js index c0dbc8f1cab..e3ad75a5cc7 100644 --- a/src/vs/base/browser/dompurify/dompurify.js +++ b/src/vs/base/browser/dompurify/dompurify.js @@ -1354,4 +1354,3 @@ function createDOMPurify() { var purify = createDOMPurify(); export { purify as default }; -//# sourceMappingURL=purify.es.mjs.map \ No newline at end of file diff --git a/src/vs/base/browser/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts index b0ba04a66f5..6b675d06535 100644 --- a/src/vs/base/browser/keyboardEvent.ts +++ b/src/vs/base/browser/keyboardEvent.ts @@ -4,12 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as browser from './browser.js'; -import { EVENT_KEY_CODE_MAP, KeyCode, KeyCodeUtils, KeyMod } from '../common/keyCodes.js'; +import { EVENT_KEY_CODE_MAP, isModifierKey, KeyCode, KeyCodeUtils, KeyMod } from '../common/keyCodes.js'; import { KeyCodeChord } from '../common/keybindings.js'; import * as platform from '../common/platform.js'; - - function extractKeyCode(e: KeyboardEvent): KeyCode { if (e.charCode) { // "keypress" events mostly @@ -190,7 +188,7 @@ export class StandardKeyboardEvent implements IKeyboardEvent { private _computeKeybinding(): number { let key = KeyCode.Unknown; - if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) { + if (!isModifierKey(this.keyCode)) { key = this.keyCode; } @@ -214,7 +212,7 @@ export class StandardKeyboardEvent implements IKeyboardEvent { private _computeKeyCodeChord(): KeyCodeChord { let key = KeyCode.Unknown; - if (this.keyCode !== KeyCode.Ctrl && this.keyCode !== KeyCode.Shift && this.keyCode !== KeyCode.Alt && this.keyCode !== KeyCode.Meta) { + if (!isModifierKey(this.keyCode)) { key = this.keyCode; } return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key); diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index ccfab4c39e2..e3f20d96726 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -8,13 +8,13 @@ import { escapeDoubleQuotes, IMarkdownString, MarkdownStringTrustedOptions, pars import { markdownEscapeEscapedIcons } from '../common/iconLabels.js'; import { defaultGenerator } from '../common/idGenerator.js'; import { KeyCode } from '../common/keyCodes.js'; -import { Lazy } from '../common/lazy.js'; import { DisposableStore, IDisposable } from '../common/lifecycle.js'; import * as marked from '../common/marked/marked.js'; import { parse } from '../common/marshalling.js'; import { FileAccess, Schemas } from '../common/network.js'; import { cloneAndChange } from '../common/objects.js'; -import { dirname, resolvePath } from '../common/resources.js'; +import { basename as pathBasename } from '../common/path.js'; +import { basename, dirname, resolvePath } from '../common/resources.js'; import { escape } from '../common/strings.js'; import { URI, UriComponents } from '../common/uri.js'; import * as DOM from './dom.js'; @@ -220,7 +220,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende let outElement: HTMLElement; if (target) { outElement = target; - DOM.reset(target, ...renderedContent.children); + DOM.reset(target, ...renderedContent.childNodes); } else { outElement = renderedContent; } @@ -435,6 +435,7 @@ function activateLink(mdStr: IMarkdownString, options: MarkdownRenderOptions, ev onUnexpectedError(err); } finally { event.preventDefault(); + event.stopPropagation(); } } @@ -598,7 +599,9 @@ function getDomSanitizerConfig(mdStrConfig: MdStrConfig, options: MarkdownSaniti Schemas.vscodeFileResource, Schemas.vscodeRemote, Schemas.vscodeRemoteResource, - Schemas.vscodeNotebookCell + Schemas.vscodeNotebookCell, + // For links that are handled entirely by the action handler + Schemas.internal, ]; if (isTrusted) { @@ -648,6 +651,8 @@ function getDomSanitizerConfig(mdStrConfig: MdStrConfig, options: MarkdownSaniti export function renderAsPlaintext(str: IMarkdownString | string, options?: { /** Controls if the ``` of code blocks should be preserved in the output or not */ readonly includeCodeBlocksFences?: boolean; + /** Controls if we want to format empty links from "Link [](file)" to "Link file" */ + readonly useLinkFormatter?: boolean; }) { if (typeof str === 'string') { return str; @@ -659,7 +664,15 @@ export function renderAsPlaintext(str: IMarkdownString | string, options?: { value = `${value.substr(0, 100_000)}…`; } - const html = marked.parse(value, { async: false, renderer: options?.includeCodeBlocksFences ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }); + const renderer = createPlainTextRenderer(); + if (options?.includeCodeBlocksFences) { + renderer.code = codeBlockFences; + } + if (options?.useLinkFormatter) { + renderer.link = linkFormatter; + } + + const html = marked.parse(value, { async: false, renderer }); return sanitizeRenderedMarkdown(html, { isTrusted: false }, {}) .toString() .replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m) @@ -737,15 +750,22 @@ function createPlainTextRenderer(): marked.Renderer { }; return renderer; } -const plainTextRenderer = new Lazy(createPlainTextRenderer); -const plainTextWithCodeBlocksRenderer = new Lazy(() => { - const renderer = createPlainTextRenderer(); - renderer.code = ({ text }: marked.Tokens.Code): string => { - return `\n\`\`\`\n${escape(text)}\n\`\`\`\n`; - }; - return renderer; -}); +const codeBlockFences = ({ text }: marked.Tokens.Code): string => { + return `\n\`\`\`\n${escape(text)}\n\`\`\`\n`; +}; + +const linkFormatter = ({ text, href }: marked.Tokens.Link): string => { + try { + if (href) { + const uri = URI.parse(href); + return text.trim() || basename(uri); + } + } catch (e) { + return text.trim() || pathBasename(href); + } + return text; +}; function mergeRawTokenText(tokens: marked.Token[]): string { let mergedTokenText = ''; diff --git a/src/vs/base/browser/pixelRatio.ts b/src/vs/base/browser/pixelRatio.ts index 7ff456e5aa3..d2d93b66f30 100644 --- a/src/vs/base/browser/pixelRatio.ts +++ b/src/vs/base/browser/pixelRatio.ts @@ -7,6 +7,14 @@ import { getWindowId, onDidUnregisterWindow } from './dom.js'; import { Emitter, Event } from '../common/event.js'; import { Disposable, markAsSingleton } from '../common/lifecycle.js'; +type BackingStoreContext = CanvasRenderingContext2D & { + webkitBackingStorePixelRatio?: number; + mozBackingStorePixelRatio?: number; + msBackingStorePixelRatio?: number; + oBackingStorePixelRatio?: number; + backingStorePixelRatio?: number; +}; + /** * See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes */ @@ -67,13 +75,13 @@ class PixelRatioMonitorImpl extends Disposable implements IPixelRatioMonitor { } private _getPixelRatio(targetWindow: Window): number { - const ctx: any = document.createElement('canvas').getContext('2d'); + const ctx = document.createElement('canvas').getContext('2d') as BackingStoreContext | null; const dpr = targetWindow.devicePixelRatio || 1; - const bsr = ctx.webkitBackingStorePixelRatio || - ctx.mozBackingStorePixelRatio || - ctx.msBackingStorePixelRatio || - ctx.oBackingStorePixelRatio || - ctx.backingStorePixelRatio || 1; + const bsr = ctx?.webkitBackingStorePixelRatio || + ctx?.mozBackingStorePixelRatio || + ctx?.msBackingStorePixelRatio || + ctx?.oBackingStorePixelRatio || + ctx?.backingStorePixelRatio || 1; return dpr / bsr; } } diff --git a/src/vs/base/browser/touch.ts b/src/vs/base/browser/touch.ts index ac526347c90..8e857e292df 100644 --- a/src/vs/base/browser/touch.ts +++ b/src/vs/base/browser/touch.ts @@ -122,6 +122,9 @@ export class Gesture extends Disposable { return toDisposable(remove); } + /** + * Whether the device is able to represent touch events. + */ @memoize static isTouchDevice(): boolean { // `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be @@ -129,6 +132,14 @@ export class Gesture extends Disposable { return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0; } + /** + * Whether the device's primary input is able to hover. + */ + @memoize + static isHoverDevice(): boolean { + return mainWindow.matchMedia('(hover: hover)').matches; + } + public override dispose(): void { if (this.handle) { this.handle.dispose(); diff --git a/src/vs/base/browser/trustedTypes.ts b/src/vs/base/browser/trustedTypes.ts index ac3fb0eea3b..310ce79f0f9 100644 --- a/src/vs/base/browser/trustedTypes.ts +++ b/src/vs/base/browser/trustedTypes.ts @@ -24,7 +24,7 @@ export function createTrustedTypesPolicy .monaco-button.monaco-dropdown-button { border: 1px solid var(--vscode-button-border, transparent); border-left-width: 0 !important; - border-radius: 0 2px 2px 0; + border-radius: 0 4px 4px 0; display: flex; align-items: center; } .monaco-button-dropdown > .monaco-button.monaco-text-button { - border-radius: 2px 0 0 2px; + border-radius: 4px 0 0 4px; } .monaco-description-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index b4317a323f1..5b32ddc9d85 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -35,6 +35,7 @@ export interface IButtonOptions extends Partial { readonly supportIcons?: boolean; readonly supportShortLabel?: boolean; readonly secondary?: boolean; + readonly small?: boolean; readonly hoverDelegate?: IHoverDelegate; readonly disabled?: boolean; } @@ -47,6 +48,7 @@ export interface IButtonStyles { readonly buttonSecondaryBackground: string | undefined; readonly buttonSecondaryHoverBackground: string | undefined; readonly buttonSecondaryForeground: string | undefined; + readonly buttonSecondaryBorder: string | undefined; readonly buttonBorder: string | undefined; } @@ -58,7 +60,8 @@ export const unthemedButtonStyles: IButtonStyles = { buttonBorder: undefined, buttonSecondaryBackground: undefined, buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined + buttonSecondaryHoverBackground: undefined, + buttonSecondaryBorder: undefined }; export interface IButton extends IDisposable { @@ -116,11 +119,16 @@ export class Button extends Disposable implements IButton { this._element.setAttribute('role', 'button'); this._element.classList.toggle('secondary', !!options.secondary); + this._element.classList.toggle('small', !!options.small); const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground; const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground; + const border = options.secondary ? options.buttonSecondaryBorder : options.buttonBorder; this._element.style.color = foreground || ''; this._element.style.backgroundColor = background || ''; + if (border) { + this._element.style.border = `1px solid ${border}`; + } if (options.supportShortLabel) { this._labelShortElement = document.createElement('div'); @@ -176,18 +184,18 @@ export class Button extends Disposable implements IButton { this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => { if (!this._element.classList.contains('disabled')) { - this.updateBackground(true); + this.updateStyles(true); } })); this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => { - this.updateBackground(false); // restore standard styles + this.updateStyles(false); // restore standard styles })); // Also set hover background when button is focused for feedback this.focusTracker = this._register(trackFocus(this._element)); - this._register(this.focusTracker.onDidFocus(() => { if (this.enabled) { this.updateBackground(true); } })); - this._register(this.focusTracker.onDidBlur(() => { if (this.enabled) { this.updateBackground(false); } })); + this._register(this.focusTracker.onDidFocus(() => { if (this.enabled) { this.updateStyles(true); } })); + this._register(this.focusTracker.onDidBlur(() => { if (this.enabled) { this.updateStyles(false); } })); } public override dispose(): void { @@ -218,16 +226,23 @@ export class Button extends Disposable implements IButton { return elements; } - private updateBackground(hover: boolean): void { + private updateStyles(hover: boolean): void { let background; + let foreground; + let border; if (this.options.secondary) { background = hover ? this.options.buttonSecondaryHoverBackground : this.options.buttonSecondaryBackground; + foreground = this.options.buttonSecondaryForeground; + border = this.options.buttonSecondaryBorder; } else { background = hover ? this.options.buttonHoverBackground : this.options.buttonBackground; + foreground = this.options.buttonForeground; + border = this.options.buttonBorder; } - if (background) { - this._element.style.backgroundColor = background; - } + + this._element.style.backgroundColor = background || ''; + this._element.style.color = foreground || ''; + this._element.style.border = border ? `1px solid ${border}` : ''; } get element(): HTMLElement { @@ -327,6 +342,12 @@ export class Button extends Disposable implements IButton { return !this._element.classList.contains('disabled'); } + set secondary(value: boolean) { + this._element.classList.toggle('secondary', value); + (this.options as { secondary?: boolean }).secondary = value; + this.updateStyles(false); + } + set checked(value: boolean) { if (value) { this._element.classList.add('checked'); @@ -622,6 +643,8 @@ export class ButtonWithIcon extends Button { public get labelElement() { return this._mdlabelElement; } + public get iconElement() { return this._iconElement; } + constructor(container: HTMLElement, options: IButtonOptions) { super(container, options); diff --git a/src/vs/base/browser/ui/codicons/codicon/README.md b/src/vs/base/browser/ui/codicons/codicon/README.md new file mode 100644 index 00000000000..34e498f7d55 --- /dev/null +++ b/src/vs/base/browser/ui/codicons/codicon/README.md @@ -0,0 +1,5 @@ +# Codicons + +## Where does the codicon.ttf come from? + +It is added via the `@vscode/codicons` npm package, then copied to this directory during compile time. diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css index 9666216f6ae..1f5e70c95c0 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css @@ -16,18 +16,15 @@ .codicon-sync.codicon-modifier-spin, .codicon-loading.codicon-modifier-spin, .codicon-gear.codicon-modifier-spin, -.codicon-notebook-state-executing.codicon-modifier-spin { +.codicon-notebook-state-executing.codicon-modifier-spin, +.codicon-loading, +.codicon-tree-item-loading::before { /* Use steps to throttle FPS to reduce CPU usage */ animation: codicon-spin 1.5s steps(30) infinite; + /* Ensure rotation happens around exact center to prevent wobble */ + transform-origin: center center; } .codicon-modifier-disabled { opacity: 0.4; } - -/* custom speed & easing for loading icon */ -.codicon-loading, -.codicon-tree-item-loading::before { - animation-duration: 1s !important; - animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important; -} diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf deleted file mode 100644 index 98b967e3922..00000000000 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and /dev/null differ diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index 2da6a90f283..44c3c080e24 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -126,10 +126,12 @@ export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAn return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor } - if (viewSize <= viewportSize - layoutAfterAnchorBoundary) { + + if (viewSize <= viewportSize - layoutAfterAnchorBoundary && layoutBeforeAnchorBoundary < viewSize / 2) { return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor } + return 0; // sad case, lay it over the anchor } } diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index fe18c9a447b..c484fa86dbd 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -194,7 +194,6 @@ } .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { - padding: 4px 10px; overflow: hidden; text-overflow: ellipsis; margin: 4px 5px; /* allows button focus outline to be visible */ @@ -228,19 +227,14 @@ outline-width: 1px; outline-style: solid; outline-color: var(--vscode-focusBorder); - border-radius: 2px; + border-radius: 4px; } -.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-text-button { - padding-left: 10px; - padding-right: 10px; -} .monaco-dialog-box.align-vertical > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-text-button { width: 100%; } .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button-dropdown > .monaco-dropdown-button { - padding-left: 5px; - padding-right: 5px; + padding: 0 4px; } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index 1842d6444d4..6b3610b67c1 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -73,6 +73,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => { this.element = append(el, $('a.action-label')); + this.setAriaLabelAttributes(this.element); return this.renderLabel(this.element); }; diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index 2c9c47f4d5e..80f9fea1f87 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -13,6 +13,8 @@ import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBox import { Widget } from '../widget.js'; import { Emitter, Event } from '../../../common/event.js'; import { KeyCode } from '../../../common/keyCodes.js'; +import { IAction } from '../../../common/actions.js'; +import type { IActionViewItemProvider } from '../actionbar/actionbar.js'; import './findInput.css'; import * as nls from '../../../../nls.js'; import { DisposableStore, MutableDisposable } from '../../../common/lifecycle.js'; @@ -34,11 +36,14 @@ export interface IFindInputOptions { readonly appendWholeWordsLabel?: string; readonly appendRegexLabel?: string; readonly additionalToggles?: Toggle[]; + readonly actions?: ReadonlyArray; + readonly actionViewItemProvider?: IActionViewItemProvider; readonly showHistoryHint?: () => boolean; readonly toggleStyles: IToggleStyles; readonly inputBoxStyles: IInputBoxStyles; readonly history?: IHistory; readonly hoverLifecycleOptions?: IHoverLifecycleOptions; + readonly hideHoverOnValueChange?: boolean; } const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); @@ -112,7 +117,10 @@ export class FindInput extends Widget { flexibleWidth, flexibleMaxHeight, inputBoxStyles: options.inputBoxStyles, - history: options.history + history: options.history, + actions: options.actions, + actionViewItemProvider: options.actionViewItemProvider, + hideHoverOnValueChange: options.hideHoverOnValueChange })); if (this.showCommonFindToggles) { @@ -307,6 +315,10 @@ export class FindInput extends Widget { this.updateInputBoxPadding(); } + public setActions(actions: ReadonlyArray | undefined, actionViewItemProvider?: IActionViewItemProvider): void { + this.inputBox.setActions(actions, actionViewItemProvider); + } + private updateInputBoxPadding(controlsHidden = false) { if (controlsHidden) { this.inputBox.paddingRight = 0; diff --git a/src/vs/base/browser/ui/hover/hover.ts b/src/vs/base/browser/ui/hover/hover.ts index 50510f6e9a6..467baa91644 100644 --- a/src/vs/base/browser/ui/hover/hover.ts +++ b/src/vs/base/browser/ui/hover/hover.ts @@ -235,6 +235,12 @@ export interface IHoverOptions { * Options that define how the hover looks. */ appearance?: IHoverAppearanceOptions; + + /** + * An optional callback that is called when the hover is shown. This is called + * later for delayed hovers. + */ + onDidShow?(): void; } // `target` is ignored for delayed hover methods as it's included in the method and added @@ -267,6 +273,12 @@ export interface IHoverLifecycleOptions { */ groupId?: string; + /** + * Whether to use a reduced delay before showing the hover. If true, the + * `workbench.hover.reducedDelay` setting is used instead of `workbench.hover.delay`. + */ + reducedDelay?: boolean; + /** * Whether to set up space and enter keyboard events for the hover, when these are pressed when * the hover's target is focused it will show and focus the hover. diff --git a/src/vs/base/browser/ui/hover/hoverWidget.css b/src/vs/base/browser/ui/hover/hoverWidget.css index 11f1fdccc7c..85379221cf2 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.css +++ b/src/vs/base/browser/ui/hover/hoverWidget.css @@ -181,19 +181,6 @@ color: var(--vscode-textLink-activeForeground); } -/** - * Spans in markdown hovers need a margin-bottom to avoid looking cramped: - * https://github.com/microsoft/vscode/issues/101496 - - * This was later refined to only apply when the last child of a rendered markdown block (before the - * border or a `hr`) uses background color: - * https://github.com/microsoft/vscode/issues/228136 - */ -.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) p:last-child [style*="background-color"] { - margin-bottom: 4px; - display: inline-block; -} - /** * Add a slight margin to try vertically align codicons with any text * https://github.com/microsoft/vscode/issues/221359 diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css index f6005a48f78..827a19f29b4 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.css +++ b/src/vs/base/browser/ui/inputbox/inputBox.css @@ -8,7 +8,7 @@ display: block; padding: 0; box-sizing: border-box; - border-radius: 2px; + border-radius: 4px; /* Customizable */ font-size: inherit; diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 44af8e92aee..9b55cd2ec68 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -8,7 +8,7 @@ import * as cssJs from '../../cssValue.js'; import { DomEmitter } from '../../event.js'; import { renderFormattedText, renderText } from '../../formattedTextRenderer.js'; import { IHistoryNavigationWidget } from '../../history.js'; -import { ActionBar } from '../actionbar/actionbar.js'; +import { ActionBar, IActionViewItemProvider } from '../actionbar/actionbar.js'; import * as aria from '../aria/aria.js'; import { AnchorAlignment, IContextViewProvider } from '../contextview/contextview.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; @@ -37,8 +37,10 @@ export interface IInputOptions { readonly flexibleWidth?: boolean; readonly flexibleMaxHeight?: number; readonly actions?: ReadonlyArray; + readonly actionViewItemProvider?: IActionViewItemProvider; readonly inputBoxStyles: IInputBoxStyles; readonly history?: IHistory; + readonly hideHoverOnValueChange?: boolean; } export interface IInputBoxStyles { @@ -206,13 +208,29 @@ export class InputBox extends Widget { // Support actions if (this.options.actions) { - this.actionbar = this._register(new ActionBar(this.element)); + this.actionbar = this._register(new ActionBar(this.element, { + actionViewItemProvider: this.options.actionViewItemProvider + })); this.actionbar.push(this.options.actions, { icon: true, label: false }); } this.applyStyles(); } + public setActions(actions: ReadonlyArray | undefined, actionViewItemProvider?: IActionViewItemProvider): void { + if (this.actionbar) { + this.actionbar.clear(); + if (actions) { + this.actionbar.push(actions, { icon: true, label: false }); + } + } else if (actions) { + this.actionbar = this._register(new ActionBar(this.element, { + actionViewItemProvider: actionViewItemProvider ?? this.options.actionViewItemProvider + })); + this.actionbar.push(actions, { icon: true, label: false }); + } + } + protected onBlur(): void { this._hideMessage(); if (this.options.showPlaceholderOnFocus) { @@ -536,6 +554,12 @@ export class InputBox extends Widget { this.state = 'idle'; } + private layoutMessage(): void { + if (this.state === 'open' && this.contextViewProvider) { + this.contextViewProvider.layout(); + } + } + private onValueChange(): void { this._onDidChange.fire(this.value); @@ -546,6 +570,10 @@ export class InputBox extends Widget { if (this.state === 'open' && this.contextViewProvider) { this.contextViewProvider.layout(); } + + if (this.options.hideHoverOnValueChange) { + getBaseLayerHoverDelegate().hideHover(); + } } private updateMirror(): void { @@ -586,6 +614,7 @@ export class InputBox extends Widget { public layout(): void { if (!this.mirror) { + this.layoutMessage(); return; } @@ -597,6 +626,8 @@ export class InputBox extends Widget { this.input.style.height = this.cachedHeight + 'px'; this._onDidHeightChange.fire(this.cachedContentHeight); } + + this.layoutMessage(); } public insertAtCursor(text: string): void { diff --git a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.css b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.css index 7f203773f5e..b528d495fb9 100644 --- a/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.css +++ b/src/vs/base/browser/ui/keybindingLabel/keybindingLabel.css @@ -10,11 +10,13 @@ } .monaco-keybinding > .monaco-keybinding-key { - display: inline-block; + display: inline-flex; + align-items: center; border-style: solid; border-width: 1px; border-radius: 3px; - vertical-align: middle; + justify-content: center; + min-width: 12px; font-size: 11px; padding: 3px 5px; margin: 0 2px; diff --git a/src/vs/base/browser/ui/list/list.css b/src/vs/base/browser/ui/list/list.css index 672f03e42f0..512d74df27b 100644 --- a/src/vs/base/browser/ui/list/list.css +++ b/src/vs/base/browser/ui/list/list.css @@ -8,6 +8,7 @@ height: 100%; width: 100%; white-space: nowrap; + overflow: hidden; } .monaco-list.mouse-support { diff --git a/src/vs/base/browser/ui/list/list.ts b/src/vs/base/browser/ui/list/list.ts index c3085ba6ee2..3e7cb017967 100644 --- a/src/vs/base/browser/ui/list/list.ts +++ b/src/vs/base/browser/ui/list/list.ts @@ -73,8 +73,12 @@ export interface IListContextMenuEvent { readonly anchor: HTMLElement | IMouseEvent; } +export const NotSelectableGroupId = 'notSelectable'; +export type NotSelectableGroupIdType = typeof NotSelectableGroupId; + export interface IIdentityProvider { getId(element: T): { toString(): string }; + getGroupId?(element: T): number | NotSelectableGroupIdType; } export interface IKeyboardNavigationLabelProvider { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 2356673101d..512430ae805 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -1612,7 +1612,15 @@ export class ListView implements IListView { private probeDynamicHeight(index: number): number { const item = this.items[index]; + const diff = this.probeDynamicHeightForItem(item, index); + if (diff > 0) { + this.virtualDelegate.setDynamicHeight?.(item.element, item.size); + } + + return diff; + } + private probeDynamicHeightForItem(item: IItem, index: number): number { if (!!this.virtualDelegate.getDynamicHeight) { const newSize = this.virtualDelegate.getDynamicHeight(item.element); if (newSize !== null) { @@ -1636,8 +1644,12 @@ export class ListView implements IListView { if (item.row) { item.row.domNode.style.height = ''; item.size = item.row.domNode.offsetHeight; - if (item.size === 0 && !isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) { - console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack); + if (item.size === 0) { + if (!isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) { + console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack); + } else { + console.warn('Measured item node at 0px- ensure that ListView is not display:none before measuring row height!', new Error().stack); + } } item.lastDynamicHeightWidth = this.renderWidth; return item.size - size; @@ -1657,8 +1669,6 @@ export class ListView implements IListView { item.size = row.domNode.offsetHeight; renderer.disposeElement?.(item.element, index, row.templateData); - this.virtualDelegate.setDynamicHeight?.(item.element, item.size); - item.lastDynamicHeightWidth = this.renderWidth; row.domNode.remove(); this.cache.release(row); diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index c637ba43c88..4b191b9dd83 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -27,7 +27,7 @@ import { ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js' import { ISpliceable } from '../../../common/sequence.js'; import { isNumber } from '../../../common/types.js'; import './list.css'; -import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError } from './list.js'; +import { IIdentityProvider, IKeyboardNavigationDelegate, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListEvent, IListGestureEvent, IListMouseEvent, IListElementRenderDetails, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListError, NotSelectableGroupId, NotSelectableGroupIdType } from './list.js'; import { IListView, IListViewAccessibilityProvider, IListViewDragAndDrop, IListViewOptions, IListViewOptionsUpdate, ListViewTargetSector, ListView } from './listView.js'; import { IMouseWheelEvent, StandardMouseEvent } from '../../mouseEvent.js'; import { autorun, constObservable, IObservable } from '../../../common/observable.js'; @@ -403,7 +403,17 @@ class KeyboardController implements IDisposable { private onCtrlA(e: StandardKeyboardEvent): void { e.preventDefault(); e.stopPropagation(); - this.list.setSelection(range(this.list.length), e.browserEvent); + + let selection = range(this.list.length); + + // Filter by group if identity provider has getGroupId + const focusedElements = this.list.getFocus(); + const referenceGroupId = focusedElements.length > 0 ? this.list.getElementGroupId(focusedElements[0]) : undefined; + if (referenceGroupId !== undefined) { + selection = this.list.filterIndicesByGroup(selection, referenceGroupId); + } + + this.list.setSelection(selection, e.browserEvent); this.list.setAnchor(undefined); this.view.domNode.focus(); } @@ -777,7 +787,11 @@ export class MouseController implements IDisposable { this.list.setAnchor(focus); if (!isMouseRightClick(e.browserEvent)) { - this.list.setSelection([focus], e.browserEvent); + // Check if the element is selectable (getGroupId must not return undefined) + const focusGroupId = this.list.getElementGroupId(focus); + if (focusGroupId !== NotSelectableGroupId) { + this.list.setSelection([focus], e.browserEvent); + } } this._onPointer.fire(e); @@ -814,7 +828,16 @@ export class MouseController implements IDisposable { const min = Math.min(anchor, focus); const max = Math.max(anchor, focus); - const rangeSelection = range(min, max + 1); + let rangeSelection = range(min, max + 1); + + const selectedElement = this.list.getSelection()[0]; + if (selectedElement !== undefined) { + const referenceGroupId = this.list.getElementGroupId(selectedElement); + if (referenceGroupId !== undefined) { + rangeSelection = this.list.filterIndicesByGroup(rangeSelection, referenceGroupId); + } + } + const selection = this.list.getSelection(); const contiguousRange = getContiguousRangeContaining(disjunction(selection, [anchor]), anchor); @@ -833,8 +856,16 @@ export class MouseController implements IDisposable { this.list.setFocus([focus]); this.list.setAnchor(focus); + const focusGroupId = this.list.getElementGroupId(focus); + if (focusGroupId === NotSelectableGroupId) { + return; // Cannot select this element, do nothing + } + if (selection.length === newSelection.length) { - this.list.setSelection([...newSelection, focus], e.browserEvent); + const itemsToBeSelected = focusGroupId !== undefined ? + this.list.filterIndicesByGroup([...newSelection, focus], focusGroupId) + : [...newSelection, focus]; + this.list.setSelection(itemsToBeSelected, e.browserEvent); } else { this.list.setSelection(newSelection, e.browserEvent); } @@ -1698,6 +1729,8 @@ export class List implements ISpliceable, IDisposable { } } + indexes = indexes.filter(i => this.getElementGroupId(i) !== NotSelectableGroupId); + this.selection.set(indexes, browserEvent); } @@ -1731,6 +1764,42 @@ export class List implements ISpliceable, IDisposable { return typeof anchor === 'undefined' ? undefined : this.element(anchor); } + /** + * Gets the group ID for an element at the given index. + * Returns undefined if no identity provider, no getGroupId method, or if the group ID is undefined. + */ + getElementGroupId(index: number): number | NotSelectableGroupIdType | undefined { + const identityProvider = this.options.identityProvider; + if (!identityProvider?.getGroupId) { + return undefined; + } + + const element = this.element(index); + return identityProvider.getGroupId(element); + } + + /** + * Filters the given indices to only include those with a matching group ID. + * If no identity provider or getGroupId method exists, returns the original indices. + * If referenceGroupId is undefined, returns an empty array (elements without group IDs are not selectable). + */ + filterIndicesByGroup(indices: number[], referenceGroupId: number | NotSelectableGroupIdType): number[] { + const identityProvider = this.options.identityProvider; + if (!identityProvider?.getGroupId) { + return indices; + } + + if (referenceGroupId === NotSelectableGroupId) { + return []; + } + + return indices.filter(index => { + const element = this.element(index); + const groupId = identityProvider.getGroupId!(element); + return groupId === referenceGroupId; + }); + } + setFocus(indexes: number[], browserEvent?: UIEvent): void { for (const index of indexes) { if (index < 0 || index >= this.length) { diff --git a/src/vs/base/browser/ui/list/rowCache.ts b/src/vs/base/browser/ui/list/rowCache.ts index 4ec97d40923..b1d60d1fd45 100644 --- a/src/vs/base/browser/ui/list/rowCache.ts +++ b/src/vs/base/browser/ui/list/rowCache.ts @@ -33,10 +33,7 @@ export class RowCache implements IDisposable { let isStale = false; if (result) { - isStale = this.transactionNodesPendingRemoval.has(result.domNode); - if (isStale) { - this.transactionNodesPendingRemoval.delete(result.domNode); - } + isStale = this.transactionNodesPendingRemoval.delete(result.domNode); } else { const domNode = $('.monaco-list-row'); const renderer = this.getRenderer(templateId); diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index ff37ececff9..237797008ef 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -140,9 +140,9 @@ export class Menu extends ActionBar { if (options.enableMnemonics) { this._register(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => { const key = e.key.toLocaleLowerCase(); - if (this.mnemonics.has(key)) { + const actions = this.mnemonics.get(key); + if (actions !== undefined) { EventHelper.stop(e, true); - const actions = this.mnemonics.get(key)!; if (actions.length === 1) { if (actions[0] instanceof SubmenuMenuActionViewItem && actions[0].container) { @@ -398,14 +398,12 @@ export class Menu extends ActionBar { if (options.enableMnemonics) { const mnemonic = menuActionViewItem.getMnemonic(); if (mnemonic && menuActionViewItem.isEnabled()) { - let actionViewItems: BaseMenuActionViewItem[] = []; - if (this.mnemonics.has(mnemonic)) { - actionViewItems = this.mnemonics.get(mnemonic)!; + const actionViewItems = this.mnemonics.get(mnemonic); + if (actionViewItems !== undefined) { + actionViewItems.push(menuActionViewItem); + } else { + this.mnemonics.set(mnemonic, [menuActionViewItem]); } - - actionViewItems.push(menuActionViewItem); - - this.mnemonics.set(mnemonic, actionViewItems); } } @@ -423,14 +421,12 @@ export class Menu extends ActionBar { if (options.enableMnemonics) { const mnemonic = menuActionViewItem.getMnemonic(); if (mnemonic && menuActionViewItem.isEnabled()) { - let actionViewItems: BaseMenuActionViewItem[] = []; - if (this.mnemonics.has(mnemonic)) { - actionViewItems = this.mnemonics.get(mnemonic)!; + const actionViewItems = this.mnemonics.get(mnemonic); + if (actionViewItems !== undefined) { + actionViewItems.push(menuActionViewItem); + } else { + this.mnemonics.set(mnemonic, [menuActionViewItem]); } - - actionViewItems.push(menuActionViewItem); - - this.mnemonics.set(mnemonic, actionViewItems); } } diff --git a/src/vs/base/browser/ui/selectBox/selectBox.css b/src/vs/base/browser/ui/selectBox/selectBox.css index 7242251e9b4..2b0011a842b 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.css +++ b/src/vs/base/browser/ui/selectBox/selectBox.css @@ -6,7 +6,7 @@ .monaco-select-box { width: 100%; cursor: pointer; - border-radius: 2px; + border-radius: 4px; } .monaco-select-box-dropdown-container { @@ -30,6 +30,6 @@ .mac .monaco-action-bar .action-item .monaco-select-box { font-size: 11px; - border-radius: 3px; + border-radius: 4px; min-height: 24px; } diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index 1e023ae4e4e..335c2c9c09b 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -53,6 +53,11 @@ export interface ISelectOptionItem { isDisabled?: boolean; } +export const SeparatorSelectOption: Readonly = Object.freeze({ + text: '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500', + isDisabled: true, +}); + export interface ISelectBoxStyles extends IListStyles { readonly selectBackground: string | undefined; readonly selectListBackground: string | undefined; diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css index 292cd2dd1e1..2ca9a99a7bc 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.css +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.css @@ -3,21 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* Use custom CSS vars to expose padding into parent select for padding calculation */ -.monaco-select-box-dropdown-padding { - --dropdown-padding-top: 1px; - --dropdown-padding-bottom: 1px; -} - -.hc-black .monaco-select-box-dropdown-padding, -.hc-light .monaco-select-box-dropdown-padding { - --dropdown-padding-top: 3px; - --dropdown-padding-bottom: 4px; -} - .monaco-select-box-dropdown-container { display: none; - box-sizing: border-box; + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); } .monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * { @@ -41,33 +31,23 @@ text-align: left; width: 1px; overflow: hidden; - border-bottom-left-radius: 3px; - border-bottom-right-radius: 3px; } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container { flex: 0 0 auto; align-self: flex-start; - padding-top: var(--dropdown-padding-top); - padding-bottom: var(--dropdown-padding-bottom); - padding-left: 1px; - padding-right: 1px; width: 100%; overflow: hidden; - box-sizing: border-box; + box-sizing: border-box; } .monaco-select-box-dropdown-container > .select-box-details-pane { - padding: 5px; -} - -.hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container { - padding-top: var(--dropdown-padding-top); - padding-bottom: var(--dropdown-padding-bottom); + padding: 5px 6px; } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row { cursor: pointer; + padding-left: 2px; } .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text { @@ -100,12 +80,12 @@ /* https://webaim.org/techniques/css/invisiblecontent/ */ .monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .visually-hidden { - position: absolute; - left: -10000px; - top: auto; - width: 1px; - height: 1px; - overflow: hidden; + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + overflow: hidden; } .monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control { diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 54f205cc2c4..f6c2ff1cb4f 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -125,9 +125,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } this.selectElement = document.createElement('select'); - - // Use custom CSS vars for padding calculation - this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding'; + this.selectElement.className = 'monaco-select-box'; if (typeof this.selectBoxOptions.ariaLabel === 'string') { this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel); @@ -176,8 +174,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi // SetUp ContextView container to hold select Dropdown this.contextViewProvider = contextViewProvider; this.selectDropDownContainer = dom.$('.monaco-select-box-dropdown-container'); - // Use custom CSS vars for padding calculation (shared with parent select) - this.selectDropDownContainer.classList.add('monaco-select-box-dropdown-padding'); // Setup container for select option details this.selectionDetailsPane = dom.append(this.selectDropDownContainer, $('.select-box-details-pane')); @@ -472,7 +468,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi }, onHide: () => { this.selectDropDownContainer.classList.remove('visible'); - this.selectElement.classList.remove('synthetic-focus'); }, anchorPosition: this._dropDownPosition }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); @@ -487,7 +482,6 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi layout: () => this.layoutSelectDropDown(), onHide: () => { this.selectDropDownContainer.classList.remove('visible'); - this.selectElement.classList.remove('synthetic-focus'); }, anchorPosition: this._dropDownPosition }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); @@ -559,15 +553,13 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi const window = dom.getWindow(this.selectElement); const selectPosition = dom.getDomNodePagePosition(this.selectElement); - const styles = dom.getWindow(this.selectElement).getComputedStyle(this.selectElement); - const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom')); const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - (this.selectBoxOptions.minBottomMargin || 0)); const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN); // Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled) const selectWidth = this.selectElement.offsetWidth; const selectMinWidth = this.setWidthControlElement(this.widthControlElement); - const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px'; + const selectOptimalWidth = `${Math.max(selectMinWidth, Math.round(selectWidth))}px`; this.selectDropDownContainer.style.width = selectOptimalWidth; @@ -581,9 +573,9 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } const maxDetailsPaneHeight = this._hasDetails ? this._cachedMaxDetailsHeight! : 0; - const minRequiredDropDownHeight = listHeight + verticalPadding + maxDetailsPaneHeight; - const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding - maxDetailsPaneHeight) / this.getHeight()))); - const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding - maxDetailsPaneHeight) / this.getHeight()))); + const minRequiredDropDownHeight = listHeight + maxDetailsPaneHeight; + const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - maxDetailsPaneHeight) / this.getHeight()))); + const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - maxDetailsPaneHeight) / this.getHeight()))); // If we are only doing pre-layout check/adjust position only // Calculate vertical space available, flip up if insufficient @@ -673,20 +665,16 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi if (this._hasDetails) { // Leave the selectDropDownContainer to size itself according to children (list + details) - #57447 - this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px'; + this.selectList.getHTMLElement().style.height = `${listHeight}px`; this.selectDropDownContainer.style.height = ''; } else { - this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px'; + this.selectDropDownContainer.style.height = `${listHeight}px`; } this.updateDetail(this.selected); this.selectDropDownContainer.style.width = selectOptimalWidth; - - // Maintain focus outline on parent select as well as list container - tabindex for focus this.selectDropDownListContainer.setAttribute('tabindex', '0'); - this.selectElement.classList.add('synthetic-focus'); - this.selectDropDownContainer.classList.add('synthetic-focus'); return true; } else { @@ -713,7 +701,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi }); - container.textContent = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); + container.textContent = this.options[longest].text + (!!this.options[longest].decoratorRight ? `${this.options[longest].decoratorRight} ` : ''); elementWidth = dom.getTotalWidth(container); } diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index e490c9820d6..0b2fcbbb274 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -6,11 +6,14 @@ import { IAction } from '../../../common/actions.js'; import { Codicon } from '../../../common/codicons.js'; import { Emitter, Event } from '../../../common/event.js'; +import { IMarkdownString, isMarkdownString } from '../../../common/htmlContent.js'; +import { getCodiconAriaLabel, stripIcons } from '../../../common/iconLabels.js'; import { KeyCode } from '../../../common/keyCodes.js'; import { ThemeIcon } from '../../../common/themables.js'; -import { $, addDisposableListener, EventType, isActiveElement } from '../../dom.js'; +import { $, addDisposableListener, EventType, isActiveElement, isHTMLElement } from '../../dom.js'; import { IKeyboardEvent } from '../../keyboardEvent.js'; import { BaseActionViewItem, IActionViewItemOptions } from '../actionbar/actionViewItems.js'; +import { IActionViewItemProvider } from '../actionbar/actionbar.js'; import { HoverStyle, IHoverLifecycleOptions } from '../hover/hover.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { Widget } from '../widget.js'; @@ -19,7 +22,7 @@ import './toggle.css'; export interface IToggleOpts extends IToggleStyles { readonly actionClassName?: string; readonly icon?: ThemeIcon; - readonly title: string; + readonly title: string | IMarkdownString | HTMLElement; readonly isChecked: boolean; readonly notFocusable?: boolean; readonly hoverLifecycleOptions?: IHoverLifecycleOptions; @@ -125,7 +128,7 @@ export class Toggle extends Widget { get onKeyDown(): Event { return this._onKeyDown.event; } private readonly _opts: IToggleOpts; - private _title: string; + private _title: string | IMarkdownString | HTMLElement; private _icon: ThemeIcon | undefined; readonly domNode: HTMLElement; @@ -152,7 +155,7 @@ export class Toggle extends Widget { this.domNode = document.createElement('div'); this._register(getBaseLayerHoverDelegate().setupDelayedHover(this.domNode, () => ({ - content: this._title, + content: !isMarkdownString(this._title) && !isHTMLElement(this._title) ? stripIcons(this._title) : this._title, style: HoverStyle.Pointer, }), this._opts.hoverLifecycleOptions)); this.domNode.classList.add(...classes); @@ -170,6 +173,7 @@ export class Toggle extends Widget { this.checked = !this._checked; this._onChange.fire(false); ev.preventDefault(); + ev.stopPropagation(); } }); @@ -245,9 +249,12 @@ export class Toggle extends Widget { this.domNode.classList.add('disabled'); } - setTitle(newTitle: string): void { + setTitle(newTitle: string | IMarkdownString | HTMLElement): void { this._title = newTitle; - this.domNode.setAttribute('aria-label', newTitle); + + const ariaLabel = typeof newTitle === 'string' ? newTitle : isMarkdownString(newTitle) ? newTitle.value : newTitle.textContent; + + this.domNode.setAttribute('aria-label', getCodiconAriaLabel(ariaLabel)); } set visible(visible: boolean) { @@ -496,3 +503,21 @@ export class CheckboxActionViewItem extends BaseActionViewItem { } } + +/** + * Creates an action view item provider that renders toggles for actions with a checked state + * and falls back to default button rendering for regular actions. + * + * @param toggleStyles - Optional styles to apply to toggle items + * @returns An IActionViewItemProvider that can be used with ActionBar + */ +export function createToggleActionViewItemProvider(toggleStyles?: IToggleStyles): IActionViewItemProvider { + return (action: IAction, options: IActionViewItemOptions) => { + // Only render as a toggle if the action has a checked property + if (action.checked !== undefined) { + return new ToggleActionViewItem(null, action, { ...options, toggleStyles }); + } + // Return undefined to fall back to default button rendering + return undefined; + }; +} diff --git a/src/vs/base/browser/ui/toolbar/toolbar.css b/src/vs/base/browser/ui/toolbar/toolbar.css index 4c4c684755e..ea443012a3e 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.css +++ b/src/vs/base/browser/ui/toolbar/toolbar.css @@ -12,9 +12,21 @@ padding: 0; } -.monaco-toolbar.responsive { +.monaco-toolbar.responsive.responsive-all { .monaco-action-bar > .actions-container > .action-item { flex-shrink: 1; - min-width: 20px; + min-width: var(--vscode-toolbar-action-min-width, 20px); + } +} + +.monaco-toolbar.responsive.responsive-last { + .monaco-action-bar > .actions-container > .action-item { + flex-shrink: 0; + } + + .monaco-action-bar:not(.has-overflow) > .actions-container > .action-item:last-child, + .monaco-action-bar.has-overflow > .actions-container > .action-item:nth-last-child(2) { + flex-shrink: 1; + min-width: var(--vscode-toolbar-action-min-width, 20px); } } diff --git a/src/vs/base/browser/ui/toolbar/toolbar.ts b/src/vs/base/browser/ui/toolbar/toolbar.ts index 343761f6e6a..21696911bd1 100644 --- a/src/vs/base/browser/ui/toolbar/toolbar.ts +++ b/src/vs/base/browser/ui/toolbar/toolbar.ts @@ -18,7 +18,10 @@ import * as nls from '../../../../nls.js'; import { IHoverDelegate } from '../hover/hoverDelegate.js'; import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js'; -const ACTION_MIN_WIDTH = 24; /* 20px codicon + 4px left padding*/ +const ACTION_MIN_WIDTH = 20; /* 20px codicon */ +const ACTION_PADDING = 4; /* 4px padding */ + +const ACTION_MIN_WIDTH_VAR = '--vscode-toolbar-action-min-width'; export interface IToolBarOptions { orientation?: ActionsOrientation; @@ -51,9 +54,13 @@ export interface IToolBarOptions { label?: boolean; /** - * Hiding actions that are not visible + * Controls the responsive behavior of the primary group of the toolbar. + * - `enabled`: Whether the responsive behavior is enabled. + * - `kind`: The kind of responsive behavior to apply. Can be either `last` to only shrink the last item, or `all` to shrink all items equally. + * - `minItems`: The minimum number of items that should always be visible. + * - `actionMinWidth`: The minimum width of each action item. Defaults to `ACTION_MIN_WIDTH` (24px). */ - responsive?: boolean; + responsiveBehavior?: { enabled: boolean; kind: 'last' | 'all'; minItems?: number; actionMinWidth?: number }; } /** @@ -74,8 +81,9 @@ export class ToolBar extends Disposable { private originalSecondaryActions: ReadonlyArray = []; private hiddenActions: { action: IAction; size: number }[] = []; private readonly disposables = this._register(new DisposableStore()); + private readonly actionMinWidth: number; - constructor(container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { + constructor(private readonly container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) { super(); options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate()); @@ -153,12 +161,18 @@ export class ToolBar extends Disposable { } })); + // Store effective action min width + this.actionMinWidth = (options.responsiveBehavior?.actionMinWidth ?? ACTION_MIN_WIDTH) + ACTION_PADDING; + // Responsive support - if (this.options.responsive) { - this.element.classList.add('responsive'); + if (this.options.responsiveBehavior?.enabled) { + this.element.classList.toggle('responsive', true); + this.element.classList.toggle('responsive-all', this.options.responsiveBehavior.kind === 'all'); + this.element.classList.toggle('responsive-last', this.options.responsiveBehavior.kind === 'last'); + this.element.style.setProperty(ACTION_MIN_WIDTH_VAR, `${this.actionMinWidth - ACTION_PADDING}px`); const observer = new ResizeObserver(() => { - this.setToolbarMaxWidth(this.element.getBoundingClientRect().width); + this.updateActions(this.element.getBoundingClientRect().width); }); observer.observe(this.element); this._store.add(toDisposable(() => observer.disconnect())); @@ -237,12 +251,34 @@ export class ToolBar extends Disposable { this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) }); }); - if (this.options.responsive) { + this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction)); + + if (this.options.responsiveBehavior?.enabled) { // Reset hidden actions this.hiddenActions.length = 0; - // Update toolbar to fit with container width - this.setToolbarMaxWidth(this.element.getBoundingClientRect().width); + // Set the minimum width + if (this.options.responsiveBehavior?.minItems !== undefined) { + const itemCount = this.options.responsiveBehavior.minItems; + + // Account for overflow menu + let overflowWidth = 0; + if ( + this.originalSecondaryActions.length > 0 || + itemCount < this.originalPrimaryActions.length + ) { + overflowWidth = ACTION_MIN_WIDTH + ACTION_PADDING; + } + + this.container.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`; + this.element.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`; + } else { + this.container.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`; + this.element.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`; + } + + // Update toolbar actions to fit with container width + this.updateActions(this.element.getBoundingClientRect().width); } } @@ -256,31 +292,66 @@ export class ToolBar extends Disposable { return key?.getLabel() ?? undefined; } - private getItemsWidthResponsive(): number { + private updateActions(containerWidth: number) { + // Actions bar is empty + if (this.actionBar.isEmpty()) { + return; + } + + // Ensure that the container width respects the minimum width of the + // element which is set based on the `responsiveBehavior.minItems` option + containerWidth = Math.max(containerWidth, parseInt(this.element.style.minWidth)); + // Each action is assumed to have a minimum width so that actions with a label // can shrink to the action's minimum width. We do this so that action visibility // takes precedence over the action label. - return this.actionBar.length() * ACTION_MIN_WIDTH; - } + const actionBarWidth = (actualWidth: boolean) => { + if (this.options.responsiveBehavior?.kind === 'last') { + const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction); + const primaryActionsCount = hasToggleMenuAction + ? this.actionBar.length() - 1 + : this.actionBar.length(); + + let itemsWidth = 0; + for (let i = 0; i < primaryActionsCount - 1; i++) { + itemsWidth += this.actionBar.getWidth(i) + ACTION_PADDING; + } + + itemsWidth += actualWidth ? this.actionBar.getWidth(primaryActionsCount - 1) : this.actionMinWidth; // item to shrink + itemsWidth += hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0; // toggle menu action - private setToolbarMaxWidth(maxWidth: number) { - if ( - this.actionBar.isEmpty() || - (this.getItemsWidthResponsive() <= maxWidth && this.hiddenActions.length === 0) - ) { + return itemsWidth; + } else { + return this.actionBar.length() * this.actionMinWidth; + } + }; + + // Action bar fits and there are no hidden actions to show + if (actionBarWidth(false) <= containerWidth && this.hiddenActions.length === 0) { return; } - if (this.getItemsWidthResponsive() > maxWidth) { + if (actionBarWidth(false) > containerWidth) { + // Check for max items limit + if (this.options.responsiveBehavior?.minItems !== undefined) { + const primaryActionsCount = this.actionBar.hasAction(this.toggleMenuAction) + ? this.actionBar.length() - 1 + : this.actionBar.length(); + + if (primaryActionsCount <= this.options.responsiveBehavior.minItems) { + return; + } + } + // Hide actions from the right - while (this.getItemsWidthResponsive() > maxWidth && this.actionBar.length() > 0) { + while (actionBarWidth(true) > containerWidth && this.actionBar.length() > 0) { const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1; if (index < 0) { break; } // Store the action and its size - const size = Math.min(ACTION_MIN_WIDTH, this.getItemWidth(index)); + const size = Math.min(this.actionMinWidth, this.getItemWidth(index)); const action = this.originalPrimaryActions[index]; this.hiddenActions.unshift({ action, size }); @@ -302,7 +373,7 @@ export class ToolBar extends Disposable { // Show actions from the top of the toggle menu while (this.hiddenActions.length > 0) { const entry = this.hiddenActions.shift()!; - if (this.getItemsWidthResponsive() + entry.size > maxWidth) { + if (actionBarWidth(true) + entry.size > containerWidth) { // Not enough space to show the action this.hiddenActions.unshift(entry); break; @@ -318,7 +389,7 @@ export class ToolBar extends Disposable { // There are no secondary actions, and there is only one hidden item left so we // remove the overflow menu making space for the last hidden action to be shown. - if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) { + if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 0) { this.toggleMenuAction.menuActions = []; this.actionBar.pull(this.actionBar.length() - 1); } @@ -331,6 +402,8 @@ export class ToolBar extends Disposable { const secondaryActions = this.originalSecondaryActions.slice(0); this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions); } + + this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction)); } private clear(): void { @@ -342,6 +415,7 @@ export class ToolBar extends Disposable { override dispose(): void { this.clear(); this.disposables.dispose(); + this.element.remove(); super.dispose(); } } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 80c6ab42e38..ece9f2d016c 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -29,6 +29,7 @@ import { Emitter, Event, EventBufferer, Relay } from '../../../common/event.js'; import { fuzzyScore, FuzzyScore } from '../../../common/filters.js'; import { KeyCode } from '../../../common/keyCodes.js'; import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../common/lifecycle.js'; +import { isMacintosh } from '../../../common/platform.js'; import { clamp } from '../../../common/numbers.js'; import { ScrollEvent } from '../../../common/scrollable.js'; import './media/tree.css'; @@ -162,7 +163,10 @@ function asListOptions(modelProvider: () => ITreeModel { + return options.identityProvider!.getGroupId!(el.element); + } : undefined }, dnd: options.dnd && disposableStore.add(new TreeNodeListDragAndDrop(modelProvider, options.dnd)), multipleSelectionController: options.multipleSelectionController && { @@ -304,11 +308,12 @@ export enum RenderIndentGuides { Always = 'always' } -interface ITreeRendererOptions { +interface ITreeRendererOptions { readonly indent?: number; readonly renderIndentGuides?: RenderIndentGuides; // TODO@joao replace this with collapsible: boolean | 'ondemand' readonly hideTwistiesOfChildlessElements?: boolean; + readonly twistieAdditionalCssClass?: (element: T) => string | undefined; } interface Collection { @@ -343,6 +348,7 @@ export class TreeRenderer implements IListR private renderedNodes = new Map, ITreeListTemplateData>(); private indent: number = TreeRenderer.DefaultIndent; private hideTwistiesOfChildlessElements: boolean = false; + private twistieAdditionalCssClass?: (element: T) => string | undefined; private shouldRenderIndentGuides: boolean = false; private activeIndentNodes = new Set>(); @@ -356,7 +362,7 @@ export class TreeRenderer implements IListR onDidChangeCollapseState: Event>, private readonly activeNodes: Collection>, private readonly renderedIndentGuides: SetMap, HTMLDivElement>, - options: ITreeRendererOptions = {} + options: ITreeRendererOptions = {} ) { this.templateId = renderer.templateId; this.updateOptions(options); @@ -365,7 +371,7 @@ export class TreeRenderer implements IListR renderer.onDidChangeTwistieState?.(this.onDidChangeTwistieState, this, this.disposables); } - updateOptions(options: ITreeRendererOptions = {}): void { + updateOptions(options: ITreeRendererOptions = {}): void { if (typeof options.indent !== 'undefined') { const indent = clamp(options.indent, 0, 40); @@ -404,6 +410,10 @@ export class TreeRenderer implements IListR if (typeof options.hideTwistiesOfChildlessElements !== 'undefined') { this.hideTwistiesOfChildlessElements = options.hideTwistiesOfChildlessElements; } + + if (typeof options.twistieAdditionalCssClass !== 'undefined') { + this.twistieAdditionalCssClass = options.twistieAdditionalCssClass; + } } renderTemplate(container: HTMLElement): ITreeListTemplateData { @@ -462,6 +472,7 @@ export class TreeRenderer implements IListR } private renderTreeElement(node: ITreeNode, templateData: ITreeListTemplateData): void { + templateData.twistie.className = templateData.twistie.classList.item(0)!; templateData.twistie.style.paddingLeft = `${templateData.indentSize}px`; templateData.indent.style.width = `${templateData.indentSize + this.indent - 16}px`; @@ -490,6 +501,14 @@ export class TreeRenderer implements IListR templateData.twistie.classList.remove('collapsible', 'collapsed'); } + // Additional twistie class + if (this.twistieAdditionalCssClass) { + const additionalClass = this.twistieAdditionalCssClass(node.element); + if (additionalClass) { + templateData.twistie.classList.add(additionalClass); + } + } + this._renderIndentGuides(node, templateData); } @@ -1170,7 +1189,7 @@ export class FindController extends AbstractFindController this.filter.reset())); } - updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { + updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { if (optionsUpdate.defaultFindMode !== undefined) { this.mode = optionsUpdate.defaultFindMode; } @@ -1618,7 +1637,7 @@ class StickyScrollController extends Disposable { return this._widget.focusedLast(); } - updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { + updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { if (optionsUpdate.paddingTop !== undefined) { this.paddingTop = optionsUpdate.paddingTop; } @@ -1632,7 +1651,7 @@ class StickyScrollController extends Disposable { } } - validateStickySettings(options: IAbstractTreeOptionsUpdate): { stickyScrollMaxItemCount: number } { + validateStickySettings(options: IAbstractTreeOptionsUpdate): { stickyScrollMaxItemCount: number } { let stickyScrollMaxItemCount = 7; if (typeof options.stickyScrollMaxItemCount === 'number') { stickyScrollMaxItemCount = Math.max(options.stickyScrollMaxItemCount, 1); @@ -2172,7 +2191,7 @@ function asTreeContextMenuEvent(event: IListContextMenuEv }; } -export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { +export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly multipleSelectionSupport?: boolean; readonly typeNavigationEnabled?: boolean; readonly typeNavigationMode?: TypeNavigationMode; @@ -2185,13 +2204,13 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly mouseWheelScrollSensitivity?: number; readonly fastScrollSensitivity?: number; readonly expandOnDoubleClick?: boolean; - readonly expandOnlyOnTwistieClick?: boolean | ((e: any) => boolean); // e is the tree element (T) + readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean); readonly enableStickyScroll?: boolean; readonly stickyScrollMaxItemCount?: number; readonly paddingTop?: number; } -export interface IAbstractTreeOptions extends IAbstractTreeOptionsUpdate, IListOptions { +export interface IAbstractTreeOptions extends IAbstractTreeOptionsUpdate, IListOptions { readonly contextViewProvider?: IContextViewProvider; readonly collapseByDefault?: boolean; // defaults to false readonly allowNonCollapsibleParents?: boolean; // defaults to false @@ -2582,6 +2601,7 @@ export abstract class AbstractTree implements IDisposable get onMouseClick(): Event> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); } get onMouseDblClick(): Event> { return Event.filter(Event.map(this.view.onMouseDblClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); } + get onMouseMiddleClick(): Event> { return Event.filter(Event.map(this.view.onMouseMiddleClick, asTreeMouseEvent), e => e.target !== TreeMouseEventTarget.Filter); } get onMouseOver(): Event> { return Event.map(this.view.onMouseOver, asTreeMouseEvent); } get onMouseOut(): Event> { return Event.map(this.view.onMouseOut, asTreeMouseEvent); } get onContextMenu(): Event> { return Event.any(Event.filter(Event.map(this.view.onContextMenu, asTreeContextMenuEvent), e => !e.isStickyScroll), this.stickyScrollController?.onContextMenu ?? Event.None); } @@ -2694,7 +2714,7 @@ export abstract class AbstractTree implements IDisposable this.getHTMLElement().classList.toggle('always', this._options.renderIndentGuides === RenderIndentGuides.Always); } - updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { + updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void { this._options = { ...this._options, ...optionsUpdate }; for (const renderer of this.renderers) { @@ -2714,7 +2734,7 @@ export abstract class AbstractTree implements IDisposable return this._options; } - private updateStickyScroll(optionsUpdate: IAbstractTreeOptionsUpdate) { + private updateStickyScroll(optionsUpdate: IAbstractTreeOptionsUpdate) { if (!this.stickyScrollController && this._options.enableStickyScroll) { this.stickyScrollController = new StickyScrollController(this, this.model, this.view, this.renderers, this.treeDelegate, this._options); this.onDidChangeStickyScrollFocused = this.stickyScrollController.onDidChangeHasFocus; @@ -3202,7 +3222,7 @@ export abstract class AbstractTree implements IDisposable // a nice to have UI feature. const activeNodesEmitter = this.modelDisposables.add(new Emitter[]>()); const activeNodesDebounce = this.modelDisposables.add(new Delayer(0)); - this.modelDisposables.add(Event.any(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)(() => { + this.modelDisposables.add(Event.any(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange)(() => { activeNodesDebounce.trigger(() => { const set = new Set>(); @@ -3223,6 +3243,17 @@ export abstract class AbstractTree implements IDisposable this.onDidChangeCollapseStateRelay.input = model.onDidChangeCollapseState; this.onDidChangeRenderNodeCountRelay.input = model.onDidChangeRenderNodeCount; this.onDidSpliceModelRelay.input = model.onDidSpliceModel; + + // Announce collapse state changes for screen readers (VoiceOver doesn't reliably + // announce aria-expanded changes on already-focused elements) + if (isMacintosh) { + this.modelDisposables.add(model.onDidChangeCollapseState(e => { + const { node, deep } = e; + if (node.collapsible && !deep && this.isDOMFocused()) { + alert(node.collapsed ? localize('treeNodeCollapsed', "collapsed") : localize('treeNodeExpanded', "expanded")); + } + })); + } } navigate(start?: TRef): ITreeNavigator { @@ -3241,12 +3272,12 @@ export abstract class AbstractTree implements IDisposable } } -interface ITreeNavigatorView, TFilterData> { +interface ITreeNavigatorView { readonly length: number; element(index: number): ITreeNode; } -class TreeNavigator, TFilterData, TRef> implements ITreeNavigator { +class TreeNavigator implements ITreeNavigator { private index: number; diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 043483e4b0e..565340518ff 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -7,7 +7,7 @@ import { IDragAndDropData } from '../../dnd.js'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListTouchEvent, IListVirtualDelegate } from '../list/list.js'; import { ElementsDragAndDropData, ListViewTargetSector } from '../list/listView.js'; import { IListStyles } from '../list/listWidget.js'; -import { ComposedTreeDelegate, TreeFindMode as TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent, IFindControllerOptions, IStickyScrollDelegate, AbstractTree } from './abstractTree.js'; +import { ComposedTreeDelegate, TreeFindMode, IAbstractTreeOptions, IAbstractTreeOptionsUpdate, TreeFindMatchType, AbstractTreePart, LabelFuzzyScore, FindFilter, FindController, ITreeFindToggleChangeEvent, IFindControllerOptions, IStickyScrollDelegate, AbstractTree } from './abstractTree.js'; import { ICompressedTreeElement, ICompressedTreeNode } from './compressedObjectTreeModel.js'; import { getVisibleState, isFilterResult } from './indexTreeModel.js'; import { CompressibleObjectTree, ICompressibleKeyboardNavigationLabelProvider, ICompressibleObjectTreeOptions, ICompressibleTreeRenderer, IObjectTreeOptions, IObjectTreeSetChildrenOptions, ObjectTree } from './objectTree.js'; @@ -27,7 +27,7 @@ import { FuzzyScore } from '../../../common/filters.js'; import { insertInto, splice } from '../../../common/arrays.js'; import { localize } from '../../../../nls.js'; -interface IAsyncDataTreeNode { +export interface IAsyncDataTreeNode { element: TInput | T; readonly parent: IAsyncDataTreeNode | null; readonly children: IAsyncDataTreeNode[]; @@ -424,7 +424,10 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt identityProvider: options.identityProvider && { getId(el) { return options.identityProvider!.getId(el.element as T); - } + }, + getGroupId: options.identityProvider!.getGroupId ? (el) => { + return options.identityProvider!.getGroupId!(el.element as T); + } : undefined }, dnd: options.dnd && new AsyncDataTreeNodeListDragAndDrop(options.dnd), multipleSelectionController: options.multipleSelectionController && { @@ -478,6 +481,9 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt ((e: IAsyncDataTreeNode) => (options.expandOnlyOnTwistieClick as ((e: T) => boolean))(e.element as T)) as ((e: unknown) => boolean) ) ), + twistieAdditionalCssClass: typeof options.twistieAdditionalCssClass === 'undefined' ? undefined : ( + ((e: IAsyncDataTreeNode) => (options.twistieAdditionalCssClass as ((e: T) => string | undefined))(e.element as T)) as ((e: unknown) => string | undefined) + ), defaultFindVisibility: (e: IAsyncDataTreeNode) => { if (e.hasChildren && e.stale) { return TreeVisibility.Visible; @@ -492,10 +498,10 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt stickyScrollDelegate: options.stickyScrollDelegate as IStickyScrollDelegate, TFilterData> | undefined }; } -export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { } +export interface IAsyncDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { } export interface IAsyncDataTreeUpdateChildrenOptions extends IObjectTreeSetChildrenOptions { } -export interface IAsyncDataTreeOptions extends IAsyncDataTreeOptionsUpdate, Pick, Exclude, 'collapseByDefault'>> { +export interface IAsyncDataTreeOptions extends IAsyncDataTreeOptionsUpdate, Pick, Exclude, 'collapseByDefault'>> { readonly collapseByDefault?: { (e: T): boolean }; readonly identityProvider?: IIdentityProvider; readonly sorter?: ITreeSorter; @@ -564,7 +570,7 @@ export class AsyncDataTree implements IDisposable get onDidChangeModel(): Event { return this.tree.onDidChangeModel; } get onDidChangeCollapseState(): Event | null, TFilterData>> { return this.tree.onDidChangeCollapseState; } - get onDidUpdateOptions(): Event { return this.tree.onDidUpdateOptions; } + get onDidUpdateOptions(): Event>> { return this.tree.onDidUpdateOptions; } private focusNavigationFilter: ((node: ITreeNode | null, TFilterData>) => boolean) | undefined; @@ -594,7 +600,7 @@ export class AsyncDataTree implements IDisposable protected user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], + renderers: ITreeRenderer[], private dataSource: IAsyncDataSource, options: IAsyncDataTreeOptions = {} ) { @@ -654,7 +660,7 @@ export class AsyncDataTree implements IDisposable user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], + renderers: ITreeRenderer[], options: IAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); @@ -664,7 +670,7 @@ export class AsyncDataTree implements IDisposable return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions); } - updateOptions(optionsUpdate: IAsyncDataTreeOptionsUpdate = {}): void { + updateOptions(optionsUpdate: IAsyncDataTreeOptionsUpdate | null> = {}): void { if (this.findController) { if (optionsUpdate.defaultFindMode !== undefined) { this.findController.mode = optionsUpdate.defaultFindMode; @@ -1181,7 +1187,7 @@ export class AsyncDataTree implements IDisposable } } - private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent | null, any>): void { + private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent | null, TFilterData>): void { if (node.element === null) { return; } @@ -1489,7 +1495,7 @@ export interface ICompressibleAsyncDataTreeOptions extend readonly keyboardNavigationLabelProvider?: ICompressibleKeyboardNavigationLabelProvider; } -export interface ICompressibleAsyncDataTreeOptionsUpdate extends IAsyncDataTreeOptionsUpdate { +export interface ICompressibleAsyncDataTreeOptionsUpdate extends IAsyncDataTreeOptionsUpdate { readonly compressionEnabled?: boolean; } @@ -1504,7 +1510,7 @@ export class CompressibleAsyncDataTree extends As container: HTMLElement, virtualDelegate: IListVirtualDelegate, private compressionDelegate: ITreeCompressionDelegate, - renderers: ICompressibleTreeRenderer[], + renderers: ICompressibleTreeRenderer[], dataSource: IAsyncDataSource, options: ICompressibleAsyncDataTreeOptions = {} ) { @@ -1521,7 +1527,7 @@ export class CompressibleAsyncDataTree extends As user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ICompressibleTreeRenderer[], + renderers: ICompressibleTreeRenderer[], options: ICompressibleAsyncDataTreeOptions ): ObjectTree, TFilterData> { const objectTreeDelegate = new ComposedTreeDelegate>(delegate); diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index f8057a418c0..e4adc832676 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -117,7 +117,7 @@ const wrapIdentityProvider = (base: IIdentityProvider): IIdentityProvider< }); // Exported only for test reasons, do not use directly -export class CompressedObjectTreeModel, TFilterData extends NonNullable = void> implements ITreeModel | null, TFilterData, T | null> { +export class CompressedObjectTreeModel implements ITreeModel | null, TFilterData, T | null> { readonly rootRef = null; @@ -351,7 +351,7 @@ export class CompressedObjectTreeModel, TFilterData e // Compressible Object Tree export type ElementMapper = (elements: T[]) => T; -export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; +export const DefaultElementMapper: ElementMapper = elements => elements[elements.length - 1]; export type CompressedNodeUnwrapper = (node: ICompressedTreeNode) => T; type CompressedNodeWeakMapper = WeakMapper | null, TFilterData>, ITreeNode>; @@ -405,7 +405,7 @@ export interface ICompressibleObjectTreeModelOptions extends IOb readonly elementMapper?: ElementMapper; } -export class CompressibleObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { +export class CompressibleObjectTreeModel implements IObjectTreeModel { readonly rootRef = null; @@ -443,7 +443,7 @@ export class CompressibleObjectTreeModel, TFilterData user: string, options: ICompressibleObjectTreeModelOptions = {} ) { - this.elementMapper = options.elementMapper || DefaultElementMapper; + this.elementMapper = options.elementMapper || (DefaultElementMapper as ElementMapper); const compressedNodeUnwrapper: CompressedNodeUnwrapper = node => this.elementMapper(node.elements); this.nodeMapper = new WeakMapper(node => new CompressedTreeNodeWrapper(compressedNodeUnwrapper, node)); @@ -478,11 +478,11 @@ export class CompressibleObjectTreeModel, TFilterData return this.model.getListRenderCount(location); } - getNode(location?: T | null | undefined): ITreeNode { + getNode(location?: T | null | undefined): ITreeNode { return this.nodeMapper.map(this.model.getNode(location)); } - getNodeLocation(node: ITreeNode): T | null { + getNodeLocation(node: ITreeNode): T | null { return node.element; } diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index 5acc18dc7a2..ff3fd98af78 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -25,7 +25,7 @@ export class DataTree extends AbstractTree, - renderers: ITreeRenderer[], + renderers: ITreeRenderer[], private dataSource: IDataSource, options: IDataTreeOptions = {} ) { diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts index cc43faca89e..f3e2bb30c75 100644 --- a/src/vs/base/browser/ui/tree/indexTree.ts +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -20,7 +20,7 @@ export class IndexTree extends AbstractTree, - renderers: ITreeRenderer[], + renderers: ITreeRenderer[], private rootElement: T, options: IIndexTreeOptions = {} ) { diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index a7a2f738145..a9a2a8ae65b 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -89,7 +89,7 @@ function isCollapsibleStateUpdate(update: CollapseStateUpdate): update is Collap return 'collapsible' in update; } -export class IndexTreeModel, TFilterData = void> implements ITreeModel { +export class IndexTreeModel, TFilterData = void> implements ITreeModel { readonly rootRef = []; diff --git a/src/vs/base/browser/ui/tree/media/tree.css b/src/vs/base/browser/ui/tree/media/tree.css index b5330b90041..34fff0f9749 100644 --- a/src/vs/base/browser/ui/tree/media/tree.css +++ b/src/vs/base/browser/ui/tree/media/tree.css @@ -70,6 +70,8 @@ .monaco-tl-twistie.codicon-tree-item-loading::before { /* Use steps to throttle FPS to reduce CPU usage */ animation: codicon-spin 1.25s steps(30) infinite; + /* Ensure rotation happens around exact center to prevent wobble */ + transform-origin: center center; } .monaco-tree-type-filter { diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 7b603cf35fe..d9100592a4b 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -35,7 +35,7 @@ export interface IObjectTreeSetChildrenOptions { readonly diffIdentityProvider?: IIdentityProvider; } -export class ObjectTree, TFilterData = void> extends AbstractTree { +export class ObjectTree extends AbstractTree { protected declare model: IObjectTreeModel; @@ -45,7 +45,7 @@ export class ObjectTree, TFilterData = void> extends protected readonly user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ITreeRenderer[], + renderers: ITreeRenderer[], options: IObjectTreeOptions = {} ) { super(user, container, delegate, renderers, options as IObjectTreeOptions); @@ -100,7 +100,7 @@ interface CompressibleTemplateData { readonly data: TTemplateData; } -class CompressibleRenderer, TFilterData, TTemplateData> implements ITreeRenderer> { +class CompressibleRenderer implements ITreeRenderer> { readonly templateId: string; readonly onDidChangeTwistieState: Event | undefined; @@ -271,11 +271,11 @@ function asObjectTreeOptions(compressedTreeNodeProvider: () => I }; } -export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { +export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptionsUpdate { readonly compressionEnabled?: boolean; } -export class CompressibleObjectTree, TFilterData = void> extends ObjectTree implements ICompressedTreeNodeProvider { +export class CompressibleObjectTree extends ObjectTree implements ICompressedTreeNodeProvider { protected declare model: CompressibleObjectTreeModel; @@ -283,12 +283,12 @@ export class CompressibleObjectTree, TFilterData = vo user: string, container: HTMLElement, delegate: IListVirtualDelegate, - renderers: ICompressibleTreeRenderer[], + renderers: ICompressibleTreeRenderer[], options: ICompressibleObjectTreeOptions = {} ) { const compressedTreeNodeProvider = () => this; const stickyScrollDelegate = new CompressibleStickyScrollDelegate(() => this.model); - const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, stickyScrollDelegate, r)); + const compressibleRenderers = renderers.map(r => new CompressibleRenderer(compressedTreeNodeProvider, stickyScrollDelegate, r)); super(user, container, delegate, compressibleRenderers, { ...asObjectTreeOptions(compressedTreeNodeProvider, options), stickyScrollDelegate }); } @@ -301,7 +301,7 @@ export class CompressibleObjectTree, TFilterData = vo return new CompressibleObjectTreeModel(user, options); } - override updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void { + override updateOptions(optionsUpdate: ICompressibleObjectTreeOptionsUpdate = {}): void { super.updateOptions(optionsUpdate); if (typeof optionsUpdate.compressionEnabled !== 'undefined') { diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 57bec495e51..39bb3412e0c 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -11,7 +11,7 @@ import { Iterable } from '../../../common/iterator.js'; export type ITreeNodeCallback = (node: ITreeNode) => void; -export interface IObjectTreeModel, TFilterData extends NonNullable = void> extends ITreeModel { +export interface IObjectTreeModel extends ITreeModel { setChildren(element: T | null, children: Iterable> | undefined, options?: IObjectTreeModelSetChildrenOptions): void; resort(element?: T | null, recursive?: boolean): void; } @@ -24,7 +24,7 @@ export interface IObjectTreeModelOptions extends IIndexTreeModel readonly identityProvider?: IIdentityProvider; } -export class ObjectTreeModel, TFilterData extends NonNullable = void> implements IObjectTreeModel { +export class ObjectTreeModel implements IObjectTreeModel { readonly rootRef = null; diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index a3474b3dfa3..fe579cfa9f8 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -142,8 +142,8 @@ export interface ITreeModel { getListIndex(location: TRef): number; getListRenderCount(location: TRef): number; - getNode(location?: TRef): ITreeNode; - getNodeLocation(node: ITreeNode): TRef; + getNode(location?: TRef): ITreeNode; + getNodeLocation(node: ITreeNode): TRef; getParentNodeLocation(location: TRef): TRef | undefined; getFirstElementChild(location: TRef): T | undefined; diff --git a/src/vs/base/browser/webWorkerFactory.ts b/src/vs/base/browser/webWorkerFactory.ts deleted file mode 100644 index 83c587f16f1..00000000000 --- a/src/vs/base/browser/webWorkerFactory.ts +++ /dev/null @@ -1,204 +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 { createTrustedTypesPolicy } from './trustedTypes.js'; -import { onUnexpectedError } from '../common/errors.js'; -import { COI } from '../common/network.js'; -import { URI } from '../common/uri.js'; -import { IWebWorker, IWebWorkerClient, Message, WebWorkerClient } from '../common/worker/webWorker.js'; -import { Disposable, toDisposable } from '../common/lifecycle.js'; -import { coalesce } from '../common/arrays.js'; -import { getNLSLanguage, getNLSMessages } from '../../nls.js'; -import { Emitter } from '../common/event.js'; -import { getMonacoEnvironment } from './browser.js'; - -// Reuse the trusted types policy defined from worker bootstrap -// when available. -// Refs https://github.com/microsoft/vscode/issues/222193 -let ttPolicy: ReturnType; -// eslint-disable-next-line local/code-no-any-casts -if (typeof self === 'object' && self.constructor && self.constructor.name === 'DedicatedWorkerGlobalScope' && (globalThis as any).workerttPolicy !== undefined) { - // eslint-disable-next-line local/code-no-any-casts - ttPolicy = (globalThis as any).workerttPolicy; -} else { - ttPolicy = createTrustedTypesPolicy('defaultWorkerFactory', { createScriptURL: value => value }); -} - -export function createBlobWorker(blobUrl: string, options?: WorkerOptions): Worker { - if (!blobUrl.startsWith('blob:')) { - throw new URIError('Not a blob-url: ' + blobUrl); - } - return new Worker(ttPolicy ? ttPolicy.createScriptURL(blobUrl) as unknown as string : blobUrl, { ...options, type: 'module' }); -} - -function getWorker(descriptor: IWebWorkerDescriptor, id: number): Worker | Promise { - const label = descriptor.label || 'anonymous' + id; - - // Option for hosts to overwrite the worker script (used in the standalone editor) - const monacoEnvironment = getMonacoEnvironment(); - if (monacoEnvironment) { - if (typeof monacoEnvironment.getWorker === 'function') { - return monacoEnvironment.getWorker('workerMain.js', label); - } - if (typeof monacoEnvironment.getWorkerUrl === 'function') { - const workerUrl = monacoEnvironment.getWorkerUrl('workerMain.js', label); - return new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label, type: 'module' }); - } - } - - const esmWorkerLocation = descriptor.esmModuleLocation; - if (esmWorkerLocation) { - const workerUrl = getWorkerBootstrapUrl(label, esmWorkerLocation.toString(true)); - const worker = new Worker(ttPolicy ? ttPolicy.createScriptURL(workerUrl) as unknown as string : workerUrl, { name: label, type: 'module' }); - return whenESMWorkerReady(worker); - } - - throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker`); -} - -function getWorkerBootstrapUrl(label: string, workerScriptUrl: string): string { - if (/^((http:)|(https:)|(file:))/.test(workerScriptUrl) && workerScriptUrl.substring(0, globalThis.origin.length) !== globalThis.origin) { - // this is the cross-origin case - // i.e. the webpage is running at a different origin than where the scripts are loaded from - } else { - const start = workerScriptUrl.lastIndexOf('?'); - const end = workerScriptUrl.lastIndexOf('#', start); - const params = start > 0 - ? new URLSearchParams(workerScriptUrl.substring(start + 1, ~end ? end : undefined)) - : new URLSearchParams(); - - COI.addSearchParam(params, true, true); - const search = params.toString(); - if (!search) { - workerScriptUrl = `${workerScriptUrl}#${label}`; - } else { - workerScriptUrl = `${workerScriptUrl}?${params.toString()}#${label}`; - } - } - - // In below blob code, we are using JSON.stringify to ensure the passed - // in values are not breaking our script. The values may contain string - // terminating characters (such as ' or "). - const blob = new Blob([coalesce([ - `/*${label}*/`, - `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`, - `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`, - `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`, - `const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`, - `globalThis.workerttPolicy = ttPolicy;`, - `await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`, - `globalThis.postMessage({ type: 'vscode-worker-ready' });`, - `/*${label}*/` - ]).join('')], { type: 'application/javascript' }); - return URL.createObjectURL(blob); -} - -function whenESMWorkerReady(worker: Worker): Promise { - return new Promise((resolve, reject) => { - worker.onmessage = function (e) { - if (e.data.type === 'vscode-worker-ready') { - worker.onmessage = null; - resolve(worker); - } - }; - worker.onerror = reject; - }); -} - -function isPromiseLike(obj: unknown): obj is PromiseLike { - return !!obj && typeof (obj as PromiseLike).then === 'function'; -} - -/** - * A worker that uses HTML5 web workers so that is has - * its own global scope and its own thread. - */ -class WebWorker extends Disposable implements IWebWorker { - - private static LAST_WORKER_ID = 0; - - private readonly id: number; - private worker: Promise | null; - - private readonly _onMessage = this._register(new Emitter()); - public readonly onMessage = this._onMessage.event; - - private readonly _onError = this._register(new Emitter()); - public readonly onError = this._onError.event; - - constructor(descriptorOrWorker: IWebWorkerDescriptor | Worker | Promise) { - super(); - this.id = ++WebWorker.LAST_WORKER_ID; - const workerOrPromise = ( - descriptorOrWorker instanceof Worker - ? descriptorOrWorker : - 'then' in descriptorOrWorker ? descriptorOrWorker - : getWorker(descriptorOrWorker, this.id) - ); - if (isPromiseLike(workerOrPromise)) { - this.worker = workerOrPromise; - } else { - this.worker = Promise.resolve(workerOrPromise); - } - this.postMessage('-please-ignore-', []); // TODO: Eliminate this extra message - const errorHandler = (ev: ErrorEvent) => { - this._onError.fire(ev); - }; - this.worker.then((w) => { - w.onmessage = (ev) => { - this._onMessage.fire(ev.data); - }; - w.onmessageerror = (ev) => { - this._onError.fire(ev); - }; - if (typeof w.addEventListener === 'function') { - w.addEventListener('error', errorHandler); - } - }); - this._register(toDisposable(() => { - this.worker?.then(w => { - w.onmessage = null; - w.onmessageerror = null; - w.removeEventListener('error', errorHandler); - w.terminate(); - }); - this.worker = null; - })); - } - - public getId(): number { - return this.id; - } - - public postMessage(message: unknown, transfer: Transferable[]): void { - this.worker?.then(w => { - try { - w.postMessage(message, transfer); - } catch (err) { - onUnexpectedError(err); - onUnexpectedError(new Error(`FAILED to post message to worker`, { cause: err })); - } - }); - } -} - -export interface IWebWorkerDescriptor { - readonly esmModuleLocation: URI | undefined; - readonly label: string | undefined; -} - -export class WebWorkerDescriptor implements IWebWorkerDescriptor { - constructor( - public readonly esmModuleLocation: URI, - public readonly label: string | undefined, - ) { } -} - -export function createWebWorker(esmModuleLocation: URI, label: string | undefined): IWebWorkerClient; -export function createWebWorker(workerDescriptor: IWebWorkerDescriptor | Worker | Promise): IWebWorkerClient; -export function createWebWorker(arg0: URI | IWebWorkerDescriptor | Worker | Promise, arg1?: string | undefined): IWebWorkerClient { - const workerDescriptorOrWorker = (URI.isUri(arg0) ? new WebWorkerDescriptor(arg0, arg1) : arg0); - return new WebWorkerClient(new WebWorker(workerDescriptorOrWorker)); -} diff --git a/src/vs/base/common/actions.ts b/src/vs/base/common/actions.ts index 9660e763095..6d3e3f2b3db 100644 --- a/src/vs/base/common/actions.ts +++ b/src/vs/base/common/actions.ts @@ -228,7 +228,7 @@ export class Separator implements IAction { readonly tooltip: string = ''; readonly class: string = 'separator'; readonly enabled: boolean = false; - readonly checked: boolean = false; + readonly checked: undefined = undefined; async run() { } } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 287b1a227d1..a7b52b435bd 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -109,7 +109,16 @@ export function binarySearch2(length: number, compareToKey: (index: number) => n type Compare = (a: T, b: T) => number; - +/** + * Finds the nth smallest element in the array using quickselect algorithm. + * The data does not need to be sorted. + * + * @param nth The zero-based index of the element to find (0 = smallest, 1 = second smallest, etc.) + * @param data The unsorted array + * @param compare A comparator function that defines the sort order + * @returns The nth smallest element + * @throws TypeError if nth is >= data.length + */ export function quickSelect(nth: number, data: T[], compare: Compare): T { nth = nth | 0; @@ -193,8 +202,8 @@ export function forEachWithNeighbors(arr: T[], f: (before: T | undefined, ele } } -export function concatArrays(...arrays: TArr): TArr[number][number][] { - return ([] as any[]).concat(...arrays); +export function concatArrays(...arrays: T): T[number][number][] { + return [].concat(...arrays); } interface IMutableSplice extends ISplice { diff --git a/src/vs/base/common/assert.ts b/src/vs/base/common/assert.ts index 860c3e816d5..b8f47aefd66 100644 --- a/src/vs/base/common/assert.ts +++ b/src/vs/base/common/assert.ts @@ -29,6 +29,10 @@ export function assertNever(value: never, message = 'Unreachable'): never { throw new Error(message); } +export function softAssertNever(value: never): void { + // no-op +} + /** * Asserts that a condition is `truthy`. * diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 0d5163ceff1..3dcfa0c5130 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -2430,6 +2430,42 @@ export class AsyncIterableProducer implements AsyncIterable { }); } + public static tee(iterable: AsyncIterable): [AsyncIterableProducer, AsyncIterableProducer] { + let emitter1: AsyncIterableEmitter | undefined; + let emitter2: AsyncIterableEmitter | undefined; + + const defer = new DeferredPromise(); + + const start = async () => { + if (!emitter1 || !emitter2) { + return; // not yet ready + } + try { + for await (const item of iterable) { + emitter1.emitOne(item); + emitter2.emitOne(item); + } + } catch (err) { + emitter1.reject(err); + emitter2.reject(err); + } finally { + defer.complete(); + } + }; + + const p1 = new AsyncIterableProducer(async (emitter) => { + emitter1 = emitter; + start(); + return defer.p; + }); + const p2 = new AsyncIterableProducer(async (emitter) => { + emitter2 = emitter; + start(); + return defer.p; + }); + return [p1, p2]; + } + public map(mapFn: (item: T) => R): AsyncIterableProducer { return AsyncIterableProducer.map(this, mapFn); } diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 5de29e7d74f..b105bdfeb86 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -127,7 +127,7 @@ export class VSBuffer { return this.buffer.toString(); } else { if (!textDecoder) { - textDecoder = new TextDecoder(); + textDecoder = new TextDecoder(undefined, { ignoreBOM: true }); } return textDecoder.decode(this.buffer); } @@ -213,7 +213,7 @@ export function binaryIndexOf(haystack: Uint8Array, needle: Uint8Array, offset = } if (needleLen === 1) { - return haystack.indexOf(needle[0]); + return haystack.indexOf(needle[0], offset); } if (needleLen > haystackLen - offset) { diff --git a/src/vs/base/common/cancellation.ts b/src/vs/base/common/cancellation.ts index 3be4a90a103..e04c9277b85 100644 --- a/src/vs/base/common/cancellation.ts +++ b/src/vs/base/common/cancellation.ts @@ -21,10 +21,10 @@ export interface CancellationToken { * * @event */ - readonly onCancellationRequested: (listener: (e: any) => any, thisArgs?: any, disposables?: IDisposable[]) => IDisposable; + readonly onCancellationRequested: (listener: (e: void) => unknown, thisArgs?: unknown, disposables?: IDisposable[]) => IDisposable; } -const shortcutEvent: Event = Object.freeze(function (callback, context?): IDisposable { +const shortcutEvent: Event = Object.freeze(function (callback, context?): IDisposable { const handle = setTimeout(callback.bind(context), 0); return { dispose() { clearTimeout(handle); } }; }); @@ -60,7 +60,7 @@ export namespace CancellationToken { class MutableToken implements CancellationToken { private _isCancelled: boolean = false; - private _emitter: Emitter | null = null; + private _emitter: Emitter | null = null; public cancel() { if (!this._isCancelled) { @@ -76,12 +76,12 @@ class MutableToken implements CancellationToken { return this._isCancelled; } - get onCancellationRequested(): Event { + get onCancellationRequested(): Event { if (this._isCancelled) { return shortcutEvent; } if (!this._emitter) { - this._emitter = new Emitter(); + this._emitter = new Emitter(); } return this._emitter.event; } diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 1727e8ffe30..0ff9490a058 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -260,6 +260,7 @@ export const codiconsLibrary = { italic: register('italic', 0xeb0d), jersey: register('jersey', 0xeb0e), json: register('json', 0xeb0f), + bracket: register('bracket', 0xeb0f), kebabVertical: register('kebab-vertical', 0xeb10), key: register('key', 0xeb11), law: register('law', 0xeb12), @@ -486,7 +487,6 @@ export const codiconsLibrary = { graphLine: register('graph-line', 0xebe2), graphScatter: register('graph-scatter', 0xebe3), pieChart: register('pie-chart', 0xebe4), - bracket: register('bracket', 0xeb0f), bracketDot: register('bracket-dot', 0xebe5), bracketError: register('bracket-error', 0xebe6), lockSmall: register('lock-small', 0xebe7), @@ -617,7 +617,6 @@ export const codiconsLibrary = { cursor: register('cursor', 0xec5c), eraser: register('eraser', 0xec5d), fileText: register('file-text', 0xec5e), - gitLens: register('git-lens', 0xec5f), quotes: register('quotes', 0xec60), rename: register('rename', 0xec61), runWithDeps: register('run-with-deps', 0xec62), @@ -638,4 +637,21 @@ export const codiconsLibrary = { gitBranchDelete: register('git-branch-delete', 0xec6f), searchLarge: register('search-large', 0xec70), terminalGitBash: register('terminal-git-bash', 0xec71), + windowActive: register('window-active', 0xec72), + forward: register('forward', 0xec73), + download: register('download', 0xec74), + clockface: register('clockface', 0xec75), + unarchive: register('unarchive', 0xec76), + sessionInProgress: register('session-in-progress', 0xec77), + collectionSmall: register('collection-small', 0xec78), + vmSmall: register('vm-small', 0xec79), + cloudSmall: register('cloud-small', 0xec7a), + addSmall: register('add-small', 0xec7b), + removeSmall: register('remove-small', 0xec7c), + worktreeSmall: register('worktree-small', 0xec7d), + worktree: register('worktree', 0xec7e), + screenCut: register('screen-cut', 0xec7f), + ask: register('ask', 0xec80), + openai: register('openai', 0xec81), + claude: register('claude', 0xec82), } as const; diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index 845c5d9a2a9..f64ad848bf4 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -96,7 +96,7 @@ export function intersection(setA: Set, setB: Iterable): Set { } export class SetWithKey implements Set { - private _map = new Map(); + private _map = new Map(); constructor(values: T[], private toKey: (t: T) => unknown) { for (const value of values) { @@ -142,7 +142,7 @@ export class SetWithKey implements Set { this._map.clear(); } - forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: any): void { + forEach(callbackfn: (value: T, value2: T, set: Set) => void, thisArg?: unknown): void { this._map.forEach(entry => callbackfn.call(thisArg, entry, entry, this)); } diff --git a/src/vs/base/common/controlFlow.ts b/src/vs/base/common/controlFlow.ts index 35f860a222d..8fc6b19d3b1 100644 --- a/src/vs/base/common/controlFlow.ts +++ b/src/vs/base/common/controlFlow.ts @@ -53,9 +53,8 @@ export class ReentrancyBarrier { return this._isOccupied; } - public makeExclusiveOrSkip(fn: TFunction): TFunction { - // eslint-disable-next-line local/code-no-any-casts - return ((...args: any[]) => { + public makeExclusiveOrSkip(fn: (...args: TArgs) => void): (...args: TArgs) => void { + return ((...args: TArgs) => { if (this._isOccupied) { return; } @@ -65,6 +64,6 @@ export class ReentrancyBarrier { } finally { this._isOccupied = false; } - }) as any; + }); } } diff --git a/src/vs/base/common/date.ts b/src/vs/base/common/date.ts index 3a169c2bdf5..6562ab5a7d9 100644 --- a/src/vs/base/common/date.ts +++ b/src/vs/base/common/date.ts @@ -24,6 +24,10 @@ const year = day * 365; * is less than 30 seconds. */ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTimeWords?: boolean, disallowNow?: boolean): string { + if (typeof date === 'undefined') { + return localize('date.fromNow.unknown', 'unknown'); + } + if (typeof date !== 'number') { date = date.getTime(); } @@ -65,7 +69,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } if (seconds < hour) { - value = Math.floor(seconds / minute); + value = Math.round(seconds / minute); if (appendAgoLabel) { if (value === 1) { return useFullTimeWords @@ -90,7 +94,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } if (seconds < day) { - value = Math.floor(seconds / hour); + value = Math.round(seconds / hour); if (appendAgoLabel) { if (value === 1) { return useFullTimeWords @@ -115,7 +119,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } if (seconds < week) { - value = Math.floor(seconds / day); + value = Math.round(seconds / day); if (appendAgoLabel) { return value === 1 ? localize('date.fromNow.days.singular.ago', '{0} day ago', value) @@ -128,7 +132,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } if (seconds < month) { - value = Math.floor(seconds / week); + value = Math.round(seconds / week); if (appendAgoLabel) { if (value === 1) { return useFullTimeWords @@ -153,7 +157,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } if (seconds < year) { - value = Math.floor(seconds / month); + value = Math.round(seconds / month); if (appendAgoLabel) { if (value === 1) { return useFullTimeWords @@ -177,7 +181,7 @@ export function fromNow(date: number | Date, appendAgoLabel?: boolean, useFullTi } } - value = Math.floor(seconds / year); + value = Math.round(seconds / year); if (appendAgoLabel) { if (value === 1) { return useFullTimeWords diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts index 0310d1e50a2..352fa5e4b34 100644 --- a/src/vs/base/common/defaultAccount.ts +++ b/src/vs/base/common/defaultAccount.ts @@ -3,18 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export interface IDefaultAccount { - readonly sessionId: string; - readonly enterprise: boolean; - readonly access_type_sku?: string; - readonly assigned_date?: string; - readonly can_signup_for_limited?: boolean; - readonly chat_enabled?: boolean; - readonly chat_preview_features_enabled?: boolean; - readonly mcp?: boolean; - readonly mcpRegistryUrl?: string; - readonly mcpAccess?: 'allow_all' | 'registry_only'; - readonly analytics_tracking_id?: string; +export interface IQuotaSnapshotData { + readonly entitlement: number; + readonly overage_count: number; + readonly overage_permitted: boolean; + readonly percent_remaining: number; + readonly remaining: number; + readonly unlimited: boolean; +} + +export interface ILegacyQuotaSnapshotData { readonly limited_user_quotas?: { readonly chat: number; readonly completions: number; @@ -23,6 +21,42 @@ export interface IDefaultAccount { readonly chat: number; readonly completions: number; }; - readonly limited_user_reset_date?: string; +} + +export interface IEntitlementsData extends ILegacyQuotaSnapshotData { + readonly access_type_sku: string; + readonly assigned_date: string; + readonly can_signup_for_limited: boolean; + readonly copilot_plan: string; + readonly organization_login_list: string[]; + readonly analytics_tracking_id: string; + readonly limited_user_reset_date?: string; // for Copilot Free + readonly quota_reset_date?: string; // for all other Copilot SKUs + readonly quota_reset_date_utc?: string; // for all other Copilot SKUs (includes time) + readonly quota_snapshots?: { + chat?: IQuotaSnapshotData; + completions?: IQuotaSnapshotData; + premium_interactions?: IQuotaSnapshotData; + }; +} + +export interface IPolicyData { + readonly mcp?: boolean; + readonly chat_preview_features_enabled?: boolean; readonly chat_agent_enabled?: boolean; + readonly mcpRegistryUrl?: string; + readonly mcpAccess?: 'allow_all' | 'registry_only'; +} + +export interface IDefaultAccountAuthenticationProvider { + readonly id: string; + readonly name: string; + readonly enterprise: boolean; +} + +export interface IDefaultAccount { + readonly authenticationProvider: IDefaultAccountAuthenticationProvider; + readonly sessionId: string; + readonly enterprise: boolean; + readonly entitlementsData?: IEntitlementsData | null; } diff --git a/src/vs/base/common/equals.ts b/src/vs/base/common/equals.ts index df2db9256f1..495d43d066b 100644 --- a/src/vs/base/common/equals.ts +++ b/src/vs/base/common/equals.ts @@ -5,59 +5,46 @@ import * as arrays from './arrays.js'; +/* + * Each function in this file which offers an equality comparison, has an accompanying + * `*C` variant which returns an EqualityComparer function. + * + * The `*C` variant allows for easier composition of equality comparers and improved type-inference. +*/ + + +/** Represents a function that decides if two values are equal. */ export type EqualityComparer = (a: T, b: T) => boolean; +export interface IEquatable { + equals(other: T): boolean; +} + /** * Compares two items for equality using strict equality. */ -export const strictEquals: EqualityComparer = (a, b) => a === b; - -/** - * Checks if the items of two arrays are equal. - * By default, strict equality is used to compare elements, but a custom equality comparer can be provided. - */ -export function itemsEquals(itemEquals: EqualityComparer = strictEquals): EqualityComparer { - return (a, b) => arrays.equals(a, b, itemEquals); +export function strictEquals(a: T, b: T): boolean { + return a === b; } -/** - * Two items are considered equal, if their stringified representations are equal. -*/ -export function jsonStringifyEquals(): EqualityComparer { - return (a, b) => JSON.stringify(a) === JSON.stringify(b); +export function strictEqualsC(): EqualityComparer { + return (a, b) => a === b; } /** - * Uses `item.equals(other)` to determine equality. + * Checks if the items of two arrays are equal. + * By default, strict equality is used to compare elements, but a custom equality comparer can be provided. */ -export function itemEquals(): EqualityComparer { - return (a, b) => a.equals(b); +export function arrayEquals(a: readonly T[], b: readonly T[], itemEquals?: EqualityComparer): boolean { + return arrays.equals(a, b, itemEquals ?? strictEquals); } /** - * Checks if two items are both null or undefined, or are equal according to the provided equality comparer. -*/ -export function equalsIfDefined(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer): boolean; -/** - * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer. -*/ -export function equalsIfDefined(equals: EqualityComparer): EqualityComparer; -export function equalsIfDefined(equalsOrV1: EqualityComparer | T, v2?: T | undefined | null, equals?: EqualityComparer): EqualityComparer | boolean { - if (equals !== undefined) { - const v1 = equalsOrV1 as T | undefined; - if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { - return v2 === v1; - } - return equals(v1, v2); - } else { - const equals = equalsOrV1 as EqualityComparer; - return (v1, v2) => { - if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { - return v2 === v1; - } - return equals(v1, v2); - }; - } + * Checks if the items of two arrays are equal. + * By default, strict equality is used to compare elements, but a custom equality comparer can be provided. + */ +export function arrayEqualsC(itemEquals?: EqualityComparer): EqualityComparer { + return (a, b) => arrays.equals(a, b, itemEquals ?? strictEquals); } /** @@ -108,6 +95,10 @@ export function structuralEquals(a: T, b: T): boolean { return false; } +export function structuralEqualsC(): EqualityComparer { + return (a, b) => structuralEquals(a, b); +} + /** * `getStructuralKey(a) === getStructuralKey(b) <=> structuralEquals(a, b)` * (assuming that a and b are not cyclic structures and nothing extends globalThis Array). @@ -144,3 +135,72 @@ function toNormalizedJsonStructure(t: unknown): unknown { } return t; } + + +/** + * Two items are considered equal, if their stringified representations are equal. +*/ +export function jsonStringifyEquals(a: T, b: T): boolean { + return JSON.stringify(a) === JSON.stringify(b); +} + +/** + * Two items are considered equal, if their stringified representations are equal. +*/ +export function jsonStringifyEqualsC(): EqualityComparer { + return (a, b) => JSON.stringify(a) === JSON.stringify(b); +} + +/** + * Uses `item.equals(other)` to determine equality. + */ +export function thisEqualsC>(): EqualityComparer { + return (a, b) => a.equals(b); +} + +/** + * Checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefined(v1: T | undefined | null, v2: T | undefined | null, equals: EqualityComparer): boolean { + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); +} + +/** + * Returns an equality comparer that checks if two items are both null or undefined, or are equal according to the provided equality comparer. +*/ +export function equalsIfDefinedC(equals: EqualityComparer): EqualityComparer { + return (v1, v2) => { + if (v1 === undefined || v1 === null || v2 === undefined || v2 === null) { + return v2 === v1; + } + return equals(v1, v2); + }; +} + +/** + * Each function in this file which offers an equality comparison, has an accompanying + * `*C` variant which returns an EqualityComparer function. + * + * The `*C` variant allows for easier composition of equality comparers and improved type-inference. +*/ +export namespace equals { + export const strict = strictEquals; + export const strictC = strictEqualsC; + + export const array = arrayEquals; + export const arrayC = arrayEqualsC; + + export const structural = structuralEquals; + export const structuralC = structuralEqualsC; + + export const jsonStringify = jsonStringifyEquals; + export const jsonStringifyC = jsonStringifyEqualsC; + + export const thisC = thisEqualsC; + + export const ifDefined = equalsIfDefined; + export const ifDefinedC = equalsIfDefinedC; +} diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index fa3e81ab8d6..de0fce1d4fd 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -70,10 +70,13 @@ export namespace Event { * returned event causes this utility to leak a listener on the original event. * * @param event The event source for the new event. + * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not + * specified, some events could go missing. Use this if it's important that all events are processed, even if the + * listener gets disposed before the debounced event fires. * @param disposable A disposable store to add the new EventEmitter to. */ - export function defer(event: Event, disposable?: DisposableStore): Event { - return debounce(event, () => void 0, 0, undefined, true, undefined, disposable); + export function defer(event: Event, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event { + return debounce(event, () => void 0, 0, undefined, flushOnListenerRemove ?? true, undefined, disposable); } /** @@ -324,15 +327,105 @@ export namespace Event { * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the * returned event causes this utility to leak a listener on the original event. + * + * @param event The event source for the new event. + * @param delay The number of milliseconds to debounce. + * @param flushOnListenerRemove Whether to fire all debounced events when a listener is removed. If this is not + * specified, some events could go missing. Use this if it's important that all events are processed, even if the + * listener gets disposed before the debounced event fires. + * @param disposable A disposable store to add the new EventEmitter to. */ - export function accumulate(event: Event, delay: number | typeof MicrotaskDelay = 0, disposable?: DisposableStore): Event { + export function accumulate(event: Event, delay: number | typeof MicrotaskDelay = 0, flushOnListenerRemove?: boolean, disposable?: DisposableStore): Event { return Event.debounce(event, (last, e) => { if (!last) { return [e]; } last.push(e); return last; - }, delay, undefined, true, undefined, disposable); + }, delay, undefined, flushOnListenerRemove ?? true, undefined, disposable); + } + + /** + * Throttles an event, ensuring the event is fired at most once during the specified delay period. + * Unlike debounce, throttle will fire immediately on the leading edge and/or after the delay on the trailing edge. + * + * *NOTE* that this function returns an `Event` and it MUST be called with a `DisposableStore` whenever the returned + * event is accessible to "third parties", e.g the event is a public property. Otherwise a leaked listener on the + * returned event causes this utility to leak a listener on the original event. + * + * @param event The event source for the new event. + * @param merge An accumulator function that merges events if multiple occur during the throttle period. + * @param delay The number of milliseconds to throttle. + * @param leading Whether to fire on the leading edge (immediately on first event). + * @param trailing Whether to fire on the trailing edge (after delay with the last value). + * @param leakWarningThreshold See {@link EmitterOptions.leakWarningThreshold}. + * @param disposable A disposable store to register the throttle emitter to. + */ + export function throttle(event: Event, merge: (last: T | undefined, event: T) => T, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event; + export function throttle(event: Event, merge: (last: O | undefined, event: I) => O, delay?: number | typeof MicrotaskDelay, leading?: boolean, trailing?: boolean, leakWarningThreshold?: number, disposable?: DisposableStore): Event; + export function throttle(event: Event, merge: (last: O | undefined, event: I) => O, delay: number | typeof MicrotaskDelay = 100, leading = true, trailing = true, leakWarningThreshold?: number, disposable?: DisposableStore): Event { + let subscription: IDisposable; + let output: O | undefined = undefined; + let handle: Timeout | undefined = undefined; + let numThrottledCalls = 0; + + const options: EmitterOptions | undefined = { + leakWarningThreshold, + onWillAddFirstListener() { + subscription = event(cur => { + numThrottledCalls++; + output = merge(output, cur); + + // If not currently throttling, fire immediately if leading is enabled + if (handle === undefined) { + if (leading) { + emitter.fire(output); + output = undefined; + numThrottledCalls = 0; + } + + // Set up the throttle period + if (typeof delay === 'number') { + handle = setTimeout(() => { + // Fire on trailing edge if there were calls during throttle period + if (trailing && numThrottledCalls > 0) { + emitter.fire(output!); + } + output = undefined; + handle = undefined; + numThrottledCalls = 0; + }, delay); + } else { + // Use a special marker to indicate microtask is pending + handle = 0 as unknown as Timeout; + queueMicrotask(() => { + // Fire on trailing edge if there were calls during throttle period + if (trailing && numThrottledCalls > 0) { + emitter.fire(output!); + } + output = undefined; + handle = undefined; + numThrottledCalls = 0; + }); + } + } + // If already throttling, just accumulate the value for trailing edge + }); + }, + onDidRemoveLastListener() { + subscription.dispose(); + } + }; + + if (!disposable) { + _addLeakageTraceLogic(options); + } + + const emitter = new Emitter(options); + + disposable?.add(emitter); + + return emitter.event; } /** @@ -601,13 +694,22 @@ export namespace Event { */ export function toPromise(event: Event, disposables?: IDisposable[] | DisposableStore): CancelablePromise { let cancelRef: () => void; - const promise = new Promise((resolve, reject) => { - const listener = once(event)(resolve, null, disposables); + let listener: IDisposable; + const promise = new Promise((resolve) => { + listener = once(event)(resolve); + addToDisposables(listener, disposables); + // not resolved, matching the behavior of a normal disposal - cancelRef = () => listener.dispose(); + cancelRef = () => { + disposeAndRemove(listener, disposables); + }; }) as CancelablePromise; promise.cancel = cancelRef!; + if (disposables) { + promise.finally(() => disposeAndRemove(listener, disposables)); + } + return promise; } @@ -746,11 +848,7 @@ export namespace Event { } }; - if (disposables instanceof DisposableStore) { - disposables.add(disposable); - } else if (Array.isArray(disposables)) { - disposables.push(disposable); - } + addToDisposables(disposable, disposables); return disposable; }; @@ -1129,11 +1227,7 @@ export class Emitter { removeMonitor?.(); this._removeListener(contained); }); - if (disposables instanceof DisposableStore) { - disposables.add(result); - } else if (Array.isArray(disposables)) { - disposables.push(result); - } + addToDisposables(result, disposables); return result; }; @@ -1778,3 +1872,24 @@ export function trackSetChanges(getData: () => ReadonlySet, onDidChangeDat store.add(map); return store; } + + +function addToDisposables(result: IDisposable, disposables: DisposableStore | IDisposable[] | undefined) { + if (disposables instanceof DisposableStore) { + disposables.add(result); + } else if (Array.isArray(disposables)) { + disposables.push(result); + } +} + +function disposeAndRemove(result: IDisposable, disposables: DisposableStore | IDisposable[] | undefined) { + if (disposables instanceof DisposableStore) { + disposables.delete(result); + } else if (Array.isArray(disposables)) { + const index = disposables.indexOf(result); + if (index !== -1) { + disposables.splice(index, 1); + } + } + result.dispose(); +} diff --git a/src/vs/base/common/filters.ts b/src/vs/base/common/filters.ts index aa0b036ac76..fd159b40ab4 100644 --- a/src/vs/base/common/filters.ts +++ b/src/vs/base/common/filters.ts @@ -6,6 +6,7 @@ import { CharCode } from './charCode.js'; import { LRUCache } from './map.js'; import { getKoreanAltChars } from './naturalLanguage/korean.js'; +import { tryNormalizeToBase } from './normalization.js'; import * as strings from './strings.js'; export interface IFilter { @@ -65,6 +66,10 @@ function _matchesPrefix(ignoreCase: boolean, word: string, wordToMatchAgainst: s // Contiguous Substring export function matchesContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] | null { + if (word.length > wordToMatchAgainst.length) { + return null; + } + const index = wordToMatchAgainst.toLowerCase().indexOf(word.toLowerCase()); if (index === -1) { return null; @@ -73,9 +78,28 @@ export function matchesContiguousSubString(word: string, wordToMatchAgainst: str return [{ start: index, end: index + word.length }]; } +export function matchesBaseContiguousSubString(word: string, wordToMatchAgainst: string): IMatch[] | null { + if (word.length > wordToMatchAgainst.length) { + return null; + } + + word = tryNormalizeToBase(word); + wordToMatchAgainst = tryNormalizeToBase(wordToMatchAgainst); + const index = wordToMatchAgainst.indexOf(word); + if (index === -1) { + return null; + } + + return [{ start: index, end: index + word.length }]; +} + // Substring export function matchesSubString(word: string, wordToMatchAgainst: string): IMatch[] | null { + if (word.length > wordToMatchAgainst.length) { + return null; + } + return _matchesSubString(word.toLowerCase(), wordToMatchAgainst.toLowerCase(), 0, 0); } @@ -121,7 +145,7 @@ function isWhitespace(code: number): boolean { } const wordSeparators = new Set(); -// These are chosen as natural word separators based on writen text. +// These are chosen as natural word separators based on written text. // It is a subset of the word separators used by the monaco editor. '()[]{}<>`\'"-/;:,.?!' .split('') @@ -319,8 +343,8 @@ export function matchesWords(word: string, target: string, contiguous: boolean = let result: IMatch[] | null = null; let targetIndex = 0; - word = word.toLowerCase(); - target = target.toLowerCase(); + word = tryNormalizeToBase(word); + target = tryNormalizeToBase(target); while (targetIndex < target.length) { result = _matchesWords(word, target, 0, targetIndex, contiguous); if (result !== null) { diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index d11929f7aae..65f94a78032 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -322,7 +322,7 @@ function doScoreFuzzy2Multiple(target: string, query: IPreparedQueryPiece[], pat } function doScoreFuzzy2Single(target: string, query: IPreparedQueryPiece, patternStart: number, wordStart: number): FuzzyScore2 { - const score = fuzzyScore(query.original, query.originalLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true }); + const score = fuzzyScore(query.normalized, query.normalizedLowercase, patternStart, target, target.toLowerCase(), wordStart, { firstMatchCanBeWeak: true, boostFullMatch: true }); if (!score) { return NO_SCORE2; } @@ -811,7 +811,7 @@ export interface IPreparedQueryPiece { /** * In addition to the normalized path, will have - * whitespace and wildcards removed. + * whitespace, wildcards, quotes, ellipsis, and trailing hash characters removed. */ normalized: string; normalizedLowercase: string; @@ -905,7 +905,8 @@ function normalizeQuery(original: string): { pathNormalized: string; normalized: // - wildcards: are used for fuzzy matching // - whitespace: are used to separate queries // - ellipsis: sometimes used to indicate any path segments - const normalized = pathNormalized.replace(/[\*\u2026\s"]/g, ''); + // - trailing hash: used by some language servers (e.g. rust-analyzer) as query modifiers + const normalized = pathNormalized.replace(/[\*\u2026\s"]/g, '').replace(/(?<=.)#$/, ''); return { pathNormalized, diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index 7b983362ea5..f7fc624c5aa 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -5,9 +5,14 @@ import { IDisposable } from './lifecycle.js'; +let _isHotReloadEnabled = false; + +export function enableHotReload() { + _isHotReloadEnabled = true; +} + export function isHotReloadEnabled(): boolean { - // return env && !!env['VSCODE_DEV_DEBUG']; - return false; // TODO@hediet investigate how to get hot reload + return _isHotReloadEnabled; } export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable { if (!isHotReloadEnabled()) { diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index ecd9eb445c8..070279f045a 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -206,9 +206,13 @@ export function parseHrefAndDimensions(href: string): { href: string; dimensions return { href, dimensions }; } -export function markdownCommandLink(command: { title: string; id: string; arguments?: unknown[]; tooltip?: string }, escapeTokens = true): string { +export function createMarkdownLink(text: string, href: string, title?: string, escapeTokens = true): string { + return `[${escapeTokens ? escapeMarkdownSyntaxTokens(text) : text}](${href}${title ? ` "${escapeMarkdownSyntaxTokens(title)}"` : ''})`; +} + +export function createMarkdownCommandLink(command: { title: string; id: string; arguments?: unknown[]; tooltip?: string }, escapeTokens = true): string { const uri = createCommandUri(command.id, ...(command.arguments || [])).toString(); - return `[${escapeTokens ? escapeMarkdownSyntaxTokens(command.title) : command.title}](${uri}${command.tooltip ? ` "${escapeMarkdownSyntaxTokens(command.tooltip)}"` : ''})`; + return createMarkdownLink(command.title, uri, command.tooltip, escapeTokens); } export function createCommandUri(commandId: string, ...commandArgs: unknown[]): URI { diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 59d92ca3994..1e8c84d9af9 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -7,13 +7,13 @@ import { isIterable } from './types.js'; export namespace Iterable { - export function is(thing: unknown): thing is Iterable { + export function is(thing: unknown): thing is Iterable { return !!thing && typeof thing === 'object' && typeof (thing as Iterable)[Symbol.iterator] === 'function'; } - const _empty: Iterable = Object.freeze([]); - export function empty(): Iterable { - return _empty; + const _empty: Iterable = Object.freeze([]); + export function empty(): readonly never[] { + return _empty as readonly never[]; } export function* single(element: T): Iterable { @@ -29,7 +29,7 @@ export namespace Iterable { } export function from(iterable: Iterable | undefined | null): Iterable { - return iterable || _empty; + return iterable ?? (_empty as Iterable); } export function* reverse(array: ReadonlyArray): Iterable { diff --git a/src/vs/base/common/jsonSchema.ts b/src/vs/base/common/jsonSchema.ts index ac98a0ddd5f..3a674ccdd0f 100644 --- a/src/vs/base/common/jsonSchema.ts +++ b/src/vs/base/common/jsonSchema.ts @@ -87,6 +87,7 @@ export interface IJSONSchema { suggestSortText?: string; allowComments?: boolean; allowTrailingCommas?: boolean; + secret?: boolean; } export interface IJSONSchemaMap { diff --git a/src/vs/base/common/keyCodes.ts b/src/vs/base/common/keyCodes.ts index 9f1fd59fddc..14f5f4b5fcc 100644 --- a/src/vs/base/common/keyCodes.ts +++ b/src/vs/base/common/keyCodes.ts @@ -456,6 +456,7 @@ const userSettingsUSMap = new KeyCodeStrMap(); const userSettingsGeneralMap = new KeyCodeStrMap(); export const EVENT_KEY_CODE_MAP: { [keyCode: number]: KeyCode } = new Array(230); export const NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE: { [nativeKeyCode: string]: KeyCode } = {}; +export const SCAN_CODE_STR_TO_EVENT_KEY_CODE: { [scanCodeStr: string]: number } = {}; const scanCodeIntToStr: string[] = []; const scanCodeStrToInt: { [code: string]: number } = Object.create(null); const scanCodeLowerCaseStrToInt: { [code: string]: number } = Object.create(null); @@ -738,14 +739,7 @@ for (let i = 0; i <= KeyCode.MAX_VALUE; i++) { scanCodeLowerCaseStrToInt[scanCodeStr.toLowerCase()] = scanCode; if (immutable) { IMMUTABLE_CODE_TO_KEY_CODE[scanCode] = keyCode; - if ( - (keyCode !== KeyCode.Unknown) - && (keyCode !== KeyCode.Enter) - && (keyCode !== KeyCode.Ctrl) - && (keyCode !== KeyCode.Shift) - && (keyCode !== KeyCode.Alt) - && (keyCode !== KeyCode.Meta) - ) { + if ((keyCode !== KeyCode.Unknown) && (keyCode !== KeyCode.Enter) && !isModifierKey(keyCode)) { IMMUTABLE_KEY_CODE_TO_CODE[keyCode] = scanCode; } } @@ -762,6 +756,9 @@ for (let i = 0; i <= KeyCode.MAX_VALUE; i++) { if (eventKeyCode) { EVENT_KEY_CODE_MAP[eventKeyCode] = keyCode; } + if (scanCodeStr) { + SCAN_CODE_STR_TO_EVENT_KEY_CODE[scanCodeStr] = eventKeyCode; + } if (vkey) { NATIVE_WINDOWS_KEY_CODE_TO_KEY_CODE[vkey] = keyCode; } @@ -828,3 +825,12 @@ export function KeyChord(firstPart: number, secondPart: number): number { const chordPart = ((secondPart & 0x0000FFFF) << 16) >>> 0; return (firstPart | chordPart) >>> 0; } + +export function isModifierKey(keyCode: KeyCode): boolean { + return ( + keyCode === KeyCode.Ctrl + || keyCode === KeyCode.Shift + || keyCode === KeyCode.Alt + || keyCode === KeyCode.Meta + ); +} diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index f78aca05b53..c6ecaec1793 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -5,7 +5,8 @@ import { compareBy, numberComparator } from './arrays.js'; import { groupBy } from './collections.js'; -import { SetMap } from './map.js'; +import { SetMap, ResourceMap } from './map.js'; +import { URI } from './uri.js'; import { createSingleCallFunction } from './functional.js'; import { Iterable } from './iterator.js'; import { BugIndicatingError, onUnexpectedError } from './errors.js'; @@ -505,8 +506,7 @@ export class DisposableStore implements IDisposable { if (!o) { return; } - if (this._toDispose.has(o)) { - this._toDispose.delete(o); + if (this._toDispose.delete(o)) { setParentOfDisposable(o, null); } } @@ -756,10 +756,11 @@ export function disposeOnReturn(fn: (store: DisposableStore) => void): void { */ export class DisposableMap implements IDisposable { - private readonly _store = new Map(); + private readonly _store: Map; private _isDisposed = false; - constructor() { + constructor(store: Map = new Map()) { + this._store = store; trackDisposable(this); } @@ -879,3 +880,9 @@ export function thenRegisterOrDispose(promise: Promise return disposable; }); } + +export class DisposableResourceMap extends DisposableMap { + constructor() { + super(new ResourceMap()); + } +} diff --git a/src/vs/base/common/linkedList.ts b/src/vs/base/common/linkedList.ts index 42a1c2aad94..b436c611717 100644 --- a/src/vs/base/common/linkedList.ts +++ b/src/vs/base/common/linkedList.ts @@ -5,11 +5,11 @@ class Node { - static readonly Undefined = new Node(undefined); + static readonly Undefined = new Node(undefined); element: E; - next: Node; - prev: Node; + next: Node | typeof Node.Undefined; + prev: Node | typeof Node.Undefined; constructor(element: E) { this.element = element; @@ -20,8 +20,8 @@ class Node { export class LinkedList { - private _first: Node = Node.Undefined; - private _last: Node = Node.Undefined; + private _first: Node | typeof Node.Undefined = Node.Undefined; + private _last: Node | typeof Node.Undefined = Node.Undefined; private _size: number = 0; get size(): number { @@ -91,7 +91,7 @@ export class LinkedList { } else { const res = this._first.element; this._remove(this._first); - return res; + return res as E; } } @@ -101,11 +101,20 @@ export class LinkedList { } else { const res = this._last.element; this._remove(this._last); - return res; + return res as E; } } - private _remove(node: Node): void { + peek(): E | undefined { + if (this._last === Node.Undefined) { + return undefined; + } else { + const res = this._last.element; + return res as E; + } + } + + private _remove(node: Node | typeof Node.Undefined): void { if (node.prev !== Node.Undefined && node.next !== Node.Undefined) { // middle const anchor = node.prev; @@ -135,7 +144,7 @@ export class LinkedList { *[Symbol.iterator](): Iterator { let node = this._first; while (node !== Node.Undefined) { - yield node.element; + yield node.element as E; node = node.next; } } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 377e37e6ea1..622923e4450 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -116,7 +116,7 @@ export class ResourceMap implements Map { return this.map.delete(this.toKey(resource)); } - forEach(clb: (value: T, key: URI, map: Map) => void, thisArg?: any): void { + forEach(clb: (value: T, key: URI, map: Map) => void, thisArg?: object): void { if (typeof thisArg !== 'undefined') { clb = clb.bind(thisArg); } @@ -185,7 +185,7 @@ export class ResourceSet implements Set { return this._map.delete(value); } - forEach(callbackfn: (value: URI, value2: URI, set: Set) => void, thisArg?: any): void { + forEach(callbackfn: (value: URI, value2: URI, set: Set) => void, thisArg?: unknown): void { this._map.forEach((_value, key) => callbackfn.call(thisArg, key, key, this)); } @@ -340,7 +340,7 @@ export class LinkedMap implements Map { return item.value; } - forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: unknown): void { const state = this._state; let current = this._head; while (current) { @@ -789,7 +789,7 @@ export class BidirectionalMap { return true; } - forEach(callbackfn: (value: V, key: K, map: BidirectionalMap) => void, thisArg?: any): void { + forEach(callbackfn: (value: V, key: K, map: BidirectionalMap) => void, thisArg?: unknown): void { this._m1.forEach((value, key) => { callbackfn.call(thisArg, value, key, this); }); @@ -894,10 +894,12 @@ export class NKeyMap { public set(value: TValue, ...keys: [...TKeys]): void { let currentMap = this._data; for (let i = 0; i < keys.length - 1; i++) { - if (!currentMap.has(keys[i])) { - currentMap.set(keys[i], new Map()); + let nextMap = currentMap.get(keys[i]); + if (nextMap === undefined) { + nextMap = new Map(); + currentMap.set(keys[i], nextMap); } - currentMap = currentMap.get(keys[i]); + currentMap = nextMap; } currentMap.set(keys[keys.length - 1], value); } @@ -905,10 +907,11 @@ export class NKeyMap { public get(...keys: [...TKeys]): TValue | undefined { let currentMap = this._data; for (let i = 0; i < keys.length - 1; i++) { - if (!currentMap.has(keys[i])) { + const nextMap = currentMap.get(keys[i]); + if (nextMap === undefined) { return undefined; } - currentMap = currentMap.get(keys[i]); + currentMap = nextMap; } return currentMap.get(keys[keys.length - 1]); } diff --git a/src/vs/base/common/marked/marked.js b/src/vs/base/common/marked/marked.js index b7b6ecccd16..ea5462500bf 100644 --- a/src/vs/base/common/marked/marked.js +++ b/src/vs/base/common/marked/marked.js @@ -2479,4 +2479,3 @@ const parser = _Parser.parse; const lexer = _Lexer.lex; export { _Hooks as Hooks, _Lexer as Lexer, Marked, _Parser as Parser, _Renderer as Renderer, _TextRenderer as TextRenderer, _Tokenizer as Tokenizer, _defaults as defaults, _getDefaults as getDefaults, lexer, marked, options, parse, parseInline, parser, setOptions, use, walkTokens }; -//# sourceMappingURL=marked.esm.js.map diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index 4400c6246f3..730fbd61533 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -28,6 +28,6 @@ export const enum MarshalledId { LanguageModelThinkingPart, LanguageModelPromptTsxPart, LanguageModelDataPart, - ChatSessionContext, + AgentSessionContext, ChatResponsePullRequestPart, } diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 30d59bc0540..d5100e49152 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -16,10 +16,10 @@ export const Mimes = Object.freeze({ }); interface MapExtToMediaMimes { - [index: string]: string; + [index: string]: string | string[]; } -const mapExtToTextMimes: MapExtToMediaMimes = { +const mapExtToTextMimes: Record = { '.css': 'text/css', '.csv': 'text/csv', '.htm': 'text/html', @@ -39,9 +39,9 @@ const mapExtToMediaMimes: MapExtToMediaMimes = { '.flv': 'video/x-flv', '.gif': 'image/gif', '.ico': 'image/x-icon', - '.jpe': 'image/jpg', - '.jpeg': 'image/jpg', - '.jpg': 'image/jpg', + '.jpe': ['image/jpg', 'image/jpeg'], + '.jpeg': ['image/jpg', 'image/jpeg'], + '.jpg': ['image/jpg', 'image/jpeg'], '.m1v': 'video/mpeg', '.m2a': 'audio/mpeg', '.m2v': 'video/mpeg', @@ -96,12 +96,14 @@ export function getMediaOrTextMime(path: string): string | undefined { export function getMediaMime(path: string): string | undefined { const ext = extname(path); - return mapExtToMediaMimes[ext.toLowerCase()]; + const mimeType = mapExtToMediaMimes[ext.toLowerCase()]; + return Array.isArray(mimeType) ? mimeType[0] : mimeType; } export function getExtensionForMimeType(mimeType: string): string | undefined { for (const extension in mapExtToMediaMimes) { - if (mapExtToMediaMimes[extension] === mimeType) { + const value = mapExtToMediaMimes[extension]; + if (Array.isArray(value) ? value.includes(mimeType) : value === mimeType) { return extension; } } diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index 5a9ba7fd940..74cb106fd3c 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -100,6 +100,11 @@ export namespace Schemas { */ export const vscodeWebview = 'vscode-webview'; + /** + * Scheme used for integrated browser tabs using WebContentsView. + */ + export const vscodeBrowser = 'vscode-browser'; + /** * Scheme used for extension pages */ @@ -418,8 +423,7 @@ export namespace COI { * isn't enabled the current context */ export function addSearchParam(urlOrSearch: URLSearchParams | Record, coop: boolean, coep: boolean): void { - // eslint-disable-next-line local/code-no-any-casts - if (!(globalThis).crossOriginIsolated) { + if (!(globalThis as typeof globalThis & { crossOriginIsolated?: boolean }).crossOriginIsolated) { // depends on the current context being COI return; } diff --git a/src/vs/base/common/normalization.ts b/src/vs/base/common/normalization.ts index 1d4fbef7f72..8e426391512 100644 --- a/src/vs/base/common/normalization.ts +++ b/src/vs/base/common/normalization.ts @@ -39,11 +39,25 @@ function normalize(str: string, form: string, normalizedCache: LRUCache string = (function () { - // transform into NFD form and remove accents - // see: https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript/37511463#37511463 - const regex = /[\u0300-\u036f]/g; - return function (str: string) { - return normalizeNFD(str).replace(regex, ''); +/** + * Attempts to normalize the string to Unicode base format (NFD -> remove accents -> lower case). + * When original string contains accent characters directly, only lower casing will be performed. + * This is done so as to keep the string length the same and not affect indices. + * + * @see https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript/37511463#37511463 + */ +export const tryNormalizeToBase: (str: string) => string = function () { + const cache = new LRUCache(10000); // bounded to 10000 elements + const accentsRegex = /[\u0300-\u036f]/g; + return function (str: string): string { + const cached = cache.get(str); + if (cached) { + return cached; + } + + const noAccents = normalizeNFD(str).replace(accentsRegex, ''); + const result = (noAccents.length === str.length ? noAccents : str).toLowerCase(); + cache.set(str, result); + return result; }; -})(); +}(); diff --git a/src/vs/base/common/oauth.ts b/src/vs/base/common/oauth.ts index c808bf818b9..ea5a3cbbe35 100644 --- a/src/vs/base/common/oauth.ts +++ b/src/vs/base/common/oauth.ts @@ -1080,7 +1080,7 @@ export function scopesMatch(scopes1: readonly string[] | undefined, scopes2: rea interface CommonResponse { status: number; statusText: string; - json(): Promise; + json(): Promise; text(): Promise; } @@ -1105,14 +1105,14 @@ export interface IFetchResourceMetadataOptions { * @param targetResource The target resource URL to compare origins with (e.g., the MCP server URL) * @param resourceMetadataUrl Optional URL to fetch the resource metadata from. If not provided, will try well-known URIs. * @param options Configuration options for the fetch operation - * @returns Promise that resolves to the validated resource metadata - * @throws Error if the fetch fails, returns non-200 status, or the response is invalid + * @returns Promise that resolves to an object containing the validated resource metadata and any errors encountered during discovery + * @throws Error if the fetch fails, returns non-200 status, or the response is invalid on all attempted URLs */ export async function fetchResourceMetadata( targetResource: string, resourceMetadataUrl: string | undefined, options: IFetchResourceMetadataOptions = {} -): Promise { +): Promise<{ metadata: IAuthorizationProtectedResourceMetadata; discoveryUrl: string; errors: Error[] }> { const { sameOriginHeaders = {}, fetch: fetchImpl = fetch @@ -1120,73 +1120,79 @@ export async function fetchResourceMetadata( const targetResourceUrlObj = new URL(targetResource); - // If no resourceMetadataUrl is provided, try well-known URIs as per RFC 9728 - let urlsToTry: string[]; - if (!resourceMetadataUrl) { - // Try in order: 1) with path appended, 2) at root - const pathComponent = targetResourceUrlObj.pathname === '/' ? undefined : targetResourceUrlObj.pathname; - const rootUrl = `${targetResourceUrlObj.origin}${AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH}`; - if (pathComponent) { - // Only try both URLs if we have a path component - urlsToTry = [ - `${rootUrl}${pathComponent}`, - rootUrl - ]; + const fetchPrm = async (prmUrl: string, validateUrl: string) => { + // Determine if we should include same-origin headers + let headers: Record = { + 'Accept': 'application/json' + }; + + const resourceMetadataUrlObj = new URL(prmUrl); + if (resourceMetadataUrlObj.origin === targetResourceUrlObj.origin) { + headers = { + ...headers, + ...sameOriginHeaders + }; + } + + const response = await fetchImpl(prmUrl, { method: 'GET', headers }); + if (response.status !== 200) { + let errorText: string; + try { + errorText = await response.text(); + } catch { + errorText = response.statusText; + } + throw new Error(`Failed to fetch resource metadata from ${prmUrl}: ${response.status} ${errorText}`); + } + + const body = await response.json(); + if (isAuthorizationProtectedResourceMetadata(body)) { + // Validate that the resource matches the target resource + // Use URL constructor for normalization - it handles hostname case and trailing slashes + const prmValue = new URL(body.resource).toString(); + const expectedResource = new URL(validateUrl).toString(); + if (prmValue !== expectedResource) { + throw new Error(`Protected Resource Metadata 'resource' property value "${prmValue}" does not match expected value "${expectedResource}" for URL ${prmUrl}. Per RFC 9728, these MUST match. See https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`); + } + return body; } else { - // If target is already at root, only try the root URL once - urlsToTry = [rootUrl]; + throw new Error(`Invalid resource metadata from ${prmUrl}. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`); } - } else { - urlsToTry = [resourceMetadataUrl]; - } + }; const errors: Error[] = []; - for (const urlToTry of urlsToTry) { + if (resourceMetadataUrl) { try { - // Determine if we should include same-origin headers - let headers: Record = { - 'Accept': 'application/json' - }; - - const resourceMetadataUrlObj = new URL(urlToTry); - if (resourceMetadataUrlObj.origin === targetResourceUrlObj.origin) { - headers = { - ...headers, - ...sameOriginHeaders - }; - } + const metadata = await fetchPrm(resourceMetadataUrl, targetResource); + return { metadata, discoveryUrl: resourceMetadataUrl, errors }; + } catch (e) { + errors.push(e instanceof Error ? e : new Error(String(e))); + } + } - const response = await fetchImpl(urlToTry, { method: 'GET', headers }); - if (response.status !== 200) { - let errorText: string; - try { - errorText = await response.text(); - } catch { - errorText = response.statusText; - } - errors.push(new Error(`Failed to fetch resource metadata from ${urlToTry}: ${response.status} ${errorText}`)); - continue; - } + // Try well-known URIs starting with path-appended, then root + const hasPathComponent = targetResourceUrlObj.pathname !== '/'; + const rootUrl = `${targetResourceUrlObj.origin}${AUTH_PROTECTED_RESOURCE_METADATA_DISCOVERY_PATH}`; - const body = await response.json(); - if (isAuthorizationProtectedResourceMetadata(body)) { - // Use URL constructor for normalization - it handles hostname case and trailing slashes - const prmValue = new URL(body.resource).toString(); - const targetValue = targetResourceUrlObj.toString(); - if (prmValue !== targetValue) { - throw new Error(`Protected Resource Metadata resource property value "${prmValue}" (length: ${prmValue.length}) does not match target server url "${targetValue}" (length: ${targetValue.length}). These MUST match to follow OAuth spec https://datatracker.ietf.org/doc/html/rfc9728#PRConfigurationValidation`); - } - return body; - } else { - errors.push(new Error(`Invalid resource metadata from ${urlToTry}. Expected to follow shape of https://datatracker.ietf.org/doc/html/rfc9728#name-protected-resource-metadata (Hints: is scopes_supported an array? Is resource a string?). Current payload: ${JSON.stringify(body)}`)); - continue; - } + if (hasPathComponent) { + const pathAppendedUrl = `${rootUrl}${targetResourceUrlObj.pathname}`; + try { + const metadata = await fetchPrm(pathAppendedUrl, targetResource); + return { metadata, discoveryUrl: pathAppendedUrl, errors }; } catch (e) { errors.push(e instanceof Error ? e : new Error(String(e))); - continue; } } - // If we've tried all URLs and none worked, throw the error(s) + + // Finally, try root discovery + try { + const metadata = await fetchPrm(rootUrl, targetResourceUrlObj.origin); + return { metadata, discoveryUrl: rootUrl, errors }; + } catch (e) { + errors.push(e instanceof Error ? e : new Error(String(e))); + } + + // If we've tried all methods and none worked, throw the error(s) if (errors.length === 1) { throw errors[0]; } else { @@ -1255,7 +1261,7 @@ async function getErrText(res: CommonResponse): Promise { export async function fetchAuthorizationServerMetadata( authorizationServer: string, options: IFetchAuthorizationServerMetadataOptions = {} -): Promise { +): Promise<{ metadata: IAuthorizationServerMetadata; discoveryUrl: string; errors: Error[] }> { const { additionalHeaders = {}, fetch: fetchImpl = fetch @@ -1264,34 +1270,47 @@ export async function fetchAuthorizationServerMetadata( const authorizationServerUrl = new URL(authorizationServer); const extraPath = authorizationServerUrl.pathname === '/' ? '' : authorizationServerUrl.pathname; - const doFetch = async (url: string): Promise<{ metadata: IAuthorizationServerMetadata | undefined; rawResponse: CommonResponse }> => { - const rawResponse = await fetchImpl(url, { - method: 'GET', - headers: { - ...additionalHeaders, - 'Accept': 'application/json' + const errors: Error[] = []; + + const doFetch = async (url: string): Promise => { + try { + const rawResponse = await fetchImpl(url, { + method: 'GET', + headers: { + ...additionalHeaders, + 'Accept': 'application/json' + } + }); + const metadata = await tryParseAuthServerMetadata(rawResponse); + if (metadata) { + return metadata; } - }); - const metadata = await tryParseAuthServerMetadata(rawResponse); - return { metadata, rawResponse }; + // No metadata found, collect error from response + errors.push(new Error(`Failed to fetch authorization server metadata from ${url}: ${rawResponse.status} ${await getErrText(rawResponse)}`)); + return undefined; + } catch (e) { + // Collect error from fetch failure + errors.push(e instanceof Error ? e : new Error(String(e))); + return undefined; + } }; // For the oauth server metadata discovery path, we _INSERT_ // the well known path after the origin and before the path. // https://datatracker.ietf.org/doc/html/rfc8414#section-3 const pathToFetch = new URL(AUTH_SERVER_METADATA_DISCOVERY_PATH, authorizationServer).toString() + extraPath; - let result = await doFetch(pathToFetch); - if (result.metadata) { - return result.metadata; + let metadata = await doFetch(pathToFetch); + if (metadata) { + return { metadata, discoveryUrl: pathToFetch, errors }; } // Try fetching the OpenID Connect Discovery with path insertion. // For issuer URLs with path components, this inserts the well-known path // after the origin and before the path. const openidPathInsertionUrl = new URL(OPENID_CONNECT_DISCOVERY_PATH, authorizationServer).toString() + extraPath; - result = await doFetch(openidPathInsertionUrl); - if (result.metadata) { - return result.metadata; + metadata = await doFetch(openidPathInsertionUrl); + if (metadata) { + return { metadata, discoveryUrl: openidPathInsertionUrl, errors }; } // Try fetching the other discovery URL. For the openid metadata discovery @@ -1300,10 +1319,15 @@ export async function fetchAuthorizationServerMetadata( const openidPathAdditionUrl = authorizationServer.endsWith('/') ? authorizationServer + OPENID_CONNECT_DISCOVERY_PATH.substring(1) // Remove leading slash if authServer ends with slash : authorizationServer + OPENID_CONNECT_DISCOVERY_PATH; - result = await doFetch(openidPathAdditionUrl); - if (result.metadata) { - return result.metadata; + metadata = await doFetch(openidPathAdditionUrl); + if (metadata) { + return { metadata, discoveryUrl: openidPathAdditionUrl, errors }; } - throw new Error(`Failed to fetch authorization server metadata: ${result.rawResponse.status} ${await getErrText(result.rawResponse)}`); + // If we've tried all URLs and none worked, throw the error(s) + if (errors.length === 1) { + throw errors[0]; + } else { + throw new AggregateError(errors, 'Failed to fetch authorization server metadata from all attempted URLs'); + } } diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 69922fce403..7973ab63893 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -95,11 +95,6 @@ export interface IObservableWithChange { */ readonly debugName: string; - /** - * ONLY FOR DEBUGGING! - */ - debugGetDependencyGraph(): string; - /** * This property captures the type of the change object. Do not use it at runtime! */ @@ -153,6 +148,10 @@ export interface IObserver { handleChange(observable: IObservableWithChange, change: TChange): void; } +/** + * A reader allows code to track what it depends on, so the caller knows when the computed value or produced side-effect is no longer valid. + * Use `derived(reader => ...)` to turn code that needs a reader into an observable value. +*/ export interface IReader { /** * Reads the value of an observable and subscribes to it. diff --git a/src/vs/base/common/observableInternal/debugLocation.ts b/src/vs/base/common/observableInternal/debugLocation.ts index 43da5b908a2..a0e0d07676f 100644 --- a/src/vs/base/common/observableInternal/debugLocation.ts +++ b/src/vs/base/common/observableInternal/debugLocation.ts @@ -16,8 +16,7 @@ export namespace DebugLocation { if (!enabled) { return undefined; } - // eslint-disable-next-line local/code-no-any-casts - const Err = Error as any as { stackTraceLimit: number }; // For the monaco editor checks, which don't have the nodejs types. + const Err = Error as ErrorConstructor & { stackTraceLimit: number }; const l = Err.stackTraceLimit; Err.stackTraceLimit = 3; diff --git a/src/vs/base/common/observableInternal/debugName.ts b/src/vs/base/common/observableInternal/debugName.ts index d5174f75ab4..a6ce5b71450 100644 --- a/src/vs/base/common/observableInternal/debugName.ts +++ b/src/vs/base/common/observableInternal/debugName.ts @@ -103,8 +103,7 @@ function computeDebugName(self: object, data: DebugNameData): string | undefined function findKey(obj: object, value: object): string | undefined { for (const key in obj) { - // eslint-disable-next-line local/code-no-any-casts - if ((obj as any)[key] === value) { + if ((obj as Record)[key] === value) { return key; } } diff --git a/src/vs/base/common/observableInternal/experimental/time.ts b/src/vs/base/common/observableInternal/experimental/time.ts new file mode 100644 index 00000000000..af167bb8667 --- /dev/null +++ b/src/vs/base/common/observableInternal/experimental/time.ts @@ -0,0 +1,118 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../lifecycle.js'; +import { IObservable } from '../base.js'; +import { DisposableStore, IDisposable, toDisposable } from '../commonFacade/deps.js'; +import { observableValue } from '../observables/observableValue.js'; +import { autorun } from '../reactions/autorun.js'; + +/** Measures the total time an observable had the value "true". */ +export class TotalTrueTimeObservable extends Disposable { + private _totalTime = 0; + private _startTime: number | undefined = undefined; + + constructor( + private readonly value: IObservable, + ) { + super(); + this._register(autorun(reader => { + const isTrue = this.value.read(reader); + if (isTrue) { + this._startTime = Date.now(); + } else { + if (this._startTime !== undefined) { + const delta = Date.now() - this._startTime; + this._totalTime += delta; + this._startTime = undefined; + } + } + })); + } + + /** + * Reports the total time the observable has been true in milliseconds. + * E.g. `true` for 100ms, then `false` for 50ms, then `true` for 200ms results in 300ms. + */ + public totalTimeMs(): number { + if (this._startTime !== undefined) { + return this._totalTime + (Date.now() - this._startTime); + } + return this._totalTime; + } + + /** + * Runs the callback when the total time the observable has been true increased by the given delta in milliseconds. + */ + public fireWhenTimeIncreasedBy(deltaTimeMs: number, callback: () => void): IDisposable { + const store = new DisposableStore(); + let accumulatedTime = 0; + let startTime: number | undefined = undefined; + + store.add(autorun(reader => { + const isTrue = this.value.read(reader); + + if (isTrue) { + startTime = Date.now(); + const remainingTime = deltaTimeMs - accumulatedTime; + + if (remainingTime <= 0) { + callback(); + store.dispose(); + return; + } + + const handle = setTimeout(() => { + accumulatedTime += (Date.now() - startTime!); + startTime = undefined; + callback(); + store.dispose(); + }, remainingTime); + + reader.store.add(toDisposable(() => { + clearTimeout(handle); + if (startTime !== undefined) { + accumulatedTime += (Date.now() - startTime); + startTime = undefined; + } + })); + } + })); + + return store; + } +} + +/** + * Returns an observable that is true when the input observable was true within the last `timeMs` milliseconds. + */ +export function wasTrueRecently(obs: IObservable, timeMs: number, store: DisposableStore): IObservable { + const result = observableValue('wasTrueRecently', false); + let timeout: ReturnType | undefined; + + store.add(autorun(reader => { + const value = obs.read(reader); + if (value) { + result.set(true, undefined); + if (timeout !== undefined) { + clearTimeout(timeout); + timeout = undefined; + } + } else { + timeout = setTimeout(() => { + result.set(false, undefined); + timeout = undefined; + }, timeMs); + } + })); + + store.add(toDisposable(() => { + if (timeout !== undefined) { + clearTimeout(timeout); + } + })); + + return result; +} diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts index fd23c427331..af010d118c3 100644 --- a/src/vs/base/common/observableInternal/index.ts +++ b/src/vs/base/common/observableInternal/index.ts @@ -14,10 +14,11 @@ export { type IDerivedReader } from './observables/derivedImpl.js'; export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './utils/promise.js'; export { derivedWithCancellationToken, waitForState } from './utils/utilsCancellation.js'; export { - debouncedObservableDeprecated, debouncedObservable, derivedObservableWithCache, + debouncedObservable, debouncedObservable2, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, mapObservableArrayCached, observableFromPromise, recomputeInitiallyAndOnChange, signalFromObservable, wasEventTriggeredRecently, + isObservable, } from './utils/utils.js'; export { type DebugOwner } from './debugName.js'; export { type IChangeContext, type IChangeTracker, recordChanges, recordChangesLazy } from './changeTracker.js'; @@ -40,10 +41,10 @@ import { addLogger, setLogObservableFn } from './logging/logging.js'; import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js'; import { DevToolsLogger } from './logging/debugger/devToolsLogger.js'; import { env } from '../process.js'; -import { _setDebugGetDependencyGraph } from './observables/baseObservable.js'; -import { debugGetDependencyGraph } from './logging/debugGetDependencyGraph.js'; +import { _setDebugGetObservableGraph } from './observables/baseObservable.js'; +import { debugGetObservableGraph } from './logging/debugGetDependencyGraph.js'; -_setDebugGetDependencyGraph(debugGetDependencyGraph); +_setDebugGetObservableGraph(debugGetObservableGraph); setLogObservableFn(logObservableToConsole); // Remove "//" in the next line to enable logging diff --git a/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts b/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts index 88c9346d28c..9a13ba8840f 100644 --- a/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts +++ b/src/vs/base/common/observableInternal/logging/debugGetDependencyGraph.ts @@ -10,7 +10,12 @@ import { ObservableValue } from '../observables/observableValue.js'; import { AutorunObserver } from '../reactions/autorunImpl.js'; import { formatValue } from './consoleObservableLogger.js'; -export function debugGetDependencyGraph(obs: IObservable | IObserver, options?: { debugNamePostProcessor?: (name: string) => string }): string { +interface IOptions { + type: 'dependencies' | 'observers'; + debugNamePostProcessor?: (name: string) => string; +} + +export function debugGetObservableGraph(obs: IObservable | IObserver, options: IOptions): string { const debugNamePostProcessor = options?.debugNamePostProcessor ?? ((str: string) => str); const info = Info.from(obs, debugNamePostProcessor); if (!info) { @@ -18,10 +23,15 @@ export function debugGetDependencyGraph(obs: IObservable | IObserver, optio } const alreadyListed = new Set | IObserver>(); - return formatObservableInfo(info, 0, alreadyListed).trim(); + + if (options.type === 'observers') { + return formatObservableInfoWithObservers(info, 0, alreadyListed, options).trim(); + } else { + return formatObservableInfoWithDependencies(info, 0, alreadyListed, options).trim(); + } } -function formatObservableInfo(info: Info, indentLevel: number, alreadyListed: Set | IObserver>): string { +function formatObservableInfoWithDependencies(info: Info, indentLevel: number, alreadyListed: Set | IObserver>, options: IOptions): string { const indent = '\t\t'.repeat(indentLevel); const lines: string[] = []; @@ -40,7 +50,35 @@ function formatObservableInfo(info: Info, indentLevel: number, alreadyListed: Se if (info.dependencies.length > 0) { lines.push(`${indent} dependencies:`); for (const dep of info.dependencies) { - lines.push(formatObservableInfo(dep, indentLevel + 1, alreadyListed)); + const info = Info.from(dep, options.debugNamePostProcessor ?? (name => name)) ?? Info.unknown(dep); + lines.push(formatObservableInfoWithDependencies(info, indentLevel + 1, alreadyListed, options)); + } + } + + return lines.join('\n'); +} + +function formatObservableInfoWithObservers(info: Info, indentLevel: number, alreadyListed: Set | IObserver>, options: IOptions): string { + const indent = '\t\t'.repeat(indentLevel); + const lines: string[] = []; + + const isAlreadyListed = alreadyListed.has(info.sourceObj); + if (isAlreadyListed) { + lines.push(`${indent}* ${info.type} ${info.name} (already listed)`); + return lines.join('\n'); + } + + alreadyListed.add(info.sourceObj); + + lines.push(`${indent}* ${info.type} ${info.name}:`); + lines.push(`${indent} value: ${formatValue(info.value, 50)}`); + lines.push(`${indent} state: ${info.state}`); + + if (info.observers.length > 0) { + lines.push(`${indent} observers:`); + for (const observer of info.observers) { + const info = Info.from(observer, options.debugNamePostProcessor ?? (name => name)) ?? Info.unknown(observer); + lines.push(formatObservableInfoWithObservers(info, indentLevel + 1, alreadyListed, options)); } } @@ -57,7 +95,8 @@ class Info { 'autorun', undefined, state.stateStr, - Array.from(state.dependencies).map(dep => Info.from(dep, debugNamePostProcessor) || Info.unknown(dep)) + Array.from(state.dependencies), + [] ); } else if (obs instanceof Derived) { const state = obs.debugGetState(); @@ -67,7 +106,8 @@ class Info { 'derived', state.value, state.stateStr, - Array.from(state.dependencies).map(dep => Info.from(dep, debugNamePostProcessor) || Info.unknown(dep)) + Array.from(state.dependencies), + Array.from(obs.debugGetObservers()) ); } else if (obs instanceof ObservableValue) { const state = obs.debugGetState(); @@ -77,7 +117,8 @@ class Info { 'observableValue', state.value, 'upToDate', - [] + [], + Array.from(obs.debugGetObservers()) ); } else if (obs instanceof FromEventObservable) { const state = obs.debugGetState(); @@ -87,7 +128,8 @@ class Info { 'fromEvent', state.value, state.hasValue ? 'upToDate' : 'initial', - [] + [], + Array.from(obs.debugGetObservers()) ); } return undefined; @@ -100,6 +142,7 @@ class Info { 'unknown', undefined, 'unknown', + [], [] ); } @@ -110,6 +153,7 @@ class Info { public readonly type: string, public readonly value: any, public readonly state: string, - public readonly dependencies: Info[] + public readonly dependencies: (IObservable | IObserver)[], + public readonly observers: (IObservable | IObserver)[], ) { } } diff --git a/src/vs/base/common/observableInternal/map.ts b/src/vs/base/common/observableInternal/map.ts index 1db8c9ebb26..5cd028db280 100644 --- a/src/vs/base/common/observableInternal/map.ts +++ b/src/vs/base/common/observableInternal/map.ts @@ -51,7 +51,7 @@ export class ObservableMap implements Map { } } - forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: unknown): void { this._data.forEach((value, key, _map) => { callbackfn.call(thisArg, value, key, this); }); diff --git a/src/vs/base/common/observableInternal/observables/baseObservable.ts b/src/vs/base/common/observableInternal/observables/baseObservable.ts index 1fe2181695e..4903e1a6e57 100644 --- a/src/vs/base/common/observableInternal/observables/baseObservable.ts +++ b/src/vs/base/common/observableInternal/observables/baseObservable.ts @@ -7,7 +7,7 @@ import { IObservableWithChange, IObserver, IReader, IObservable } from '../base. import { DisposableStore } from '../commonFacade/deps.js'; import { DebugLocation } from '../debugLocation.js'; import { DebugOwner, getFunctionName } from '../debugName.js'; -import { debugGetDependencyGraph } from '../logging/debugGetDependencyGraph.js'; +import { debugGetObservableGraph } from '../logging/debugGetDependencyGraph.js'; import { getLogger, logObservable } from '../logging/logging.js'; import type { keepObserved, recomputeInitiallyAndOnChange } from '../utils/utils.js'; import { derivedOpts } from './derived.js'; @@ -31,9 +31,9 @@ export function _setKeepObserved(keepObserved: typeof _keepObserved) { _keepObserved = keepObserved; } -let _debugGetDependencyGraph: typeof debugGetDependencyGraph; -export function _setDebugGetDependencyGraph(debugGetDependencyGraph: typeof _debugGetDependencyGraph) { - _debugGetDependencyGraph = debugGetDependencyGraph; +let _debugGetObservableGraph: typeof debugGetObservableGraph; +export function _setDebugGetObservableGraph(debugGetObservableGraph: typeof _debugGetObservableGraph) { + _debugGetObservableGraph = debugGetObservableGraph; } export abstract class ConvenientObservable implements IObservableWithChange { @@ -128,8 +128,21 @@ export abstract class ConvenientObservable implements IObservableWit return this.get(); } - debugGetDependencyGraph(): string { - return _debugGetDependencyGraph(this); + get debug(): DebugHelper { + return new DebugHelper(this); + } +} + +class DebugHelper { + constructor(public readonly observable: IObservableWithChange) { + } + + getDependencyGraph(): string { + return _debugGetObservableGraph(this.observable, { type: 'dependencies' }); + } + + getObserverGraph(): string { + return _debugGetObservableGraph(this.observable, { type: 'observers' }); } } diff --git a/src/vs/base/common/observableInternal/observables/derivedImpl.ts b/src/vs/base/common/observableInternal/observables/derivedImpl.ts index e21e374f2d6..d7414fa2038 100644 --- a/src/vs/base/common/observableInternal/observables/derivedImpl.ts +++ b/src/vs/base/common/observableInternal/observables/derivedImpl.ts @@ -381,9 +381,7 @@ export class Derived extends BaseObserv super.addObserver(observer); if (shouldCallBeginUpdate) { - if (this._removedObserverToCallEndUpdateOn && this._removedObserverToCallEndUpdateOn.has(observer)) { - this._removedObserverToCallEndUpdateOn.delete(observer); - } else { + if (!this._removedObserverToCallEndUpdateOn?.delete(observer)) { observer.beginUpdate(this); } } @@ -416,10 +414,15 @@ export class Derived extends BaseObserv } public debugRecompute(): void { - if (!this._isComputing) { - this._recompute(); - } else { - this._state = DerivedState.stale; + this.beginUpdate(this); + try { + if (!this._isComputing) { + this._recompute(); + } else { + this._state = DerivedState.stale; + } + } finally { + this.endUpdate(this); } } diff --git a/src/vs/base/common/observableInternal/observables/observableFromEvent.ts b/src/vs/base/common/observableInternal/observables/observableFromEvent.ts index 3be9151158e..009387b39e3 100644 --- a/src/vs/base/common/observableInternal/observables/observableFromEvent.ts +++ b/src/vs/base/common/observableInternal/observables/observableFromEvent.ts @@ -48,6 +48,7 @@ export function observableFromEvent(...args: export function observableFromEventOpts( options: IDebugNameData & { equalsFn?: EqualityComparer; + getTransaction?: () => ITransaction | undefined; }, event: Event, getValue: (args: TArgs | undefined) => T, @@ -56,7 +57,10 @@ export function observableFromEventOpts( return new FromEventObservable( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue), event, - getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals, debugLocation + getValue, + () => options.getTransaction?.() ?? FromEventObservable.globalTransaction, + options.equalsFn ?? strictEquals, + debugLocation ); } diff --git a/src/vs/base/common/observableInternal/reactions/autorunImpl.ts b/src/vs/base/common/observableInternal/reactions/autorunImpl.ts index 90b265c2339..046bccfa54b 100644 --- a/src/vs/base/common/observableInternal/reactions/autorunImpl.ts +++ b/src/vs/base/common/observableInternal/reactions/autorunImpl.ts @@ -41,6 +41,7 @@ export class AutorunObserver implements IObserver, IReader private _dependenciesToBeRemoved = new Set>(); private _changeSummary: TChangeSummary | undefined; private _isRunning = false; + private _iteration = 0; public get debugName(): string { return this._debugNameData.getDebugName(this) ?? '(anonymous)'; @@ -136,6 +137,7 @@ export class AutorunObserver implements IObserver, IReader // IObserver implementation public beginUpdate(_observable: IObservable): void { if (this._state === AutorunState.upToDate) { + this._checkIterations(); this._state = AutorunState.dependenciesMightHaveChanged; } this._updateCount++; @@ -144,7 +146,11 @@ export class AutorunObserver implements IObserver, IReader public endUpdate(_observable: IObservable): void { try { if (this._updateCount === 1) { + this._iteration = 1; do { + if (this._checkIterations()) { + return; + } if (this._state === AutorunState.dependenciesMightHaveChanged) { this._state = AutorunState.upToDate; for (const d of this._dependencies) { @@ -156,6 +162,7 @@ export class AutorunObserver implements IObserver, IReader } } + this._iteration++; if (this._state !== AutorunState.upToDate) { this._run(); // Warning: indirect external call! } @@ -170,6 +177,7 @@ export class AutorunObserver implements IObserver, IReader public handlePossibleChange(observable: IObservable): void { if (this._state === AutorunState.upToDate && this._isDependency(observable)) { + this._checkIterations(); this._state = AutorunState.dependenciesMightHaveChanged; } } @@ -186,6 +194,7 @@ export class AutorunObserver implements IObserver, IReader didChange: (o): this is any => o === observable as any, }, this._changeSummary!) : true; if (shouldReact) { + this._checkIterations(); this._state = AutorunState.stale; } } catch (e) { @@ -262,4 +271,12 @@ export class AutorunObserver implements IObserver, IReader this._state = AutorunState.stale; } } + + private _checkIterations(): boolean { + if (this._iteration > 100) { + onBugIndicatingError(new BugIndicatingError(`Autorun '${this.debugName}' is stuck in an infinite update loop.`)); + return true; + } + return false; + } } diff --git a/src/vs/base/common/observableInternal/utils/utils.ts b/src/vs/base/common/observableInternal/utils/utils.ts index ed35204c7e3..a7dda800004 100644 --- a/src/vs/base/common/observableInternal/utils/utils.ts +++ b/src/vs/base/common/observableInternal/utils/utils.ts @@ -5,7 +5,6 @@ import { autorun } from '../reactions/autorun.js'; import { IObservable, IObservableWithChange, IObserver, IReader, ITransaction } from '../base.js'; -import { transaction } from '../transaction.js'; import { observableValue } from '../observables/observableValue.js'; import { DebugOwner } from '../debugName.js'; import { DisposableStore, Event, IDisposable, toDisposable } from '../commonFacade/deps.js'; @@ -13,6 +12,7 @@ import { derived, derivedOpts } from '../observables/derived.js'; import { observableFromEvent } from '../observables/observableFromEvent.js'; import { observableSignal } from '../observables/observableSignal.js'; import { _setKeepObserved, _setRecomputeInitiallyAndOnChange } from '../observables/baseObservable.js'; +import { DebugLocation } from '../debugLocation.js'; export function observableFromPromise(promise: Promise): IObservable<{ value?: T }> { const observable = observableValue<{ value?: T }>('promiseValue', {}); @@ -31,42 +31,16 @@ export function signalFromObservable(owner: DebugOwner | undefined, observabl }); } -/** - * @deprecated Use `debouncedObservable` instead. - */ -export function debouncedObservableDeprecated(observable: IObservable, debounceMs: number, disposableStore: DisposableStore): IObservable { - const debouncedObservable = observableValue('debounced', undefined); - - let timeout: Timeout | undefined = undefined; - - disposableStore.add(autorun(reader => { - /** @description debounce */ - const value = observable.read(reader); - - if (timeout) { - clearTimeout(timeout); - } - timeout = setTimeout(() => { - transaction(tx => { - debouncedObservable.set(value, tx); - }); - }, debounceMs); - - })); - - return debouncedObservable; -} - /** * Creates an observable that debounces the input observable. */ -export function debouncedObservable(observable: IObservable, debounceMs: number): IObservable { +export function debouncedObservable(observable: IObservable, debounceMs: number | ((lastValue: T | undefined, newValue: T) => number), debugLocation = DebugLocation.ofCaller()): IObservable { let hasValue = false; let lastValue: T | undefined; let timeout: Timeout | undefined = undefined; - return observableFromEvent(cb => { + return observableFromEvent(undefined, cb => { const d = autorun(reader => { const value = observable.read(reader); @@ -77,10 +51,16 @@ export function debouncedObservable(observable: IObservable, debounceMs: n if (timeout) { clearTimeout(timeout); } + const debounceDuration = typeof debounceMs === 'number' ? debounceMs : debounceMs(lastValue, value); + if (debounceDuration === 0) { + lastValue = value; + cb(); + return; + } timeout = setTimeout(() => { lastValue = value; cb(); - }, debounceMs); + }, debounceDuration); } }); return { @@ -96,7 +76,48 @@ export function debouncedObservable(observable: IObservable, debounceMs: n } else { return observable.get(); } - }); + }, debugLocation); +} + +/** + * Creates an observable that debounces the input observable. + */ +export function debouncedObservable2(observable: IObservable, debounceMs: number | ((currentValue: T | undefined, newValue: T) => number), debugLocation = DebugLocation.ofCaller()): IObservable { + const s = observableSignal('handleTimeout'); + + let currentValue: T | undefined = undefined; + let timeout: Timeout | undefined = undefined; + + const d = derivedOpts({ + owner: undefined, + onLastObserverRemoved: () => { + currentValue = undefined; + } + }, reader => { + const val = observable.read(reader); + s.read(reader); + + if (val !== currentValue) { + const debounceDuration = typeof debounceMs === 'number' ? debounceMs : debounceMs(currentValue, val); + + if (debounceDuration === 0) { + currentValue = val; + return val; + } + + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(() => { + currentValue = val; + s.trigger(undefined); + }, debounceDuration); + } + + return currentValue!; + }, debugLocation); + + return d; } export function wasEventTriggeredRecently(event: Event, timeoutMs: number, disposableStore: DisposableStore): IObservable { @@ -225,7 +246,8 @@ export function mapObservableArrayCached(owner: DebugOwne m = new ArrayMap(map); } }, (reader) => { - m.setItems(items.read(reader)); + const i = items.read(reader); + m.setItems(i); return m.getItems(); }); return self; @@ -277,3 +299,7 @@ class ArrayMap implements IDisposable { return this._items; } } + +export function isObservable(obj: unknown): obj is IObservable { + return !!obj && (>obj).read !== undefined && (>obj).reportChanges !== undefined; +} diff --git a/src/vs/base/common/paging.ts b/src/vs/base/common/paging.ts index 295600dac63..74123dd25b5 100644 --- a/src/vs/base/common/paging.ts +++ b/src/vs/base/common/paging.ts @@ -258,9 +258,7 @@ export class PageIteratorPager implements IPager { } return this.cachedPages[pageIndex]; } finally { - if (this.pendingRequests.has(pageIndex)) { - this.pendingRequests.delete(pageIndex); - } + this.pendingRequests.delete(pageIndex); } } diff --git a/src/vs/base/common/platform.ts b/src/vs/base/common/platform.ts index 10bb68dfd97..3013e09489b 100644 --- a/src/vs/base/common/platform.ts +++ b/src/vs/base/common/platform.ts @@ -275,10 +275,6 @@ export const isSafari = !!(!isChrome && (userAgent && userAgent.indexOf('Safari' export const isEdge = !!(userAgent && userAgent.indexOf('Edg/') >= 0); export const isAndroid = !!(userAgent && userAgent.indexOf('Android') >= 0); -export function isBigSurOrNewer(osVersion: string): boolean { - return parseFloat(osVersion) >= 20; -} - export function isTahoeOrNewer(osVersion: string): boolean { return parseFloat(osVersion) >= 25; } diff --git a/src/vs/base/common/policy.ts b/src/vs/base/common/policy.ts index 8141b0f9b5d..c27030fe03a 100644 --- a/src/vs/base/common/policy.ts +++ b/src/vs/base/common/policy.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../nls.js'; -import { IDefaultAccount } from './defaultAccount.js'; +import { IPolicyData } from './defaultAccount.js'; /** * System-wide policy file path for Linux systems. @@ -96,5 +96,5 @@ export interface IPolicy { * * If `undefined`, the feature's setting is not locked and can be overridden by other means. */ - readonly value?: (account: IDefaultAccount) => string | number | boolean | undefined; + readonly value?: (policyData: IPolicyData) => string | number | boolean | undefined; } diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 9529eb95910..11aba6dd528 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -43,6 +43,7 @@ export interface IChatSessionRecommendation { readonly displayName: string; readonly name: string; readonly description: string; + readonly postInstallCommand?: string; } export type ConfigurationSyncStore = { @@ -75,6 +76,7 @@ export interface IProductConfiguration { readonly win32AppUserModelId?: string; readonly win32MutexName?: string; readonly win32RegValueName?: string; + readonly win32VersionedUpdate?: boolean; readonly applicationName: string; readonly embedderIdentifier?: string; @@ -203,22 +205,11 @@ export interface IProductConfiguration { readonly hasPrereleaseVersion?: boolean; readonly excludeVersionRange?: string; }>; + readonly extensionsForceVersionByQuality?: readonly string[]; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; - readonly defaultAccount?: { - readonly authenticationProvider: { - readonly id: string; - readonly enterpriseProviderId: string; - readonly enterpriseProviderConfig: string; - readonly enterpriseProviderUriSetting: string; - readonly scopes: string[]; - }; - readonly tokenEntitlementUrl: string; - readonly chatEntitlementUrl: string; - readonly mcpRegistryDataUrl: string; - }; readonly authClientIdMetadataUrl?: string; readonly 'configurationSync.store'?: ConfigurationSyncStore; @@ -231,7 +222,7 @@ export interface IProductConfiguration { readonly commonlyUsedSettings?: string[]; readonly aiGeneratedWorkspaceTrust?: IAiGeneratedWorkspaceTrust; - readonly defaultChatAgent?: IDefaultChatAgent; + readonly defaultChatAgent: IDefaultChatAgent; readonly chatParticipantRegistry?: string; readonly chatSessionRecommendations?: IChatSessionRecommendation[]; readonly emergencyAlertUrl?: string; @@ -348,6 +339,8 @@ export interface IDefaultChatAgent { readonly extensionId: string; readonly chatExtensionId: string; + readonly chatExtensionOutputId: string; + readonly documentationUrl: string; readonly skusDocumentationUrl: string; readonly publicCodeMatchesUrl: string; @@ -371,6 +364,8 @@ export interface IDefaultChatAgent { readonly entitlementUrl: string; readonly entitlementSignupLimitedUrl: string; + readonly tokenEntitlementUrl: string; + readonly mcpRegistryDataUrl: string; readonly chatQuotaExceededContext: string; readonly completionsQuotaExceededContext: string; diff --git a/src/vs/base/common/resourceTree.ts b/src/vs/base/common/resourceTree.ts index c1a1c951bb2..5328c30b448 100644 --- a/src/vs/base/common/resourceTree.ts +++ b/src/vs/base/common/resourceTree.ts @@ -75,7 +75,7 @@ function collect(node: IResourceNode, result: T[]): T[] { return result; } -export class ResourceTree, C> { +export class ResourceTree, C> { readonly root: Node; diff --git a/src/vs/base/common/skipList.ts b/src/vs/base/common/skipList.ts deleted file mode 100644 index 295adb603fe..00000000000 --- a/src/vs/base/common/skipList.ts +++ /dev/null @@ -1,206 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - -class Node { - readonly forward: Node[]; - constructor(readonly level: number, readonly key: K, public value: V) { - this.forward = []; - } -} - -const NIL: undefined = undefined; - -interface Comparator { - (a: K, b: K): number; -} - -export class SkipList implements Map { - - readonly [Symbol.toStringTag] = 'SkipList'; - - private _maxLevel: number; - private _level: number = 0; - private _header: Node; - private _size: number = 0; - - /** - * - * @param capacity Capacity at which the list performs best - */ - constructor( - readonly comparator: (a: K, b: K) => number, - capacity: number = 2 ** 16 - ) { - this._maxLevel = Math.max(1, Math.log2(capacity) | 0); - // eslint-disable-next-line local/code-no-any-casts - this._header = new Node(this._maxLevel, NIL, NIL); - } - - get size(): number { - return this._size; - } - - clear(): void { - // eslint-disable-next-line local/code-no-any-casts - this._header = new Node(this._maxLevel, NIL, NIL); - this._size = 0; - } - - has(key: K): boolean { - return Boolean(SkipList._search(this, key, this.comparator)); - } - - get(key: K): V | undefined { - return SkipList._search(this, key, this.comparator)?.value; - } - - set(key: K, value: V): this { - if (SkipList._insert(this, key, value, this.comparator)) { - this._size += 1; - } - return this; - } - - delete(key: K): boolean { - const didDelete = SkipList._delete(this, key, this.comparator); - if (didDelete) { - this._size -= 1; - } - return didDelete; - } - - // --- iteration - - forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { - let node = this._header.forward[0]; - while (node) { - callbackfn.call(thisArg, node.value, node.key, this); - node = node.forward[0]; - } - } - - [Symbol.iterator](): IterableIterator<[K, V]> { - return this.entries(); - } - - *entries(): IterableIterator<[K, V]> { - let node = this._header.forward[0]; - while (node) { - yield [node.key, node.value]; - node = node.forward[0]; - } - } - - *keys(): IterableIterator { - let node = this._header.forward[0]; - while (node) { - yield node.key; - node = node.forward[0]; - } - } - - *values(): IterableIterator { - let node = this._header.forward[0]; - while (node) { - yield node.value; - node = node.forward[0]; - } - } - - toString(): string { - // debug string... - let result = '[SkipList]:'; - let node = this._header.forward[0]; - while (node) { - result += `node(${node.key}, ${node.value}, lvl:${node.level})`; - node = node.forward[0]; - } - return result; - } - - // from https://www.epaperpress.com/sortsearch/download/skiplist.pdf - - private static _search(list: SkipList, searchKey: K, comparator: Comparator) { - let x = list._header; - for (let i = list._level - 1; i >= 0; i--) { - while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { - x = x.forward[i]; - } - } - x = x.forward[0]; - if (x && comparator(x.key, searchKey) === 0) { - return x; - } - return undefined; - } - - private static _insert(list: SkipList, searchKey: K, value: V, comparator: Comparator) { - const update: Node[] = []; - let x = list._header; - for (let i = list._level - 1; i >= 0; i--) { - while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { - x = x.forward[i]; - } - update[i] = x; - } - x = x.forward[0]; - if (x && comparator(x.key, searchKey) === 0) { - // update - x.value = value; - return false; - } else { - // insert - const lvl = SkipList._randomLevel(list); - if (lvl > list._level) { - for (let i = list._level; i < lvl; i++) { - update[i] = list._header; - } - list._level = lvl; - } - x = new Node(lvl, searchKey, value); - for (let i = 0; i < lvl; i++) { - x.forward[i] = update[i].forward[i]; - update[i].forward[i] = x; - } - return true; - } - } - - private static _randomLevel(list: SkipList, p: number = 0.5): number { - let lvl = 1; - while (Math.random() < p && lvl < list._maxLevel) { - lvl += 1; - } - return lvl; - } - - private static _delete(list: SkipList, searchKey: K, comparator: Comparator) { - const update: Node[] = []; - let x = list._header; - for (let i = list._level - 1; i >= 0; i--) { - while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { - x = x.forward[i]; - } - update[i] = x; - } - x = x.forward[0]; - if (!x || comparator(x.key, searchKey) !== 0) { - // not found - return false; - } - for (let i = 0; i < list._level; i++) { - if (update[i].forward[i] !== x) { - break; - } - update[i].forward[i] = x.forward[i]; - } - while (list._level > 0 && list._header.forward[list._level - 1] === NIL) { - list._level -= 1; - } - return true; - } - -} diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 3d60229f8fa..146bd1d690f 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -23,6 +23,7 @@ const _formatRegexp = /{(\d+)}/g; * @param value string to which formatting is applied * @param args replacements for {n}-entries */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function format(value: string, ...args: any[]): string { if (args.length === 0) { return value; @@ -142,14 +143,16 @@ export function ltrim(haystack: string, needle: string): string { } const needleLen = needle.length; - if (needleLen === 0 || haystack.length === 0) { - return haystack; - } - let offset = 0; - - while (haystack.indexOf(needle, offset) === offset) { - offset = offset + needleLen; + if (needleLen === 1) { + const ch = needle.charCodeAt(0); + while (offset < haystack.length && haystack.charCodeAt(offset) === ch) { + offset++; + } + } else { + while (haystack.startsWith(needle, offset)) { + offset += needleLen; + } } return haystack.substring(offset); } @@ -167,22 +170,18 @@ export function rtrim(haystack: string, needle: string): string { const needleLen = needle.length, haystackLen = haystack.length; - if (needleLen === 0 || haystackLen === 0) { - return haystack; + if (needleLen === 1) { + let end = haystackLen; + const ch = needle.charCodeAt(0); + while (end > 0 && haystack.charCodeAt(end - 1) === ch) { + end--; + } + return haystack.substring(0, end); } - let offset = haystackLen, - idx = -1; - - while (true) { - idx = haystack.lastIndexOf(needle, offset - 1); - if (idx === -1 || idx + needleLen !== offset) { - break; - } - if (idx === 0) { - return ''; - } - offset = idx; + let offset = haystackLen; + while (offset > 0 && haystack.endsWith(needle, offset)) { + offset -= needleLen; } return haystack.substring(0, offset); @@ -322,7 +321,7 @@ export function getIndentationLength(str: string): number { * Function that works identically to String.prototype.replace, except, the * replace function is allowed to be async and return a Promise. */ -export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: any[]) => Promise): Promise { +export function replaceAsync(str: string, search: RegExp, replacer: (match: string, ...args: unknown[]) => Promise): Promise { const parts: (string | Promise)[] = []; let last = 0; @@ -731,12 +730,14 @@ export function isFullWidthCharacter(charCode: number): boolean { // FF00 - FFEF Halfwidth and Fullwidth Forms // [https://en.wikipedia.org/wiki/Halfwidth_and_fullwidth_forms] // of which FF01 - FF5E fullwidth ASCII of 21 to 7E + // and FFE0 - FFE6 fullwidth symbol variants // [IGNORE] and FF65 - FFDC halfwidth of Katakana and Hangul // [IGNORE] FFF0 - FFFF Specials return ( (charCode >= 0x2E80 && charCode <= 0xD7AF) || (charCode >= 0xF900 && charCode <= 0xFAFF) || (charCode >= 0xFF01 && charCode <= 0xFF5E) + || (charCode >= 0xFFE0 && charCode <= 0xFFE6) ); } @@ -784,6 +785,53 @@ export function lcut(text: string, n: number, prefix = ''): string { return prefix + trimmed.substring(i).trimStart(); } +/** + * Given a string and a max length returns a shortened version keeping the beginning. + * Shortening happens at favorable positions - such as whitespace or punctuation characters. + * Trailing whitespace is always trimmed. + */ +export function rcut(text: string, n: number, suffix = ''): string { + const trimmed = text.trimEnd(); + + if (trimmed.length <= n) { + return trimmed; + } + + const re = /\b/g; + let lastGoodBreak = 0; + let foundBoundaryAfterN = false; + while (re.test(trimmed)) { + if (re.lastIndex > n) { + foundBoundaryAfterN = true; + break; + } + lastGoodBreak = re.lastIndex; + re.lastIndex += 1; + } + + // If no boundary was found after n, return the full trimmed string + // (there's no good place to cut) + if (!foundBoundaryAfterN) { + return trimmed; + } + + // If the only boundary <= n is at position 0 (start of string), + // cutting there gives empty string, so just return the suffix + if (lastGoodBreak === 0) { + return suffix; + } + + const result = trimmed.substring(0, lastGoodBreak).trimEnd(); + + // If trimEnd removed more than half of what we cut (meaning we cut + // mostly through whitespace), return the full string instead + if (result.length < lastGoodBreak / 2) { + return trimmed; + } + + return result + suffix; +} + // Defacto standard: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html const CSI_SEQUENCE = /(?:\x1b\[|\x9b)[=?>!]?[\d;:]*["$#'* ]?[a-zA-Z@^`{}|~]/; const OSC_SEQUENCE = /(?:\x1b\]|\x9d).*?(?:\x1b\\|\x07|\x9c)/; @@ -1196,10 +1244,9 @@ export class AmbiguousCharacters { ); }); - private static readonly cache = new LRUCachedFunction< - string[], - AmbiguousCharacters - >({ getCacheKey: JSON.stringify }, (locales) => { + private static readonly cache = new LRUCachedFunction((localesStr) => { + const locales = localesStr.split(','); + function arrayToMap(arr: number[]): Map { const result = new Map(); for (let i = 0; i < arr.length; i += 2) { @@ -1256,8 +1303,8 @@ export class AmbiguousCharacters { return new AmbiguousCharacters(map); }); - public static getInstance(locales: Set): AmbiguousCharacters { - return AmbiguousCharacters.cache.get(Array.from(locales)); + public static getInstance(locales: Iterable): AmbiguousCharacters { + return AmbiguousCharacters.cache.get(Array.from(locales).join(',')); } private static _locales = new Lazy(() => diff --git a/src/vs/base/common/ternarySearchTree.ts b/src/vs/base/common/ternarySearchTree.ts index 0364b263f69..d184ed1765e 100644 --- a/src/vs/base/common/ternarySearchTree.ts +++ b/src/vs/base/common/ternarySearchTree.ts @@ -781,7 +781,7 @@ export class TernarySearchTree { // for debug/testing _isBalanced(): boolean { - const nodeIsBalanced = (node: TernarySearchTreeNode | undefined): boolean => { + const nodeIsBalanced = (node: TernarySearchTreeNode | undefined): boolean => { if (!node) { return true; } diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 1517e0ceb68..154d8199690 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -348,6 +348,13 @@ export type DeepImmutable = T extends (infer U)[] */ export type SingleOrMany = T | T[]; +/** + * Given a `type X = { foo?: string }` checking that an object `satisfies X` + * will ensure each property was explicitly defined, ensuring no properties + * are omitted or forgotten. + */ +export type WithDefinedProps = { [K in keyof Required]: T[K] }; + /** * A type that recursively makes all properties of `T` required @@ -372,7 +379,7 @@ export type PartialExcept = Partial> & Pick = T extends T ? keyof T : never; type FilterType = T extends TTest ? T : never; -type MakeOptionalAndBool = { [K in keyof T]?: boolean }; +type MakeOptionalAndTrue = { [K in keyof T]?: true }; /** * Type guard that checks if an object has specific keys and narrows the type accordingly. @@ -393,7 +400,7 @@ type MakeOptionalAndBool = { [K in keyof T]?: boolean }; * } * ``` */ -export function hasKey(x: T, key: TKeys & MakeOptionalAndBool): x is FilterType & keyof TKeys]: unknown }> { +export function hasKey>(x: T, key: TKeys): x is FilterType & keyof TKeys]: unknown }> { for (const k in key) { if (!(k in x)) { return false; diff --git a/src/vs/base/common/uriIpc.ts b/src/vs/base/common/uriIpc.ts index 2022176c1f3..67bf4c3428c 100644 --- a/src/vs/base/common/uriIpc.ts +++ b/src/vs/base/common/uriIpc.ts @@ -30,8 +30,7 @@ export interface IRawURITransformer { } function toJSON(uri: URI): UriComponents { - // eslint-disable-next-line local/code-no-any-casts - return uri.toJSON(); + return uri.toJSON(); } export class URITransformer implements IURITransformer { diff --git a/src/vs/base/common/worker/webWorker.ts b/src/vs/base/common/worker/webWorker.ts index 46856d37583..0ad11b838aa 100644 --- a/src/vs/base/common/worker/webWorker.ts +++ b/src/vs/base/common/worker/webWorker.ts @@ -243,19 +243,21 @@ class WebWorkerProtocol { } private _handleEventMessage(msg: EventMessage): void { - if (!this._pendingEmitters.has(msg.req)) { + const emitter = this._pendingEmitters.get(msg.req); + if (emitter === undefined) { console.warn('Got event for unknown req'); return; } - this._pendingEmitters.get(msg.req)!.fire(msg.event); + emitter.fire(msg.event); } private _handleUnsubscribeEventMessage(msg: UnsubscribeEventMessage): void { - if (!this._pendingEvents.has(msg.req)) { + const event = this._pendingEvents.get(msg.req); + if (event === undefined) { console.warn('Got unsubscribe for unknown req'); return; } - this._pendingEvents.get(msg.req)!.dispose(); + event.dispose(); this._pendingEvents.delete(msg.req); } @@ -399,11 +401,12 @@ export class WebWorkerClient extends Disposable implements IWe } public getChannel(channel: string): Proxied { - if (!this._remoteChannels.has(channel)) { - const inst = this._protocol.createProxyToRemoteChannel(channel, async () => { await this._onModuleLoaded; }); + let inst = this._remoteChannels.get(channel); + if (inst === undefined) { + inst = this._protocol.createProxyToRemoteChannel(channel, async () => { await this._onModuleLoaded; }); this._remoteChannels.set(channel, inst); } - return this._remoteChannels.get(channel) as Proxied; + return inst as Proxied; } private _onError(message: string, error?: unknown): void { @@ -511,11 +514,12 @@ export class WebWorkerServer implement } public getChannel(channel: string): Proxied { - if (!this._remoteChannels.has(channel)) { - const inst = this._protocol.createProxyToRemoteChannel(channel); + let inst = this._remoteChannels.get(channel); + if (inst === undefined) { + inst = this._protocol.createProxyToRemoteChannel(channel); this._remoteChannels.set(channel, inst); } - return this._remoteChannels.get(channel) as Proxied; + return inst as Proxied; } private async initialize(workerId: number): Promise { diff --git a/src/vs/base/node/osDisplayProtocolInfo.ts b/src/vs/base/node/osDisplayProtocolInfo.ts index 2dbc302e02a..41ed6b7eb0a 100644 --- a/src/vs/base/node/osDisplayProtocolInfo.ts +++ b/src/vs/base/node/osDisplayProtocolInfo.ts @@ -18,7 +18,7 @@ const enum DisplayProtocolType { Unknown = 'unknown' } -export async function getDisplayProtocol(errorLogger: (error: any) => void): Promise { +export async function getDisplayProtocol(errorLogger: (error: string | Error) => void): Promise { const xdgSessionType = env[XDG_SESSION_TYPE]; if (xdgSessionType) { diff --git a/src/vs/base/node/osReleaseInfo.ts b/src/vs/base/node/osReleaseInfo.ts index 8c34493531f..890bc254e16 100644 --- a/src/vs/base/node/osReleaseInfo.ts +++ b/src/vs/base/node/osReleaseInfo.ts @@ -13,7 +13,7 @@ type ReleaseInfo = { version_id?: string; }; -export async function getOSReleaseInfo(errorLogger: (error: any) => void): Promise { +export async function getOSReleaseInfo(errorLogger: (error: string | Error) => void): Promise { if (Platform.isMacintosh || Platform.isWindows) { return; } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index e13b27372fc..324c60d8452 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -107,9 +107,9 @@ async function readdir(path: string, options?: { withFileTypes: true }): Promise try { return await doReaddir(path, options); } catch (error) { - // TODO@bpasero workaround for #252361 that should be removed - // once the upstream issue in node.js is resolved. Adds a trailing - // dot to a root drive letter path (G:\ => G:\.) as a workaround. + // Workaround for #252361 that should be removed once the upstream issue + // in node.js is resolved. Adds a trailing dot to a root drive letter path + // (G:\ => G:\.) as a workaround. if (error.code === 'ENOENT' && isWindows && isRootOrDriveLetter(path)) { try { return await doReaddir(`${path}.`, options); @@ -129,7 +129,9 @@ async function safeReaddirWithFileTypes(path: string): Promise { try { return await fs.promises.readdir(path, { withFileTypes: true }); } catch (error) { - console.warn('[node.js fs] readdir with filetypes failed with error: ', error); + if (error.code !== 'ENOENT') { + console.warn('[node.js fs] readdir with filetypes failed with error: ', error); + } } // Fallback to manually reading and resolving each @@ -152,7 +154,9 @@ async function safeReaddirWithFileTypes(path: string): Promise { isDirectory = lstat.isDirectory(); isSymbolicLink = lstat.isSymbolicLink(); } catch (error) { - console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + if (error.code !== 'ENOENT') { + console.warn('[node.js fs] unexpected error from lstat after readdir: ', error); + } } result.push({ diff --git a/src/vs/base/node/ps.ts b/src/vs/base/node/ps.ts index 6079d798d8c..40fbfb442f6 100644 --- a/src/vs/base/node/ps.ts +++ b/src/vs/base/node/ps.ts @@ -9,19 +9,17 @@ import { FileAccess } from '../common/network.js'; import { ProcessItem } from '../common/processes.js'; import { isWindows } from '../common/platform.js'; -export function listProcesses(rootPid: number): Promise { +export const JS_FILENAME_PATTERN = /[a-zA-Z-]+\.js\b/g; +export function listProcesses(rootPid: number): Promise { return new Promise((resolve, reject) => { - let rootItem: ProcessItem | undefined; const map = new Map(); const totalMemory = totalmem(); function addToTree(pid: number, ppid: number, cmd: string, load: number, mem: number) { - const parent = map.get(ppid); if (pid === rootPid || parent) { - const item: ProcessItem = { name: findName(cmd), cmd, @@ -49,10 +47,8 @@ export function listProcesses(rootPid: number): Promise { } function findName(cmd: string): string { - const UTILITY_NETWORK_HINT = /--utility-sub-type=network/i; const WINDOWS_CRASH_REPORTER = /--crashes-directory/i; - const WINPTY = /\\pipe\\winpty-control/i; const CONPTY = /conhost\.exe.+--headless/i; const TYPE = /--type=([a-zA-Z-]+)/; @@ -61,11 +57,6 @@ export function listProcesses(rootPid: number): Promise { return 'electron-crash-reporter'; } - // find winpty process - if (WINPTY.exec(cmd)) { - return 'winpty-agent'; - } - // find conpty process if (CONPTY.exec(cmd)) { return 'conpty-agent'; @@ -88,19 +79,17 @@ export function listProcesses(rootPid: number): Promise { return matches[1]; } - // find all xxxx.js - const JS = /[a-zA-Z-]+\.js/g; - let result = ''; - do { - matches = JS.exec(cmd); - if (matches) { - result += matches + ' '; - } - } while (matches); + if (cmd.indexOf('node ') < 0 && cmd.indexOf('node.exe') < 0) { + let result = ''; // find all xyz.js + do { + matches = JS_FILENAME_PATTERN.exec(cmd); + if (matches) { + result += matches + ' '; + } + } while (matches); - if (result) { - if (cmd.indexOf('node ') < 0 && cmd.indexOf('node.exe') < 0) { - return `electron-nodejs (${result})`; + if (result) { + return `electron-nodejs (${result.trim()})`; } } @@ -108,7 +97,6 @@ export function listProcesses(rootPid: number): Promise { } if (process.platform === 'win32') { - const cleanUNCPrefix = (value: string): string => { if (value.indexOf('\\\\?\\') === 0) { return value.substring(4); @@ -167,8 +155,12 @@ export function listProcesses(rootPid: number): Promise { }); }, windowsProcessTree.ProcessDataFlag.CommandLine | windowsProcessTree.ProcessDataFlag.Memory); }); - } else { // OS X & Linux + } + + // OS X & Linux + else { function calculateLinuxCpuUsage() { + // Flatten rootItem to get a list of all VSCode processes let processes = [rootItem]; const pids: number[] = []; diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 59a4f9f1d99..b22e9fd9e42 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -682,6 +682,7 @@ export class ChannelClient implements IChannelClient, IDisposable { this.activeRequests.delete(emitter); this.sendRequest({ id, type: RequestType.EventDispose }); } + this.handlers.delete(id); } }); diff --git a/src/vs/base/parts/ipc/electron-main/ipcMain.ts b/src/vs/base/parts/ipc/electron-main/ipcMain.ts index ace40529015..0137b8924eb 100644 --- a/src/vs/base/parts/ipc/electron-main/ipcMain.ts +++ b/src/vs/base/parts/ipc/electron-main/ipcMain.ts @@ -128,6 +128,12 @@ class ValidatedIpcMain implements Event.NodeEventEmitter { return false; // unexpected URL } + if (process.env.VSCODE_DEV) { + if (url === process.env.DEV_WINDOW_SRC && (host === 'localhost' || host.startsWith('localhost:'))) { + return true; // development support where the window is served from localhost + } + } + if (host !== VSCODE_AUTHORITY) { onUnexpectedError(`Refused to handle ipcMain event for channel '${channel}' because of a bad origin of '${host}'.`); return false; // unexpected sender diff --git a/src/vs/base/parts/ipc/node/ipc.net.ts b/src/vs/base/parts/ipc/node/ipc.net.ts index e58f678f986..9a508286e23 100644 --- a/src/vs/base/parts/ipc/node/ipc.net.ts +++ b/src/vs/base/parts/ipc/node/ipc.net.ts @@ -22,10 +22,12 @@ export function upgradeToISocket(req: http.IncomingMessage, socket: Socket, { debugLabel, skipWebSocketFrames = false, disableWebSocketCompression = false, + enableMessageSplitting = true, }: { debugLabel: string; skipWebSocketFrames?: boolean; disableWebSocketCompression?: boolean; + enableMessageSplitting?: boolean; }): NodeSocket | WebSocketNodeSocket | undefined { if (req.headers.upgrade === undefined || req.headers.upgrade.toLowerCase() !== 'websocket') { socket.end('HTTP/1.1 400 Bad Request'); @@ -78,7 +80,7 @@ export function upgradeToISocket(req: http.IncomingMessage, socket: Socket, { if (skipWebSocketFrames) { return new NodeSocket(socket, debugLabel); } else { - return new WebSocketNodeSocket(new NodeSocket(socket, debugLabel), permessageDeflate, null, true); + return new WebSocketNodeSocket(new NodeSocket(socket, debugLabel), permessageDeflate, null, true, enableMessageSplitting); } } @@ -295,6 +297,7 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT private readonly _incomingData: ChunkStream; private readonly _onData = this._register(new Emitter()); private readonly _onClose = this._register(new Emitter()); + private readonly _maxSocketMessageLength: number; private _isEnded = false; private readonly _state = { @@ -331,9 +334,10 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT * @param inflateBytes "Seed" zlib inflate with these bytes. * @param recordInflateBytes Record all bytes sent to inflate */ - constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean) { + constructor(socket: NodeSocket, permessageDeflate: boolean, inflateBytes: VSBuffer | null, recordInflateBytes: boolean, enableMessageSplitting = true) { super(); this.socket = socket; + this._maxSocketMessageLength = enableMessageSplitting ? Constants.MaxWebSocketMessageLength : Infinity; this.traceSocketEvent(SocketDiagnosticsEventType.Created, { type: 'WebSocketNodeSocket', permessageDeflate, inflateBytesLength: inflateBytes?.byteLength || 0, recordInflateBytes }); this._flowManager = this._register(new WebSocketFlowManager( this, @@ -404,8 +408,8 @@ export class WebSocketNodeSocket extends Disposable implements ISocket, ISocketT let start = 0; while (start < buffer.byteLength) { - this._flowManager.writeMessage(buffer.slice(start, Math.min(start + Constants.MaxWebSocketMessageLength, buffer.byteLength)), { compressed: true, opcode: 0x02 /* Binary frame */ }); - start += Constants.MaxWebSocketMessageLength; + this._flowManager.writeMessage(buffer.slice(start, Math.min(start + this._maxSocketMessageLength, buffer.byteLength)), { compressed: true, opcode: 0x02 /* Binary frame */ }); + start += this._maxSocketMessageLength; } } diff --git a/src/vs/base/test/browser/actionbar.test.ts b/src/vs/base/test/browser/actionbar.test.ts index 7ec25b1bf11..60f9902bc3f 100644 --- a/src/vs/base/test/browser/actionbar.test.ts +++ b/src/vs/base/test/browser/actionbar.test.ts @@ -7,6 +7,8 @@ import assert from 'assert'; import { ActionBar, prepareActions } from '../../browser/ui/actionbar/actionbar.js'; import { Action, Separator } from '../../common/actions.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; +import { createToggleActionViewItemProvider, ToggleActionViewItem, unthemedToggleStyles } from '../../browser/ui/toggle/toggle.js'; +import { ActionViewItem } from '../../browser/ui/actionbar/actionViewItems.js'; suite('Actionbar', () => { @@ -60,4 +62,110 @@ suite('Actionbar', () => { actionbar.clear(); assert.strictEqual(actionbar.hasAction(a1), false); }); + + suite('ToggleActionViewItemProvider', () => { + + test('renders toggle for actions with checked state', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle', undefined, true, undefined)); + toggleAction.checked = true; + + actionbar.push(toggleAction); + + // Verify that the action was rendered as a toggle + assert.strictEqual(actionbar.viewItems.length, 1); + assert(actionbar.viewItems[0] instanceof ToggleActionViewItem, 'Action with checked state should render as ToggleActionViewItem'); + }); + + test('renders button for actions without checked state', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const buttonAction = store.add(new Action('button', 'Button')); + + actionbar.push(buttonAction); + + // Verify that the action was rendered as a regular button (ActionViewItem) + assert.strictEqual(actionbar.viewItems.length, 1); + assert(actionbar.viewItems[0] instanceof ActionViewItem, 'Action without checked state should render as ActionViewItem'); + assert(!(actionbar.viewItems[0] instanceof ToggleActionViewItem), 'Action without checked state should not render as ToggleActionViewItem'); + }); + + test('handles mixed actions (toggles and buttons)', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle')); + toggleAction.checked = false; + const buttonAction = store.add(new Action('button', 'Button')); + + actionbar.push([toggleAction, buttonAction]); + + // Verify that we have both types of items + assert.strictEqual(actionbar.viewItems.length, 2); + assert(actionbar.viewItems[0] instanceof ToggleActionViewItem, 'First action should be a toggle'); + assert(actionbar.viewItems[1] instanceof ActionViewItem, 'Second action should be a button'); + assert(!(actionbar.viewItems[1] instanceof ToggleActionViewItem), 'Second action should not be a toggle'); + }); + + test('toggle state changes when action checked changes', function () { + const container = document.createElement('div'); + const provider = createToggleActionViewItemProvider(unthemedToggleStyles); + const actionbar = store.add(new ActionBar(container, { + actionViewItemProvider: provider + })); + + const toggleAction = store.add(new Action('toggle', 'Toggle')); + toggleAction.checked = false; + + actionbar.push(toggleAction); + + // Verify the toggle view item was created + const toggleViewItem = actionbar.viewItems[0] as ToggleActionViewItem; + assert(toggleViewItem instanceof ToggleActionViewItem, 'Toggle view item should exist'); + + // Change the action's checked state + toggleAction.checked = true; + // The view item should reflect the updated checked state + assert.strictEqual(toggleAction.checked, true, 'Toggle action should update checked state'); + }); + + test('quick input button with toggle property creates action with checked state', async function () { + const { quickInputButtonToAction } = await import('../../../platform/quickinput/browser/quickInputUtils.js'); + + // Create a button with toggle property + const toggleButton = { + iconClass: 'test-icon', + tooltip: 'Toggle Button', + toggle: { checked: true } + }; + + const action = quickInputButtonToAction(toggleButton, 'test-id', () => { }); + + // Verify the action has checked property set + assert.strictEqual(action.checked, true, 'Action should have checked property set to true'); + + // Create a button without toggle property + const regularButton = { + iconClass: 'test-icon', + tooltip: 'Regular Button' + }; + + const regularAction = quickInputButtonToAction(regularButton, 'test-id-2', () => { }); + + // Verify the action doesn't have checked property + assert.strictEqual(regularAction.checked, undefined, 'Regular action should not have checked property'); + }); + }); }); diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index a7975897761..bf78a2afb13 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { $, h, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle } from '../../browser/dom.js'; +import { $, h, trackAttributes, copyAttributes, disposableWindowInterval, getWindows, getWindowsCount, getWindowId, getWindowById, hasWindow, getWindow, getDocument, isHTMLElement, SafeTriangle, AnimationFrameScheduler } from '../../browser/dom.js'; import { asCssValueWithDefault } from '../../../base/browser/cssValue.js'; import { ensureCodeWindow, isAuxiliaryWindow, mainWindow } from '../../browser/window.js'; import { DeferredPromise, timeout } from '../../common/async.js'; @@ -435,5 +435,102 @@ suite('dom', () => { }); }); + suite('AnimationFrameScheduler', () => { + // Helper to wait for an animation frame + const waitForAnimationFrame = () => new Promise(resolve => mainWindow.requestAnimationFrame(() => resolve())); + + test('schedules and runs the callback', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + assert.strictEqual(scheduler.isScheduled(), false); + scheduler.schedule(); + assert.strictEqual(scheduler.isScheduled(), true); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 1); + assert.strictEqual(scheduler.isScheduled(), false); + scheduler.dispose(); + }); + + test('coalesces multiple schedule calls', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + scheduler.schedule(); + scheduler.schedule(); + + assert.strictEqual(scheduler.isScheduled(), true); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 1); + scheduler.dispose(); + }); + + test('cancel prevents execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + assert.strictEqual(scheduler.isScheduled(), true); + scheduler.cancel(); + assert.strictEqual(scheduler.isScheduled(), false); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 0); + scheduler.dispose(); + }); + + test('dispose prevents execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + scheduler.dispose(); + + // Wait for the animation frame + await waitForAnimationFrame(); + + assert.strictEqual(callCount, 0); + }); + + test('can schedule again after execution', async () => { + const node = document.createElement('div'); + let callCount = 0; + const scheduler = new AnimationFrameScheduler(node, () => { + callCount++; + }); + + scheduler.schedule(); + await waitForAnimationFrame(); + assert.strictEqual(callCount, 1); + + scheduler.schedule(); + await waitForAnimationFrame(); + assert.strictEqual(callCount, 2); + + scheduler.dispose(); + }); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/browser/markdownRenderer.test.ts b/src/vs/base/test/browser/markdownRenderer.test.ts index 5a02b84c7dc..cdfbe914fa9 100644 --- a/src/vs/base/test/browser/markdownRenderer.test.ts +++ b/src/vs/base/test/browser/markdownRenderer.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import { fillInIncompleteTokens, renderMarkdown, renderAsPlaintext } from '../../browser/markdownRenderer.js'; import { IMarkdownString, MarkdownString } from '../../common/htmlContent.js'; diff --git a/src/vs/base/test/browser/ui/splitview/splitview.test.ts b/src/vs/base/test/browser/ui/splitview/splitview.test.ts index e470e11a3f1..76d7639b04c 100644 --- a/src/vs/base/test/browser/ui/splitview/splitview.test.ts +++ b/src/vs/base/test/browser/ui/splitview/splitview.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import { Sash, SashState } from '../../../../browser/ui/sash/sash.js'; import { IView, LayoutPriority, Sizing, SplitView } from '../../../../browser/ui/splitview/splitview.js'; diff --git a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts index 25840582aab..4eb5e2b7e28 100644 --- a/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/asyncDataTree.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../browser/ui/list/list.js'; import { AsyncDataTree, CompressibleAsyncDataTree, ITreeCompressionDelegate } from '../../../../browser/ui/tree/asyncDataTree.js'; diff --git a/src/vs/base/test/browser/ui/tree/objectTree.test.ts b/src/vs/base/test/browser/ui/tree/objectTree.test.ts index b3813240e60..aa11fbe6036 100644 --- a/src/vs/base/test/browser/ui/tree/objectTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/objectTree.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../browser/ui/list/list.js'; import { ICompressedTreeNode } from '../../../../browser/ui/tree/compressedObjectTreeModel.js'; diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 7c501389c2b..c011d89aa7c 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -2181,6 +2181,137 @@ suite('Async', () => { { id: 3, name: 'third' } ]); }); + + test('tee - both iterators receive all values', async () => { + // TODO: Implementation bug - executors don't await start(), causing producers to finalize early + async function* sourceGenerator() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + } + + const [iter1, iter2] = async.AsyncIterableProducer.tee(sourceGenerator()); + + const result1: number[] = []; + const result2: number[] = []; + + // Consume both iterables concurrently + await Promise.all([ + (async () => { + for await (const item of iter1) { + result1.push(item); + } + })(), + (async () => { + for await (const item of iter2) { + result2.push(item); + } + })() + ]); + + assert.deepStrictEqual(result1, [1, 2, 3, 4, 5]); + assert.deepStrictEqual(result2, [1, 2, 3, 4, 5]); + }); + + test('tee - sequential consumption', async () => { + // TODO: Implementation bug - executors don't await start(), causing producers to finalize early + const source = new async.AsyncIterableProducer(emitter => { + emitter.emitMany([1, 2, 3]); + }); + + const [iter1, iter2] = async.AsyncIterableProducer.tee(source); + + // Consume first iterator completely + const result1: number[] = []; + for await (const item of iter1) { + result1.push(item); + } + + // Then consume second iterator + const result2: number[] = []; + for await (const item of iter2) { + result2.push(item); + } + + assert.deepStrictEqual(result1, [1, 2, 3]); + assert.deepStrictEqual(result2, [1, 2, 3]); + }); + + test.skip('tee - empty source', async () => { + // TODO: Implementation bug - executors don't await start(), causing producers to finalize early + const source = new async.AsyncIterableProducer(emitter => { + // Emit nothing + }); + + const [iter1, iter2] = async.AsyncIterableProducer.tee(source); + + const result1: number[] = []; + const result2: number[] = []; + + await Promise.all([ + (async () => { + for await (const item of iter1) { + result1.push(item); + } + })(), + (async () => { + for await (const item of iter2) { + result2.push(item); + } + })() + ]); + + assert.deepStrictEqual(result1, []); + assert.deepStrictEqual(result2, []); + }); + + test.skip('tee - handles errors in source', async () => { + // TODO: Implementation bug - executors don't await start(), causing producers to finalize early + const expectedError = new Error('source error'); + const source = new async.AsyncIterableProducer(async emitter => { + emitter.emitOne(1); + emitter.emitOne(2); + throw expectedError; + }); + + const [iter1, iter2] = async.AsyncIterableProducer.tee(source); + + let error1: Error | undefined; + let error2: Error | undefined; + const result1: number[] = []; + const result2: number[] = []; + + await Promise.all([ + (async () => { + try { + for await (const item of iter1) { + result1.push(item); + } + } catch (e) { + error1 = e as Error; + } + })(), + (async () => { + try { + for await (const item of iter2) { + result2.push(item); + } + } catch (e) { + error2 = e as Error; + } + })() + ]); + + // Both iterators should have received the same values before error + assert.deepStrictEqual(result1, [1, 2]); + assert.deepStrictEqual(result2, [1, 2]); + + // Both should have received the error + assert.strictEqual(error1, expectedError); + assert.strictEqual(error2, expectedError); + }); }); suite('AsyncReader', () => { diff --git a/src/vs/base/test/common/buffer.test.ts b/src/vs/base/test/common/buffer.test.ts index f4d9c5a04e4..998d3dc8119 100644 --- a/src/vs/base/test/common/buffer.test.ts +++ b/src/vs/base/test/common/buffer.test.ts @@ -19,6 +19,20 @@ suite('Buffer', () => { assert.deepStrictEqual(buffer.toString(), 'hi'); }); + test('issue #251527 - VSBuffer#toString preserves BOM character in filenames', () => { + // BOM character (U+FEFF) is a zero-width character that was being stripped + // when deserializing messages in the IPC layer. This test verifies that + // the BOM character is preserved when using VSBuffer.toString(). + const bomChar = '\uFEFF'; + const filename = `${bomChar}c.txt`; + const buffer = VSBuffer.fromString(filename); + const result = buffer.toString(); + + // Verify the BOM character is preserved + assert.strictEqual(result, filename); + assert.strictEqual(result.charCodeAt(0), 0xFEFF); + }); + test('bufferToReadable / readableToBuffer', () => { const content = 'Hello World'; const readable = bufferToReadable(VSBuffer.fromString(content)); @@ -423,10 +437,12 @@ suite('Buffer', () => { assert.strictEqual(haystack.indexOf(VSBuffer.fromString('a')), 0); assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c')), 2); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('c'), 4), 7); assert.strictEqual(haystack.indexOf(VSBuffer.fromString('abcaa')), 0); assert.strictEqual(haystack.indexOf(VSBuffer.fromString('caaab')), 8); assert.strictEqual(haystack.indexOf(VSBuffer.fromString('ccc')), 15); + assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cc'), 9), 15); assert.strictEqual(haystack.indexOf(VSBuffer.fromString('cccb')), -1); }); diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 002bb687f41..4f0369b28b9 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -464,6 +464,111 @@ suite('Event', function () { }); }); + suite('Event.toPromise', () => { + class DisposableStoreWithSize extends DisposableStore { + public size = 0; + public override add(o: T): T { + this.size++; + return super.add(o); + } + + public override delete(o: T): void { + this.size--; + return super.delete(o); + } + } + test('resolves on first event', async () => { + const emitter = ds.add(new Emitter()); + const promise = Event.toPromise(emitter.event); + + emitter.fire(42); + const result = await promise; + + assert.strictEqual(result, 42); + }); + + test('disposes listener after resolution', async () => { + const emitter = ds.add(new Emitter()); + const promise = Event.toPromise(emitter.event); + + emitter.fire(1); + await promise; + + // Listener should be disposed, firing again should not affect anything + emitter.fire(2); + assert.ok(true); // No errors + }); + + test('adds to DisposableStore', async () => { + const emitter = ds.add(new Emitter()); + const store = ds.add(new DisposableStoreWithSize()); + const promise = Event.toPromise(emitter.event, store); + + assert.strictEqual(store.size, 1); + + emitter.fire(42); + await promise; + + // Should be removed from store after resolution + assert.strictEqual(store.size, 0); + }); + + test('adds to disposables array', async () => { + const emitter = ds.add(new Emitter()); + const disposables: IDisposable[] = []; + const promise = Event.toPromise(emitter.event, disposables); + + assert.strictEqual(disposables.length, 1); + + emitter.fire(42); + await promise; + + // Should be removed from array after resolution + assert.strictEqual(disposables.length, 0); + }); + + test('cancel removes from DisposableStore', () => { + const emitter = ds.add(new Emitter()); + const store = ds.add(new DisposableStoreWithSize()); + const promise = Event.toPromise(emitter.event, store); + + assert.strictEqual(store.size, 1); + + promise.cancel(); + + // Should be removed from store after cancellation + assert.strictEqual(store.size, 0); + }); + + test('cancel removes from disposables array', () => { + const emitter = ds.add(new Emitter()); + const disposables: IDisposable[] = []; + const promise = Event.toPromise(emitter.event, disposables); + + assert.strictEqual(disposables.length, 1); + + promise.cancel(); + + // Should be removed from array after cancellation + assert.strictEqual(disposables.length, 0); + }); + + test('cancel does not resolve promise', async () => { + const emitter = ds.add(new Emitter()); + const promise = Event.toPromise(emitter.event); + + promise.cancel(); + emitter.fire(42); + + // Promise should not resolve after cancellation + let resolved = false; + promise.then(() => resolved = true); + + await timeout(10); + assert.strictEqual(resolved, false); + }); + }); + test('Microtask Emitter', (done) => { let count = 0; assert.strictEqual(count, 0); @@ -1493,6 +1598,273 @@ suite('Event utils', () => { }); }); + suite('throttle', () => { + test('leading only', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, 10, /*leading=*/true, /*trailing=*/false); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // First event fires immediately + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // Subsequent events during throttle period are ignored + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, [1]); + + // Wait for throttle period to end + await timeout(15); + assert.deepStrictEqual(calls, [1], 'no trailing edge fire with trailing=false'); + + // After throttle period, next event fires immediately + emitter.fire(4); + assert.deepStrictEqual(calls, [1, 1]); + }); + }); + + test('trailing only', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, 10, /*leading=*/false, /*trailing=*/true); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // First event does not fire immediately + emitter.fire(1); + assert.deepStrictEqual(calls, []); + + // Multiple events during throttle period + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, []); + + // Wait for throttle period - should fire with accumulated value + await timeout(15); + assert.deepStrictEqual(calls, [3]); + + // New events start a new throttle period + emitter.fire(4); + emitter.fire(5); + assert.deepStrictEqual(calls, [3]); + + await timeout(15); + assert.deepStrictEqual(calls, [3, 2]); + }); + }); + + test('both leading and trailing', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, 10, /*leading=*/true, /*trailing=*/true); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // First event fires immediately (leading) + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // Events during throttle period are accumulated + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, [1]); + + // Wait for throttle period - should fire trailing edge with accumulated value + await timeout(15); + assert.deepStrictEqual(calls, [1, 2]); + }); + }); + + test('only leading edge if no subsequent events', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, 10, /*leading=*/true, /*trailing=*/true); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // Single event fires immediately (leading) + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // No more events during throttle period + await timeout(15); + // Should not fire trailing edge since there were no more events + assert.deepStrictEqual(calls, [1]); + }); + }); + + test('microtask delay', function (done: () => void) { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, MicrotaskDelay); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // First event fires immediately (leading by default) + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // Events during microtask + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, [1]); + + // Check after microtask + queueMicrotask(() => { + // Should have fired trailing edge + assert.deepStrictEqual(calls, [1, 2]); + done(); + }); + }); + + test('merge function accumulates values', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle( + emitter.event, + (last, cur) => (last || 0) + cur, + 10, + /*leading=*/true, + /*trailing=*/true + ); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // First event fires immediately with value 1 + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // Accumulate more values: 2 + 3 = 5 + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, [1]); + + await timeout(15); + // Trailing edge fires with accumulated sum + assert.deepStrictEqual(calls, [1, 5]); + }); + }); + + test('rapid consecutive throttle periods', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => e, 10, /*leading=*/true, /*trailing=*/true); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + // Period 1 + emitter.fire(1); + emitter.fire(2); + assert.deepStrictEqual(calls, [1]); + + await timeout(15); + assert.deepStrictEqual(calls, [1, 2]); + + // Period 2 + emitter.fire(3); + emitter.fire(4); + assert.deepStrictEqual(calls, [1, 2, 3]); + + await timeout(15); + assert.deepStrictEqual(calls, [1, 2, 3, 4]); + + // Period 3 + emitter.fire(5); + assert.deepStrictEqual(calls, [1, 2, 3, 4, 5]); + + await timeout(15); + // No trailing fire since only one event + assert.deepStrictEqual(calls, [1, 2, 3, 4, 5]); + }); + }); + + test('default parameters', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + // Default: delay=100, leading=true, trailing=true + const throttled = Event.throttle(emitter.event, (l, e) => e); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + emitter.fire(1); + assert.deepStrictEqual(calls, [1], 'should fire leading edge by default'); + + emitter.fire(2); + await timeout(110); + assert.deepStrictEqual(calls, [1, 2], 'should fire trailing edge by default'); + }); + }); + + test('disposal cleans up', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => e, 10); + + const calls: number[] = []; + const listener = throttled((e) => calls.push(e)); + + emitter.fire(1); + emitter.fire(2); + assert.deepStrictEqual(calls, [1]); + + listener.dispose(); + + // Events after disposal should not fire + await timeout(15); + emitter.fire(3); + assert.deepStrictEqual(calls, [1]); + }); + }); + + test('no events during throttle with trailing=false', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => l ? l + 1 : 1, 10, /*leading=*/true, /*trailing=*/false); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + emitter.fire(1); + assert.deepStrictEqual(calls, [1]); + + // No more events + await timeout(15); + assert.deepStrictEqual(calls, [1]); + + // Next event after throttle period + emitter.fire(2); + assert.deepStrictEqual(calls, [1, 1]); + }); + }); + + test('neither leading nor trailing', async function () { + return runWithFakedTimers({}, async function () { + const emitter = ds.add(new Emitter()); + const throttled = Event.throttle(emitter.event, (l, e) => e, 10, /*leading=*/false, /*trailing=*/false); + + const calls: number[] = []; + ds.add(throttled((e) => calls.push(e))); + + emitter.fire(1); + emitter.fire(2); + emitter.fire(3); + assert.deepStrictEqual(calls, []); + + await timeout(15); + assert.deepStrictEqual(calls, [], 'no events should fire with both leading and trailing false'); + }); + }); + }); + test('issue #230401', () => { let count = 0; const emitter = ds.add(new Emitter()); diff --git a/src/vs/base/test/common/filters.perf.data.d.ts b/src/vs/base/test/common/filters.perf.data.d.ts index b2ef6866955..3393c4f5a02 100644 --- a/src/vs/base/test/common/filters.perf.data.d.ts +++ b/src/vs/base/test/common/filters.perf.data.d.ts @@ -2,4 +2,4 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export const data: string[]; \ No newline at end of file +export declare const data: string[]; diff --git a/src/vs/base/test/common/filters.test.ts b/src/vs/base/test/common/filters.test.ts index 5d6643d2378..6aeaefd59fb 100644 --- a/src/vs/base/test/common/filters.test.ts +++ b/src/vs/base/test/common/filters.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 { anyScore, createMatches, fuzzyScore, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, IFilter, IMatch, matchesCamelCase, matchesContiguousSubString, matchesPrefix, matchesStrictPrefix, matchesSubString, matchesWords, or } from '../../common/filters.js'; +import { anyScore, createMatches, fuzzyScore, fuzzyScoreGraceful, fuzzyScoreGracefulAggressive, FuzzyScorer, IFilter, IMatch, matchesBaseContiguousSubString, matchesCamelCase, matchesContiguousSubString, matchesPrefix, matchesStrictPrefix, matchesSubString, matchesWords, or } from '../../common/filters.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; function filterOk(filter: IFilter, word: string, wordToMatchAgainst: string, highlights?: { start: number; end: number }[]) { @@ -158,6 +158,30 @@ suite('Filters', () => { ]); }); + test('matchesBaseContiguousSubString', () => { + filterOk(matchesBaseContiguousSubString, 'cela', 'cancelAnimationFrame()', [ + { start: 3, end: 7 } + ]); + filterOk(matchesBaseContiguousSubString, 'cafe', 'café', [ + { start: 0, end: 4 } + ]); + filterOk(matchesBaseContiguousSubString, 'cafe', 'caféBar', [ + { start: 0, end: 4 } + ]); + filterOk(matchesBaseContiguousSubString, 'resume', 'résumé', [ + { start: 0, end: 6 } + ]); + filterOk(matchesBaseContiguousSubString, 'naïve', 'naïve', [ + { start: 0, end: 5 } + ]); + filterOk(matchesBaseContiguousSubString, 'naive', 'naïve', [ + { start: 0, end: 5 } + ]); + filterOk(matchesBaseContiguousSubString, 'aeou', 'àéöü', [ + { start: 0, end: 4 } + ]); + }); + test('matchesSubString', () => { filterOk(matchesSubString, 'cmm', 'cancelAnimationFrame()', [ { start: 0, end: 1 }, diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 81a3773baa7..f120298e22b 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -1141,6 +1141,10 @@ suite('Fuzzy Scorer', () => { test('prepareQuery', () => { assert.strictEqual(prepareQuery(' f*a ').normalized, 'fa'); assert.strictEqual(prepareQuery(' f…a ').normalized, 'fa'); + assert.strictEqual(prepareQuery('main#').normalized, 'main'); + assert.strictEqual(prepareQuery('main#').original, 'main#'); + assert.strictEqual(prepareQuery('foo*').normalized, 'foo'); + assert.strictEqual(prepareQuery('foo*').original, 'foo*'); assert.strictEqual(prepareQuery('model Tester.ts').original, 'model Tester.ts'); assert.strictEqual(prepareQuery('model Tester.ts').originalLowercase, 'model Tester.ts'.toLowerCase()); assert.strictEqual(prepareQuery('model Tester.ts').normalized, 'modelTester.ts'); @@ -1295,5 +1299,59 @@ suite('Fuzzy Scorer', () => { assert.strictEqual(score[1][1], 8); }); + test('Workspace symbol search with special characters (#, *)', function () { + // Simulates the scenario from the issue where rust-analyzer uses # and * as query modifiers + // The original query (with special chars) should reach the language server + // but normalized query (without special chars) should be used for fuzzy matching + + // Test #: User types "main#", language server returns "main" symbol + let query = prepareQuery('main#'); + assert.strictEqual(query.original, 'main#'); // Sent to language server + assert.strictEqual(query.normalized, 'main'); // Used for fuzzy matching + let [score, matches] = _doScore2('main', 'main#'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "main" symbol when query is "main#"'); + assert.ok(matches.length > 0); + + // Test *: User types "foo*", language server returns "foo" symbol + query = prepareQuery('foo*'); + assert.strictEqual(query.original, 'foo*'); // Sent to language server + assert.strictEqual(query.normalized, 'foo'); // Used for fuzzy matching + [score, matches] = _doScore2('foo', 'foo*'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "foo" symbol when query is "foo*"'); + assert.ok(matches.length > 0); + + // Test both: User types "MyClass#*", should match "MyClass" + query = prepareQuery('MyClass#*'); + assert.strictEqual(query.original, 'MyClass#*'); + assert.strictEqual(query.normalized, 'MyClass'); + [score, matches] = _doScore2('MyClass', 'MyClass#*'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "MyClass" symbol when query is "MyClass#*"'); + assert.ok(matches.length > 0); + + // Test fuzzy matching still works: User types "MC#", should match "MyClass" + query = prepareQuery('MC#'); + assert.strictEqual(query.original, 'MC#'); + assert.strictEqual(query.normalized, 'MC'); + [score, matches] = _doScore2('MyClass', 'MC#'); + assert.ok(typeof score === 'number' && score > 0, 'Should fuzzy match "MyClass" symbol when query is "MC#"'); + assert.ok(matches.length > 0); + + // Make sure leading # or # in the middle are not removed. + query = prepareQuery('#SpecialFunction'); + assert.strictEqual(query.original, '#SpecialFunction'); + assert.strictEqual(query.normalized, '#SpecialFunction'); + [score, matches] = _doScore2('#SpecialFunction', '#SpecialFunction'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "#SpecialFunction" symbol when query is "#SpecialFunction"'); + assert.ok(matches.length > 0); + + // Make sure standalone # is not removed + query = prepareQuery('#'); + assert.strictEqual(query.original, '#'); + assert.strictEqual(query.normalized, '#', 'Standalone # should not be removed'); + [score, matches] = _doScore2('#', '#'); + assert.ok(typeof score === 'number' && score > 0, 'Should match "#" symbol when query is "#"'); + assert.ok(matches.length > 0); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index 324ed8bf882..9a2fcff3e19 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { normalizeMimeType } from '../../common/mime.js'; +import { getExtensionForMimeType, normalizeMimeType } from '../../common/mime.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; suite('Mime', () => { @@ -18,5 +18,19 @@ suite('Mime', () => { assert.strictEqual(normalizeMimeType('Text/plain;lower'), 'text/plain;lower'); }); + test('getExtensionForMimeType', () => { + // Note: for MIME types with multiple extensions (e.g., image/jpg -> .jpe, .jpeg, .jpg), + // the function returns the first matching extension in iteration order + assert.ok(['.jpe', '.jpeg', '.jpg'].includes(getExtensionForMimeType('image/jpg')!)); + // image/jpeg is an alias for image/jpg and should also return a valid extension + assert.ok(['.jpe', '.jpeg', '.jpg'].includes(getExtensionForMimeType('image/jpeg')!)); + assert.strictEqual(getExtensionForMimeType('image/png'), '.png'); + assert.strictEqual(getExtensionForMimeType('image/gif'), '.gif'); + assert.strictEqual(getExtensionForMimeType('image/webp'), '.webp'); + assert.ok(['.mp2', '.mp2a', '.mp3', '.mpga', '.m2a', '.m3a'].includes(getExtensionForMimeType('audio/mpeg')!)); + assert.ok(['.mp4', '.mp4v', '.mpg4'].includes(getExtensionForMimeType('video/mp4')!)); + assert.strictEqual(getExtensionForMimeType('unknown/type'), undefined); + }); + ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/src/vs/base/test/common/normalization.test.ts b/src/vs/base/test/common/normalization.test.ts index 8ef33d54cc4..651f2b4f6ac 100644 --- a/src/vs/base/test/common/normalization.test.ts +++ b/src/vs/base/test/common/normalization.test.ts @@ -4,64 +4,86 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { removeAccents } from '../../common/normalization.js'; +import { tryNormalizeToBase } from '../../common/normalization.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; suite('Normalization', () => { ensureNoDisposablesAreLeakedInTestSuite(); - test('removeAccents', function () { - assert.strictEqual(removeAccents('joào'), 'joao'); - assert.strictEqual(removeAccents('joáo'), 'joao'); - assert.strictEqual(removeAccents('joâo'), 'joao'); - assert.strictEqual(removeAccents('joäo'), 'joao'); - // assert.strictEqual(strings.removeAccents('joæo'), 'joao'); // not an accent - assert.strictEqual(removeAccents('joão'), 'joao'); - assert.strictEqual(removeAccents('joåo'), 'joao'); - assert.strictEqual(removeAccents('joåo'), 'joao'); - assert.strictEqual(removeAccents('joāo'), 'joao'); + test('tryNormalizeToBase', function () { + assert.strictEqual(tryNormalizeToBase('joào'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joáo'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joâo'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joäo'), 'joao'); + // assert.strictEqual(strings.tryNormalizeToBase('joæo'), 'joao'); // not an accent + assert.strictEqual(tryNormalizeToBase('joão'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joåo'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joåo'), 'joao'); + assert.strictEqual(tryNormalizeToBase('joāo'), 'joao'); - assert.strictEqual(removeAccents('fôo'), 'foo'); - assert.strictEqual(removeAccents('föo'), 'foo'); - assert.strictEqual(removeAccents('fòo'), 'foo'); - assert.strictEqual(removeAccents('fóo'), 'foo'); - // assert.strictEqual(strings.removeAccents('fœo'), 'foo'); - // assert.strictEqual(strings.removeAccents('føo'), 'foo'); - assert.strictEqual(removeAccents('fōo'), 'foo'); - assert.strictEqual(removeAccents('fõo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('fôo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('föo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('fòo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('fóo'), 'foo'); + // assert.strictEqual(strings.tryNormalizeToBase('fœo'), 'foo'); + // assert.strictEqual(strings.tryNormalizeToBase('føo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('fōo'), 'foo'); + assert.strictEqual(tryNormalizeToBase('fõo'), 'foo'); - assert.strictEqual(removeAccents('andrè'), 'andre'); - assert.strictEqual(removeAccents('andré'), 'andre'); - assert.strictEqual(removeAccents('andrê'), 'andre'); - assert.strictEqual(removeAccents('andrë'), 'andre'); - assert.strictEqual(removeAccents('andrē'), 'andre'); - assert.strictEqual(removeAccents('andrė'), 'andre'); - assert.strictEqual(removeAccents('andrę'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrè'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andré'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrê'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrë'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrē'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrė'), 'andre'); + assert.strictEqual(tryNormalizeToBase('andrę'), 'andre'); - assert.strictEqual(removeAccents('hvîc'), 'hvic'); - assert.strictEqual(removeAccents('hvïc'), 'hvic'); - assert.strictEqual(removeAccents('hvíc'), 'hvic'); - assert.strictEqual(removeAccents('hvīc'), 'hvic'); - assert.strictEqual(removeAccents('hvįc'), 'hvic'); - assert.strictEqual(removeAccents('hvìc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvîc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvïc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvíc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvīc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvįc'), 'hvic'); + assert.strictEqual(tryNormalizeToBase('hvìc'), 'hvic'); - assert.strictEqual(removeAccents('ûdo'), 'udo'); - assert.strictEqual(removeAccents('üdo'), 'udo'); - assert.strictEqual(removeAccents('ùdo'), 'udo'); - assert.strictEqual(removeAccents('údo'), 'udo'); - assert.strictEqual(removeAccents('ūdo'), 'udo'); + assert.strictEqual(tryNormalizeToBase('ûdo'), 'udo'); + assert.strictEqual(tryNormalizeToBase('üdo'), 'udo'); + assert.strictEqual(tryNormalizeToBase('ùdo'), 'udo'); + assert.strictEqual(tryNormalizeToBase('údo'), 'udo'); + assert.strictEqual(tryNormalizeToBase('ūdo'), 'udo'); - assert.strictEqual(removeAccents('heÿ'), 'hey'); + assert.strictEqual(tryNormalizeToBase('heÿ'), 'hey'); - // assert.strictEqual(strings.removeAccents('gruß'), 'grus'); - assert.strictEqual(removeAccents('gruś'), 'grus'); - assert.strictEqual(removeAccents('gruš'), 'grus'); + // assert.strictEqual(strings.tryNormalizeToBase('gruß'), 'grus'); + assert.strictEqual(tryNormalizeToBase('gruś'), 'grus'); + assert.strictEqual(tryNormalizeToBase('gruš'), 'grus'); - assert.strictEqual(removeAccents('çool'), 'cool'); - assert.strictEqual(removeAccents('ćool'), 'cool'); - assert.strictEqual(removeAccents('čool'), 'cool'); + assert.strictEqual(tryNormalizeToBase('çool'), 'cool'); + assert.strictEqual(tryNormalizeToBase('ćool'), 'cool'); + assert.strictEqual(tryNormalizeToBase('čool'), 'cool'); - assert.strictEqual(removeAccents('ñice'), 'nice'); - assert.strictEqual(removeAccents('ńice'), 'nice'); + assert.strictEqual(tryNormalizeToBase('ñice'), 'nice'); + assert.strictEqual(tryNormalizeToBase('ńice'), 'nice'); + + // Different cases + assert.strictEqual(tryNormalizeToBase('CAFÉ'), 'cafe'); + assert.strictEqual(tryNormalizeToBase('Café'), 'cafe'); + assert.strictEqual(tryNormalizeToBase('café'), 'cafe'); + assert.strictEqual(tryNormalizeToBase('JOÃO'), 'joao'); + assert.strictEqual(tryNormalizeToBase('João'), 'joao'); + + // Mixed cases with accents + assert.strictEqual(tryNormalizeToBase('CaFé'), 'cafe'); + assert.strictEqual(tryNormalizeToBase('JoÃo'), 'joao'); + assert.strictEqual(tryNormalizeToBase('AnDrÉ'), 'andre'); + + // Precomposed accents + assert.strictEqual(tryNormalizeToBase('\u00E9'), 'e'); + assert.strictEqual(tryNormalizeToBase('\u00E0'), 'a'); + assert.strictEqual(tryNormalizeToBase('caf\u00E9'), 'cafe'); + + // Base + combining accents - lower only + assert.strictEqual(tryNormalizeToBase('\u0065\u0301'), '\u0065\u0301'); + assert.strictEqual(tryNormalizeToBase('Ã\u0061\u0300'), 'ã\u0061\u0300'); + assert.strictEqual(tryNormalizeToBase('CaF\u0065\u0301'), 'caf\u0065\u0301'); }); }); diff --git a/src/vs/base/test/common/oauth.test.ts b/src/vs/base/test/common/oauth.test.ts index 57a6ad3b16a..c554a8b7bdd 100644 --- a/src/vs/base/test/common/oauth.test.ts +++ b/src/vs/base/test/common/oauth.test.ts @@ -879,7 +879,9 @@ suite('OAuth', () => { { fetch: fetchStub } ); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl); assert.strictEqual(fetchStub.firstCall.args[1].method, 'GET'); @@ -903,12 +905,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], 'test-value'); @@ -931,12 +934,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], undefined); @@ -946,6 +950,7 @@ suite('OAuth', () => { const targetResource = 'https://example.com/api'; const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + // Stub all possible URLs to return 404 for robust fallback testing fetchStub.resolves({ status: 404, text: async () => 'Not Found' @@ -953,7 +958,11 @@ suite('OAuth', () => { await assert.rejects( async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), - /Failed to fetch resource metadata from.*404 Not Found/ + (error: any) => { + // Should be AggregateError since all URLs fail + assert.ok(error instanceof AggregateError || /Failed to fetch resource metadata from.*404 Not Found/.test(error.message)); + return true; + } ); }); @@ -961,6 +970,7 @@ suite('OAuth', () => { const targetResource = 'https://example.com/api'; const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + // Stub all possible URLs to return 500 for robust fallback testing fetchStub.resolves({ status: 500, statusText: 'Internal Server Error', @@ -969,7 +979,11 @@ suite('OAuth', () => { await assert.rejects( async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), - /Failed to fetch resource metadata from.*500 Internal Server Error/ + (error: any) => { + // Should be AggregateError since all URLs fail + assert.ok(error instanceof AggregateError || /Failed to fetch resource metadata from.*500 Internal Server Error/.test(error.message)); + return true; + } ); }); @@ -980,6 +994,7 @@ suite('OAuth', () => { resource: 'https://different.com/api' }; + // Stub all possible URLs to return invalid metadata for robust fallback testing fetchStub.resolves({ status: 200, json: async () => metadata, @@ -988,7 +1003,12 @@ suite('OAuth', () => { await assert.rejects( async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), - /Protected Resource Metadata resource property value.*does not match target server url.*These MUST match to follow OAuth spec/ + (error: any) => { + // Should be AggregateError since all URLs fail validation + assert.ok(error instanceof AggregateError); + assert.ok(error.errors.some((e: Error) => /does not match expected value/.test(e.message))); + return true; + } ); }); @@ -1007,24 +1027,8 @@ suite('OAuth', () => { // URL normalization should handle hostname case differences const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); - assert.deepStrictEqual(result, metadata); - }); - - test('should normalize hostnames when comparing resource values', async () => { - const targetResource = 'https://EXAMPLE.COM/api'; - const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; - const metadata = { - resource: 'https://example.com/api' - }; - - fetchStub.resolves({ - status: 200, - json: async () => metadata, - text: async () => JSON.stringify(metadata) - }); - - const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); - assert.deepStrictEqual(result, metadata); + assert.deepStrictEqual(result.metadata, metadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); }); test('should throw error when response is not valid resource metadata', async () => { @@ -1035,6 +1039,7 @@ suite('OAuth', () => { scopes_supported: ['read', 'write'] }; + // Stub all possible URLs to return invalid metadata for robust fallback testing fetchStub.resolves({ status: 200, json: async () => invalidMetadata, @@ -1043,7 +1048,11 @@ suite('OAuth', () => { await assert.rejects( async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), - /Invalid resource metadata.*Expected to follow shape of.*is scopes_supported an array\? Is resource a string\?/ + (error: any) => { + // Should be AggregateError since all URLs return invalid metadata + assert.ok(error instanceof AggregateError || /Invalid resource metadata/.test(error.message)); + return true; + } ); }); @@ -1055,6 +1064,7 @@ suite('OAuth', () => { scopes_supported: 'not an array' }; + // Stub all possible URLs to return invalid metadata for robust fallback testing fetchStub.resolves({ status: 200, json: async () => invalidMetadata, @@ -1063,7 +1073,11 @@ suite('OAuth', () => { await assert.rejects( async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), - /Invalid resource metadata/ + (error: any) => { + // Should be AggregateError since all URLs return invalid metadata + assert.ok(error instanceof AggregateError || /Invalid resource metadata/.test(error.message)); + return true; + } ); }); @@ -1087,7 +1101,7 @@ suite('OAuth', () => { }); const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); - assert.deepStrictEqual(result, metadata); + assert.deepStrictEqual(result.metadata, metadata); }); test('should use global fetch when custom fetch is not provided', async () => { @@ -1106,7 +1120,8 @@ suite('OAuth', () => { const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl); - assert.deepStrictEqual(result, metadata); + assert.deepStrictEqual(result.metadata, metadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); assert.strictEqual(globalFetchStub.callCount, 1); }); @@ -1126,12 +1141,13 @@ suite('OAuth', () => { text: async () => JSON.stringify(metadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); // Different ports mean different origins const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Test-Header'], undefined); @@ -1153,24 +1169,26 @@ suite('OAuth', () => { text: async () => JSON.stringify(metadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, resourceMetadataUrl, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); // Different protocols mean different origins const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Test-Header'], undefined); }); - test('should include error details in message with length information', async () => { + test('should include error details in message with resource values', async () => { const targetResource = 'https://example.com/api'; const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; const metadata = { resource: 'https://different.com/other' }; + // Stub all possible URLs to return invalid metadata for robust fallback testing fetchStub.resolves({ status: 200, json: async () => metadata, @@ -1181,9 +1199,11 @@ suite('OAuth', () => { await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); assert.fail('Should have thrown an error'); } catch (error: any) { - assert.ok(/length:/.test(error.message), 'Error message should include length information'); - assert.ok(/https:\/\/different\.com\/other/.test(error.message), 'Error message should include actual resource value'); - assert.ok(/https:\/\/example\.com\/api/.test(error.message), 'Error message should include expected resource value'); + // Should be AggregateError with validation errors + const errorMessage = error instanceof AggregateError ? error.errors.map((e: Error) => e.message).join(' ') : error.message; + assert.ok(/does not match expected value/.test(errorMessage), 'Error message should mention mismatch'); + assert.ok(/https:\/\/different\.com\/other/.test(errorMessage), 'Error message should include actual resource value'); + assert.ok(/https:\/\/example\.com\/api/.test(errorMessage), 'Error message should include expected resource value'); } }); @@ -1206,7 +1226,8 @@ suite('OAuth', () => { { fetch: fetchStub } ); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource/api/v1'); assert.strictEqual(fetchStub.callCount, 1); // Should try path-appended version first assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1215,7 +1236,7 @@ suite('OAuth', () => { test('should fallback to well-known URI at root when path version fails', async () => { const targetResource = 'https://example.com/api/v1'; const expectedMetadata = { - resource: 'https://example.com/api/v1', + resource: 'https://example.com/', scopes_supported: ['read', 'write'] }; @@ -1238,7 +1259,9 @@ suite('OAuth', () => { { fetch: fetchStub } ); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); // First attempt with path assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); @@ -1286,7 +1309,8 @@ suite('OAuth', () => { { fetch: fetchStub } ); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); assert.strictEqual(fetchStub.callCount, 1); // Both URLs should be the same when path is / assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); @@ -1308,17 +1332,301 @@ suite('OAuth', () => { text: async () => JSON.stringify(expectedMetadata) }); - await fetchResourceMetadata( + const result = await fetchResourceMetadata( targetResource, undefined, { fetch: fetchStub, sameOriginHeaders } ); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource/api'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); assert.strictEqual(headers['X-Test-Header'], 'test-value'); assert.strictEqual(headers['X-Custom-Header'], 'value'); }); + + test('should handle fetchImpl throwing network error and continue to next URL', async () => { + const targetResource = 'https://example.com/api/v1'; + const expectedMetadata = { + resource: 'https://example.com/', + scopes_supported: ['read', 'write'] + }; + + // First call throws network error, second succeeds + fetchStub.onFirstCall().rejects(new Error('Network connection failed')); + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); + assert.ok(/Network connection failed/.test(result.errors[0].message)); + assert.strictEqual(fetchStub.callCount, 2); + // First attempt with path should have thrown error + assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); + // Second attempt at root should succeed + assert.strictEqual(fetchStub.secondCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); + }); + + test('should throw AggregateError when fetchImpl throws on all URLs', async () => { + const targetResource = 'https://example.com/api/v1'; + + // Both calls throw network errors + fetchStub.rejects(new Error('Network connection failed')); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }), + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 2, 'Should contain 2 errors'); + assert.ok(/Network connection failed/.test(error.errors[0].message), 'First error should mention network failure'); + assert.ok(/Network connection failed/.test(error.errors[1].message), 'Second error should mention network failure'); + return true; + } + ); + + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should handle mix of fetch error and non-200 response', async () => { + const targetResource = 'https://example.com/api/v1'; + + // First call throws network error + fetchStub.onFirstCall().rejects(new Error('Connection timeout')); + + // Second call returns 404 + fetchStub.onSecondCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }), + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 2, 'Should contain 2 errors'); + assert.ok(/Connection timeout/.test(error.errors[0].message), 'First error should be network error'); + assert.ok(/Failed to fetch resource metadata.*404/.test(error.errors[1].message), 'Second error should be 404'); + return true; + } + ); + + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should accept root URL in PRM resource when using root discovery fallback (no trailing slash)', async () => { + const targetResource = 'https://example.com/api/v1'; + // Per RFC 9728: when metadata retrieved from root discovery URL, + // the resource value must match the root URL (where well-known was inserted) + const expectedMetadata = { + resource: 'https://example.com', + scopes_supported: ['read', 'write'] + }; + + // First call (path-appended) fails, second (root) succeeds + fetchStub.onFirstCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should accept root URL in PRM resource when using root discovery fallback (with trailing slash)', async () => { + const targetResource = 'https://example.com/api/v1'; + // Test that trailing slash form is also accepted (URL normalization) + const expectedMetadata = { + resource: 'https://example.com/', + scopes_supported: ['read', 'write'] + }; + + // First call (path-appended) fails, second (root) succeeds + fetchStub.onFirstCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + undefined, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should reject PRM with full path resource when using root discovery fallback', async () => { + const targetResource = 'https://example.com/api/v1'; + // This violates RFC 9728: root discovery PRM should have root URL, not full path + const invalidMetadata = { + resource: 'https://example.com/api/v1', + scopes_supported: ['read'] + }; + + // First call (path-appended) fails, second (root) returns invalid metadata + fetchStub.onFirstCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found' + }); + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => invalidMetadata, + text: async () => JSON.stringify(invalidMetadata) + }); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }), + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 2); + // First error is 404 from path-appended attempt + assert.ok(/404/.test(error.errors[0].message)); + // Second error is validation failure from root attempt + assert.ok(/does not match expected value/.test(error.errors[1].message)); + // Check that validation was against root URL (origin) not full path + assert.ok(/https:\/\/example\.com\/api\/v1.*https:\/\/example\.com/.test(error.errors[1].message)); + return true; + } + ); + + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should reject PRM with root resource when using path-appended discovery', async () => { + const targetResource = 'https://example.com/api/v1'; + // This violates RFC 9728: path-appended discovery PRM should match full target URL + const invalidMetadata = { + resource: 'https://example.com/', + scopes_supported: ['read'] + }; + + // First attempt (path-appended) gets the wrong resource value + // It will fail validation and continue to second URL (root) + // Second attempt (root) will succeed because root expects root resource + fetchStub.resolves({ + status: 200, + json: async () => invalidMetadata, + text: async () => JSON.stringify(invalidMetadata) + }); + + // This should actually succeed on the second (root) attempt + const result = await fetchResourceMetadata(targetResource, undefined, { fetch: fetchStub }); + + assert.deepStrictEqual(result.metadata, invalidMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.strictEqual(result.errors.length, 1); + assert.strictEqual(fetchStub.callCount, 2); + // Verify both URLs were tried + assert.strictEqual(fetchStub.firstCall.args[0], 'https://example.com/.well-known/oauth-protected-resource/api/v1'); + assert.strictEqual(fetchStub.secondCall.args[0], 'https://example.com/.well-known/oauth-protected-resource'); + }); + + test('should validate against targetResource when resourceMetadataUrl is explicitly provided', async () => { + const targetResource = 'https://example.com/api/v1'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + // When explicit URL provided (e.g., from WWW-Authenticate), must match targetResource + const validMetadata = { + resource: 'https://example.com/api/v1', + scopes_supported: ['read'] + }; + + fetchStub.resolves({ + status: 200, + json: async () => validMetadata, + text: async () => JSON.stringify(validMetadata) + }); + + const result = await fetchResourceMetadata( + targetResource, + resourceMetadataUrl, + { fetch: fetchStub } + ); + + assert.deepStrictEqual(result.metadata, validMetadata); + assert.strictEqual(result.discoveryUrl, resourceMetadataUrl); + assert.strictEqual(fetchStub.callCount, 1); + assert.strictEqual(fetchStub.firstCall.args[0], resourceMetadataUrl); + }); + + test('should fallback to root discovery when explicit resourceMetadataUrl validation fails', async () => { + const targetResource = 'https://example.com/api/v1'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + const invalidMetadata = { + resource: 'https://example.com/', + scopes_supported: ['read'] + }; + + // Stub all URLs to return root resource metadata + // Explicit URL returns root (validation fails), path-appended fails, root succeeds + fetchStub.resolves({ + status: 200, + json: async () => invalidMetadata, + text: async () => JSON.stringify(invalidMetadata) + }); + + // Should succeed on root discovery fallback + const result = await fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }); + assert.deepStrictEqual(result.metadata, invalidMetadata); + assert.strictEqual(result.discoveryUrl, 'https://example.com/.well-known/oauth-protected-resource'); + assert.ok(result.errors.length >= 1); + // Should have tried explicit URL, path-appended, then succeeded on root + assert.ok(fetchStub.callCount >= 2); + }); + + test('should handle fetchImpl throwing error with explicit resourceMetadataUrl', async () => { + const targetResource = 'https://example.com/api'; + const resourceMetadataUrl = 'https://example.com/.well-known/oauth-protected-resource'; + + // Stub all possible URLs to throw network error for robust fallback testing + fetchStub.rejects(new Error('DNS resolution failed')); + + await assert.rejects( + async () => fetchResourceMetadata(targetResource, resourceMetadataUrl, { fetch: fetchStub }), + (error: any) => { + // Should be AggregateError since all URLs fail + assert.ok(error instanceof AggregateError || /DNS resolution failed/.test(error.message)); + return true; + } + ); + + // Should have tried explicit URL and well-known discovery + assert.ok(fetchStub.callCount >= 2); + }); }); suite('fetchAuthorizationServerMetadata', () => { @@ -1352,7 +1660,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // Should try OAuth discovery with path insertion: https://auth.example.com/.well-known/oauth-authorization-server/tenant assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1385,7 +1695,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/openid-configuration/tenant'); + assert.strictEqual(result.errors.length, 1); assert.strictEqual(fetchStub.callCount, 2); // First attempt: OAuth discovery assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1426,7 +1738,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/tenant/.well-known/openid-configuration'); + assert.strictEqual(result.errors.length, 2); assert.strictEqual(fetchStub.callCount, 3); // First attempt: OAuth discovery assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); @@ -1454,7 +1768,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // For root URLs, no extra path is added assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server'); @@ -1478,7 +1794,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -1500,14 +1818,15 @@ suite('OAuth', () => { statusText: 'OK' }); - await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders }); + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders }); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['X-Custom-Header'], 'custom-value'); assert.strictEqual(headers['Authorization'], 'Bearer token123'); assert.strictEqual(headers['Accept'], 'application/json'); }); - test('should throw error when all discovery endpoints fail', async () => { + test('should throw AggregateError when all discovery endpoints fail', async () => { const authorizationServer = 'https://auth.example.com/tenant'; fetchStub.resolves({ @@ -1519,13 +1838,92 @@ suite('OAuth', () => { await assert.rejects( async () => fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }), - /Failed to fetch authorization server metadata: 404 Not Found/ + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 3, 'Should contain 3 errors (one for each URL)'); + assert.strictEqual(error.message, 'Failed to fetch authorization server metadata from all attempted URLs'); + // Verify each error includes the URL it attempted + assert.ok(/oauth-authorization-server.*404/.test(error.errors[0].message), 'First error should mention OAuth discovery and 404'); + assert.ok(/openid-configuration.*404/.test(error.errors[1].message), 'Second error should mention OpenID path insertion and 404'); + assert.ok(/openid-configuration.*404/.test(error.errors[2].message), 'Third error should mention OpenID path addition and 404'); + return true; + } ); // Should have tried all three endpoints assert.strictEqual(fetchStub.callCount, 3); }); + test('should throw single error (not AggregateError) when only one URL is tried and fails', async () => { + const authorizationServer = 'https://auth.example.com'; + + // First attempt succeeds on second try, so only one error is collected for first URL + fetchStub.onFirstCall().resolves({ + status: 500, + text: async () => 'Internal Server Error', + statusText: 'Internal Server Error', + json: async () => { throw new Error('Not JSON'); } + }); + + const expectedMetadata: IAuthorizationServerMetadata = { + issuer: 'https://auth.example.com/', + response_types_supported: ['code'] + }; + + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata), + statusText: 'OK' + }); + + // Should succeed on second attempt + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 1); + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should throw AggregateError when multiple URLs fail with mixed error types', async () => { + const authorizationServer = 'https://auth.example.com/tenant'; + + // First call: network error + fetchStub.onFirstCall().rejects(new Error('Connection timeout')); + + // Second call: 404 + fetchStub.onSecondCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found', + json: async () => { throw new Error('Not JSON'); } + }); + + // Third call: 500 + fetchStub.onThirdCall().resolves({ + status: 500, + text: async () => 'Internal Server Error', + statusText: 'Internal Server Error', + json: async () => { throw new Error('Not JSON'); } + }); + + await assert.rejects( + async () => fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }), + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 3, 'Should contain 3 errors'); + // First error is network error + assert.ok(/Connection timeout/.test(error.errors[0].message), 'First error should be network error'); + // Second error is 404 + assert.ok(/404.*Not Found/.test(error.errors[1].message), 'Second error should be 404'); + // Third error is 500 + assert.ok(/500.*Internal Server Error/.test(error.errors[2].message), 'Third error should be 500'); + return true; + } + ); + + assert.strictEqual(fetchStub.callCount, 3); + }); + test('should handle invalid JSON response', async () => { const authorizationServer = 'https://auth.example.com'; @@ -1579,19 +1977,92 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(globalFetchStub.callCount, 1); }); - test('should handle network fetch failure', async () => { + test('should handle network fetch failure and continue to next endpoint', async () => { + const authorizationServer = 'https://auth.example.com'; + const expectedMetadata: IAuthorizationServerMetadata = { + issuer: 'https://auth.example.com/', + response_types_supported: ['code'] + }; + + // First call throws network error, second succeeds + fetchStub.onFirstCall().rejects(new Error('Network error')); + fetchStub.onSecondCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata), + statusText: 'OK' + }); + + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); + + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 1); + assert.ok(/Network error/.test(result.errors[0].message)); + // Should have tried two endpoints + assert.strictEqual(fetchStub.callCount, 2); + }); + + test('should throw error when network fails on all endpoints', async () => { const authorizationServer = 'https://auth.example.com'; fetchStub.rejects(new Error('Network error')); await assert.rejects( async () => fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }), - /Network error/ + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 3, 'Should contain 3 errors'); + assert.strictEqual(error.message, 'Failed to fetch authorization server metadata from all attempted URLs'); + // All errors should be network errors + assert.ok(/Network error/.test(error.errors[0].message), 'First error should be network error'); + assert.ok(/Network error/.test(error.errors[1].message), 'Second error should be network error'); + assert.ok(/Network error/.test(error.errors[2].message), 'Third error should be network error'); + return true; + } ); + + // Should have tried all three endpoints + assert.strictEqual(fetchStub.callCount, 3); + }); + + test('should handle mix of network error and non-200 response', async () => { + const authorizationServer = 'https://auth.example.com/tenant'; + const expectedMetadata: IAuthorizationServerMetadata = { + issuer: 'https://auth.example.com/tenant', + response_types_supported: ['code'] + }; + + // First call throws network error + fetchStub.onFirstCall().rejects(new Error('Connection timeout')); + + // Second call returns 404 + fetchStub.onSecondCall().resolves({ + status: 404, + text: async () => 'Not Found', + statusText: 'Not Found', + json: async () => { throw new Error('Not JSON'); } + }); + + // Third call succeeds + fetchStub.onThirdCall().resolves({ + status: 200, + json: async () => expectedMetadata, + text: async () => JSON.stringify(expectedMetadata), + statusText: 'OK' + }); + + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); + + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.errors.length, 2); + // Should have tried all three endpoints + assert.strictEqual(fetchStub.callCount, 3); }); test('should handle response.text() failure in error case', async () => { @@ -1606,7 +2077,15 @@ suite('OAuth', () => { await assert.rejects( async () => fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }), - /Failed to fetch authorization server metadata: 500 Internal Server Error/ + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 3, 'Should contain 3 errors'); + // All errors should include status code and statusText (fallback when text() fails) + for (const err of error.errors) { + assert.ok(/500 Internal Server Error/.test(err.message), `Error should mention 500 and statusText: ${err.message}`); + } + return true; + } ); }); @@ -1641,7 +2120,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/tenant/.well-known/openid-configuration'); + assert.strictEqual(result.errors.length, 2); assert.strictEqual(fetchStub.callCount, 3); // Third attempt should correctly handle trailing slash (not double-slash) assert.strictEqual(fetchStub.thirdCall.args[0], 'https://auth.example.com/tenant/.well-known/openid-configuration'); @@ -1663,7 +2144,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/org/sub'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); // Should correctly insert well-known path with nested paths assert.strictEqual(fetchStub.firstCall.args[0], 'https://auth.example.com/.well-known/oauth-authorization-server/tenant/org/sub'); @@ -1685,7 +2168,15 @@ suite('OAuth', () => { await assert.rejects( async () => fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }), - /Failed to fetch authorization server metadata/ + (error: any) => { + assert.ok(error instanceof AggregateError, 'Should be an AggregateError'); + assert.strictEqual(error.errors.length, 3, 'Should contain 3 errors'); + // All errors should indicate failed to fetch with status code + for (const err of error.errors) { + assert.ok(/Failed to fetch authorization server metadata from/.test(err.message), `Error should mention failed fetch: ${err.message}`); + } + return true; + } ); // Should try all three endpoints @@ -1713,7 +2204,9 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, validMetadata); + assert.deepStrictEqual(result.metadata, validMetadata); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -1733,7 +2226,10 @@ suite('OAuth', () => { const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub }); - assert.deepStrictEqual(result, expectedMetadata); + assert.deepStrictEqual(result.metadata, expectedMetadata); + // Query parameters are not included in the discovery URL (only pathname is extracted) + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server/tenant'); + assert.deepStrictEqual(result.errors, []); assert.strictEqual(fetchStub.callCount, 1); }); @@ -1751,8 +2247,9 @@ suite('OAuth', () => { statusText: 'OK' }); - await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders: {} }); + const result = await fetchAuthorizationServerMetadata(authorizationServer, { fetch: fetchStub, additionalHeaders: {} }); + assert.strictEqual(result.discoveryUrl, 'https://auth.example.com/.well-known/oauth-authorization-server'); const headers = fetchStub.firstCall.args[1].headers; assert.strictEqual(headers['Accept'], 'application/json'); }); diff --git a/src/vs/base/test/common/observables/debug.test.ts b/src/vs/base/test/common/observables/debug.test.ts index 5be20a336d6..c046999ef59 100644 --- a/src/vs/base/test/common/observables/debug.test.ts +++ b/src/vs/base/test/common/observables/debug.test.ts @@ -7,7 +7,7 @@ import assert from 'assert'; import { observableValue, derived, autorun } from '../../../common/observable.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../utils.js'; // eslint-disable-next-line local/code-no-deep-import-of-internal -import { debugGetDependencyGraph } from '../../../common/observableInternal/logging/debugGetDependencyGraph.js'; +import { debugGetObservableGraph } from '../../../common/observableInternal/logging/debugGetDependencyGraph.js'; suite('debug', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +50,7 @@ suite('debug', () => { let idx = 0; assert.deepStrictEqual( - debugGetDependencyGraph(myComputed3, { debugNamePostProcessor: name => `name${++idx}` }), + debugGetObservableGraph(myComputed3, { type: 'dependencies', debugNamePostProcessor: name => `name${++idx}` }), '* derived name1:\n value: 0\n state: upToDate\n dependencies:\n\t\t* derived name2:\n\t\t value: 0\n\t\t state: upToDate\n\t\t dependencies:\n\t\t\t\t* derived name3:\n\t\t\t\t value: 0\n\t\t\t\t state: upToDate\n\t\t\t\t dependencies:\n\t\t\t\t\t\t* observableValue name4:\n\t\t\t\t\t\t value: 0\n\t\t\t\t\t\t state: upToDate\n\t\t\t\t\t\t* observableValue name5:\n\t\t\t\t\t\t value: 0\n\t\t\t\t\t\t state: upToDate\n\t\t\t\t* observableValue name6 (already listed)\n\t\t\t\t* observableValue name7 (already listed)\n\t\t* observableValue name8 (already listed)\n\t\t* observableValue name9 (already listed)', ); }); diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts deleted file mode 100644 index d827e70c087..00000000000 --- a/src/vs/base/test/common/skipList.test.ts +++ /dev/null @@ -1,233 +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 { binarySearch } from '../../common/arrays.js'; -import { SkipList } from '../../common/skipList.js'; -import { StopWatch } from '../../common/stopwatch.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; - - -suite('SkipList', function () { - - ensureNoDisposablesAreLeakedInTestSuite(); - - function assertValues(list: SkipList, expected: V[]) { - assert.strictEqual(list.size, expected.length); - assert.deepStrictEqual([...list.values()], expected); - - const valuesFromEntries = [...list.entries()].map(entry => entry[1]); - assert.deepStrictEqual(valuesFromEntries, expected); - - const valuesFromIter = [...list].map(entry => entry[1]); - assert.deepStrictEqual(valuesFromIter, expected); - - let i = 0; - list.forEach((value, _key, map) => { - assert.ok(map === list); - assert.deepStrictEqual(value, expected[i++]); - }); - } - - function assertKeys(list: SkipList, expected: K[]) { - assert.strictEqual(list.size, expected.length); - assert.deepStrictEqual([...list.keys()], expected); - - const keysFromEntries = [...list.entries()].map(entry => entry[0]); - assert.deepStrictEqual(keysFromEntries, expected); - - const keysFromIter = [...list].map(entry => entry[0]); - assert.deepStrictEqual(keysFromIter, expected); - - let i = 0; - list.forEach((_value, key, map) => { - assert.ok(map === list); - assert.deepStrictEqual(key, expected[i++]); - }); - } - - test('set/get/delete', function () { - const list = new SkipList((a, b) => a - b); - - assert.strictEqual(list.get(3), undefined); - list.set(3, 1); - assert.strictEqual(list.get(3), 1); - assertValues(list, [1]); - - list.set(3, 3); - assertValues(list, [3]); - - list.set(1, 1); - list.set(4, 4); - assert.strictEqual(list.get(3), 3); - assert.strictEqual(list.get(1), 1); - assert.strictEqual(list.get(4), 4); - assertValues(list, [1, 3, 4]); - - assert.strictEqual(list.delete(17), false); - - assert.strictEqual(list.delete(1), true); - assert.strictEqual(list.get(1), undefined); - assert.strictEqual(list.get(3), 3); - assert.strictEqual(list.get(4), 4); - - assertValues(list, [3, 4]); - }); - - test('Figure 3', function () { - const list = new SkipList((a, b) => a - b); - list.set(3, true); - list.set(6, true); - list.set(7, true); - list.set(9, true); - list.set(12, true); - list.set(19, true); - list.set(21, true); - list.set(25, true); - - assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]); - - list.set(17, true); - assert.deepStrictEqual(list.size, 9); - assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]); - }); - - test('clear ( CPU pegged after some builds #194853)', function () { - const list = new SkipList((a, b) => a - b); - list.set(1, true); - list.set(2, true); - list.set(3, true); - assert.strictEqual(list.size, 3); - list.clear(); - assert.strictEqual(list.size, 0); - assert.strictEqual(list.get(1), undefined); - assert.strictEqual(list.get(2), undefined); - assert.strictEqual(list.get(3), undefined); - }); - - test('capacity max', function () { - const list = new SkipList((a, b) => a - b, 10); - list.set(1, true); - list.set(2, true); - list.set(3, true); - list.set(4, true); - list.set(5, true); - list.set(6, true); - list.set(7, true); - list.set(8, true); - list.set(9, true); - list.set(10, true); - list.set(11, true); - list.set(12, true); - - assertKeys(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); - }); - - const cmp = (a: number, b: number): number => { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; - } - }; - - function insertArraySorted(array: number[], element: number) { - let idx = binarySearch(array, element, cmp); - if (idx >= 0) { - array[idx] = element; - } else { - idx = ~idx; - // array = array.slice(0, idx).concat(element, array.slice(idx)); - array.splice(idx, 0, element); - } - return array; - } - - function delArraySorted(array: number[], element: number) { - const idx = binarySearch(array, element, cmp); - if (idx >= 0) { - // array = array.slice(0, idx).concat(array.slice(idx)); - array.splice(idx, 1); - } - return array; - } - - - test.skip('perf', function () { - - // data - const max = 2 ** 16; - const values = new Set(); - for (let i = 0; i < max; i++) { - const value = Math.floor(Math.random() * max); - values.add(value); - } - console.log(values.size); - - // init - const list = new SkipList(cmp, max); - let sw = new StopWatch(); - values.forEach(value => list.set(value, true)); - sw.stop(); - console.log(`[LIST] ${list.size} elements after ${sw.elapsed()}ms`); - let array: number[] = []; - sw = new StopWatch(); - values.forEach(value => array = insertArraySorted(array, value)); - sw.stop(); - console.log(`[ARRAY] ${array.length} elements after ${sw.elapsed()}ms`); - - // get - sw = new StopWatch(); - const someValues = [...values].slice(0, values.size / 4); - someValues.forEach(key => { - const value = list.get(key); // find - console.assert(value, '[LIST] must have ' + key); - list.get(-key); // miss - }); - sw.stop(); - console.log(`[LIST] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); - sw = new StopWatch(); - someValues.forEach(key => { - const idx = binarySearch(array, key, cmp); // find - console.assert(idx >= 0, '[ARRAY] must have ' + key); - binarySearch(array, -key, cmp); // miss - }); - sw.stop(); - console.log(`[ARRAY] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); - - - // insert - sw = new StopWatch(); - someValues.forEach(key => { - list.set(-key, false); - }); - sw.stop(); - console.log(`[LIST] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`); - sw = new StopWatch(); - someValues.forEach(key => { - array = insertArraySorted(array, -key); - }); - sw.stop(); - console.log(`[ARRAY] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`); - - // delete - sw = new StopWatch(); - someValues.forEach(key => { - list.delete(key); // find - list.delete(-key); // miss - }); - sw.stop(); - console.log(`[LIST] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); - sw = new StopWatch(); - someValues.forEach(key => { - array = delArraySorted(array, key); // find - array = delArraySorted(array, -key); // miss - }); - sw.stop(); - console.log(`[ARRAY] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); - }); -}); diff --git a/src/vs/base/test/common/strings.test.ts b/src/vs/base/test/common/strings.test.ts index cfdf7836392..aeac4aa7b16 100644 --- a/src/vs/base/test/common/strings.test.ts +++ b/src/vs/base/test/common/strings.test.ts @@ -196,6 +196,40 @@ suite('Strings', () => { assert.strictEqual(strings.lcut('............a', 10, '…'), '............a'); }); + test('rcut', () => { + assert.strictEqual(strings.rcut('foo bar', 0), ''); + assert.strictEqual(strings.rcut('foo bar', 1), ''); + assert.strictEqual(strings.rcut('foo bar', 3), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 4), 'foo'); // Trailing whitespace trimmed + assert.strictEqual(strings.rcut('foo bar', 5), 'foo'); + assert.strictEqual(strings.rcut('foo bar', 7), 'foo bar'); + assert.strictEqual(strings.rcut('foo bar', 10), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 6), 'test'); + + assert.strictEqual(strings.rcut('foo bar', 0, '…'), '…'); + assert.strictEqual(strings.rcut('foo bar', 1, '…'), '…'); + assert.strictEqual(strings.rcut('foo bar', 3, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 4, '…'), 'foo…'); // Trailing whitespace trimmed + assert.strictEqual(strings.rcut('foo bar', 5, '…'), 'foo…'); + assert.strictEqual(strings.rcut('foo bar', 7, '…'), 'foo bar'); + assert.strictEqual(strings.rcut('foo bar', 10, '…'), 'foo bar'); + assert.strictEqual(strings.rcut('test string 0.1.2.3', 6, '…'), 'test…'); + + assert.strictEqual(strings.rcut('', 10), ''); + assert.strictEqual(strings.rcut('a', 10), 'a'); + assert.strictEqual(strings.rcut('a ', 10), 'a'); + assert.strictEqual(strings.rcut('a ', 10), 'a'); + assert.strictEqual(strings.rcut('a bbbb ', 10), 'a bbbb'); + assert.strictEqual(strings.rcut('a............', 10), 'a............'); + + assert.strictEqual(strings.rcut('', 10, '…'), ''); + assert.strictEqual(strings.rcut('a', 10, '…'), 'a'); + assert.strictEqual(strings.rcut('a ', 10, '…'), 'a'); + assert.strictEqual(strings.rcut('a ', 10, '…'), 'a'); + assert.strictEqual(strings.rcut('a bbbb ', 10, '…'), 'a bbbb'); + assert.strictEqual(strings.rcut('a............', 10, '…'), 'a............'); + }); + test('escape', () => { assert.strictEqual(strings.escape(''), ''); assert.strictEqual(strings.escape('foo'), 'foo'); @@ -215,6 +249,11 @@ suite('Strings', () => { assert.strictEqual(strings.ltrim('///', '/'), ''); assert.strictEqual(strings.ltrim('', ''), ''); assert.strictEqual(strings.ltrim('', '/'), ''); + // Multi-character needle with consecutive repetitions + assert.strictEqual(strings.ltrim('---hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('------hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('---------hello', '---'), 'hello'); + assert.strictEqual(strings.ltrim('hello---', '---'), 'hello---'); }); test('rtrim', () => { @@ -228,6 +267,13 @@ suite('Strings', () => { assert.strictEqual(strings.rtrim('///', '/'), ''); assert.strictEqual(strings.rtrim('', ''), ''); assert.strictEqual(strings.rtrim('', '/'), ''); + // Multi-character needle with consecutive repetitions (bug fix) + assert.strictEqual(strings.rtrim('hello---', '---'), 'hello'); + assert.strictEqual(strings.rtrim('hello------', '---'), 'hello'); + assert.strictEqual(strings.rtrim('hello---------', '---'), 'hello'); + assert.strictEqual(strings.rtrim('---hello', '---'), '---hello'); + assert.strictEqual(strings.rtrim('hello world' + '---'.repeat(10), '---'), 'hello world'); + assert.strictEqual(strings.rtrim('path/to/file///', '//'), 'path/to/file/'); }); test('trim', () => { @@ -273,6 +319,24 @@ suite('Strings', () => { assert.strictEqual(strings.isEmojiImprecise(codePoint), true); }); + test('isFullWidthCharacter', () => { + // Fullwidth ASCII (FF01-FF5E) + assert.strictEqual(strings.isFullWidthCharacter('A'.charCodeAt(0)), true, 'A U+FF21 fullwidth A'); + assert.strictEqual(strings.isFullWidthCharacter('?'.charCodeAt(0)), true, '? U+FF1F fullwidth question mark'); + assert.strictEqual(strings.isFullWidthCharacter('#'.charCodeAt(0)), true, '# U+FF03 fullwidth number sign'); + assert.strictEqual(strings.isFullWidthCharacter('='.charCodeAt(0)), true, '= U+FF1D fullwidth equals sign'); + + // Hiragana (3040-309F) + assert.strictEqual(strings.isFullWidthCharacter('あ'.charCodeAt(0)), true, 'あ U+3042 hiragana'); + + // Fullwidth symbols (FFE0-FFE6) + assert.strictEqual(strings.isFullWidthCharacter('¥'.charCodeAt(0)), true, '¥ U+FFE5 fullwidth yen sign'); + + // Regular ASCII should not be full width + assert.strictEqual(strings.isFullWidthCharacter('A'.charCodeAt(0)), false, 'A regular ASCII'); + assert.strictEqual(strings.isFullWidthCharacter('?'.charCodeAt(0)), false, '? regular ASCII'); + }); + test('isBasicASCII', () => { function assertIsBasicASCII(str: string, expected: boolean): void { assert.strictEqual(strings.isBasicASCII(str), expected, str + ` (${str.charCodeAt(0)})`); diff --git a/src/vs/base/test/common/timeTravelScheduler.ts b/src/vs/base/test/common/timeTravelScheduler.ts index f706b997622..72f8d8985ce 100644 --- a/src/vs/base/test/common/timeTravelScheduler.ts +++ b/src/vs/base/test/common/timeTravelScheduler.ts @@ -38,15 +38,19 @@ const scheduledTaskComparator = tieBreakComparators( export class TimeTravelScheduler implements Scheduler { private taskCounter = 0; - private _now: TimeOffset = 0; + private _nowMs: TimeOffset = 0; private readonly queue: PriorityQueue = new SimplePriorityQueue([], scheduledTaskComparator); private readonly taskScheduledEmitter = new Emitter<{ task: ScheduledTask }>(); public readonly onTaskScheduled = this.taskScheduledEmitter.event; + constructor(startTimeMs: number) { + this._nowMs = startTimeMs; + } + schedule(task: ScheduledTask): IDisposable { - if (task.time < this._now) { - throw new Error(`Scheduled time (${task.time}) must be equal to or greater than the current time (${this._now}).`); + if (task.time < this._nowMs) { + throw new Error(`Scheduled time (${task.time}) must be equal to or greater than the current time (${this._nowMs}).`); } const extendedTask: ExtendedScheduledTask = { ...task, id: this.taskCounter++ }; this.queue.add(extendedTask); @@ -55,7 +59,7 @@ export class TimeTravelScheduler implements Scheduler { } get now(): TimeOffset { - return this._now; + return this._nowMs; } get hasScheduledTasks(): boolean { @@ -69,7 +73,7 @@ export class TimeTravelScheduler implements Scheduler { runNext(): ScheduledTask | undefined { const task = this.queue.removeMin(); if (task) { - this._now = task.time; + this._nowMs = task.time; task.run(); } @@ -164,26 +168,30 @@ export class AsyncSchedulerProcessor extends Disposable { } -export async function runWithFakedTimers(options: { useFakeTimers?: boolean; useSetImmediate?: boolean; maxTaskCount?: number }, fn: () => Promise): Promise { +export async function runWithFakedTimers(options: { startTime?: number; useFakeTimers?: boolean; useSetImmediate?: boolean; maxTaskCount?: number }, fn: () => Promise): Promise { const useFakeTimers = options.useFakeTimers === undefined ? true : options.useFakeTimers; if (!useFakeTimers) { return fn(); } - const scheduler = new TimeTravelScheduler(); + const scheduler = new TimeTravelScheduler(options.startTime ?? 0); const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { useSetImmediate: options.useSetImmediate, maxTaskCount: options.maxTaskCount }); const globalInstallDisposable = scheduler.installGlobally(); + let didThrow = true; let result: T; try { result = await fn(); + didThrow = false; } finally { globalInstallDisposable.dispose(); try { - // We process the remaining scheduled tasks. - // The global override is no longer active, so during this, no more tasks will be scheduled. - await schedulerProcessor.waitForEmptyQueue(); + if (!didThrow) { + // We process the remaining scheduled tasks. + // The global override is no longer active, so during this, no more tasks will be scheduled. + await schedulerProcessor.waitForEmptyQueue(); + } } finally { schedulerProcessor.dispose(); } diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index d940031606a..f0812718722 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -922,6 +922,7 @@ suite('Types', () => { type B = { b: number }; const obj: A | B = { b: 42 }; + // @ts-expect-error assert(!types.hasKey(obj, { a: true })); }); @@ -962,12 +963,17 @@ suite('Types', () => { const objB: TypeA | TypeB | TypeC = { kind: 'b', count: 5 }; assert(types.hasKey(objA, { value: true })); + // @ts-expect-error assert(!types.hasKey(objA, { count: true })); - // assert(!types.hasKey(objA, { items: true })); + // @ts-expect-error + assert(!types.hasKey(objA, { items: true })); + // @ts-expect-error assert(!types.hasKey(objB, { value: true })); - // assert(types.hasKey(objB, { count: true })); - // assert(!types.hasKey(objB, { items: true })); + // @ts-expect-error + assert(types.hasKey(objB, { count: true })); + // @ts-expect-error + assert(!types.hasKey(objB, { items: true })); }); test('should handle objects with optional properties', () => { @@ -989,6 +995,7 @@ suite('Types', () => { const obj: A | B = { data: { nested: 'test' } }; assert(types.hasKey(obj, { data: true })); + // @ts-expect-error assert(!types.hasKey(obj, { value: true })); }); }); diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index 0e64d16cb80..e2f22d32ef8 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -635,4 +635,15 @@ suite('URI', () => { assert.strictEqual(URI.parse('http://user@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html').toString(), 'http://user@[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:80/index.html'); assert.strictEqual(URI.parse('http://us[er@[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html').toString(), 'http://us%5Ber@[fedc:ba98:7654:3210:fedc:ba98:7654:3210]:80/index.html'); }); + + test('File paths containing apostrophes break URI parsing and cannot be opened #276075', function () { + if (isWindows) { + const filePath = 'C:\\Users\\Abd-al-Haseeb\'s_Dell\\Studio\\w3mage\\wp-content\\database.ht.sqlite'; + const uri = URI.file(filePath); + assert.strictEqual(uri.path, '/C:/Users/Abd-al-Haseeb\'s_Dell/Studio/w3mage/wp-content/database.ht.sqlite'); + assert.strictEqual(uri.fsPath, 'c:\\Users\\Abd-al-Haseeb\'s_Dell\\Studio\\w3mage\\wp-content\\database.ht.sqlite'); + } + }); + + }); diff --git a/src/vs/base/test/node/ps.test.ts b/src/vs/base/test/node/ps.test.ts new file mode 100644 index 00000000000..e3d103205e4 --- /dev/null +++ b/src/vs/base/test/node/ps.test.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../common/utils.js'; +import { JS_FILENAME_PATTERN } from '../../node/ps.js'; + +suite('Process Utils', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('JS file regex', () => { + + function findJsFiles(cmd: string): string[] { + const matches: string[] = []; + let match; + while ((match = JS_FILENAME_PATTERN.exec(cmd)) !== null) { + matches.push(match[0]); + } + return matches; + } + + test('should match simple .js files', () => { + deepStrictEqual(findJsFiles('node bootstrap.js'), ['bootstrap.js']); + }); + + test('should match multiple .js files', () => { + deepStrictEqual(findJsFiles('node server.js --require helper.js'), ['server.js', 'helper.js']); + }); + + test('should match .js files with hyphens', () => { + deepStrictEqual(findJsFiles('node my-script.js'), ['my-script.js']); + }); + + test('should not match .json files', () => { + deepStrictEqual(findJsFiles('cat package.json'), []); + }); + + test('should not match .js prefix in .json extension (regression test for \\b fix)', () => { + // Without the \b word boundary, the regex would incorrectly match "package.js" from "package.json" + deepStrictEqual(findJsFiles('node --config tsconfig.json'), []); + deepStrictEqual(findJsFiles('eslint.json'), []); + }); + + test('should not match .jsx files', () => { + deepStrictEqual(findJsFiles('node component.jsx'), []); + }); + + test('should match .js but not .json in same command', () => { + deepStrictEqual(findJsFiles('node app.js --config settings.json'), ['app.js']); + }); + + test('should not match partial matches inside other extensions', () => { + deepStrictEqual(findJsFiles('file.jsmith'), []); + }); + + test('should match .js at end of command', () => { + deepStrictEqual(findJsFiles('/path/to/script.js'), ['script.js']); + }); + }); +}); + diff --git a/src/vs/code/electron-browser/workbench/workbench-dev.html b/src/vs/code/electron-browser/workbench/workbench-dev.html index 1121fc7c047..13ff778a58c 100644 --- a/src/vs/code/electron-browser/workbench/workbench-dev.html +++ b/src/vs/code/electron-browser/workbench/workbench-dev.html @@ -73,5 +73,5 @@ - + diff --git a/src/vs/code/electron-browser/workbench/workbench.ts b/src/vs/code/electron-browser/workbench/workbench.ts index 7d6c8fac0c7..767fbe08db3 100644 --- a/src/vs/code/electron-browser/workbench/workbench.ts +++ b/src/vs/code/electron-browser/workbench/workbench.ts @@ -24,7 +24,11 @@ function showSplash(configuration: INativeWindowConfiguration) { performance.mark('code/willShowPartsSplash'); + showDefaultSplash(configuration); + performance.mark('code/didShowPartsSplash'); + } + function showDefaultSplash(configuration: INativeWindowConfiguration) { let data = configuration.partsSplash; if (data) { if (configuration.autoDetectHighContrast && configuration.colorScheme.highContrast) { @@ -265,15 +269,13 @@ window.document.body.appendChild(splash); } - - performance.mark('code/didShowPartsSplash'); } //#endregion //#region Window Helpers - async function load(esModule: string, options: ILoadOptions): Promise> { + async function load(options: ILoadOptions): Promise> { // Window Configuration from Preload Script const configuration = await resolveWindowConfiguration(); @@ -296,8 +298,14 @@ // ESM Import try { - const result = await import(new URL(`${esModule}.js`, baseUrl).href); + let workbenchUrl: string; + if (!!safeProcess.env['VSCODE_DEV'] && globalThis._VSCODE_USE_RELATIVE_IMPORTS) { + workbenchUrl = '../../../workbench/workbench.desktop.main.js'; // for dev purposes only + } else { + workbenchUrl = new URL(`vs/workbench/workbench.desktop.main.js`, baseUrl).href; + } + const result = await import(workbenchUrl); if (developerDeveloperKeybindingsDisposable && removeDeveloperKeybindingsAfterLoad) { developerDeveloperKeybindingsDisposable(); } @@ -449,6 +457,10 @@ // DEV: a blob URL that loads the CSS via a dynamic @import-rule. // DEV --------------------------------------------------------------------------------------- + if (globalThis._VSCODE_DISABLE_CSS_IMPORT_MAP) { + return; // disabled in certain development setups + } + if (Array.isArray(configuration.cssModules) && configuration.cssModules.length > 0) { performance.mark('code/willAddCssLoader'); @@ -484,7 +496,7 @@ //#endregion - const { result, configuration } = await load('vs/workbench/workbench.desktop.main', + const { result, configuration } = await load( { configureDeveloperSettings: function (windowConfig) { return { diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 35ea469b72a..d3ca580a42c 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -144,6 +144,14 @@ class CodeMain { evt.join('instanceLockfile', promises.unlink(environmentMainService.mainLockfile).catch(() => { /* ignored */ })); }); + // Check if Inno Setup is running + const innoSetupActive = await this.checkInnoSetupMutex(productService); + if (innoSetupActive) { + const message = `${productService.nameShort} is currently being updated. Please wait for the update to complete before launching.`; + instantiationService.invokeFunction(this.quit, new Error(message)); + return; + } + return instantiationService.createInstance(CodeApplication, mainProcessNodeIpcServer, instanceEnvironment).startup(); }); } catch (error) { @@ -487,6 +495,21 @@ class CodeMain { lifecycleMainService.kill(exitCode); } + private async checkInnoSetupMutex(productService: IProductService): Promise { + if (!(isWindows && productService.win32MutexName && productService.win32VersionedUpdate)) { + return false; + } + + try { + const readyMutexName = `${productService.win32MutexName}setup`; + const mutex = await import('@vscode/windows-mutex'); + return mutex.isActive(readyMutexName); + } catch (error) { + console.error('Failed to check Inno Setup mutex:', error); + return false; + } + } + //#region Command line arguments utilities private resolveArgs(): NativeParsedArgs { diff --git a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts index 0098f682199..f940df7cd09 100644 --- a/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts +++ b/src/vs/code/electron-utility/sharedProcess/contrib/defaultExtensionsInitializer.ts @@ -13,6 +13,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { FileOperationResult, IFileService, IFileStat, toFileOperationResult } from '../../../../platform/files/common/files.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; const defaultExtensionsInitStatusKey = 'initializing-default-extensions'; @@ -23,6 +24,7 @@ export class DefaultExtensionsInitializer extends Disposable { @IStorageService storageService: IStorageService, @IFileService private readonly fileService: IFileService, @ILogService private readonly logService: ILogService, + @IProductService private readonly productService: IProductService, ) { super(); @@ -70,9 +72,15 @@ export class DefaultExtensionsInitializer extends Disposable { } private getDefaultExtensionVSIXsLocation(): URI { - // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app - // extensionsPath = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\bootstrap\extensions - return URI.file(join(dirname(dirname(this.environmentService.appRoot)), 'bootstrap', 'extensions')); + if (this.productService.win32VersionedUpdate) { + // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\\resources\app + // extensionsPath = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\\bootstrap\extensions + return URI.file(join(dirname(dirname(dirname(this.environmentService.appRoot))), 'bootstrap', 'extensions')); + } else { + // appRoot = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\resources\app + // extensionsPath = C:\Users\\AppData\Local\Programs\Microsoft VS Code Insiders\bootstrap\extensions + return URI.file(join(dirname(dirname(this.environmentService.appRoot)), 'bootstrap', 'extensions')); + } } } diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5c52caf40c2..b3bdca721dd 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -5,7 +5,7 @@ import { ChildProcess, spawn, SpawnOptions, StdioOptions } from 'child_process'; import { chmodSync, existsSync, readFileSync, statSync, truncateSync, unlinkSync } from 'fs'; -import { homedir, release, tmpdir } from 'os'; +import { homedir, tmpdir } from 'os'; import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { Event } from '../../base/common/event.js'; import { isAbsolute, resolve, join, dirname } from '../../base/common/path.js'; @@ -320,8 +320,6 @@ export async function main(argv: string[]): Promise { } } - const isMacOSBigSurOrNewer = isMacintosh && release() > '20.0.0'; - // If we are started with --wait create a random temporary file // and pass it over to the starting instance. We can use this file // to wait for it to be deleted to monitor that the edited file @@ -339,8 +337,8 @@ export async function main(argv: string[]): Promise { // - the launched process terminates (e.g. due to a crash) processCallbacks.push(async child => { let childExitPromise; - if (isMacOSBigSurOrNewer) { - // On Big Sur, we resolve the following promise only when the child, + if (isMacintosh) { + // On macOS, we resolve the following promise only when the child, // i.e. the open command, exited with a signal or error. Otherwise, we // wait for the marker file to be deleted or for the child to error. childExitPromise = new Promise(resolve => { @@ -482,15 +480,14 @@ export async function main(argv: string[]): Promise { } let child: ChildProcess; - if (!isMacOSBigSurOrNewer) { + if (!isMacintosh) { if (!args.verbose && args.status) { options['stdio'] = ['ignore', 'pipe', 'ignore']; // restore ability to see output when --status is used } - // We spawn process.execPath directly child = spawn(process.execPath, argv.slice(2), options); } else { - // On Big Sur, we spawn using the open command to obtain behavior + // On macOS, we spawn using the open command to obtain behavior // similar to if the app was launched from the dock // https://github.com/microsoft/vscode/issues/102975 diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index e0a2115ade8..41d94cc492f 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -314,27 +314,27 @@ class CliMain extends Disposable { // List Extensions if (this.argv['list-extensions']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).listExtensions(!!this.argv['show-versions'], this.argv['category'], profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).listExtensions(!!this.argv['show-versions'], this.argv['category'], profileLocation); } // Install Extension else if (this.argv['install-extension'] || this.argv['install-builtin-extension']) { const installOptions: InstallOptions = { isMachineScoped: !!this.argv['do-not-sync'], installPreReleaseVersion: !!this.argv['pre-release'], donotIncludePackAndDependencies: !!this.argv['do-not-include-pack-dependencies'], profileLocation }; - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.asExtensionIdOrVSIX(this.argv['install-builtin-extension'] || []), installOptions, !!this.argv['force']); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).installExtensions(this.asExtensionIdOrVSIX(this.argv['install-extension'] || []), this.asExtensionIdOrVSIX(this.argv['install-builtin-extension'] || []), installOptions, !!this.argv['force']); } // Uninstall Extension else if (this.argv['uninstall-extension']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force'], profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).uninstallExtensions(this.asExtensionIdOrVSIX(this.argv['uninstall-extension']), !!this.argv['force'], profileLocation); } else if (this.argv['update-extensions']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).updateExtensions(profileLocation); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).updateExtensions(profileLocation); } // Locate Extension else if (this.argv['locate-extension']) { - return instantiationService.createInstance(ExtensionManagementCLI, new ConsoleLogger(LogLevel.Info, false)).locateExtension(this.argv['locate-extension']); + return instantiationService.createInstance(ExtensionManagementCLI, [], new ConsoleLogger(LogLevel.Info, false)).locateExtension(this.argv['locate-extension']); } // Install MCP server diff --git a/src/vs/editor/browser/config/elementSizeObserver.ts b/src/vs/editor/browser/config/elementSizeObserver.ts index f6c6ac5e926..c1f886e2c35 100644 --- a/src/vs/editor/browser/config/elementSizeObserver.ts +++ b/src/vs/editor/browser/config/elementSizeObserver.ts @@ -47,10 +47,10 @@ export class ElementSizeObserver extends Disposable { // Otherwise we will postpone to the next animation frame. // We'll use `observeContentRect` to store the content rect we received. - let observedDimenstion: IDimension | null = null; + let observedDimension: IDimension | null = null; const observeNow = () => { - if (observedDimenstion) { - this.observe({ width: observedDimenstion.width, height: observedDimenstion.height }); + if (observedDimension) { + this.observe({ width: observedDimension.width, height: observedDimension.height }); } else { this.observe(); } @@ -76,9 +76,9 @@ export class ElementSizeObserver extends Disposable { this._resizeObserver = new ResizeObserver((entries) => { if (entries && entries[0] && entries[0].contentRect) { - observedDimenstion = { width: entries[0].contentRect.width, height: entries[0].contentRect.height }; + observedDimension = { width: entries[0].contentRect.width, height: entries[0].contentRect.height }; } else { - observedDimenstion = null; + observedDimension = null; } shouldObserve = true; update(); diff --git a/src/vs/editor/browser/config/fontMeasurements.ts b/src/vs/editor/browser/config/fontMeasurements.ts index d9a5cb897d9..759add4ccc8 100644 --- a/src/vs/editor/browser/config/fontMeasurements.ts +++ b/src/vs/editor/browser/config/fontMeasurements.ts @@ -271,7 +271,7 @@ class FontMeasurementsCache { this._values[itemId] = value; } - public remove(item: BareFontInfo): void { + public remove(item: FontInfo): void { const itemId = item.getId(); delete this._keys[itemId]; delete this._values[itemId]; diff --git a/src/vs/editor/browser/config/migrateOptions.ts b/src/vs/editor/browser/config/migrateOptions.ts index 1d5584c88ab..5ecd03e14a0 100644 --- a/src/vs/editor/browser/config/migrateOptions.ts +++ b/src/vs/editor/browser/config/migrateOptions.ts @@ -251,3 +251,10 @@ registerEditorSettingMigration('inlineSuggest.edits.codeShifting', (value, read, write('inlineSuggest.edits.allowCodeShifting', value ? 'always' : 'never'); } }); + +// Migrate Hover +registerEditorSettingMigration('hover.enabled', (value, read, write) => { + if (typeof value === 'boolean') { + write('hover.enabled', value ? 'on' : 'off'); + } +}); diff --git a/src/vs/editor/browser/config/tabFocus.ts b/src/vs/editor/browser/config/tabFocus.ts index 6d821bc2725..4cf0b237248 100644 --- a/src/vs/editor/browser/config/tabFocus.ts +++ b/src/vs/editor/browser/config/tabFocus.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; -class TabFocusImpl { +class TabFocusImpl extends Disposable { private _tabFocus: boolean = false; - private readonly _onDidChangeTabFocus = new Emitter(); + private readonly _onDidChangeTabFocus = this._register(new Emitter()); public readonly onDidChangeTabFocus: Event = this._onDidChangeTabFocus.event; public getTabFocusMode(): boolean { diff --git a/src/vs/editor/browser/controller/dragScrolling.ts b/src/vs/editor/browser/controller/dragScrolling.ts index 830d4b5ec5d..bba8a03f777 100644 --- a/src/vs/editor/browser/controller/dragScrolling.ts +++ b/src/vs/editor/browser/controller/dragScrolling.ts @@ -134,6 +134,7 @@ export class TopBottomDragScrollingOperation extends DragScrollingOperation { const viewportData = this._context.viewLayout.getLinesViewportData(); const edgeLineNumber = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber : viewportData.endLineNumber); + const cannotScrollAnymore = (this._position.outsidePosition === 'above' ? viewportData.startLineNumber === 1 : viewportData.endLineNumber === this._context.viewModel.getLineCount()); // First, try to find a position that matches the horizontal position of the mouse let mouseTarget: IMouseTarget; @@ -144,7 +145,7 @@ export class TopBottomDragScrollingOperation extends DragScrollingOperation { const relativePos = createCoordinatesRelativeToEditor(this._viewHelper.viewDomNode, editorPos, pos); mouseTarget = this._mouseTargetFactory.createMouseTarget(this._viewHelper.getLastRenderData(), editorPos, pos, relativePos, null); } - if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber) { + if (!mouseTarget.position || mouseTarget.position.lineNumber !== edgeLineNumber || cannotScrollAnymore) { if (this._position.outsidePosition === 'above') { mouseTarget = MouseTarget.createOutsideEditor(this._position.mouseColumn, new Position(edgeLineNumber, 1), 'above', this._position.outsideDistance); } else { diff --git a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts index 66c01da1d36..47c64ef1c5b 100644 --- a/src/vs/editor/browser/controller/editContext/clipboardUtils.ts +++ b/src/vs/editor/browser/controller/editContext/clipboardUtils.ts @@ -6,18 +6,54 @@ import { IViewModel } from '../../../common/viewModel.js'; import { Range } from '../../../common/core/range.js'; import { isWindows } from '../../../../base/common/platform.js'; import { Mimes } from '../../../../base/common/mime.js'; +import { ViewContext } from '../../../common/viewModel/viewContext.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import { VSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { toExternalVSDataTransfer } from '../../dataTransfer.js'; -export function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy { - const rawTextToCopy = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows); +export function generateDataToCopyAndStoreInMemory(viewModel: IViewModel, id: string | undefined, isFirefox: boolean): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } { + const { dataToCopy, metadata } = generateDataToCopy(viewModel); + storeMetadataInMemory(dataToCopy.text, metadata, isFirefox); + return { dataToCopy, metadata }; +} + +function storeMetadataInMemory(textToCopy: string, metadata: ClipboardStoredMetadata, isFirefox: boolean): void { + InMemoryClipboardMetadataManager.INSTANCE.set( + // When writing "LINE\r\n" to the clipboard and then pasting, + // Firefox pastes "LINE\n", so let's work around this quirk + (isFirefox ? textToCopy.replace(/\r\n/g, '\n') : textToCopy), + metadata + ); +} + +function generateDataToCopy(viewModel: IViewModel): { dataToCopy: ClipboardDataToCopy; metadata: ClipboardStoredMetadata } { + const emptySelectionClipboard = viewModel.getEditorOption(EditorOption.emptySelectionClipboard); + const copyWithSyntaxHighlighting = viewModel.getEditorOption(EditorOption.copyWithSyntaxHighlighting); + const selections = viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection); + const dataToCopy = getDataToCopy(viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting); + const metadata: ClipboardStoredMetadata = { + version: 1, + id: generateUuid(), + isFromEmptySelection: dataToCopy.isFromEmptySelection, + multicursorText: dataToCopy.multicursorText, + mode: dataToCopy.mode + }; + return { dataToCopy, metadata }; +} + +function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], emptySelectionClipboard: boolean, copyWithSyntaxHighlighting: boolean): ClipboardDataToCopy { + const { sourceRanges, sourceText } = viewModel.getPlainTextToCopy(modelSelections, emptySelectionClipboard, isWindows); const newLineCharacter = viewModel.model.getEOL(); const isFromEmptySelection = (emptySelectionClipboard && modelSelections.length === 1 && modelSelections[0].isEmpty()); - const multicursorText = (Array.isArray(rawTextToCopy) ? rawTextToCopy : null); - const text = (Array.isArray(rawTextToCopy) ? rawTextToCopy.join(newLineCharacter) : rawTextToCopy); + const multicursorText = (Array.isArray(sourceText) ? sourceText : null); + const text = (Array.isArray(sourceText) ? sourceText.join(newLineCharacter) : sourceText); let html: string | null | undefined = undefined; let mode: string | null = null; - if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && text.length < 65536)) { + if (CopyOptions.forceCopyWithSyntaxHighlighting || (copyWithSyntaxHighlighting && sourceText.length < 65536)) { const richText = viewModel.getRichTextToCopy(modelSelections, emptySelectionClipboard); if (richText) { html = richText.html; @@ -26,6 +62,7 @@ export function getDataToCopy(viewModel: IViewModel, modelSelections: Range[], e } const dataToCopy: ClipboardDataToCopy = { isFromEmptySelection, + sourceRanges, multicursorText, text, html, @@ -64,6 +101,7 @@ export class InMemoryClipboardMetadataManager { export interface ClipboardDataToCopy { isFromEmptySelection: boolean; + sourceRanges: Range[]; multicursorText: string[] | null | undefined; text: string; html: string | null | undefined; @@ -79,7 +117,8 @@ export interface ClipboardStoredMetadata { } export const CopyOptions = { - forceCopyWithSyntaxHighlighting: false + forceCopyWithSyntaxHighlighting: false, + electronBugWorkaroundCopyEventHasFired: false }; interface InMemoryClipboardMetadata { @@ -87,9 +126,9 @@ interface InMemoryClipboardMetadata { data: ClipboardStoredMetadata; } -export const ClipboardEventUtils = { +const ClipboardEventUtils = { - getTextData(clipboardData: DataTransfer): [string, ClipboardStoredMetadata | null] { + getTextData(clipboardData: IReadableClipboardData | DataTransfer): [string, ClipboardStoredMetadata | null] { const text = clipboardData.getData(Mimes.text); let metadata: ClipboardStoredMetadata | null = null; const rawmetadata = clipboardData.getData('vscode-editor-data'); @@ -111,7 +150,7 @@ export const ClipboardEventUtils = { return [text, metadata]; }, - setTextData(clipboardData: DataTransfer, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void { + setTextData(clipboardData: IWritableClipboardData, text: string, html: string | null | undefined, metadata: ClipboardStoredMetadata): void { clipboardData.setData(Mimes.text, text); if (typeof html === 'string') { clipboardData.setData('text/html', html); @@ -119,3 +158,173 @@ export const ClipboardEventUtils = { clipboardData.setData('vscode-editor-data', JSON.stringify(metadata)); } }; + +/** + * Readable clipboard data for paste operations. + */ +export interface IReadableClipboardData { + /** + * All MIME types present in the clipboard. + */ + types: string[]; + + /** + * Files from the clipboard (for paste operations). + */ + readonly files: readonly File[]; + + /** + * Get data for a specific MIME type. + */ + getData(type: string): string; +} + +/** + * Writable clipboard data for copy/cut operations. + */ +export interface IWritableClipboardData { + /** + * Set data for a specific MIME type. + */ + setData(type: string, value: string): void; +} + +/** + * Event data for clipboard copy/cut events. + */ +export interface IClipboardCopyEvent { + /** + * Whether this is a cut operation. + */ + readonly isCut: boolean; + + /** + * The clipboard data to write to. + */ + readonly clipboardData: IWritableClipboardData; + + /** + * The data to be copied to the clipboard. + */ + readonly dataToCopy: ClipboardDataToCopy; + + /** + * Ensure that the clipboard gets the editor data. + */ + ensureClipboardGetsEditorData(): void; + + /** + * Signal that the event has been handled and default processing should be skipped. + */ + setHandled(): void; + + /** + * Whether the event has been marked as handled. + */ + readonly isHandled: boolean; +} + +/** + * Event data for clipboard paste events. + */ +export interface IClipboardPasteEvent { + /** + * The clipboard data being pasted. + */ + readonly clipboardData: IReadableClipboardData; + + /** + * The metadata stored alongside the clipboard data, if any. + */ + readonly metadata: ClipboardStoredMetadata | null; + + /** + * The text content being pasted. + */ + readonly text: string; + + /** + * The underlying DOM event, if available. + * @deprecated Use clipboardData instead. This is provided for backward compatibility. + */ + readonly browserEvent: ClipboardEvent | undefined; + + toExternalVSDataTransfer(): VSDataTransfer | undefined; + + /** + * Signal that the event has been handled and default processing should be skipped. + */ + setHandled(): void; + + /** + * Whether the event has been marked as handled. + */ + readonly isHandled: boolean; +} + +/** + * Creates an IClipboardCopyEvent from a DOM ClipboardEvent. + */ +export function createClipboardCopyEvent(e: ClipboardEvent, isCut: boolean, context: ViewContext, logService: ILogService, isFirefox: boolean): IClipboardCopyEvent { + const { dataToCopy, metadata } = generateDataToCopy(context.viewModel); + let handled = false; + return { + isCut, + clipboardData: { + setData: (type: string, value: string) => { + e.clipboardData?.setData(type, value); + }, + }, + dataToCopy, + ensureClipboardGetsEditorData: (): void => { + e.preventDefault(); + if (e.clipboardData) { + ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, metadata); + } + storeMetadataInMemory(dataToCopy.text, metadata, isFirefox); + logService.trace('ensureClipboardGetsEditorSelection with id : ', metadata.id, ' with text.length: ', dataToCopy.text.length); + }, + setHandled: () => { + handled = true; + e.preventDefault(); + e.stopImmediatePropagation(); + }, + get isHandled() { return handled; }, + }; +} + +/** + * Creates an IClipboardPasteEvent from a DOM ClipboardEvent. + */ +export function createClipboardPasteEvent(e: ClipboardEvent): IClipboardPasteEvent { + let handled = false; + let [text, metadata] = e.clipboardData ? ClipboardEventUtils.getTextData(e.clipboardData) : ['', null]; + metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); + return { + clipboardData: createReadableClipboardData(e.clipboardData), + metadata, + text, + toExternalVSDataTransfer: () => e.clipboardData ? toExternalVSDataTransfer(e.clipboardData) : undefined, + browserEvent: e, + setHandled: () => { + handled = true; + e.preventDefault(); + e.stopImmediatePropagation(); + }, + get isHandled() { return handled; }, + }; +} + +export function createReadableClipboardData(dataTransfer: DataTransfer | undefined | null): IReadableClipboardData { + return { + types: Array.from(dataTransfer?.types ?? []), + files: Array.prototype.slice.call(dataTransfer?.files ?? [], 0), + getData: (type: string) => dataTransfer?.getData(type) ?? '', + }; +} + +export function createWritableClipboardData(dataTransfer: DataTransfer | undefined | null): IWritableClipboardData { + return { + setData: (type: string, value: string) => dataTransfer?.setData(type, value), + }; +} diff --git a/src/vs/editor/browser/controller/editContext/editContext.ts b/src/vs/editor/browser/controller/editContext/editContext.ts index edcf2be3361..c5e677252f9 100644 --- a/src/vs/editor/browser/controller/editContext/editContext.ts +++ b/src/vs/editor/browser/controller/editContext/editContext.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { FastDomNode } from '../../../../base/browser/fastDomNode.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; import { Position } from '../../../common/core/position.js'; import { IEditorAriaOptions } from '../../editorBrowser.js'; import { ViewPart } from '../../view/viewPart.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './clipboardUtils.js'; export abstract class AbstractEditContext extends ViewPart { abstract domNode: FastDomNode; @@ -16,4 +18,14 @@ export abstract class AbstractEditContext extends ViewPart { abstract setAriaOptions(options: IEditorAriaOptions): void; abstract getLastRenderData(): Position | null; abstract writeScreenReaderContent(reason: string): void; + + // Clipboard events - emitted before the default clipboard handling + protected readonly _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + protected readonly _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + protected readonly _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; } diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index b417161930f..88d97714e7c 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -16,7 +16,7 @@ import { ViewConfigurationChangedEvent, ViewCursorStateChangedEvent, ViewDecorat import { ViewContext } from '../../../../common/viewModel/viewContext.js'; import { RestrictedRenderingContext, RenderingContext, HorizontalPosition } from '../../../view/renderingContext.js'; import { ViewController } from '../../../view/viewController.js'; -import { ClipboardEventUtils, ClipboardStoredMetadata, getDataToCopy, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent } from '../clipboardUtils.js'; import { AbstractEditContext } from '../editContext.js'; import { editContextAddDisposableListener, FocusTracker, ITypeData } from './nativeEditContextUtils.js'; import { ScreenReaderSupport } from './screenReaderSupport.js'; @@ -31,8 +31,9 @@ import { IEditorAriaOptions } from '../../../editorBrowser.js'; import { isHighSurrogate, isLowSurrogate } from '../../../../../base/common/strings.js'; import { IME } from '../../../../../base/common/ime.js'; import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; -import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { inputLatency } from '../../../../../base/browser/performance.js'; +import { ViewportData } from '../../../../common/viewLayout/viewLinesViewportData.js'; // Corresponds to classes in nativeEditContext.css enum CompositionClassName { @@ -61,6 +62,7 @@ export class NativeEditContext extends AbstractEditContext { // Overflow guard container private readonly _parent: HTMLElement; + private _parentBounds: DOMRect | null = null; private _decorations: string[] = []; private _primarySelection: Selection = new Selection(1, 1, 1, 1); @@ -114,51 +116,77 @@ export class NativeEditContext extends AbstractEditContext { this._register(addDisposableListener(this.domNode.domNode, 'copy', (e) => { this.logService.trace('NativeEditContext#copy'); - this._ensureClipboardGetsEditorSelection(e); + + // !!!!! + // This is a workaround for what we think is an Electron bug where + // execCommand('copy') does not always work (it does not fire a clipboard event) + // !!!!! + // We signal that we have executed a copy command + CopyOptions.electronBugWorkaroundCopyEventHasFired = true; + + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._context, this.logService, isFirefox); + this._onWillCopy.fire(copyEvent); + if (copyEvent.isHandled) { + return; + } + copyEvent.ensureClipboardGetsEditorData(); })); this._register(addDisposableListener(this.domNode.domNode, 'cut', (e) => { this.logService.trace('NativeEditContext#cut'); + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._context, this.logService, isFirefox); + this._onWillCut.fire(cutEvent); + if (cutEvent.isHandled) { + return; + } // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._screenReaderSupport.onWillCut(); - this._ensureClipboardGetsEditorSelection(e); + cutEvent.ensureClipboardGetsEditorData(); this.logService.trace('NativeEditContext#cut (before viewController.cut)'); this._viewController.cut(); })); + this._register(addDisposableListener(this.domNode.domNode, 'selectionchange', () => { + inputLatency.onSelectionChange(); + })); this._register(addDisposableListener(this.domNode.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this.domNode.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keyup', (e) => this._onKeyUp(e))); this._register(addDisposableListener(this._imeTextArea.domNode, 'keydown', async (e) => this._onKeyDown(e))); this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { + inputLatency.onBeforeInput(); if (e.inputType === 'insertParagraph' || e.inputType === 'insertLineBreak') { this._onType(this._viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } })); this._register(addDisposableListener(this.domNode.domNode, 'paste', (e) => { this.logService.trace('NativeEditContext#paste'); + const pasteEvent = createClipboardPasteEvent(e); + this._onWillPaste.fire(pasteEvent); + if (pasteEvent.isHandled) { + e.preventDefault(); + return; + } e.preventDefault(); if (!e.clipboardData) { return; } - let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - this.logService.trace('NativeEditContext#paste with id : ', metadata?.id, ' with text.length: ', text.length); - if (!text) { + this.logService.trace('NativeEditContext#paste with id : ', pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length); + if (!pasteEvent.text) { return; } - metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); let pasteOnNewLine = false; let multicursorText: string[] | null = null; let mode: string | null = null; - if (metadata) { + if (pasteEvent.metadata) { const options = this._context.configuration.options; const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - pasteOnNewLine = emptySelectionClipboard && !!metadata.isFromEmptySelection; - multicursorText = typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null; - mode = metadata.mode; + pasteOnNewLine = emptySelectionClipboard && !!pasteEvent.metadata.isFromEmptySelection; + multicursorText = typeof pasteEvent.metadata.multicursorText !== 'undefined' ? pasteEvent.metadata.multicursorText : null; + mode = pasteEvent.metadata.mode; } this.logService.trace('NativeEditContext#paste (before viewController.paste)'); - this._viewController.paste(text, pasteOnNewLine, multicursorText, mode); + this._viewController.paste(pasteEvent.text, pasteOnNewLine, multicursorText, mode); })); // Edit context events @@ -166,6 +194,7 @@ export class NativeEditContext extends AbstractEditContext { this._register(editContextAddDisposableListener(this._editContext, 'characterboundsupdate', (e) => this._updateCharacterBounds(e))); let highSurrogateCharacter: string | undefined; this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { + inputLatency.onInput(); const text = e.text; if (text.length === 1) { const charCode = text.charCodeAt(0); @@ -241,17 +270,21 @@ export class NativeEditContext extends AbstractEditContext { return this._primarySelection.getPosition(); } + public override onBeforeRender(viewportData: ViewportData): void { + // We need to read the position of the container dom node + // It is best to do this before we begin touching the DOM at all + // Because the sync layout will be fast if we do it here + this._parentBounds = this._parent.getBoundingClientRect(); + } + public override prepareRender(ctx: RenderingContext): void { this._screenReaderSupport.prepareRender(ctx); this._updateSelectionAndControlBoundsData(ctx); } - public override onDidRender(): void { - this._updateSelectionAndControlBoundsAfterRender(); - } - public render(ctx: RestrictedRenderingContext): void { this._screenReaderSupport.render(ctx); + this._updateSelectionAndControlBounds(); } public override onCursorStateChanged(e: ViewCursorStateChangedEvent): boolean { @@ -308,17 +341,17 @@ export class NativeEditContext extends AbstractEditContext { return true; } - public onWillPaste(): void { - this.logService.trace('NativeEditContext#onWillPaste'); - this._onWillPaste(); + public handleWillPaste(): void { + this.logService.trace('NativeEditContext#handleWillPaste'); + this._prepareScreenReaderForPaste(); } - private _onWillPaste(): void { + private _prepareScreenReaderForPaste(): void { this._screenReaderSupport.onWillPaste(); } - public onWillCopy(): void { - this.logService.trace('NativeEditContext#onWillCopy'); + public handleWillCopy(): void { + this.logService.trace('NativeEditContext#handleWillCopy'); this.logService.trace('NativeEditContext#isFocused : ', this.domNode.domNode === getActiveElement()); } @@ -355,10 +388,12 @@ export class NativeEditContext extends AbstractEditContext { // --- Private methods --- private _onKeyUp(e: KeyboardEvent) { + inputLatency.onKeyUp(); this._viewController.emitKeyUp(new StandardKeyboardEvent(e)); } private _onKeyDown(e: KeyboardEvent) { + inputLatency.onKeyDown(); const standardKeyboardEvent = new StandardKeyboardEvent(e); // When the IME is visible, the keys, like arrow-left and arrow-right, should be used to navigate in the IME, and should not be propagated further if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { @@ -498,7 +533,7 @@ export class NativeEditContext extends AbstractEditContext { } } - private _updateSelectionAndControlBoundsAfterRender() { + private _updateSelectionAndControlBounds() { const options = this._context.configuration.options; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; @@ -506,8 +541,9 @@ export class NativeEditContext extends AbstractEditContext { const verticalOffsetStart = this._context.viewLayout.getVerticalOffsetForLineNumber(viewSelection.startLineNumber); const verticalOffsetEnd = this._context.viewLayout.getVerticalOffsetAfterLineNumber(viewSelection.endLineNumber); - // Make sure this doesn't force an extra layout (i.e. don't call it before rendering finished) - const parentBounds = this._parent.getBoundingClientRect(); + // !!! Make sure this doesn't force an extra layout + // !!! by using the cached parent bounds read in onBeforeRender + const parentBounds = this._parentBounds!; const top = parentBounds.top + verticalOffsetStart - this._scrollTop; const height = verticalOffsetEnd - verticalOffsetStart; let left = parentBounds.left + contentLeft - this._scrollLeft; @@ -531,7 +567,7 @@ export class NativeEditContext extends AbstractEditContext { const options = this._context.configuration.options; const typicalHalfWidthCharacterWidth = options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; const contentLeft = options.get(EditorOption.layoutInfo).contentLeft; - const parentBounds = this._parent.getBoundingClientRect(); + const parentBounds = this._parentBounds!; const characterBounds: DOMRect[] = []; const offsetTransformer = new PositionOffsetTransformer(this._editContext.text); @@ -561,34 +597,4 @@ export class NativeEditContext extends AbstractEditContext { } this._editContext.updateCharacterBounds(e.rangeStart, characterBounds); } - - private _ensureClipboardGetsEditorSelection(e: ClipboardEvent): void { - const options = this._context.configuration.options; - const emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - const copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); - const selections = this._context.viewModel.getCursorStates().map(cursorState => cursorState.modelState.selection); - const dataToCopy = getDataToCopy(this._context.viewModel, selections, emptySelectionClipboard, copyWithSyntaxHighlighting); - let id = undefined; - if (this.logService.getLevel() === LogLevel.Trace) { - id = generateUuid(); - } - const storedMetadata: ClipboardStoredMetadata = { - version: 1, - id, - isFromEmptySelection: dataToCopy.isFromEmptySelection, - multicursorText: dataToCopy.multicursorText, - mode: dataToCopy.mode - }; - InMemoryClipboardMetadataManager.INSTANCE.set( - // When writing "LINE\r\n" to the clipboard and then pasting, - // Firefox pastes "LINE\n", so let's work around this quirk - (isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), - storedMetadata - ); - e.preventDefault(); - if (e.clipboardData) { - ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata); - } - this.logService.trace('NativeEditContext#_ensureClipboardGetsEditorSelectios with id : ', id, ' with text.length: ', dataToCopy.text.length); - } } diff --git a/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts b/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts index 97b3ab5edeb..dcb036e5a72 100644 --- a/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts +++ b/src/vs/editor/browser/controller/editContext/screenReaderUtils.ts @@ -27,7 +27,7 @@ export interface ISimpleScreenReaderContentState { selectionEnd: number; /** the editor range in the view coordinate system that matches the selection inside `value` */ - selection: Range; + selection: Selection; /** the position of the start of the `value` in the editor */ startPositionWithinEditor: Position; diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts index eb40085a1c2..f19cc424373 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContext.ts @@ -37,9 +37,9 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { AbstractEditContext } from '../editContext.js'; import { ICompositionData, IPasteData, ITextAreaInputHost, TextAreaInput, TextAreaWrapper } from './textAreaEditContextInput.js'; import { ariaLabelForScreenReaderContent, newlinecount, SimplePagedScreenReaderStrategy } from '../screenReaderUtils.js'; -import { ClipboardDataToCopy, getDataToCopy } from '../clipboardUtils.js'; import { _debugComposition, ITypeData, TextAreaState } from './textAreaEditContextState.js'; import { getMapForWordSeparators, WordCharacterClass } from '../../../../common/core/wordCharacterClassifier.js'; +import { TextAreaEditContextRegistry } from './textAreaEditContextRegistry.js'; export interface IVisibleRangeProvider { visibleRangeForPosition(position: Position): HorizontalPosition | null; @@ -126,7 +126,6 @@ export class TextAreaEditContext extends AbstractEditContext { private _contentHeight: number; private _fontInfo: FontInfo; private _emptySelectionClipboard: boolean; - private _copyWithSyntaxHighlighting: boolean; /** * Defined only when the text area is visible (composition case). @@ -146,6 +145,7 @@ export class TextAreaEditContext extends AbstractEditContext { private readonly _textAreaInput: TextAreaInput; constructor( + ownerID: string, context: ViewContext, overflowGuardContainer: FastDomNode, viewController: ViewController, @@ -169,7 +169,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); this._visibleTextArea = null; this._selections = [new Selection(1, 1, 1, 1)]; @@ -205,9 +204,7 @@ export class TextAreaEditContext extends AbstractEditContext { const simplePagedScreenReaderStrategy = new SimplePagedScreenReaderStrategy(); const textAreaInputHost: ITextAreaInputHost = { - getDataToCopy: (): ClipboardDataToCopy => { - return getDataToCopy(this._context.viewModel, this._modelSelections, this._emptySelectionClipboard, this._copyWithSyntaxHighlighting); - }, + context: this._context, getScreenReaderContent: (): TextAreaState => { if (this._accessibilitySupport === AccessibilitySupport.Disabled) { // We know for a fact that a screen reader is not attached @@ -280,6 +277,11 @@ export class TextAreaEditContext extends AbstractEditContext { isSafari: browser.isSafari, })); + // Relay clipboard events from TextAreaInput + this._register(this._textAreaInput.onWillCopy(e => this._onWillCopy.fire(e))); + this._register(this._textAreaInput.onWillCut(e => this._onWillCut.fire(e))); + this._register(this._textAreaInput.onWillPaste(e => this._onWillPaste.fire(e))); + this._register(this._textAreaInput.onKeyDown((e: IKeyboardEvent) => { this._viewController.emitKeyDown(e); })); @@ -445,6 +447,8 @@ export class TextAreaEditContext extends AbstractEditContext { this._register(IME.onDidChange(() => { this._ensureReadOnlyAttribute(); })); + + this._register(TextAreaEditContextRegistry.register(ownerID, this)); } public get domNode() { @@ -573,7 +577,6 @@ export class TextAreaEditContext extends AbstractEditContext { this._contentHeight = layoutInfo.height; this._fontInfo = options.get(EditorOption.fontInfo); this._emptySelectionClipboard = options.get(EditorOption.emptySelectionClipboard); - this._copyWithSyntaxHighlighting = options.get(EditorOption.copyWithSyntaxHighlighting); this.textArea.setAttribute('wrap', this._textAreaWrapping && !this._visibleTextArea ? 'on' : 'off'); const { tabSize } = this._context.viewModel.model.getOptions(); this.textArea.domNode.style.tabSize = `${tabSize * this._fontInfo.spaceWidth}px`; diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts index 4e4cb103bc5..774fc8ab10e 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextInput.ts @@ -17,10 +17,10 @@ import * as strings from '../../../../../base/common/strings.js'; import { Position } from '../../../../common/core/position.js'; import { Selection } from '../../../../common/core/selection.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; -import { ILogService, LogLevel } from '../../../../../platform/log/common/log.js'; -import { ClipboardDataToCopy, ClipboardEventUtils, ClipboardStoredMetadata, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ClipboardStoredMetadata, CopyOptions, createClipboardCopyEvent, createClipboardPasteEvent, IClipboardCopyEvent, IClipboardPasteEvent, InMemoryClipboardMetadataManager } from '../clipboardUtils.js'; import { _debugComposition, ITextAreaWrapper, ITypeData, TextAreaState } from './textAreaEditContextState.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ViewContext } from '../../../../common/viewModel/viewContext.js'; export namespace TextAreaSyntethicEvents { export const Tap = '-monaco-textarea-synthetic-tap'; @@ -37,7 +37,7 @@ export interface IPasteData { } export interface ITextAreaInputHost { - getDataToCopy(): ClipboardDataToCopy; + readonly context: ViewContext; getScreenReaderContent(): TextAreaState; deduceModelPosition(viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position; } @@ -127,6 +127,15 @@ export class TextAreaInput extends Disposable { private _onPaste = this._register(new Emitter()); public readonly onPaste: Event = this._onPaste.event; + private _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + private _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + private _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; + private _onType = this._register(new Emitter()); public readonly onType: Event = this._onType.event; @@ -359,44 +368,70 @@ export class TextAreaInput extends Disposable { this._register(this._textArea.onCut((e) => { this._logService.trace(`TextAreaInput#onCut`, e); + + // Fire onWillCut event to allow interception + const cutEvent = createClipboardCopyEvent(e, /* isCut */ true, this._host.context, this._logService, this._browser.isFirefox); + this._onWillCut.fire(cutEvent); + if (cutEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + // Pretend here we touched the text area, as the `cut` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received cut event'); - this._ensureClipboardGetsEditorSelection(e); + cutEvent.ensureClipboardGetsEditorData(); this._asyncTriggerCut.schedule(); })); this._register(this._textArea.onCopy((e) => { this._logService.trace(`TextAreaInput#onCopy`, e); - this._ensureClipboardGetsEditorSelection(e); + + // !!!!! + // This is a workaround for what we think is an Electron bug where + // execCommand('copy') does not always work (it does not fire a clipboard event) + // !!!!! + // We signal that we have executed a copy command + CopyOptions.electronBugWorkaroundCopyEventHasFired = true; + + // Fire onWillCopy event to allow interception + const copyEvent = createClipboardCopyEvent(e, /* isCut */ false, this._host.context, this._logService, this._browser.isFirefox); + this._onWillCopy.fire(copyEvent); + if (copyEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + + copyEvent.ensureClipboardGetsEditorData(); })); this._register(this._textArea.onPaste((e) => { this._logService.trace(`TextAreaInput#onPaste`, e); + + // Fire onWillPaste event to allow interception + const pasteEvent = createClipboardPasteEvent(e); + this._onWillPaste.fire(pasteEvent); + if (pasteEvent.isHandled) { + // Event was handled externally, skip default processing + return; + } + // Pretend here we touched the text area, as the `paste` event will most likely // result in a `selectionchange` event which we want to ignore this._textArea.setIgnoreSelectionChangeTime('received paste event'); e.preventDefault(); - if (!e.clipboardData) { - return; - } - - let [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - this._logService.trace(`TextAreaInput#onPaste with id : `, metadata?.id, ' with text.length: ', text.length); - if (!text) { + this._logService.trace(`TextAreaInput#onPaste with id : `, pasteEvent.metadata?.id, ' with text.length: ', pasteEvent.text.length); + if (!pasteEvent.text) { return; } - // try the in-memory store - metadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - this._logService.trace(`TextAreaInput#onPaste (before onPaste)`); this._onPaste.fire({ - text: text, - metadata: metadata + text: pasteEvent.text, + metadata: pasteEvent.metadata }); })); @@ -608,33 +643,6 @@ export class TextAreaInput extends Disposable { } this._setAndWriteTextAreaState(reason, this._host.getScreenReaderContent()); } - - private _ensureClipboardGetsEditorSelection(e: ClipboardEvent): void { - const dataToCopy = this._host.getDataToCopy(); - let id = undefined; - if (this._logService.getLevel() === LogLevel.Trace) { - id = generateUuid(); - } - const storedMetadata: ClipboardStoredMetadata = { - version: 1, - id, - isFromEmptySelection: dataToCopy.isFromEmptySelection, - multicursorText: dataToCopy.multicursorText, - mode: dataToCopy.mode - }; - InMemoryClipboardMetadataManager.INSTANCE.set( - // When writing "LINE\r\n" to the clipboard and then pasting, - // Firefox pastes "LINE\n", so let's work around this quirk - (this._browser.isFirefox ? dataToCopy.text.replace(/\r\n/g, '\n') : dataToCopy.text), - storedMetadata - ); - - e.preventDefault(); - if (e.clipboardData) { - ClipboardEventUtils.setTextData(e.clipboardData, dataToCopy.text, dataToCopy.html, storedMetadata); - } - this._logService.trace('TextAreaEditContextInput#_ensureClipboardGetsEditorSelection with id : ', id, ' with text.length: ', dataToCopy.text.length); - } } export class TextAreaWrapper extends Disposable implements ICompleteTextAreaWrapper { diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextRegistry.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextRegistry.ts new file mode 100644 index 00000000000..d52b4ee8f58 --- /dev/null +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextRegistry.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { TextAreaEditContext } from './textAreaEditContext.js'; + +class TextAreaEditContextRegistryImpl { + + private _textAreaEditContextMapping: Map = new Map(); + + register(ownerID: string, textAreaEditContext: TextAreaEditContext): IDisposable { + this._textAreaEditContextMapping.set(ownerID, textAreaEditContext); + return { + dispose: () => { + this._textAreaEditContextMapping.delete(ownerID); + } + }; + } + + get(ownerID: string): TextAreaEditContext | undefined { + return this._textAreaEditContextMapping.get(ownerID); + } +} + +export const TextAreaEditContextRegistry = new TextAreaEditContextRegistryImpl(); diff --git a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts index c8778b2bf45..9556337f01a 100644 --- a/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts +++ b/src/vs/editor/browser/controller/editContext/textArea/textAreaEditContextState.ts @@ -6,6 +6,7 @@ import { commonPrefixLength, commonSuffixLength } from '../../../../../base/common/strings.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; +import { SelectionDirection } from '../../../../common/core/selection.js'; import { ISimpleScreenReaderContentState } from '../screenReaderUtils.js'; export const _debugComposition = false; @@ -226,10 +227,23 @@ export class TextAreaState { } public static fromScreenReaderContentState(screenReaderContentState: ISimpleScreenReaderContentState) { + let selectionStart; + let selectionEnd; + const direction = screenReaderContentState.selection.getDirection(); + switch (direction) { + case SelectionDirection.LTR: + selectionStart = screenReaderContentState.selectionStart; + selectionEnd = screenReaderContentState.selectionEnd; + break; + case SelectionDirection.RTL: + selectionStart = screenReaderContentState.selectionEnd; + selectionEnd = screenReaderContentState.selectionStart; + break; + } return new TextAreaState( screenReaderContentState.value, - screenReaderContentState.selectionStart, - screenReaderContentState.selectionEnd, + selectionStart, + selectionEnd, screenReaderContentState.selection, screenReaderContentState.newlineCountBeforeSelection ); diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts index 9c9b398eb37..4ad4ca9d817 100644 --- a/src/vs/editor/browser/controller/mouseHandler.ts +++ b/src/vs/editor/browser/controller/mouseHandler.ts @@ -23,6 +23,7 @@ import { NavigationCommandRevealType } from '../coreCommands.js'; import { MouseWheelClassifier } from '../../../base/browser/ui/scrollbar/scrollableElement.js'; import type { ViewLinesGpu } from '../viewParts/viewLinesGpu/viewLinesGpu.js'; import { TopBottomDragScrolling, LeftRightDragScrolling } from './dragScrolling.js'; +import { TextDirection } from '../../common/model.js'; export interface IPointerHandlerHelper { viewDomNode: HTMLElement; @@ -576,7 +577,8 @@ class MouseDownOperation extends Disposable { const xLeftBoundary = layoutInfo.contentLeft; if (e.relativePos.x <= xLeftBoundary) { const outsideDistance = xLeftBoundary - e.relativePos.x; - return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, 1), 'left', outsideDistance); + const isRtl = model.getTextDirection(possibleLineNumber) === TextDirection.RTL; + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, isRtl ? model.getLineMaxColumn(possibleLineNumber) : 1), 'left', outsideDistance); } const contentRight = ( @@ -587,7 +589,8 @@ class MouseDownOperation extends Disposable { const xRightBoundary = contentRight; if (e.relativePos.x >= xRightBoundary) { const outsideDistance = e.relativePos.x - xRightBoundary; - return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance); + const isRtl = model.getTextDirection(possibleLineNumber) === TextDirection.RTL; + return MouseTarget.createOutsideEditor(mouseColumn, new Position(possibleLineNumber, isRtl ? 1 : model.getLineMaxColumn(possibleLineNumber)), 'right', outsideDistance); } return null; diff --git a/src/vs/editor/browser/coreCommands.ts b/src/vs/editor/browser/coreCommands.ts index 5669797012b..9331eea48ff 100644 --- a/src/vs/editor/browser/coreCommands.ts +++ b/src/vs/editor/browser/coreCommands.ts @@ -1323,8 +1323,7 @@ export namespace CoreNavigationCommands { EditorScroll_.Unit.WrappedLine, EditorScroll_.Unit.Page, EditorScroll_.Unit.HalfPage, - EditorScroll_.Unit.Editor, - EditorScroll_.Unit.Column + EditorScroll_.Unit.Editor ]; const horizontalDirections = [EditorScroll_.Direction.Left, EditorScroll_.Direction.Right]; const verticalDirections = [EditorScroll_.Direction.Up, EditorScroll_.Direction.Down]; @@ -1359,11 +1358,13 @@ export namespace CoreNavigationCommands { if (args.revealCursor) { // must ensure cursor is in new visible range const desiredVisibleViewRange = viewModel.getCompletelyVisibleViewRangeAtScrollTop(desiredScrollTop); + const paddedRange = viewModel.getViewRangeWithCursorPadding(desiredVisibleViewRange); + viewModel.setCursorStates( source, CursorChangeReason.Explicit, [ - CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), desiredVisibleViewRange, args.select) + CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), paddedRange, args.select) ] ); } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index c2cb0d3d596..0c018c2a75b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -27,6 +27,7 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelLanguag import { IEditorWhitespace, IViewModel } from '../common/viewModel.js'; import { OverviewRulerZone } from '../common/viewModel/overviewZoneManager.js'; import { IEditorConstructionOptions } from './config/editorConfiguration.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './controller/editContext/clipboardUtils.js'; /** * A view zone is a full horizontal rectangle that 'pushes' text down. @@ -178,6 +179,13 @@ export interface IContentWidget { * Render this content widget in a location where it could overflow the editor's view dom node. */ allowEditorOverflow?: boolean; + + /** + * If true, this widget doesn't have a visual representation. + * The element will have display set to 'none'. + */ + useDisplayNone?: boolean; + /** * Call preventDefault() on mousedown events that target the content widget. */ @@ -259,6 +267,8 @@ export interface IOverlayWidgetPositionCoordinates { left: number; } + + /** * A position for rendering overlay widgets. */ @@ -272,7 +282,7 @@ export interface IOverlayWidgetPosition { * When set, stacks with other overlay widgets with the same preference, * in an order determined by the ordinal value. */ - stackOridinal?: number; + stackOrdinal?: number; } /** * An overlay widgets renders on top of the text. @@ -718,6 +728,24 @@ export interface ICodeEditor extends editorCommon.IEditor { * @event */ readonly onDidPaste: Event; + /** + * An event emitted before clipboard copy operation starts. + * @internal + * @event + */ + readonly onWillCopy: Event; + /** + * An event emitted before clipboard cut operation starts. + * @internal + * @event + */ + readonly onWillCut: Event; + /** + * An event emitted before clipboard paste operation starts. + * @internal + * @event + */ + readonly onWillPaste: Event; /** * An event emitted on a "mouseup". * @event @@ -825,6 +853,8 @@ export interface ICodeEditor extends editorCommon.IEditor { */ readonly onEndUpdate: Event; + readonly onDidChangeViewZones: Event; + /** * Saves current view state of the editor in a serializable object. */ @@ -1012,6 +1042,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ executeCommands(source: string | null | undefined, commands: (editorCommon.ICommand | null)[]): void; + /** + * Scroll vertically or horizontally as necessary and reveal the current cursors. + */ + revealAllCursors(revealHorizontal: boolean, minimalReveal?: boolean): void; + /** * @internal */ @@ -1189,11 +1224,24 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getOffsetForColumn(lineNumber: number, column: number): number; + getWidthOfLine(lineNumber: number): number; + + /** + * Reset cached line widths. Call this when the editor becomes visible after being hidden. + * @internal + */ + resetLineWidthCaches(): void; + /** * Force an editor render now. */ render(forceRedraw?: boolean): void; + /** + * Render the editor at the next animation frame. + */ + renderAsync(forceRedraw?: boolean): void; + /** * Get the hit test target at coordinates `clientX` and `clientY`. * The coordinates are relative to the top-left of the viewport. @@ -1478,3 +1526,13 @@ export function getIEditor(thing: unknown): editorCommon.IEditor | null { return null; } + +/** + *@internal + */ +export function isIOverlayWidgetPositionCoordinates(thing: unknown): thing is IOverlayWidgetPositionCoordinates { + return !!thing + && typeof thing === 'object' + && typeof (thing).top === 'number' + && typeof (thing).left === 'number'; +} diff --git a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts index 43dbd85c87a..fde2973503e 100644 --- a/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts +++ b/src/vs/editor/browser/gpu/css/decorationCssRuleExtractor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getActiveDocument } from '../../../../base/browser/dom.js'; +import { $, getActiveDocument, getActiveWindow } from '../../../../base/browser/dom.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import './media/decorationCssRuleExtractor.css'; @@ -15,6 +15,7 @@ export class DecorationCssRuleExtractor extends Disposable { private _dummyElement: HTMLSpanElement; private _ruleCache: Map = new Map(); + private _cssVariableCache: Map = new Map(); constructor() { super(); @@ -49,36 +50,75 @@ export class DecorationCssRuleExtractor extends Disposable { private _getStyleRules(className: string) { // Iterate through all stylesheets and imported stylesheets to find matching rules - const rules = []; + const rules: CSSStyleRule[] = []; const doc = getActiveDocument(); const stylesheets = [...doc.styleSheets]; + + // className can be space-separated (e.g., 'ghost-text-decoration syntax-highlighted') + // We need to search for each individual class + const classNames = className.split(' ').filter(c => c.length > 0); + for (let i = 0; i < stylesheets.length; i++) { const stylesheet = stylesheets[i]; - for (const rule of stylesheet.cssRules) { - if (rule instanceof CSSImportRule) { - if (rule.styleSheet) { - stylesheets.push(rule.styleSheet); - } - } else if (rule instanceof CSSStyleRule) { - // Note that originally `.matches(rule.selectorText)` was used but this would - // not pick up pseudo-classes which are important to determine support of the - // returned styles. - // - // Since a selector could contain a class name lookup that is simple a prefix of - // the class name we are looking for, we need to also check the character after - // it. + this._collectMatchingRules(stylesheet.cssRules, classNames, rules); + } + + return rules; + } + + private _collectMatchingRules(cssRules: CSSRuleList, classNames: string[], result: CSSStyleRule[]): void { + for (const rule of cssRules) { + if (rule instanceof CSSImportRule) { + if (rule.styleSheet) { + this._collectMatchingRules(rule.styleSheet.cssRules, classNames, result); + } + } else if (rule instanceof CSSStyleRule) { + // Note that originally `.matches(rule.selectorText)` was used but this would + // not pick up pseudo-classes which are important to determine support of the + // returned styles. + // + // Since a selector could contain a class name lookup that is simple a prefix of + // the class name we are looking for, we need to also check the character after + // it. + for (const className of classNames) { const searchTerm = `.${className}`; const index = rule.selectorText.indexOf(searchTerm); if (index !== -1) { const endOfResult = index + searchTerm.length; - if (rule.selectorText.length === endOfResult || rule.selectorText.substring(endOfResult, endOfResult + 1).match(/[ :]/)) { - rules.push(rule); + if (rule.selectorText.length === endOfResult || rule.selectorText.substring(endOfResult, endOfResult + 1).match(/[ :.]/)) { + result.push(rule); + break; // Don't add the same rule multiple times } } } + // Recursively check nested rules (CSS nesting) + if (rule.cssRules?.length) { + this._collectMatchingRules(rule.cssRules, classNames, result); + } } } + } - return rules; + /** + * Resolves a CSS variable to its computed value using the container element. + */ + resolveCssVariable(canvas: HTMLCanvasElement, variableName: string): string { + let result = this._cssVariableCache.get(variableName); + if (result === undefined) { + canvas.appendChild(this._container); + result = getActiveWindow().getComputedStyle(this._container).getPropertyValue(variableName).trim(); + canvas.removeChild(this._container); + this._cssVariableCache.set(variableName, result); + } + return result; + } + + /** + * Clears all cached CSS rules and CSS variable values. This should be called when the theme + * changes to ensure fresh values are computed. + */ + clear(): void { + this._ruleCache.clear(); + this._cssVariableCache.clear(); } } diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 1b1c07df163..9a2eea56e1a 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -18,6 +18,18 @@ export interface IDecorationStyleSet { * A number between 0 and 1 representing the opacity of the text. */ opacity: number | undefined; + /** + * Whether the text should be rendered with a strikethrough. + */ + strikethrough: boolean | undefined; + /** + * The thickness of the strikethrough line in pixels (CSS pixels, not device pixels). + */ + strikethroughThickness: number | undefined; + /** + * A 32-bit number representing the strikethrough color. + */ + strikethroughColor: number | undefined; } export interface IDecorationStyleCacheEntry extends IDecorationStyleSet { @@ -32,29 +44,49 @@ export class DecorationStyleCache { private _nextId = 1; private readonly _cacheById = new Map(); - private readonly _cacheByStyle = new NKeyMap(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, bold: boolean | undefined, - opacity: number | undefined + opacity: number | undefined, + strikethrough: boolean | undefined, + strikethroughThickness: number | undefined, + strikethroughColor: number | undefined ): number { - if (color === undefined && bold === undefined && opacity === undefined) { + if (color === undefined && bold === undefined && opacity === undefined && strikethrough === undefined && strikethroughThickness === undefined && strikethroughColor === undefined) { return 0; } - const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + const result = this._cacheByStyle.get( + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); if (result) { return result.id; } const id = this._nextId++; - const entry = { + const entry: IDecorationStyleCacheEntry = { id, color, bold, opacity, + strikethrough, + strikethroughThickness, + strikethroughColor, }; this._cacheById.set(id, entry); - this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + this._cacheByStyle.set(entry, + color ?? 0, + bold ? 1 : 0, + opacity === undefined ? '' : opacity.toFixed(2), + strikethrough ? 1 : 0, + strikethroughThickness === undefined ? '' : strikethroughThickness.toFixed(2), + strikethroughColor ?? 0 + ); return id; } diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts index cc41c8dcaa4..0f56c94fd0b 100644 --- a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -7,7 +7,7 @@ import { memoize } from '../../../../base/common/decorators.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; -import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; +import { ColorId, FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; import type { DecorationStyleCache } from '../css/decorationStyleCache.js'; import { ensureNonNullable } from '../gpuUtils.js'; import { type IBoundingBox, type IGlyphRasterizer, type IRasterizedGlyph } from './raster.js'; @@ -115,10 +115,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { // The sub-pixel x offset is the fractional part of the x pixel coordinate of the cell, this // is used to improve the spacing between rendered characters. - const xSubPixelXOffset = (tokenMetadata & 0b1111) / 10; + const subPixelXOffset = (tokenMetadata & 0b1111) / 10; const bgId = TokenMetadata.getBackground(tokenMetadata); - const bg = colorMap[bgId]; + const bg = colorMap[bgId] ?? colorMap[ColorId.DefaultBackground]; const decorationStyleSet = this._decorationStyleCache.getStyleSet(decorationStyleSetId); @@ -145,26 +145,54 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { fontSb.appendString(`${devicePixelFontSize}px ${this.fontFamily}`); this._ctx.font = fontSb.build(); - // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these - // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for - // underlines. + // TODO: Support FontStyle.Underline text decorations, these need to be drawn manually to + // the canvas. See xterm.js for "dodging" the text for underlines. const originX = devicePixelFontSize; const originY = devicePixelFontSize; + + // Apply text color if (decorationStyleSet?.color !== undefined) { this._ctx.fillStyle = `#${decorationStyleSet.color.toString(16).padStart(8, '0')}`; } else { this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(tokenMetadata)]; } - this._ctx.textBaseline = 'top'; + // Apply opacity if (decorationStyleSet?.opacity !== undefined) { this._ctx.globalAlpha = decorationStyleSet.opacity; } - this._ctx.fillText(chars, originX + xSubPixelXOffset, originY); + // The glyph baseline is top, meaning it's drawn at the top-left of the + // cell. Add `TextMetrics.alphabeticBaseline` to the drawn position to + // get the alphabetic baseline. + this._ctx.textBaseline = 'top'; + + // Draw the text + this._ctx.fillText(chars, originX + subPixelXOffset, originY); + + // Draw strikethrough + if (decorationStyleSet?.strikethrough) { + // TODO: This position could be refined further by checking + // TextMetrics of lowercase letters. + // Position strikethrough at approximately the vertical center of + // lowercase letters. + const strikethroughY = Math.round(originY - this._textMetrics.alphabeticBaseline * 0.65); + const lineWidth = decorationStyleSet?.strikethroughThickness !== undefined + ? Math.round(decorationStyleSet.strikethroughThickness * this.devicePixelRatio) + : Math.max(1, Math.floor(devicePixelFontSize / 10)); + // Apply strikethrough color if specified + if (decorationStyleSet?.strikethroughColor !== undefined) { + this._ctx.fillStyle = `#${decorationStyleSet.strikethroughColor.toString(16).padStart(8, '0')}`; + } + // Intentionally do not apply the sub pixel x offset to + // strikethrough to ensure successive glyphs form a contiguous line. + this._ctx.fillRect(originX, strikethroughY - Math.floor(lineWidth / 2), Math.ceil(this._textMetrics.width), lineWidth); + } + this._ctx.restore(); + // Extract the image data and clear the background color const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); if (this._antiAliasing === 'subpixel') { const bgR = parseInt(bg.substring(1, 3), 16); @@ -173,7 +201,10 @@ export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { this._clearColor(imageData, bgR, bgG, bgB); this._ctx.putImageData(imageData, 0, 0); } + + // Find the bounding box this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); + // const offset = { // x: textMetrics.actualBoundingBoxLeft, // y: textMetrics.actualBoundingBoxAscent diff --git a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts index 9588c78c49c..e2b8cb5f92f 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/fullFileRenderStrategy.ts @@ -172,6 +172,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { } public override onScrollChanged(e?: ViewScrollChangedEvent): boolean { + if (this._store.isDisposed) { + return false; + } const dpr = getActiveWindow().devicePixelRatio; this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; @@ -257,6 +260,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -343,7 +349,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { contentSegmenter = createContentSegmenter(lineData, viewLineOptions); charWidth = viewLineOptions.spaceWidth * dpr; - absoluteOffsetX = 0; + absoluteOffsetX = (lineData.minColumn - 1) * charWidth; tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; @@ -375,6 +381,9 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -419,6 +428,36 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': { + let colorValue = value; + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -443,7 +482,7 @@ export class FullFileRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( diff --git a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts index 9dbfa48a53f..bfb1eb63449 100644 --- a/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts +++ b/src/vs/editor/browser/gpu/renderStrategy/viewportRenderStrategy.ts @@ -65,6 +65,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { private _activeDoubleBufferIndex: 0 | 1 = 0; private _visibleObjectCount: number = 0; + private _lastViewportLineCount: number = 0; private _scrollOffsetBindBuffer: GPUBuffer; private _scrollOffsetValueBuffer: Float32Array; @@ -116,6 +117,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { new ArrayBuffer(bufferSize), ]; this._cellBindBufferLineCapacity = lineCountWithIncrement; + this._lastViewportLineCount = 0; this._onDidChangeBindGroupEntries.fire(); } @@ -155,6 +157,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } public override onScrollChanged(e?: ViewScrollChangedEvent): boolean { + if (this._store.isDisposed) { + return false; + } const dpr = getActiveWindow().devicePixelRatio; this._scrollOffsetValueBuffer[0] = (e?.scrollLeft ?? this._context.viewLayout.getCurrentScrollLeft()) * dpr; this._scrollOffsetValueBuffer[1] = (e?.scrollTop ?? this._context.viewLayout.getCurrentScrollTop()) * dpr; @@ -183,6 +188,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { buffer.fill(0, 0, buffer.length); this._device.queue.writeBuffer(this._cellBindBuffer, 0, buffer.buffer, 0, buffer.byteLength); } + this._lastViewportLineCount = 0; } update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { @@ -209,6 +215,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { let decorationStyleSetBold: boolean | undefined; let decorationStyleSetColor: number | undefined; let decorationStyleSetOpacity: number | undefined; + let decorationStyleSetStrikethrough: boolean | undefined; + let decorationStyleSetStrikethroughThickness: number | undefined; + let decorationStyleSetStrikethroughColor: number | undefined; let lineData: ViewLineRenderingData; let decoration: InlineDecoration; @@ -246,7 +255,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { contentSegmenter = createContentSegmenter(lineData, viewLineOptions); charWidth = viewLineOptions.spaceWidth * dpr; - absoluteOffsetX = 0; + absoluteOffsetX = (lineData.minColumn - 1) * charWidth; tokens = lineData.tokens; tokenStartIndex = lineData.minColumn - 1; @@ -278,6 +287,9 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetColor = undefined; decorationStyleSetBold = undefined; decorationStyleSetOpacity = undefined; + decorationStyleSetStrikethrough = undefined; + decorationStyleSetStrikethroughThickness = undefined; + decorationStyleSetStrikethroughColor = undefined; // Apply supported inline decoration styles to the cell metadata for (decoration of lineData.inlineDecorations) { @@ -322,6 +334,36 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { decorationStyleSetOpacity = parsedValue; break; } + case 'text-decoration': + case 'text-decoration-line': { + if (value === 'line-through') { + decorationStyleSetStrikethrough = true; + } + break; + } + case 'text-decoration-thickness': { + const match = value.match(/^(\d+(?:\.\d+)?)px$/); + if (match) { + decorationStyleSetStrikethroughThickness = parseFloat(match[1]); + } + break; + } + case 'text-decoration-color': { + let colorValue = value; + const varMatch = value.match(/^var\((--[^,]+),\s*(?:initial|inherit)\)$/); + if (varMatch) { + colorValue = ViewGpuContext.decorationCssRuleExtractor.resolveCssVariable(this._viewGpuContext.canvas.domNode, varMatch[1]); + } + const parsedColor = Color.Format.CSS.parse(colorValue); + if (parsedColor) { + decorationStyleSetStrikethroughColor = parsedColor.toNumber32Bit(); + } + break; + } + case 'text-decoration-style': { + // These are validated in canRender and use default behavior + break; + } default: throw new BugIndicatingError('Unexpected inline decoration style'); } } @@ -346,7 +388,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { continue; } - const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity); + const decorationStyleSetId = ViewGpuContext.decorationStyleCache.getOrCreateEntry(decorationStyleSetColor, decorationStyleSetBold, decorationStyleSetOpacity, decorationStyleSetStrikethrough, decorationStyleSetStrikethroughThickness, decorationStyleSetStrikethroughColor); glyph = this._viewGpuContext.atlas.getGlyph(this.glyphRasterizer, chars, tokenMetadata, decorationStyleSetId, absoluteOffsetX); absoluteOffsetY = Math.round( @@ -382,6 +424,7 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { } const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; + const viewportLineCount = viewportData.endLineNumber - viewportData.startLineNumber + 1; // This render strategy always uploads the whole viewport this._device.queue.writeBuffer( @@ -389,9 +432,25 @@ export class ViewportRenderStrategy extends BaseRenderStrategy { 0, cellBuffer.buffer, 0, - (viewportData.endLineNumber - viewportData.startLineNumber) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + visibleObjectCount * Float32Array.BYTES_PER_ELEMENT ); + // Clear stale lines in GPU buffer if viewport shrunk + if (viewportLineCount < this._lastViewportLineCount) { + const staleLineCount = this._lastViewportLineCount - viewportLineCount; + const staleStartOffset = visibleObjectCount * Float32Array.BYTES_PER_ELEMENT; + const staleByteCount = staleLineCount * lineIndexCount * Float32Array.BYTES_PER_ELEMENT; + // Write zeros from the zeroed cellBuffer for the stale region + this._device.queue.writeBuffer( + this._cellBindBuffer, + staleStartOffset, + cellBuffer.buffer, + visibleObjectCount * Float32Array.BYTES_PER_ELEMENT, + staleByteCount + ); + } + this._lastViewportLineCount = viewportLineCount; + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; this._visibleObjectCount = visibleObjectCount; diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts index bab5b2f9408..9d520abdb3b 100644 --- a/src/vs/editor/browser/gpu/viewGpuContext.ts +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -6,6 +6,7 @@ import * as nls from '../../../nls.js'; import { addDisposableListener, getActiveWindow } from '../../../base/browser/dom.js'; import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js'; +import { Color } from '../../../base/common/color.js'; import { BugIndicatingError } from '../../../base/common/errors.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; @@ -15,6 +16,7 @@ import { IInstantiationService } from '../../../platform/instantiation/common/in import { TextureAtlas } from './atlas/textureAtlas.js'; import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; import { INotificationService, IPromptChoice, Severity } from '../../../platform/notification/common/notification.js'; +import { IThemeService } from '../../../platform/theme/common/themeService.js'; import { GPULifecycle } from './gpuDisposable.js'; import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; import { RectangleRenderer } from './rectangleRenderer.js'; @@ -81,6 +83,7 @@ export class ViewGpuContext extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotificationService private readonly _notificationService: INotificationService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private readonly _themeService: IThemeService, ) { super(); @@ -123,6 +126,12 @@ export class ViewGpuContext extends Disposable { this.devicePixelRatio = dprObs; this._register(runOnChange(this.devicePixelRatio, () => ViewGpuContext.atlas?.clear())); + // Clear decoration CSS caches when theme changes as CSS variables may have different values + this._register(this._themeService.onDidColorThemeChange(() => { + ViewGpuContext.decorationCssRuleExtractor.clear(); + ViewGpuContext.atlas?.clear(); + })); + const canvasDevicePixelDimensions = observableValue(this, { width: this.canvas.domNode.width, height: this.canvas.domNode.height }); this._register(observeDevicePixelDimensions( this.canvas.domNode, @@ -141,7 +150,7 @@ export class ViewGpuContext extends Disposable { })); this.contentLeft = contentLeft; - this.rectangleRenderer = this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device); + this.rectangleRenderer = this._register(this._instantiationService.createInstance(RectangleRenderer, context, this.contentLeft, this.devicePixelRatio, this.canvas.domNode, this.ctx, ViewGpuContext.device)); } /** @@ -255,6 +264,11 @@ const gpuSupportedDecorationCssRules = [ 'color', 'font-weight', 'opacity', + 'text-decoration', + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-decoration-thickness', ]; function supportsCssRule(rule: string, style: CSSStyleDeclaration) { @@ -263,6 +277,31 @@ function supportsCssRule(rule: string, style: CSSStyleDeclaration) { } // Check for values that aren't supported switch (rule) { + case 'text-decoration': + case 'text-decoration-line': { + const value = style.getPropertyValue(rule); + // Only line-through is supported currently + return value === 'line-through'; + } + case 'text-decoration-color': { + const value = style.getPropertyValue(rule); + // Support var(--something, initial/inherit) which falls back to currentcolor + if (/^var\(--[^,]+,\s*(?:initial|inherit)\)$/.test(value)) { + return true; + } + // Support parsed color values + return Color.Format.CSS.parse(value) !== null; + } + case 'text-decoration-style': { + const value = style.getPropertyValue(rule); + // Only 'initial' (solid) is supported + return value === 'initial'; + } + case 'text-decoration-thickness': { + const value = style.getPropertyValue(rule); + // Only pixel values and 'initial' are supported + return value === 'initial' || /^\d+(\.\d+)?px$/.test(value); + } default: return true; } } diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 5af51f2cf79..4c415ec53dd 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js'; +import { equalsIfDefinedC, arrayEqualsC } from '../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; -import { DebugLocation, IObservable, IObservableWithChange, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; +import { DebugLocation, IObservable, IObservableWithChange, IReader, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableFromEventOpts, observableSignal, observableSignalFromEvent, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; import { LineRange } from '../common/core/ranges/lineRange.js'; import { OffsetRange } from '../common/core/ranges/offsetRange.js'; @@ -74,19 +74,19 @@ export class ObservableCodeEditor extends Disposable { this._currentTransaction = undefined; this._model = observableValue(this, this.editor.getModel()); this.model = this._model; - this.isReadonly = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); + this.isReadonly = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.readOnly)); this._versionId = observableValueOpts({ owner: this, lazy: true }, this.editor.getModel()?.getVersionId() ?? null); this.versionId = this._versionId; this._selections = observableValueOpts( - { owner: this, equalsFn: equalsIfDefined(itemsEquals(Selection.selectionsEqual)), lazy: true }, + { owner: this, equalsFn: equalsIfDefinedC(arrayEqualsC(Selection.selectionsEqual)), lazy: true }, this.editor.getSelections() ?? null ); this.selections = this._selections; this.positions = derivedOpts( - { owner: this, equalsFn: equalsIfDefined(itemsEquals(Position.equals)) }, + { owner: this, equalsFn: equalsIfDefinedC(arrayEqualsC(Position.equals)) }, reader => this.selections.read(reader)?.map(s => s.getStartPosition()) ?? null ); - this.isFocused = observableFromEvent(this, e => { + this.isFocused = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, e => { const d1 = this.editor.onDidFocusEditorWidget(e); const d2 = this.editor.onDidBlurEditorWidget(e); return { @@ -96,7 +96,7 @@ export class ObservableCodeEditor extends Disposable { } }; }, () => this.editor.hasWidgetFocus()); - this.isTextFocused = observableFromEvent(this, e => { + this.isTextFocused = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, e => { const d1 = this.editor.onDidFocusEditorText(e); const d2 = this.editor.onDidBlurEditorText(e); return { @@ -106,7 +106,7 @@ export class ObservableCodeEditor extends Disposable { } }; }, () => this.editor.hasTextFocus()); - this.inComposition = observableFromEvent(this, e => { + this.inComposition = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, e => { const d1 = this.editor.onDidCompositionStart(() => { e(undefined); }); @@ -132,22 +132,25 @@ export class ObservableCodeEditor extends Disposable { } ); this.valueIsEmpty = derived(this, reader => { this.versionId.read(reader); return this.editor.getModel()?.getValueLength() === 0; }); - this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefined(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); + this.cursorSelection = derivedOpts({ owner: this, equalsFn: equalsIfDefinedC(Selection.selectionsEqual) }, reader => this.selections.read(reader)?.[0] ?? null); this.cursorPosition = derivedOpts({ owner: this, equalsFn: Position.equals }, reader => this.selections.read(reader)?.[0]?.getPosition() ?? null); this.cursorLineNumber = derived(this, reader => this.cursorPosition.read(reader)?.lineNumber ?? null); this.onDidType = observableSignal(this); this.onDidPaste = observableSignal(this); - this.scrollTop = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollTop()); - this.scrollLeft = observableFromEvent(this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); - this.layoutInfo = observableFromEvent(this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); + this.scrollTop = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidScrollChange, () => this.editor.getScrollTop()); + this.scrollLeft = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidScrollChange, () => this.editor.getScrollLeft()); + this.layoutInfo = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidLayoutChange, () => this.editor.getLayoutInfo()); this.layoutInfoContentLeft = this.layoutInfo.map(l => l.contentLeft); this.layoutInfoDecorationsLeft = this.layoutInfo.map(l => l.decorationsLeft); this.layoutInfoWidth = this.layoutInfo.map(l => l.width); this.layoutInfoHeight = this.layoutInfo.map(l => l.height); this.layoutInfoMinimap = this.layoutInfo.map(l => l.minimap); this.layoutInfoVerticalScrollbarWidth = this.layoutInfo.map(l => l.verticalScrollbarWidth); - this.contentWidth = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); - this.contentHeight = observableFromEvent(this.editor.onDidContentSizeChange, () => this.editor.getContentHeight()); + this.contentWidth = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidContentSizeChange, () => this.editor.getContentWidth()); + this.contentHeight = observableFromEventOpts({ owner: this, getTransaction: () => this._currentTransaction }, this.editor.onDidContentSizeChange, () => this.editor.getContentHeight()); + this._onDidChangeViewZones = observableSignalFromEvent(this, this.editor.onDidChangeViewZones); + this._onDidHiddenAreasChanged = observableSignalFromEvent(this, this.editor.onDidChangeHiddenAreas); + this._onDidLineHeightChanged = observableSignalFromEvent(this, this.editor.onDidChangeLineHeight); this._widgetCounter = 0; this.openedPeekWidgets = observableValue(this, 0); @@ -211,6 +214,22 @@ export class ObservableCodeEditor extends Disposable { }); } + /** + * Batches the transactions started by observableFromEvent. + * + * If the callback causes the editor to fire an event that updates + * an observable value backed by observableFromEvent (such as scrollTop etc.), + * then all such updates will be part of the same transaction. + */ + public transaction(cb: (tx: ITransaction) => T): T { + this._beginUpdate(); + try { + return cb(this._currentTransaction!); + } finally { + this._endUpdate(); + } + } + public forceUpdate(): void; public forceUpdate(cb: (tx: ITransaction) => T): T; public forceUpdate(cb?: (tx: ITransaction) => T): T { @@ -364,9 +383,26 @@ export class ObservableCodeEditor extends Disposable { }); } + /** + * Uses an approximation if the exact position cannot be determined. + */ + getLeftOfPosition(position: Position, reader: IReader | undefined): number { + this.layoutInfo.read(reader); + this.value.read(reader); + + let offset = this.editor.getOffsetForColumn(position.lineNumber, position.column); + if (offset === -1) { + // approximation + const typicalHalfwidthCharacterWidth = this.editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const approximation = position.column * typicalHalfwidthCharacterWidth; + offset = approximation; + } + return offset; + } + public observePosition(position: IObservable, store: DisposableStore): IObservable { let pos = position.get(); - const result = observableValueOpts({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefined(Point.equals) }, new Point(0, 0)); + const result = observableValueOpts({ owner: this, debugName: () => `topLeftOfPosition${pos?.toString()}`, equalsFn: equalsIfDefinedC(Point.equals) }, new Point(0, 0)); const contentWidgetId = `observablePositionWidget` + (this._widgetCounter++); const domNode = document.createElement('div'); const w: IContentWidget = { @@ -376,6 +412,7 @@ export class ObservableCodeEditor extends Disposable { }, getId: () => contentWidgetId, allowEditorOverflow: false, + useDisplayNone: true, afterRender: (position, coordinate) => { const model = this._model.get(); if (model && pos && pos.lineNumber > model.getLineCount()) { @@ -456,6 +493,84 @@ export class ObservableCodeEditor extends Disposable { }); } + private readonly _onDidChangeViewZones; + private readonly _onDidHiddenAreasChanged; + private readonly _onDidLineHeightChanged; + + /** + * Tracks whether getWidthOfLine returned 0, indicating the editor may be hidden. + * When resize happens and this flag is set, we reset cached line widths. + */ + private _sawZeroLineWidth = false; + + /** + * Fires when the editor container resizes. + * This is lazily created only when someone subscribes to it. + * Useful for detecting when a parent element's display changes from 'none' to 'block'. + */ + private readonly _onDidContainerResize = observableFromEventOpts( + { owner: this, getTransaction: () => this._currentTransaction }, + e => { + const container = this.editor.getContainerDomNode(); + const resizeObserver = new ResizeObserver(() => { + // If we previously saw a 0 width, the editor was likely hidden. + // Now that it resized (became visible), flush the cached widths. + if (this._sawZeroLineWidth) { + this._sawZeroLineWidth = false; + this.editor.resetLineWidthCaches(); + } + e(undefined); + }); + resizeObserver.observe(container); + return { dispose: () => resizeObserver.disconnect() }; + }, + () => ({}) // Return new object each time to ensure change detection + ); + + /** + * Get the width of a line in pixels. + * Reading the returned value depends on layoutInfo, value, scrollTop, and container resize events. + * The container resize dependency ensures correct values when the editor becomes visible after being hidden. + */ + getWidthOfLine(lineNumber: number, reader: IReader | undefined): number { + this.layoutInfo.read(reader); + this.value.read(reader); + this.scrollTop.read(reader); + const width = this.editor.getWidthOfLine(lineNumber); + this._onDidContainerResize.read(reader); + if (width === 0) { + this._sawZeroLineWidth = true; + } + return width; + } + + /** + * Get the vertical position (top offset) for the line's bottom w.r.t. to the first line. + */ + observeTopForLineNumber(lineNumber: number): IObservable { + return derived(reader => { + this.layoutInfo.read(reader); + this._onDidChangeViewZones.read(reader); + this._onDidHiddenAreasChanged.read(reader); + this._onDidLineHeightChanged.read(reader); + this._versionId.read(reader); + return this.editor.getTopForLineNumber(lineNumber); + }); + } + + /** + * Get the vertical position (top offset) for the line's bottom w.r.t. to the first line. + */ + observeBottomForLineNumber(lineNumber: number): IObservable { + return derived(reader => { + this.layoutInfo.read(reader); + this._onDidChangeViewZones.read(reader); + this._onDidHiddenAreasChanged.read(reader); + this._onDidLineHeightChanged.read(reader); + this._versionId.read(reader); + return this.editor.getBottomForLineNumber(lineNumber); + }); + } } interface IObservableOverlayWidget { diff --git a/src/vs/editor/browser/services/codeEditorService.ts b/src/vs/editor/browser/services/codeEditorService.ts index 3267586bb19..570d91f6c2f 100644 --- a/src/vs/editor/browser/services/codeEditorService.ts +++ b/src/vs/editor/browser/services/codeEditorService.ts @@ -43,7 +43,7 @@ export interface ICodeEditorService { */ getFocusedCodeEditor(): ICodeEditor | null; - registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): void; + registerDecorationType(description: string, key: string, options: IDecorationRenderOptions, parentTypeKey?: string, editor?: ICodeEditor): IDisposable; listDecorationTypes(): string[]; removeDecorationType(key: string): void; resolveDecorationOptions(typeKey: string, writable: boolean): IModelDecorationOptions; diff --git a/src/vs/editor/browser/services/contribution.ts b/src/vs/editor/browser/services/contribution.ts new file mode 100644 index 00000000000..cce0e3719d7 --- /dev/null +++ b/src/vs/editor/browser/services/contribution.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton, InstantiationType } from '../../../platform/instantiation/common/extensions.js'; +import { IEditorWorkerService } from '../../common/services/editorWorker.js'; +import { EditorContributionInstantiation, registerEditorContribution } from '../editorExtensions.js'; +import { EditorWorkerService } from './editorWorkerService.js'; +import { MarkerDecorationsContribution } from './markerDecorations.js'; + +/* registers link detection and word based suggestions for any document */ +registerSingleton(IEditorWorkerService, EditorWorkerService, InstantiationType.Eager); + +// eager because it instantiates IMarkerDecorationsService which is responsible for rendering squiggles +registerEditorContribution(MarkerDecorationsContribution.ID, MarkerDecorationsContribution, EditorContributionInstantiation.Eager); diff --git a/src/vs/editor/browser/services/editorWorkerService.ts b/src/vs/editor/browser/services/editorWorkerService.ts index d8cd71a153f..d5c878af3b3 100644 --- a/src/vs/editor/browser/services/editorWorkerService.ts +++ b/src/vs/editor/browser/services/editorWorkerService.ts @@ -7,7 +7,8 @@ import { timeout } from '../../../base/common/async.js'; import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; import { URI } from '../../../base/common/uri.js'; import { logOnceWebWorkerWarning, IWebWorkerClient, Proxied } from '../../../base/common/worker/webWorker.js'; -import { createWebWorker, IWebWorkerDescriptor } from '../../../base/browser/webWorkerFactory.js'; +import { WebWorkerDescriptor } from '../../../platform/webWorker/browser/webWorkerDescriptor.js'; +import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; import { Position } from '../../common/core/position.js'; import { IRange, Range } from '../../common/core/range.js'; import { ITextModel } from '../../common/model.js'; @@ -35,6 +36,8 @@ import { WorkerTextModelSyncClient } from '../../common/services/textModelSync/t import { EditorWorkerHost } from '../../common/services/editorWorkerHost.js'; import { StringEdit } from '../../common/core/edits/stringEdit.js'; import { OffsetRange } from '../../common/core/ranges/offsetRange.js'; +import { FileAccess } from '../../../base/common/network.js'; +import { isCompletionsEnabledWithTextResourceConfig } from '../../common/services/completionsEnablement.js'; /** * Stop the worker if it was not needed for 5 min. @@ -52,25 +55,32 @@ function canSyncModel(modelService: IModelService, resource: URI): boolean { return true; } -export abstract class EditorWorkerService extends Disposable implements IEditorWorkerService { +export class EditorWorkerService extends Disposable implements IEditorWorkerService { declare readonly _serviceBrand: undefined; + public static readonly workerDescriptor = new WebWorkerDescriptor({ + esmModuleLocation: () => FileAccess.asBrowserUri('vs/editor/common/services/editorWebWorkerMain.js'), + esmModuleLocationBundler: () => new URL('../../common/services/editorWebWorkerMain.ts?esm', import.meta.url), + label: 'editorWorkerService' + }); + private readonly _modelService: IModelService; private readonly _workerManager: WorkerManager; private readonly _logService: ILogService; constructor( - workerDescriptor: IWebWorkerDescriptor, @IModelService modelService: IModelService, @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, @ILogService logService: ILogService, @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IWebWorkerService private readonly _webWorkerService: IWebWorkerService, ) { super(); this._modelService = modelService; - this._workerManager = this._register(new WorkerManager(workerDescriptor, this._modelService)); + + this._workerManager = this._register(new WorkerManager(EditorWorkerService.workerDescriptor, this._modelService, this._webWorkerService)); this._logService = logService; // register default link-provider and default completions-provider @@ -84,7 +94,7 @@ export abstract class EditorWorkerService extends Disposable implements IEditorW return links && { links }; } })); - this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService))); + this._register(languageFeaturesService.completionProvider.register('*', new WordBasedCompletionItemProvider(this._workerManager, configurationService, this._modelService, this._languageConfigurationService, this._logService, languageFeaturesService))); } public override dispose(): void { @@ -254,7 +264,8 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide configurationService: ITextResourceConfigurationService, modelService: IModelService, private readonly languageConfigurationService: ILanguageConfigurationService, - private readonly logService: ILogService + private readonly logService: ILogService, + private readonly languageFeaturesService: ILanguageFeaturesService, ) { this._workerManager = workerManager; this._configurationService = configurationService; @@ -263,13 +274,19 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide async provideCompletionItems(model: ITextModel, position: Position): Promise { type WordBasedSuggestionsConfig = { - wordBasedSuggestions?: 'off' | 'currentDocument' | 'matchingDocuments' | 'allDocuments'; + wordBasedSuggestions?: 'off' | 'currentDocument' | 'matchingDocuments' | 'allDocuments' | 'offWithInlineSuggestions'; }; const config = this._configurationService.getValue(model.uri, position, 'editor'); if (config.wordBasedSuggestions === 'off') { return undefined; } + if (config.wordBasedSuggestions === 'offWithInlineSuggestions' + && this.languageFeaturesService.inlineCompletionsProvider.has(model) + && isCompletionsEnabledWithTextResourceConfig(this._configurationService, model.uri, model.getLanguageId())) { + return undefined; + } + const models: URI[] = []; if (config.wordBasedSuggestions === 'currentDocument') { // only current file and only if not too large @@ -326,15 +343,18 @@ class WordBasedCompletionItemProvider implements languages.CompletionItemProvide class WorkerManager extends Disposable { private readonly _modelService: IModelService; + private readonly _webWorkerService: IWebWorkerService; private _editorWorkerClient: EditorWorkerClient | null; private _lastWorkerUsedTime: number; constructor( - private readonly _workerDescriptor: IWebWorkerDescriptor, - @IModelService modelService: IModelService + private readonly _workerDescriptor: WebWorkerDescriptor, + @IModelService modelService: IModelService, + @IWebWorkerService webWorkerService: IWebWorkerService ) { super(); this._modelService = modelService; + this._webWorkerService = webWorkerService; this._editorWorkerClient = null; this._lastWorkerUsedTime = (new Date()).getTime(); @@ -386,7 +406,7 @@ class WorkerManager extends Disposable { public withWorker(): Promise { this._lastWorkerUsedTime = (new Date()).getTime(); if (!this._editorWorkerClient) { - this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService); + this._editorWorkerClient = new EditorWorkerClient(this._workerDescriptor, false, this._modelService, this._webWorkerService); } return Promise.resolve(this._editorWorkerClient); } @@ -421,18 +441,21 @@ export interface IEditorWorkerClient { export class EditorWorkerClient extends Disposable implements IEditorWorkerClient { private readonly _modelService: IModelService; + private readonly _webWorkerService: IWebWorkerService; private readonly _keepIdleModels: boolean; private _worker: IWebWorkerClient | null; private _modelManager: WorkerTextModelSyncClient | null; private _disposed = false; constructor( - private readonly _workerDescriptorOrWorker: IWebWorkerDescriptor | Worker | Promise, + private readonly _workerDescriptorOrWorker: WebWorkerDescriptor | Worker | Promise, keepIdleModels: boolean, @IModelService modelService: IModelService, + @IWebWorkerService webWorkerService: IWebWorkerService ) { super(); this._modelService = modelService; + this._webWorkerService = webWorkerService; this._keepIdleModels = keepIdleModels; this._worker = null; this._modelManager = null; @@ -446,7 +469,7 @@ export class EditorWorkerClient extends Disposable implements IEditorWorkerClien private _getOrCreateWorker(): IWebWorkerClient { if (!this._worker) { try { - this._worker = this._register(createWebWorker(this._workerDescriptorOrWorker)); + this._worker = this._register(this._webWorkerService.createWorkerClient(this._workerDescriptorOrWorker)); EditorWorkerHost.setChannel(this._worker, this._createEditorWorkerHost()); } catch (err) { logOnceWebWorkerWarning(err); diff --git a/src/vs/editor/browser/services/markerDecorations.ts b/src/vs/editor/browser/services/markerDecorations.ts index eb519c43fd9..cfdfd167faf 100644 --- a/src/vs/editor/browser/services/markerDecorations.ts +++ b/src/vs/editor/browser/services/markerDecorations.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IMarkerDecorationsService } from '../../common/services/markerDecorations.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../editorExtensions.js'; import { ICodeEditor } from '../editorBrowser.js'; import { IEditorContribution } from '../../common/editorCommon.js'; @@ -22,5 +21,3 @@ export class MarkerDecorationsContribution implements IEditorContribution { dispose(): void { } } - -registerEditorContribution(MarkerDecorationsContribution.ID, MarkerDecorationsContribution, EditorContributionInstantiation.Eager); // eager because it instantiates IMarkerDecorationsService which is responsible for rendering squiggles diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 7b02d07004e..1453d666dfa 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -170,10 +170,15 @@ export class OpenerService implements IOpenerService { } async open(target: URI | string, options?: OpenOptions): Promise { + const targetURI = typeof target === 'string' ? URI.parse(target) : target; + + // Internal schemes are not openable and must instead be handled in event listeners + if (targetURI.scheme === Schemas.internal) { + return false; + } // check with contributed validators if (!options?.skipValidation) { - const targetURI = typeof target === 'string' ? URI.parse(target) : target; const validationTarget = this._resolvedUriTargets.get(targetURI) ?? target; // validate against the original URI that this URI resolves to, if one exists for (const validator of this._validators) { if (!(await validator.shouldOpen(validationTarget, options))) { diff --git a/src/vs/editor/browser/services/renameSymbolTrackerService.ts b/src/vs/editor/browser/services/renameSymbolTrackerService.ts new file mode 100644 index 00000000000..fd3dbf5ea57 --- /dev/null +++ b/src/vs/editor/browser/services/renameSymbolTrackerService.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 { IObservable, observableValue } from '../../../base/common/observable.js'; +import { Position } from '../../common/core/position.js'; +import { Range } from '../../common/core/range.js'; +import { ITextModel } from '../../common/model.js'; +import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; + +export const IRenameSymbolTrackerService = createDecorator('renameSymbolTrackerService'); + +/** + * Represents a tracked word that is being edited by the user. + */ +export interface ITrackedWord { + /** + * The model in which the word is being tracked. + */ + readonly model: ITextModel; + /** + * The original word text when tracking started. + */ + readonly originalWord: string; + /** + * The original position where the word was found. + */ + readonly originalPosition: Position; + /** + * The original range of the word when tracking started. + */ + readonly originalRange: Range; + /** + * The current word text after edits. + */ + readonly currentWord: string; + /** + * The current range of the word after edits. + */ + readonly currentRange: Range; +} + +export interface IRenameSymbolTrackerService { + readonly _serviceBrand: undefined; + + /** + * Observable that emits the currently tracked word, or undefined if no word is being tracked. + */ + readonly trackedWord: IObservable; +} + +export class NullRenameSymbolTrackerService implements IRenameSymbolTrackerService { + declare readonly _serviceBrand: undefined; + + private readonly _trackedWord = observableValue(this, undefined); + public readonly trackedWord: IObservable = this._trackedWord; + constructor() { + this._trackedWord.set(undefined, undefined); + } +} diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 534f302d207..9a544c6d28f 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -9,7 +9,7 @@ import { IMouseWheelEvent } from '../../base/browser/mouseEvent.js'; import { inputLatency } from '../../base/browser/performance.js'; import { CodeWindow } from '../../base/browser/window.js'; import { BugIndicatingError, onUnexpectedError } from '../../base/common/errors.js'; -import { Disposable, IDisposable } from '../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable } from '../../base/common/lifecycle.js'; import { IPointerHandlerHelper } from './controller/mouseHandler.js'; import { PointerHandlerLastRenderData } from './controller/mouseTarget.js'; import { PointerHandler } from './controller/pointerHandler.js'; @@ -58,6 +58,7 @@ import { IColorTheme, getThemeTypeSelector } from '../../platform/theme/common/t import { ViewGpuContext } from './gpu/viewGpuContext.js'; import { ViewLinesGpu } from './viewParts/viewLinesGpu/viewLinesGpu.js'; import { AbstractEditContext } from './controller/editContext/editContext.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from './controller/editContext/clipboardUtils.js'; import { IVisibleRangeProvider, TextAreaEditContext } from './controller/editContext/textArea/textAreaEditContext.js'; import { NativeEditContext } from './controller/editContext/native/nativeEditContext.js'; import { RulersGpu } from './viewParts/rulersGpu/rulersGpu.js'; @@ -106,8 +107,19 @@ export class View extends ViewEventHandler { private _editContextEnabled: boolean; private _accessibilitySupport: AccessibilitySupport; private _editContext: AbstractEditContext; + private readonly _editContextClipboardListeners = new DisposableStore(); private readonly _pointerHandler: PointerHandler; + // Clipboard events relayed from editContext + private readonly _onWillCopy = this._register(new Emitter()); + public readonly onWillCopy: Event = this._onWillCopy.event; + + private readonly _onWillCut = this._register(new Emitter()); + public readonly onWillCut: Event = this._onWillCut.event; + + private readonly _onWillPaste = this._register(new Emitter()); + public readonly onWillPaste: Event = this._onWillPaste.event; + // Dom nodes private readonly _linesContent: FastDomNode; public readonly domNode: FastDomNode; @@ -160,6 +172,7 @@ export class View extends ViewEventHandler { this._editContextEnabled = this._context.configuration.options.get(EditorOption.effectiveEditContext); this._accessibilitySupport = this._context.configuration.options.get(EditorOption.accessibilitySupport); this._editContext = this._instantiateEditContext(); + this._connectEditContextClipboardEvents(); this._viewParts.push(this._editContext); @@ -293,7 +306,7 @@ export class View extends ViewEventHandler { if (usingExperimentalEditContext) { return this._instantiationService.createInstance(NativeEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } else { - return this._instantiationService.createInstance(TextAreaEditContext, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); + return this._instantiationService.createInstance(TextAreaEditContext, this._ownerID, this._context, this._overflowGuardContainer, this._viewController, this._createTextAreaHandlerHelper()); } } @@ -309,6 +322,7 @@ export class View extends ViewEventHandler { const indexOfEditContext = this._viewParts.indexOf(this._editContext); this._editContext.dispose(); this._editContext = this._instantiateEditContext(); + this._connectEditContextClipboardEvents(); if (isEditContextFocused) { this._editContext.focus(); } @@ -317,6 +331,16 @@ export class View extends ViewEventHandler { } } + private _connectEditContextClipboardEvents(): void { + // Dispose old listeners + this._editContextClipboardListeners.clear(); + + // Connect to current edit context's clipboard events + this._editContextClipboardListeners.add(this._editContext.onWillCopy(e => this._onWillCopy.fire(e))); + this._editContextClipboardListeners.add(this._editContext.onWillCut(e => this._onWillCut.fire(e))); + this._editContextClipboardListeners.add(this._editContext.onWillPaste(e => this._onWillPaste.fire(e))); + } + private _computeGlyphMarginLanes(): IGlyphMarginLanesModel { const model = this._context.viewModel.model; const laneModel = this._context.viewModel.glyphLanes; @@ -474,6 +498,9 @@ export class View extends ViewEventHandler { this._renderAnimationFrame = null; } + // Dispose clipboard event listeners + this._editContextClipboardListeners.dispose(); + this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(); this._overlayWidgets.overflowingOverlayWidgetsDomNode.domNode.remove(); @@ -513,11 +540,11 @@ export class View extends ViewEventHandler { this._renderAnimationFrame = null; } }, - renderText: () => { + renderText: (viewportData: ViewportData) => { if (this._store.isDisposed) { throw new BugIndicatingError(); } - return rendering.renderText(); + return rendering.renderText(viewportData); }, prepareRender: (viewParts: ViewPart[], ctx: RenderingContext) => { if (this._store.isDisposed) { @@ -537,13 +564,17 @@ export class View extends ViewEventHandler { private _flushAccumulatedAndRenderNow(): void { const rendering = this._createCoordinatedRendering(); - safeInvokeNoArg(() => rendering.prepareRenderText()); - const data = safeInvokeNoArg(() => rendering.renderText()); - if (data) { - const [viewParts, ctx] = data; - safeInvokeNoArg(() => rendering.prepareRender(viewParts, ctx)); - safeInvokeNoArg(() => rendering.render(viewParts, ctx)); + const viewportData = safeInvokeNoArg(() => rendering.prepareRenderText()); + if (!viewportData) { + return; } + const data = safeInvokeNoArg(() => rendering.renderText(viewportData)); + if (!data) { + return; + } + const [viewParts, ctx] = data; + safeInvokeNoArg(() => rendering.prepareRender(viewParts, ctx)); + safeInvokeNoArg(() => rendering.render(viewParts, ctx)); } private _getViewPartsToRender(): ViewPart[] { @@ -566,16 +597,17 @@ export class View extends ViewEventHandler { this._context.configuration.setGlyphMarginDecorationLaneCount(model.requiredLanes); } inputLatency.onRenderStart(); - }, - renderText: (): [ViewPart[], RenderingContext] | null => { + if (!this.domNode.domNode.isConnected) { return null; } - let viewPartsToRender = this._getViewPartsToRender(); + + const viewPartsToRender = this._getViewPartsToRender(); if (!this._viewLines.shouldRender() && viewPartsToRender.length === 0) { // Nothing to render return null; } + const partialViewportData = this._context.viewLayout.getLinesViewportData(); this._context.viewModel.setViewport(partialViewportData.startLineNumber, partialViewportData.endLineNumber, partialViewportData.centeredLineNumber); @@ -586,17 +618,19 @@ export class View extends ViewEventHandler { this._context.viewModel ); - if (this._contentWidgets.shouldRender()) { - // Give the content widgets a chance to set their max width before a possible synchronous layout - this._contentWidgets.onBeforeRender(viewportData); + for (const viewPart of this._viewParts) { + if (viewPart.shouldRender()) { + viewPart.onBeforeRender(viewportData); + } } + return viewportData; + }, + renderText: (viewportData: ViewportData): [ViewPart[], RenderingContext] => { + if (this._viewLines.shouldRender()) { this._viewLines.renderText(viewportData); this._viewLines.onDidRender(); - - // Rendering of viewLines might cause scroll events to occur, so collect view parts to render again - viewPartsToRender = this._getViewPartsToRender(); } if (this._viewLinesGpu?.shouldRender()) { @@ -604,6 +638,9 @@ export class View extends ViewEventHandler { this._viewLinesGpu.onDidRender(); } + // Rendering of viewLines might cause scroll events to occur, so collect view parts to render again + const viewPartsToRender = this._getViewPartsToRender(); + return [viewPartsToRender, new RenderingContext(this._context.viewLayout, viewportData, this._viewLines, this._viewLinesGpu)]; }, prepareRender: (viewPartsToRender: ViewPart[], ctx: RenderingContext) => { @@ -652,6 +689,19 @@ export class View extends ViewEventHandler { return visibleRange.left; } + public getLineWidth(modelLineNumber: number): number { + const model = this._context.viewModel.model; + const viewLine = this._context.viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelLineNumber, model.getLineMaxColumn(modelLineNumber))).lineNumber; + this._flushAccumulatedAndRenderNow(); + const width = this._viewLines.getLineWidth(viewLine); + + return width; + } + + public resetLineWidthCaches(): void { + this._viewLines.resetLineWidthCaches(); + } + public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY); if (!mouseTarget) { @@ -723,7 +773,9 @@ export class View extends ViewEventHandler { widgetData.position?.preference ?? null, widgetData.position?.positionAffinity ?? null ); - this._scheduleRender(); + if (this._contentWidgets.shouldRender()) { + this._scheduleRender(); + } } public removeContentWidget(widgetData: IContentWidgetData): void { @@ -785,8 +837,8 @@ function safeInvokeNoArg(func: () => T): T | null { interface ICoordinatedRendering { readonly window: CodeWindow; - prepareRenderText(): void; - renderText(): [ViewPart[], RenderingContext] | null; + prepareRenderText(): ViewportData | null; + renderText(viewportData: ViewportData): [ViewPart[], RenderingContext]; prepareRender(viewParts: ViewPart[], ctx: RenderingContext): void; render(viewParts: ViewPart[], ctx: RestrictedRenderingContext): void; } @@ -836,14 +888,21 @@ class EditorRenderingCoordinator { const coordinatedRenderings = this._coordinatedRenderings.slice(0); this._coordinatedRenderings = []; - for (const rendering of coordinatedRenderings) { - safeInvokeNoArg(() => rendering.prepareRenderText()); + const viewportDatas: (ViewportData | null)[] = []; + for (let i = 0, len = coordinatedRenderings.length; i < len; i++) { + const rendering = coordinatedRenderings[i]; + viewportDatas[i] = safeInvokeNoArg(() => rendering.prepareRenderText()); } const datas: ([ViewPart[], RenderingContext] | null)[] = []; for (let i = 0, len = coordinatedRenderings.length; i < len; i++) { const rendering = coordinatedRenderings[i]; - datas[i] = safeInvokeNoArg(() => rendering.renderText()); + const viewportData = viewportDatas[i]; + if (!viewportData) { + datas[i] = null; + continue; + } + datas[i] = safeInvokeNoArg(() => rendering.renderText(viewportData)); } for (let i = 0, len = coordinatedRenderings.length; i < len; i++) { diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 6eed0a076be..881275f34af 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -130,7 +130,7 @@ function createLineBreaks(targetWindow: Window, requests: string[], fontInfo: Fo containerDomNode.innerHTML = trustedhtml as string; containerDomNode.style.position = 'absolute'; - containerDomNode.style.top = '10000'; + containerDomNode.style.top = '10000px'; if (wordBreak === 'keepAll') { // word-break: keep-all; overflow-wrap: anywhere containerDomNode.style.wordBreak = 'keep-all'; diff --git a/src/vs/editor/browser/view/renderingContext.ts b/src/vs/editor/browser/view/renderingContext.ts index fdb24034701..1ed624ecfe4 100644 --- a/src/vs/editor/browser/view/renderingContext.ts +++ b/src/vs/editor/browser/view/renderingContext.ts @@ -87,7 +87,7 @@ export class RenderingContext extends RestrictedRenderingContext { public linesVisibleRangesForRange(range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { const domRanges = this._viewLines.linesVisibleRangesForRange(range, includeNewLines); if (!this._viewLinesGpu) { - return domRanges ?? null; + return domRanges; } const gpuRanges = this._viewLinesGpu.linesVisibleRangesForRange(range, includeNewLines); if (!domRanges) { diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 935bcc5dbb0..cd312c96271 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -14,6 +14,8 @@ import { IViewModel } from '../../common/viewModel.js'; import { IMouseWheelEvent } from '../../../base/browser/mouseEvent.js'; import { EditorOption } from '../../common/config/editorOptions.js'; import * as platform from '../../../base/common/platform.js'; +import { StandardTokenType } from '../../common/encodedTokenAttributes.js'; +import { ITextModel } from '../../common/model.js'; export interface IMouseDispatchData { position: Position; @@ -129,6 +131,67 @@ export class ViewController { } } + /** + * Selects content inside brackets if the position is right after an opening bracket or right before a closing bracket. + * @param pos The position in the model. + * @param model The text model. + */ + private static _trySelectBracketContent(model: ITextModel, pos: Position): Selection | undefined { + // Try to find bracket match if we're right after an opening bracket. + if (pos.column > 1) { + const pair = model.bracketPairs.matchBracket(pos.with(undefined, pos.column - 1)); + if (pair && pair[0].getEndPosition().equals(pos)) { + return Selection.fromPositions(pair[0].getEndPosition(), pair[1].getStartPosition()); + } + } + + // Try to find bracket match if we're right before a closing bracket. + if (pos.column <= model.getLineMaxColumn(pos.lineNumber)) { + const pair = model.bracketPairs.matchBracket(pos); + if (pair && pair[1].getStartPosition().equals(pos)) { + return Selection.fromPositions(pair[0].getEndPosition(), pair[1].getStartPosition()); + } + } + + return undefined; + } + + /** + * Selects content inside a string if the position is right after an opening quote or right before a closing quote. + * @param pos The position in the model. + * @param model The text model. + */ + private static _trySelectStringContent(model: ITextModel, pos: Position): Selection | undefined { + const { lineNumber, column } = pos; + const { tokenization: tokens } = model; + + // Ensure we have accurate tokens for the line. + if (!tokens.hasAccurateTokensForLine(lineNumber)) { + if (tokens.isCheapToTokenize(lineNumber)) { + tokens.forceTokenization(lineNumber); + } else { + return undefined; + } + } + + // Check if current token is a string. + const lineTokens = tokens.getLineTokens(lineNumber); + const index = lineTokens.findTokenIndexAtOffset(column - 1); + if (lineTokens.getStandardTokenType(index) !== StandardTokenType.String) { + return undefined; + } + + // Get 1-based boundaries of the string content (excluding quotes). + const start = lineTokens.getStartOffset(index) + 2; + const end = lineTokens.getEndOffset(index); + + if (column !== start && column !== end) { + return undefined; + } + + return new Selection(lineNumber, start, lineNumber, end); + } + public dispatchMouse(data: IMouseDispatchData): void { const options = this.configuration.options; const selectionClipboardIsOn = (platform.isLinux && options.get(EditorOption.selectionClipboard)); @@ -179,7 +242,14 @@ export class ViewController { if (data.inSelectionMode) { this._wordSelectDrag(data.position, data.revealType); } else { - this._wordSelect(data.position, data.revealType); + const model = this.viewModel.model; + const modelPos = this._convertViewToModelPosition(data.position); + const selection = ViewController._trySelectBracketContent(model, modelPos) || ViewController._trySelectStringContent(model, modelPos); + if (selection) { + this._select(selection); + } else { + this._wordSelect(data.position, data.revealType); + } } } } @@ -286,6 +356,10 @@ export class ViewController { CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel, this._usualArgs(viewPosition, revealType)); } + private _select(selection: Selection): void { + CoreNavigationCommands.SetSelection.runCoreEditorCommand(this.viewModel, { source: 'mouse', selection }); + } + private _selectAll(): void { CoreNavigationCommands.SelectAll.runCoreEditorCommand(this.viewModel, { source: 'mouse' }); } diff --git a/src/vs/editor/browser/view/viewPart.ts b/src/vs/editor/browser/view/viewPart.ts index a23bcb11b59..1009c8e7ca0 100644 --- a/src/vs/editor/browser/view/viewPart.ts +++ b/src/vs/editor/browser/view/viewPart.ts @@ -7,6 +7,7 @@ import { FastDomNode } from '../../../base/browser/fastDomNode.js'; import { RenderingContext, RestrictedRenderingContext } from './renderingContext.js'; import { ViewContext } from '../../common/viewModel/viewContext.js'; import { ViewEventHandler } from '../../common/viewEventHandler.js'; +import { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; export abstract class ViewPart extends ViewEventHandler { @@ -23,6 +24,9 @@ export abstract class ViewPart extends ViewEventHandler { super.dispose(); } + public onBeforeRender(viewportData: ViewportData): void { + } + public abstract prepareRender(ctx: RenderingContext): void; public abstract render(ctx: RestrictedRenderingContext): void; } diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index 2c9595382e0..1f43f7834f0 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -116,7 +116,9 @@ export class ViewContentWidgets extends ViewPart { const myWidget = this._widgets[widget.getId()]; myWidget.setPosition(primaryAnchor, secondaryAnchor, preference, affinity); - this.setShouldRender(); + if (!myWidget.useDisplayNone) { + this.setShouldRender(); + } } public removeWidget(widget: IContentWidget): void { @@ -140,7 +142,7 @@ export class ViewContentWidgets extends ViewPart { return false; } - public onBeforeRender(viewportData: ViewportData): void { + public override onBeforeRender(viewportData: ViewportData): void { const keys = Object.keys(this._widgets); for (const widgetId of keys) { this._widgets[widgetId].onBeforeRender(viewportData); @@ -209,6 +211,7 @@ class Widget { private _isVisible: boolean; private _renderData: IRenderData | null; + public readonly useDisplayNone: boolean; constructor(context: ViewContext, viewDomNode: FastDomNode, actual: IContentWidget) { this._context = context; @@ -223,6 +226,7 @@ class Widget { this.id = this._actual.getId(); this.allowEditorOverflow = (this._actual.allowEditorOverflow || false) && allowOverflow; this.suppressMouseDown = this._actual.suppressMouseDown || false; + this.useDisplayNone = this._actual.useDisplayNone || false; this._fixedOverflowWidgets = options.get(EditorOption.fixedOverflowWidgets); this._contentWidth = layoutInfo.contentWidth; @@ -289,7 +293,7 @@ class Widget { public setPosition(primaryAnchor: IPosition | null, secondaryAnchor: IPosition | null, preference: ContentWidgetPositionPreference[] | null, affinity: PositionAffinity | null): void { this._setPosition(affinity, primaryAnchor, secondaryAnchor); this._preference = preference; - if (this._primaryAnchor.viewPosition && this._preference && this._preference.length > 0) { + if (!this.useDisplayNone && this._primaryAnchor.viewPosition && this._preference && this._preference.length > 0) { // this content widget would like to be visible if possible // we change it from `display:none` to `display:block` even if it // might be outside the viewport such that we can measure its size diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 8d627025769..56143c6a32c 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -5,7 +5,7 @@ import './currentLineHighlight.css'; import { DynamicViewOverlay } from '../../view/dynamicViewOverlay.js'; -import { editorLineHighlight, editorLineHighlightBorder } from '../../../common/core/editorColorRegistry.js'; +import { editorLineHighlight, editorInactiveLineHighlight, editorLineHighlightBorder } from '../../../common/core/editorColorRegistry.js'; import { RenderingContext } from '../../view/renderingContext.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; @@ -236,10 +236,20 @@ export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOver registerThemingParticipant((theme, collector) => { const lineHighlight = theme.getColor(editorLineHighlight); + const inactiveLineHighlight = theme.getColor(editorInactiveLineHighlight); + + // Apply active line highlight when editor is focused if (lineHighlight) { - collector.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${lineHighlight}; }`); - collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`); + collector.addRule(`.monaco-editor.focused .view-overlays .current-line { background-color: ${lineHighlight}; }`); + collector.addRule(`.monaco-editor.focused .margin-view-overlays .current-line-margin { background-color: ${lineHighlight}; border: none; }`); + } + + // Apply inactive line highlight when editor is not focused + if (inactiveLineHighlight) { + collector.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${inactiveLineHighlight}; }`); + collector.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${inactiveLineHighlight}; border: none; }`); } + if (!lineHighlight || lineHighlight.isTransparent() || theme.defines(editorLineHighlightBorder)) { const lineHighlightBorder = theme.getColor(editorLineHighlightBorder); if (lineHighlightBorder) { diff --git a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts index dd565eac9e4..875311054f8 100644 --- a/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts +++ b/src/vs/editor/browser/viewParts/glyphMargin/glyphMargin.ts @@ -94,8 +94,7 @@ export abstract class DedupOverlay extends DynamicViewOverlay { let prevClassName: string | null = null; let prevEndLineIndex = 0; - for (let i = 0, len = decorations.length; i < len; i++) { - const d = decorations[i]; + for (const d of decorations) { const className = d.className; const zIndex = d.zIndex; let startLineIndex = Math.max(d.startLineNumber, visibleStartLineNumber) - visibleStartLineNumber; @@ -110,8 +109,8 @@ export abstract class DedupOverlay extends DynamicViewOverlay { prevEndLineIndex = endLineIndex; } - for (let i = startLineIndex; i <= prevEndLineIndex; i++) { - output[i].add(new LineDecorationToRender(className, zIndex, d.tooltip)); + for (let lineIndex = startLineIndex; lineIndex <= prevEndLineIndex; lineIndex++) { + output[lineIndex].add(new LineDecorationToRender(className, zIndex, d.tooltip)); } } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 6fb1f36868b..ad53e1e16c1 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1663,7 +1663,7 @@ class InnerMinimap extends Disposable { continue; } highlightedLines.set(line, true); - const y = layout.getYForLineNumber(startLineNumber, minimapLineHeight); + const y = layout.getYForLineNumber(line, minimapLineHeight); canvasContext.fillRect(MINIMAP_GUTTER_WIDTH, y, canvasContext.canvas.width, minimapLineHeight); } } diff --git a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts index a84da6b2b1d..d286e3b2074 100644 --- a/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts +++ b/src/vs/editor/browser/viewParts/overlayWidgets/overlayWidgets.ts @@ -124,7 +124,7 @@ export class ViewOverlayWidgets extends ViewPart { public setWidgetPosition(widget: IOverlayWidget, position: IOverlayWidgetPosition | null): boolean { const widgetData = this._widgets[widget.getId()]; const preference = position ? position.preference : null; - const stack = position?.stackOridinal; + const stack = position?.stackOrdinal; if (widgetData.preference === preference && widgetData.stack === stack) { this._updateMaxMinWidth(); return false; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts index 11292eb56a1..2c9deddd77c 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts @@ -136,7 +136,7 @@ export class OverviewRuler extends ViewEventHandler implements IOverviewRuler { private _renderOneLane(ctx: CanvasRenderingContext2D, colorZones: ColorZone[], id2Color: string[], width: number): void { - let currentColorId = 0; + let currentColorId = 0; // will never match a real color id which is > 0 let currentFrom = 0; let currentTo = 0; @@ -147,7 +147,9 @@ export class OverviewRuler extends ViewEventHandler implements IOverviewRuler { const zoneTo = zone.to; if (zoneColorId !== currentColorId) { - ctx.fillRect(0, currentFrom, width, currentTo - currentFrom); + if (currentColorId !== 0) { + ctx.fillRect(0, currentFrom, width, currentTo - currentFrom); + } currentColorId = zoneColorId; ctx.fillStyle = id2Color[currentColorId]; diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.css b/src/vs/editor/browser/viewParts/rulers/rulers.css index aad356bd749..17a9da155d0 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.css +++ b/src/vs/editor/browser/viewParts/rulers/rulers.css @@ -7,4 +7,5 @@ position: absolute; top: 0; box-shadow: 1px 0 0 0 var(--vscode-editorRuler-foreground) inset; + pointer-events: none; } diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index c0a46927d17..ec1a5042e91 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -66,13 +66,11 @@ export class Rulers extends ViewPart { } if (currentCount < desiredCount) { - const { tabSize } = this._context.viewModel.model.getOptions(); - const rulerWidth = tabSize; let addCount = desiredCount - currentCount; while (addCount > 0) { const node = createFastDomNode(document.createElement('div')); node.setClassName('view-ruler'); - node.setWidth(rulerWidth); + node.setWidth('1ch'); this.domNode.appendChild(node); this._renderedRulers.push(node); addCount--; diff --git a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts index 71a9a7605c7..dc5dc300709 100644 --- a/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts +++ b/src/vs/editor/browser/viewParts/scrollDecoration/scrollDecoration.ts @@ -9,7 +9,7 @@ import { ViewPart } from '../../view/viewPart.js'; import { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; +import { EditorOption, RenderMinimap } from '../../../common/config/editorOptions.js'; export class ScrollDecorationViewPart extends ViewPart { @@ -56,7 +56,7 @@ export class ScrollDecorationViewPart extends ViewPart { const options = this._context.configuration.options; const layoutInfo = options.get(EditorOption.layoutInfo); - if (layoutInfo.minimap.renderMinimap === 0 || (layoutInfo.minimap.minimapWidth > 0 && layoutInfo.minimap.minimapLeft === 0)) { + if (layoutInfo.minimap.renderMinimap === RenderMinimap.None || (layoutInfo.minimap.minimapWidth > 0 && layoutInfo.minimap.minimapLeft === 0)) { this._width = layoutInfo.width; } else { this._width = layoutInfo.width - layoutInfo.verticalScrollbarWidth; diff --git a/src/vs/editor/browser/viewParts/viewLines/domReadingContext.ts b/src/vs/editor/browser/viewParts/viewLines/domReadingContext.ts index 1a11700242e..c336dacbcf7 100644 --- a/src/vs/editor/browser/viewParts/viewLines/domReadingContext.ts +++ b/src/vs/editor/browser/viewParts/viewLines/domReadingContext.ts @@ -20,7 +20,8 @@ export class DomReadingContext { const rect = this._domNode.getBoundingClientRect(); this.markDidDomLayout(); this._clientRectDeltaLeft = rect.left; - this._clientRectScale = rect.width / this._domNode.offsetWidth; + const offsetWidth = this._domNode.offsetWidth; + this._clientRectScale = offsetWidth > 0 ? rect.width / offsetWidth : 1; } } diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts index a3763b98430..4aaa9200561 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLine.ts @@ -323,6 +323,10 @@ export class ViewLine implements IVisibleLine { } return this._renderedViewLine.getColumnOfNodeOffset(spanNode, offset); } + + public resetCachedWidth(): void { + this._renderedViewLine?.resetCachedWidth(); + } } interface IRenderedViewLine { @@ -330,6 +334,7 @@ interface IRenderedViewLine { readonly input: RenderLineInput; getWidth(context: DomReadingContext | null): number; getWidthIsFast(): boolean; + resetCachedWidth(): void; getVisibleRangesForRange(lineNumber: number, startColumn: number, endColumn: number, context: DomReadingContext): FloatHorizontalRange[] | null; getColumnOfNodeOffset(spanNode: HTMLElement, offset: number): number; } @@ -391,6 +396,10 @@ class FastRenderedViewLine implements IRenderedViewLine { return (this.input.lineContent.length < Constants.MaxMonospaceDistance) || this._cachedWidth !== -1; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + } + public monospaceAssumptionsAreValid(): boolean { if (!this.domNode) { return monospaceAssumptionsAreValid; @@ -528,6 +537,15 @@ class RenderedViewLine implements IRenderedViewLine { return true; } + public resetCachedWidth(): void { + this._cachedWidth = -1; + if (this._pixelOffsetCache !== null) { + for (let column = 0, len = this._pixelOffsetCache.length; column < len; column++) { + this._pixelOffsetCache[column] = -1; + } + } + } + /** * Visible ranges for a model range */ diff --git a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts index ccf5bc01ef1..cb819821291 100644 --- a/src/vs/editor/browser/viewParts/viewLines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/viewLines/viewLines.ts @@ -245,12 +245,10 @@ export class ViewLines extends ViewPart implements IViewLines { return r; } public override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { - if (true/*e.inlineDecorationsChanged*/) { - const rendStartLineNumber = this._visibleLines.getStartLineNumber(); - const rendEndLineNumber = this._visibleLines.getEndLineNumber(); - for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) { - this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged(); - } + const rendStartLineNumber = this._visibleLines.getStartLineNumber(); + const rendEndLineNumber = this._visibleLines.getEndLineNumber(); + for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) { + this._visibleLines.getVisibleLine(lineNumber).onDecorationsChanged(); } return true; } @@ -318,7 +316,7 @@ export class ViewLines extends ViewPart implements IViewLines { } } this.domNode.setWidth(e.scrollWidth); - return this._visibleLines.onScrollChanged(e) || true; + return this._visibleLines.onScrollChanged(e) || e.scrollTopChanged || e.scrollLeftChanged; } public override onTokensChanged(e: viewEvents.ViewTokensChangedEvent): boolean { @@ -414,13 +412,15 @@ export class ViewLines extends ViewPart implements IViewLines { return result; } - public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { - if (this.shouldRender()) { - // Cannot read from the DOM because it is dirty - // i.e. the model & the dom are out of sync, so I'd be reading something stale - return null; + public resetLineWidthCaches(): void { + const rendStartLineNumber = this._visibleLines.getStartLineNumber(); + const rendEndLineNumber = this._visibleLines.getEndLineNumber(); + for (let lineNumber = rendStartLineNumber; lineNumber <= rendEndLineNumber; lineNumber++) { + this._visibleLines.getVisibleLine(lineNumber).resetCachedWidth(); } + } + public linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { const originalEndLineNumber = _range.endLineNumber; const range = Range.intersectRanges(_range, this._lastRenderedData.getCurrentVisibleRange()); if (!range) { @@ -480,12 +480,6 @@ export class ViewLines extends ViewPart implements IViewLines { } private _visibleRangesForLineRange(lineNumber: number, startColumn: number, endColumn: number): VisibleRanges | null { - if (this.shouldRender()) { - // Cannot read from the DOM because it is dirty - // i.e. the model & the dom are out of sync, so I'd be reading something stale - return null; - } - if (lineNumber < this._visibleLines.getStartLineNumber() || lineNumber > this._visibleLines.getEndLineNumber()) { return null; } @@ -541,7 +535,7 @@ export class ViewLines extends ViewPart implements IViewLines { // only proceed if we just did a layout return; } - if (this._asyncUpdateLineWidths.isScheduled()) { + if (!this._asyncUpdateLineWidths.isScheduled()) { // reading widths is not scheduled => widths are up-to-date return; } @@ -685,6 +679,10 @@ export class ViewLines extends ViewPart implements IViewLines { // --- width private _ensureMaxLineWidth(lineWidth: number): void { + // When GPU rendering is enabled, ViewLinesGpu handles max line width tracking + if (this._viewLineOptions.useGpu) { + return; + } const iLineWidth = Math.ceil(lineWidth); if (this._maxLineWidth < iLineWidth) { this._maxLineWidth = iLineWidth; diff --git a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts index 55ddb01f83d..53d6115a495 100644 --- a/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts +++ b/src/vs/editor/browser/viewParts/viewLinesGpu/viewLinesGpu.ts @@ -50,6 +50,12 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { private _lastViewportData?: ViewportData; private _lastViewLineOptions?: ViewLineOptions; + /** + * Tracks the maximum line width seen so far for horizontal scrollbar sizing. + * This is needed because GPU-rendered lines don't have DOM nodes to measure. + */ + private _maxLineWidth: number = 0; + private _device!: GPUDevice; private _renderPassDescriptor!: GPURenderPassDescriptor; private _renderPassColorAttachment!: GPURenderPassColorAttachment; @@ -424,14 +430,21 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { this._refreshGlyphRasterizer(); + this._maxLineWidth = 0; return true; } override onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return true; } override onDecorationsChanged(e: viewEvents.ViewDecorationsChangedEvent): boolean { return true; } - override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { return true; } + override onFlushed(e: viewEvents.ViewFlushedEvent): boolean { + this._maxLineWidth = 0; + return true; + } override onLinesChanged(e: viewEvents.ViewLinesChangedEvent): boolean { return true; } - override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { return true; } + override onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { + this._maxLineWidth = 0; + return true; + } override onLinesInserted(e: viewEvents.ViewLinesInsertedEvent): boolean { return true; } override onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { return true; } override onRevealRangeRequest(e: viewEvents.ViewRevealRangeRequestEvent): boolean { return true; } @@ -500,6 +513,75 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { this._lastViewportData = viewportData; this._lastViewLineOptions = options; + + // Update max line width for horizontal scrollbar + this._updateMaxLineWidth(viewportData, options); + } + + /** + * Update the max line width based on GPU-rendered lines. + * This is needed because GPU-rendered lines don't have DOM nodes to measure. + */ + private _updateMaxLineWidth(viewportData: ViewportData, viewLineOptions: ViewLineOptions): void { + const dpr = getActiveWindow().devicePixelRatio; + let localMaxLineWidth = 0; + + for (let lineNumber = viewportData.startLineNumber; lineNumber <= viewportData.endLineNumber; lineNumber++) { + if (!this._viewGpuContext.canRender(viewLineOptions, viewportData, lineNumber)) { + continue; + } + + const lineData = viewportData.getViewLineRenderingData(lineNumber); + const lineWidth = this._computeLineWidth(lineData, viewLineOptions, dpr); + localMaxLineWidth = Math.max(localMaxLineWidth, lineWidth); + } + + // Only update if we found a larger width (use ceil to match DOM behavior) + const iLineWidth = Math.ceil(localMaxLineWidth); + if (iLineWidth > this._maxLineWidth) { + this._maxLineWidth = iLineWidth; + this._context.viewModel.viewLayout.setMaxLineWidth(this._maxLineWidth); + } + } + + /** + * Compute the width of a line in CSS pixels. + */ + private _computeLineWidth(lineData: ViewLineRenderingData, viewLineOptions: ViewLineOptions, dpr: number): number { + const content = lineData.content; + let contentSegmenter: IContentSegmenter | undefined; + if (!(lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations)) { + contentSegmenter = createContentSegmenter(lineData, viewLineOptions); + } + + let width = 0; + let tabXOffset = 0; + + for (let x = 0; x < content.length; x++) { + let chars: string; + if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { + chars = content.charAt(x); + } else { + const segment = contentSegmenter!.getSegmentAtIndex(x); + if (segment === undefined) { + continue; + } + chars = segment; + } + + if (chars === '\t') { + const offsetBefore = x + tabXOffset; + tabXOffset = CursorColumns.nextRenderTabStop(x + tabXOffset, lineData.tabSize); + width += viewLineOptions.spaceWidth * (tabXOffset - offsetBefore); + tabXOffset -= x + 1; + } else if (lineData.isBasicASCII && viewLineOptions.useMonospaceOptimizations) { + width += viewLineOptions.spaceWidth; + } else { + width += this._renderStrategy.value!.glyphRasterizer.getTextMetrics(chars).width / dpr; + } + } + + return width; } linesVisibleRangesForRange(_range: Range, includeNewLines: boolean): LineVisibleRanges[] | null { @@ -654,7 +736,8 @@ export class ViewLinesGpu extends ViewPart implements IViewLines { const lineRange = this._visibleRangesForLineRange(lineNumber, 1, lineData.maxColumn); const lastRange = lineRange?.ranges.at(-1); if (lastRange) { - return lastRange.width; + // Total line width is the left offset plus width of the last range + return lastRange.left + lastRange.width; } return undefined; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 5e4aaddb3da..546d268130c 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -90,14 +90,6 @@ export class WhitespaceOverlay extends DynamicViewOverlay { return; } - const startLineNumber = ctx.visibleRange.startLineNumber; - const endLineNumber = ctx.visibleRange.endLineNumber; - const lineCount = endLineNumber - startLineNumber + 1; - const needed = new Array(lineCount); - for (let i = 0; i < lineCount; i++) { - needed[i] = true; - } - this._renderResult = []; for (let lineNumber = ctx.viewportData.startLineNumber; lineNumber <= ctx.viewportData.endLineNumber; lineNumber++) { const lineIndex = lineNumber - ctx.viewportData.startLineNumber; diff --git a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts index 91156fa9829..174eea21c6a 100644 --- a/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditor/codeEditorWidget.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import '../../services/markerDecorations.js'; +import '../../services/contribution.js'; import * as dom from '../../../../base/browser/dom.js'; import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; @@ -18,6 +18,7 @@ import { applyFontInfo } from '../../config/domFontInfo.js'; import { EditorConfiguration, IEditorConstructionOptions } from '../../config/editorConfiguration.js'; import { TabFocus } from '../../config/tabFocus.js'; import * as editorBrowser from '../../editorBrowser.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent } from '../../controller/editContext/clipboardUtils.js'; import { EditorExtensionsRegistry, IEditorContributionDescription } from '../../editorExtensions.js'; import { ICodeEditorService } from '../../services/codeEditorService.js'; import { IContentWidgetData, IGlyphMarginWidgetData, IOverlayWidgetData, View } from '../../view.js'; @@ -147,6 +148,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE private readonly _onDidPaste: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); public readonly onDidPaste = this._onDidPaste.event; + private readonly _onWillCopy: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillCopy = this._onWillCopy.event; + + private readonly _onWillCut: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillCut = this._onWillCut.event; + + private readonly _onWillPaste: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); + public readonly onWillPaste = this._onWillPaste.event; + private readonly _onMouseUp: Emitter = this._register(new InteractionEmitter(this._contributions, this._deliveryQueue)); public readonly onMouseUp: Event = this._onMouseUp.event; @@ -286,6 +296,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._configuration = this._register(this._createConfiguration(codeEditorWidgetOptions.isSimpleWidget || false, codeEditorWidgetOptions.contextMenuId ?? (codeEditorWidgetOptions.isSimpleWidget ? MenuId.SimpleEditorContext : MenuId.EditorContext), options, accessibilityService)); + this._domElement.style?.setProperty('--editor-font-size', this._configuration.options.get(EditorOption.fontSize) + 'px'); this._register(this._configuration.onDidChange((e) => { this._onDidChangeConfiguration.fire(e); @@ -294,6 +305,9 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const layoutInfo = options.get(EditorOption.layoutInfo); this._onDidLayoutChange.fire(layoutInfo); } + if (e.hasChanged(EditorOption.fontSize)) { + this._domElement.style.setProperty('--editor-font-size', options.get(EditorOption.fontSize) + 'px'); + } })); this._contextKeyService = this._register(contextKeyService.createScoped(this._domElement)); @@ -599,8 +613,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return -1; } - const maxCol = this._modelData.model.getLineMaxColumn(lineNumber); - return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, maxCol, includeViewZones); + return CodeEditorWidget._getVerticalOffsetAfterPosition(this._modelData, lineNumber, Number.MAX_SAFE_INTEGER, includeViewZones); } public getLineHeightForPosition(position: IPosition): number { @@ -678,6 +691,13 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._modelData.viewModel.revealRange('api', revealHorizontal, viewRange, verticalType, scrollType); } + public revealAllCursors(revealHorizontal: boolean, minimalReveal?: boolean): void { + if (!this._modelData) { + return; + } + this._modelData.viewModel.revealAllCursors('api', revealHorizontal, minimalReveal); + } + public revealLine(lineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { this._revealLine(lineNumber, VerticalRevealType.Simple, scrollType); } @@ -1280,7 +1300,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE reason = source; sourceStr = source.metadata.source; } else { - reason = EditSources.unknown({ name: sourceStr }); + reason = EditSources.unknown({ name: source }); sourceStr = source; } @@ -1436,7 +1456,12 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE delete this._decorationTypeKeysToIds[decorationTypeKey]; } if (this._decorationTypeSubtypes.hasOwnProperty(decorationTypeKey)) { + const items = this._decorationTypeSubtypes[decorationTypeKey]; + for (const subType of Object.keys(items)) { + this._removeDecorationType(decorationTypeKey + '-' + subType); + } delete this._decorationTypeSubtypes[decorationTypeKey]; + } } @@ -1662,6 +1687,20 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.view.getOffsetForColumn(lineNumber, column); } + public getWidthOfLine(lineNumber: number): number { + if (!this._modelData || !this._modelData.hasRealView) { + return -1; + } + return this._modelData.view.getLineWidth(lineNumber); + } + + public resetLineWidthCaches(): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.view.resetLineWidthCaches(); + } + public render(forceRedraw: boolean = false): void { if (!this._modelData || !this._modelData.hasRealView) { return; @@ -1671,6 +1710,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }); } + public renderAsync(forceRedraw: boolean = false): void { + if (!this._modelData || !this._modelData.hasRealView) { + return; + } + this._modelData.viewModel.batchEvents(() => { + this._modelData!.view.render(false, forceRedraw); + }); + } + public setAriaOptions(options: editorBrowser.IEditorAriaOptions): void { if (!this._modelData || !this._modelData.hasRealView) { return; @@ -1858,6 +1906,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE view.render(false, true); view.domNode.domNode.setAttribute('data-uri', model.uri.toString()); + + // Connect clipboard events from View + listenersToRemove.push(view.onWillCopy(e => this._onWillCopy.fire(e))); + listenersToRemove.push(view.onWillCut(e => this._onWillCut.fire(e))); + listenersToRemove.push(view.onWillPaste(e => this._onWillPaste.fire(e))); } this._modelData = new ModelData(model, viewModel, view, hasRealView, listenersToRemove, attachedView); diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index a55e05ee97e..a2ee0b70903 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -31,13 +31,14 @@ import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyO * Make sure to add the view zones to the editor! */ export class HideUnchangedRegionsFeature extends Disposable { - private static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>( + public static readonly _breadcrumbsSourceFactory = observableValue<((textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource)>( this, () => ({ dispose() { }, getBreadcrumbItems(startRange, reader) { return []; }, + getAt: () => [], })); public static setBreadcrumbsSourceFactory(factory: (textModel: ITextModel, instantiationService: IInstantiationService) => IDiffEditorBreadcrumbsSource) { this._breadcrumbsSourceFactory.set(factory, undefined); @@ -491,4 +492,6 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { export interface IDiffEditorBreadcrumbsSource extends IDisposable { getBreadcrumbItems(startRange: LineRange, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[]; + + getAt(lineNumber: number, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[]; } diff --git a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts index 7042f79785c..152c3d854a8 100644 --- a/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts +++ b/src/vs/editor/browser/widget/diffEditor/utils/editorGutter.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { h, reset } from '../../../../../base/browser/dom.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, IObservable, IReader, ISettableObservable, observableFromEvent, observableSignal, observableSignalFromEvent, observableValue, transaction } from '../../../../../base/common/observable.js'; import { CodeEditorWidget } from '../../codeEditor/codeEditorWidget.js'; import { LineRange } from '../../../../common/core/ranges/lineRange.js'; @@ -37,7 +37,7 @@ export class EditorGutter extends D this.editorOnDidChangeViewZones = observableSignalFromEvent('onDidChangeViewZones', this._editor.onDidChangeViewZones); this.editorOnDidContentSizeChange = observableSignalFromEvent('onDidContentSizeChange', this._editor.onDidContentSizeChange); this.domNodeSizeChanged = observableSignal('domNodeSizeChanged'); - this.views = new Map(); + this.views = this._register(new DisposableMap()); this._domNode.className = 'gutter monaco-editor'; const scrollDecoration = this._domNode.appendChild( h('div.scroll-decoration', { role: 'presentation', ariaHidden: 'true', style: { width: '100%' } }) @@ -143,20 +143,22 @@ export class EditorGutter extends D } for (const id of unusedIds) { - const view = this.views.get(id)!; - view.gutterItemView.dispose(); - view.domNode.remove(); - this.views.delete(id); + this.views.deleteAndDispose(id); } } } -class ManagedGutterItemView { +class ManagedGutterItemView implements IDisposable { constructor( public readonly item: ISettableObservable, public readonly gutterItemView: IGutterItemView, public readonly domNode: HTMLDivElement, ) { } + + dispose(): void { + this.gutterItemView.dispose(); + this.domNode.remove(); + } } export interface IGutterItemProvider { diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index e2fd4edea20..d0cea4135d2 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -10,6 +10,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { derived, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { Range } from '../../../common/core/range.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; import { ICodeEditor } from '../../editorBrowser.js'; @@ -82,6 +83,18 @@ export class MultiDiffEditorWidget extends Disposable { return this._widgetImpl.get().tryGetCodeEditor(resource); } + public getRootElement(): HTMLElement { + return this._widgetImpl.get().getRootElement(); + } + + public getContextKeyService(): IContextKeyService { + return this._widgetImpl.get().getContextKeyService(); + } + + public getScopedInstantiationService(): IInstantiationService { + return this._widgetImpl.get().getScopedInstantiationService(); + } + public findDocumentDiffItem(resource: URI): IDocumentDiffItem | undefined { return this._widgetImpl.get().findDocumentDiffItem(resource); } diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 0add04cc2d6..76f410afc23 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -259,6 +259,17 @@ export class MultiDiffEditorWidgetImpl extends Disposable { this._scrollableElement.setScrollPosition({ scrollLeft: scrollState.left, scrollTop: scrollState.top }); } + public getRootElement(): HTMLElement { + return this._elements.root; + } + + public getContextKeyService(): IContextKeyService { + return this._contextKeyService; + } + + public getScopedInstantiationService(): IInstantiationService { + return this._instantiationService; + } public reveal(resource: IMultiDiffResourceId, options?: RevealOptions): void { const viewItems = this._viewItems.get(); const index = viewItems.findIndex( diff --git a/src/vs/editor/browser/widget/multiDiffEditor/style.css b/src/vs/editor/browser/widget/multiDiffEditor/style.css index fc9c877bf78..57e2ab568cd 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/style.css +++ b/src/vs/editor/browser/widget/multiDiffEditor/style.css @@ -34,6 +34,15 @@ } } + > .multi-diff-root-floating-menu { + position: absolute; + top: auto; + right: 28px; + bottom: 24px; + left: auto; + width: auto; + } + .active { --vscode-multiDiffEditor-border: var(--vscode-focusBorder); } diff --git a/src/vs/editor/common/commands/replaceCommand.ts b/src/vs/editor/common/commands/replaceCommand.ts index 779dfd9a7b9..5836aadf7ff 100644 --- a/src/vs/editor/common/commands/replaceCommand.ts +++ b/src/vs/editor/common/commands/replaceCommand.ts @@ -45,7 +45,7 @@ export class ReplaceOvertypeCommand implements ICommand { } public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { - const intialStartPosition = this._range.getStartPosition(); + const initialStartPosition = this._range.getStartPosition(); const initialEndPosition = this._range.getEndPosition(); const initialEndLineNumber = initialEndPosition.lineNumber; const offsetDelta = this._text.length + (this._range.isEmpty() ? 0 : -1); @@ -53,7 +53,7 @@ export class ReplaceOvertypeCommand implements ICommand { if (endPosition.lineNumber > initialEndLineNumber) { endPosition = new Position(initialEndLineNumber, model.getLineMaxColumn(initialEndLineNumber)); } - const replaceRange = Range.fromPositions(intialStartPosition, endPosition); + const replaceRange = Range.fromPositions(initialStartPosition, endPosition); builder.addTrackedEditOperation(replaceRange, this._text); } diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index b4783dfc12c..2a0641f8c96 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -64,15 +64,17 @@ const editorConfiguration: IConfigurationNode = { description: nls.localize('largeFileOptimizations', "Special handling for large files to disable certain memory intensive features.") }, 'editor.wordBasedSuggestions': { - enum: ['off', 'currentDocument', 'matchingDocuments', 'allDocuments'], - default: 'matchingDocuments', + enum: ['off', 'offWithInlineSuggestions', 'currentDocument', 'matchingDocuments', 'allDocuments'], + default: 'offWithInlineSuggestions', enumDescriptions: [ nls.localize('wordBasedSuggestions.off', 'Turn off Word Based Suggestions.'), + nls.localize('wordBasedSuggestions.offWithInlineSuggestions', 'Turn off Word Based Suggestions when Inline Suggestions are present.'), nls.localize('wordBasedSuggestions.currentDocument', 'Only suggest words from the active document.'), nls.localize('wordBasedSuggestions.matchingDocuments', 'Suggest words from all open documents of the same language.'), - nls.localize('wordBasedSuggestions.allDocuments', 'Suggest words from all open documents.') + nls.localize('wordBasedSuggestions.allDocuments', 'Suggest words from all open documents.'), ], - description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document and from which documents they are computed.") + description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document and from which documents they are computed."), + experiment: { mode: 'auto' }, }, 'editor.semanticHighlighting.enabled': { enum: [true, false, 'configuredByTheme'], diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 45a1772562f..a6e7322d41c 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2294,9 +2294,9 @@ class EditorGoToLocation extends BaseEditorOption; return { - enabled: boolean(input.enabled, this.defaultValue.enabled), + enabled: stringSet<'on' | 'off' | 'onKeyboardModifier'>(input.enabled, this.defaultValue.enabled, ['on', 'off', 'onKeyboardModifier']), delay: EditorIntOption.clampedInt(input.delay, this.defaultValue.delay, 0, 10000), sticky: boolean(input.sticky, this.defaultValue.sticky), hidingDelay: EditorIntOption.clampedInt(input.hidingDelay, this.defaultValue.hidingDelay, 0, 600000), @@ -4436,6 +4442,8 @@ export interface IInlineSuggestOptions { showCollapsed?: boolean; + showLongDistanceHint?: boolean; + /** * @internal */ @@ -4494,6 +4502,7 @@ class InlineEditorSuggest extends BaseEditorOption number): Size2D { + return new Size2D(map(this.width), map(this.height)); + } + + public isZero(): boolean { + return this.width === 0 && this.height === 0; + } + + public transpose(): Size2D { + return new Size2D(this.height, this.width); + } + + public toDimension(): IDimension { + return { width: this.width, height: this.height }; + } +} diff --git a/src/vs/editor/common/core/editorColorRegistry.ts b/src/vs/editor/common/core/editorColorRegistry.ts index 15679ff7ba9..e71205d88c2 100644 --- a/src/vs/editor/common/core/editorColorRegistry.ts +++ b/src/vs/editor/common/core/editorColorRegistry.ts @@ -12,6 +12,7 @@ import { registerThemingParticipant } from '../../../platform/theme/common/theme * Definition of the editor colors */ export const editorLineHighlight = registerColor('editor.lineHighlightBackground', null, nls.localize('lineHighlight', 'Background color for the highlight of line at the cursor position.')); +export const editorInactiveLineHighlight = registerColor('editor.inactiveLineHighlightBackground', editorLineHighlight, nls.localize('inactiveLineHighlight', 'Background color for the highlight of line at the cursor position when the editor is not focused.')); export const editorLineHighlightBorder = registerColor('editor.lineHighlightBorder', { dark: '#282828', light: '#eeeeee', hcDark: '#f38518', hcLight: contrastBorder }, nls.localize('lineHighlightBorderBox', 'Background color for the border around the line at the cursor position.')); export const editorRangeHighlight = registerColor('editor.rangeHighlightBackground', { dark: '#ffffff0b', light: '#fdff0033', hcDark: null, hcLight: null }, nls.localize('rangeHighlight', 'Background color of highlighted ranges, like by quick open and find features. The color must not be opaque so as not to hide underlying decorations.'), true); export const editorRangeHighlightBorder = registerColor('editor.rangeHighlightBorder', { dark: null, light: null, hcDark: activeContrastBorder, hcLight: activeContrastBorder }, nls.localize('rangeHighlightBorder', 'Background color of the border around highlighted ranges.')); @@ -54,6 +55,7 @@ export const editorCodeLensForeground = registerColor('editorCodeLens.foreground export const editorBracketMatchBackground = registerColor('editorBracketMatch.background', { dark: '#0064001a', light: '#0064001a', hcDark: '#0064001a', hcLight: '#0000' }, nls.localize('editorBracketMatchBackground', 'Background color behind matching brackets')); export const editorBracketMatchBorder = registerColor('editorBracketMatch.border', { dark: '#888', light: '#B9B9B9', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('editorBracketMatchBorder', 'Color for matching brackets boxes')); +export const editorBracketMatchForeground = registerColor('editorBracketMatch.foreground', null, nls.localize('editorBracketMatchForeground', 'Foreground color for matching brackets')); export const editorOverviewRulerBorder = registerColor('editorOverviewRuler.border', { dark: '#7f7f7f4d', light: '#7f7f7f4d', hcDark: '#7f7f7f4d', hcLight: '#666666' }, nls.localize('editorOverviewRulerBorder', 'Color of the overview ruler border.')); export const editorOverviewRulerBackground = registerColor('editorOverviewRuler.background', null, nls.localize('editorOverviewRulerBackground', 'Background color of the editor overview ruler.')); diff --git a/src/vs/editor/common/core/edits/stringEdit.ts b/src/vs/editor/common/core/edits/stringEdit.ts index 4d00122d459..279006a2941 100644 --- a/src/vs/editor/common/core/edits/stringEdit.ts +++ b/src/vs/editor/common/core/edits/stringEdit.ts @@ -100,30 +100,33 @@ export abstract class BaseStringEdit = BaseSt while (ourIdx < this.replacements.length || baseIdx < base.replacements.length) { // take the edit that starts first - const baseEdit = base.replacements[baseIdx]; - const ourEdit = this.replacements[ourIdx]; + const baseEdit = base.replacements.at(baseIdx); + const ourEdit = this.replacements.at(ourIdx); if (!ourEdit) { // We processed all our edits break; } else if (!baseEdit) { // no more edits from base - newEdits.push(new StringReplacement( - ourEdit.replaceRange.delta(offset), - ourEdit.newText - )); + const transformedRange = ourEdit.replaceRange.delta(offset); + newEdits.push(new StringReplacement(transformedRange, ourEdit.newText)); ourIdx++; - } else if (ourEdit.replaceRange.intersectsOrTouches(baseEdit.replaceRange)) { + } else if ( + ourEdit.replaceRange.intersects(baseEdit.replaceRange) || + areConcurrentInserts(ourEdit.replaceRange, baseEdit.replaceRange) || + isInsertStrictlyInsideRange(ourEdit.replaceRange, baseEdit.replaceRange) || + isInsertStrictlyInsideRange(baseEdit.replaceRange, ourEdit.replaceRange) + ) { ourIdx++; // Don't take our edit, as it is conflicting -> skip if (noOverlap) { return undefined; } - } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start) { - // Our edit starts first - newEdits.push(new StringReplacement( - ourEdit.replaceRange.delta(offset), - ourEdit.newText - )); + } else if (ourEdit.replaceRange.start < baseEdit.replaceRange.start || + (ourEdit.replaceRange.isEmpty && ourEdit.replaceRange.start === baseEdit.replaceRange.start)) { + // Our edit starts first, or is an insert at the start of base's range + const transformedRange = ourEdit.replaceRange.delta(offset); + // Check if the transformed edit would violate the sorted/disjoint invariant + newEdits.push(new StringReplacement(transformedRange, ourEdit.newText)); ourIdx++; } else { baseIdx++; @@ -284,6 +287,25 @@ export abstract class BaseStringReplacement = * All these replacements are applied at once. */ export class StringEdit extends BaseStringEdit { + /** + * Parses an edit from its string representation. + * E.g. [[2, 12) -> "fgh", [14, 20) -> "qrst", [22, 22) -> "de\n"] + */ + public static parse(toStringValue: string): StringEdit { + const replacements: StringReplacement[] = []; + const regex = /\[(\d+),\s*(\d+)\)\s*->\s*"([^"]*)"/g; + let match; + + while ((match = regex.exec(toStringValue)) !== null) { + const start = parseInt(match[1], 10); + const endEx = parseInt(match[2], 10); + const text = match[3].replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\\\/g, '\\'); + replacements.push(new StringReplacement(new OffsetRange(start, endEx), text)); + } + + return new StringEdit(replacements); + } + public static readonly empty = new StringEdit([]); public static create(replacements: readonly StringReplacement[]): StringEdit { @@ -573,3 +595,19 @@ export class AnnotatedStringReplacement> extends BaseStri } } +/** + * Returns true if both ranges are empty (inserts) at the exact same position. + * In this case, although they don't "intersect" in the traditional sense, + * they conflict because the order of insertion matters. + */ +function areConcurrentInserts(r1: OffsetRange, r2: OffsetRange): boolean { + return r1.isEmpty && r2.isEmpty && r1.start === r2.start; +} + +/** + * Returns true if `insert` is an empty range (insert) strictly inside `range`. + * For example, insert at position 5 is inside [3, 7) but not inside [5, 7) or [3, 5). + */ +function isInsertStrictlyInsideRange(insert: OffsetRange, range: OffsetRange): boolean { + return insert.isEmpty && range.start < insert.start && insert.start < range.endExclusive; +} diff --git a/src/vs/editor/common/core/edits/textEdit.ts b/src/vs/editor/common/core/edits/textEdit.ts index 4a1472d224f..fe9b52b1afb 100644 --- a/src/vs/editor/common/core/edits/textEdit.ts +++ b/src/vs/editor/common/core/edits/textEdit.ts @@ -13,6 +13,7 @@ import { Position } from '../position.js'; import { Range } from '../range.js'; import { TextLength } from '../text/textLength.js'; import { AbstractText, StringText } from '../text/abstractText.js'; +import { IEquatable } from '../../../../base/common/equals.js'; export class TextEdit { public static fromStringEdit(edit: BaseStringEdit, initialState: AbstractText): TextEdit { @@ -204,6 +205,380 @@ export class TextEdit { return equals(this.replacements, other.replacements, (a, b) => a.equals(b)); } + /** + * Combines two edits into one with the same effect. + * WARNING: This is written by AI, but well tested. I do not understand the implementation myself. + * + * Invariant: + * ``` + * other.applyToString(this.applyToString(s0)) = this.compose(other).applyToString(s0) + * ``` + */ + compose(other: TextEdit): TextEdit { + const edits1 = this.normalize(); + const edits2 = other.normalize(); + + if (edits1.replacements.length === 0) { return edits2; } + if (edits2.replacements.length === 0) { return edits1; } + + const resultReplacements: TextReplacement[] = []; + + let edit1Idx = 0; + let lastEdit1EndS0Line = 1; + let lastEdit1EndS0Col = 1; + + let headSrcRangeStartLine = 0; + let headSrcRangeStartCol = 0; + let headSrcRangeEndLine = 0; + let headSrcRangeEndCol = 0; + let headText: string | null = null; + let headLengthLine = 0; + let headLengthCol = 0; + + let headHasValue = false; + let headIsInfinite = false; + + let currentPosInS1Line = 1; + let currentPosInS1Col = 1; + + function ensureHead() { + if (headHasValue) { return; } + + if (edit1Idx < edits1.replacements.length) { + const nextEdit = edits1.replacements[edit1Idx]; + const nextEditStart = nextEdit.range.getStartPosition(); + + const gapIsEmpty = (lastEdit1EndS0Line === nextEditStart.lineNumber) && (lastEdit1EndS0Col === nextEditStart.column); + + if (!gapIsEmpty) { + headSrcRangeStartLine = lastEdit1EndS0Line; + headSrcRangeStartCol = lastEdit1EndS0Col; + headSrcRangeEndLine = nextEditStart.lineNumber; + headSrcRangeEndCol = nextEditStart.column; + + headText = null; + + if (lastEdit1EndS0Line === nextEditStart.lineNumber) { + headLengthLine = 0; + headLengthCol = nextEditStart.column - lastEdit1EndS0Col; + } else { + headLengthLine = nextEditStart.lineNumber - lastEdit1EndS0Line; + headLengthCol = nextEditStart.column - 1; + } + + headHasValue = true; + lastEdit1EndS0Line = nextEditStart.lineNumber; + lastEdit1EndS0Col = nextEditStart.column; + } else { + const nextEditEnd = nextEdit.range.getEndPosition(); + headSrcRangeStartLine = nextEditStart.lineNumber; + headSrcRangeStartCol = nextEditStart.column; + headSrcRangeEndLine = nextEditEnd.lineNumber; + headSrcRangeEndCol = nextEditEnd.column; + + headText = nextEdit.text; + + let line = 0; + let column = 0; + const text = nextEdit.text; + for (let i = 0; i < text.length; i++) { + if (text.charCodeAt(i) === 10) { + line++; + column = 0; + } else { + column++; + } + } + headLengthLine = line; + headLengthCol = column; + + headHasValue = true; + lastEdit1EndS0Line = nextEditEnd.lineNumber; + lastEdit1EndS0Col = nextEditEnd.column; + edit1Idx++; + } + } else { + headIsInfinite = true; + headSrcRangeStartLine = lastEdit1EndS0Line; + headSrcRangeStartCol = lastEdit1EndS0Col; + headHasValue = true; + } + } + + function splitText(text: string, lenLine: number, lenCol: number): [string, string] { + if (lenLine === 0 && lenCol === 0) { return ['', text]; } + let line = 0; + let offset = 0; + while (line < lenLine) { + const idx = text.indexOf('\n', offset); + if (idx === -1) { throw new BugIndicatingError('Text length mismatch'); } + offset = idx + 1; + line++; + } + offset += lenCol; + return [text.substring(0, offset), text.substring(offset)]; + } + + for (const r2 of edits2.replacements) { + const r2Start = r2.range.getStartPosition(); + const r2End = r2.range.getEndPosition(); + + while (true) { + if (currentPosInS1Line === r2Start.lineNumber && currentPosInS1Col === r2Start.column) { break; } + ensureHead(); + + if (headIsInfinite) { + let distLine: number, distCol: number; + if (currentPosInS1Line === r2Start.lineNumber) { + distLine = 0; + distCol = r2Start.column - currentPosInS1Col; + } else { + distLine = r2Start.lineNumber - currentPosInS1Line; + distCol = r2Start.column - 1; + } + + currentPosInS1Line = r2Start.lineNumber; + currentPosInS1Col = r2Start.column; + + if (distLine === 0) { + headSrcRangeStartCol += distCol; + } else { + headSrcRangeStartLine += distLine; + headSrcRangeStartCol = distCol + 1; + } + break; + } + + let headEndInS1Line: number, headEndInS1Col: number; + if (headLengthLine === 0) { + headEndInS1Line = currentPosInS1Line; + headEndInS1Col = currentPosInS1Col + headLengthCol; + } else { + headEndInS1Line = currentPosInS1Line + headLengthLine; + headEndInS1Col = headLengthCol + 1; + } + + let r2StartIsBeforeHeadEnd = false; + if (r2Start.lineNumber < headEndInS1Line) { + r2StartIsBeforeHeadEnd = true; + } else if (r2Start.lineNumber === headEndInS1Line) { + r2StartIsBeforeHeadEnd = r2Start.column < headEndInS1Col; + } + + if (r2StartIsBeforeHeadEnd) { + let splitLenLine: number, splitLenCol: number; + if (currentPosInS1Line === r2Start.lineNumber) { + splitLenLine = 0; + splitLenCol = r2Start.column - currentPosInS1Col; + } else { + splitLenLine = r2Start.lineNumber - currentPosInS1Line; + splitLenCol = r2Start.column - 1; + } + + let remainingLenLine: number, remainingLenCol: number; + if (splitLenLine === headLengthLine) { + remainingLenLine = 0; + remainingLenCol = headLengthCol - splitLenCol; + } else { + remainingLenLine = headLengthLine - splitLenLine; + remainingLenCol = headLengthCol; + } + + if (headText !== null) { + const [t1, t2] = splitText(headText, splitLenLine, splitLenCol); + resultReplacements.push(new TextReplacement(new Range(headSrcRangeStartLine, headSrcRangeStartCol, headSrcRangeEndLine, headSrcRangeEndCol), t1)); + + headText = t2; + headLengthLine = remainingLenLine; + headLengthCol = remainingLenCol; + + headSrcRangeStartLine = headSrcRangeEndLine; + headSrcRangeStartCol = headSrcRangeEndCol; + } else { + let splitPosLine: number, splitPosCol: number; + if (splitLenLine === 0) { + splitPosLine = headSrcRangeStartLine; + splitPosCol = headSrcRangeStartCol + splitLenCol; + } else { + splitPosLine = headSrcRangeStartLine + splitLenLine; + splitPosCol = splitLenCol + 1; + } + + headSrcRangeStartLine = splitPosLine; + headSrcRangeStartCol = splitPosCol; + + headLengthLine = remainingLenLine; + headLengthCol = remainingLenCol; + } + currentPosInS1Line = r2Start.lineNumber; + currentPosInS1Col = r2Start.column; + break; + } + + if (headText !== null) { + resultReplacements.push(new TextReplacement(new Range(headSrcRangeStartLine, headSrcRangeStartCol, headSrcRangeEndLine, headSrcRangeEndCol), headText)); + } + + currentPosInS1Line = headEndInS1Line; + currentPosInS1Col = headEndInS1Col; + headHasValue = false; + } + + let consumedStartS0Line: number | null = null; + let consumedStartS0Col: number | null = null; + let consumedEndS0Line: number | null = null; + let consumedEndS0Col: number | null = null; + + while (true) { + if (currentPosInS1Line === r2End.lineNumber && currentPosInS1Col === r2End.column) { break; } + ensureHead(); + + if (headIsInfinite) { + let distLine: number, distCol: number; + if (currentPosInS1Line === r2End.lineNumber) { + distLine = 0; + distCol = r2End.column - currentPosInS1Col; + } else { + distLine = r2End.lineNumber - currentPosInS1Line; + distCol = r2End.column - 1; + } + + let rangeInS0EndLine: number, rangeInS0EndCol: number; + if (distLine === 0) { + rangeInS0EndLine = headSrcRangeStartLine; + rangeInS0EndCol = headSrcRangeStartCol + distCol; + } else { + rangeInS0EndLine = headSrcRangeStartLine + distLine; + rangeInS0EndCol = distCol + 1; + } + + if (consumedStartS0Line === null) { + consumedStartS0Line = headSrcRangeStartLine; + consumedStartS0Col = headSrcRangeStartCol; + } + consumedEndS0Line = rangeInS0EndLine; + consumedEndS0Col = rangeInS0EndCol; + + currentPosInS1Line = r2End.lineNumber; + currentPosInS1Col = r2End.column; + + headSrcRangeStartLine = rangeInS0EndLine; + headSrcRangeStartCol = rangeInS0EndCol; + break; + } + + let headEndInS1Line: number, headEndInS1Col: number; + if (headLengthLine === 0) { + headEndInS1Line = currentPosInS1Line; + headEndInS1Col = currentPosInS1Col + headLengthCol; + } else { + headEndInS1Line = currentPosInS1Line + headLengthLine; + headEndInS1Col = headLengthCol + 1; + } + + let r2EndIsBeforeHeadEnd = false; + if (r2End.lineNumber < headEndInS1Line) { + r2EndIsBeforeHeadEnd = true; + } else if (r2End.lineNumber === headEndInS1Line) { + r2EndIsBeforeHeadEnd = r2End.column < headEndInS1Col; + } + + if (r2EndIsBeforeHeadEnd) { + let splitLenLine: number, splitLenCol: number; + if (currentPosInS1Line === r2End.lineNumber) { + splitLenLine = 0; + splitLenCol = r2End.column - currentPosInS1Col; + } else { + splitLenLine = r2End.lineNumber - currentPosInS1Line; + splitLenCol = r2End.column - 1; + } + + let remainingLenLine: number, remainingLenCol: number; + if (splitLenLine === headLengthLine) { + remainingLenLine = 0; + remainingLenCol = headLengthCol - splitLenCol; + } else { + remainingLenLine = headLengthLine - splitLenLine; + remainingLenCol = headLengthCol; + } + + if (headText !== null) { + if (consumedStartS0Line === null) { + consumedStartS0Line = headSrcRangeStartLine; + consumedStartS0Col = headSrcRangeStartCol; + } + consumedEndS0Line = headSrcRangeEndLine; + consumedEndS0Col = headSrcRangeEndCol; + + const [, t2] = splitText(headText, splitLenLine, splitLenCol); + headText = t2; + headLengthLine = remainingLenLine; + headLengthCol = remainingLenCol; + + headSrcRangeStartLine = headSrcRangeEndLine; + headSrcRangeStartCol = headSrcRangeEndCol; + } else { + let splitPosLine: number, splitPosCol: number; + if (splitLenLine === 0) { + splitPosLine = headSrcRangeStartLine; + splitPosCol = headSrcRangeStartCol + splitLenCol; + } else { + splitPosLine = headSrcRangeStartLine + splitLenLine; + splitPosCol = splitLenCol + 1; + } + + if (consumedStartS0Line === null) { + consumedStartS0Line = headSrcRangeStartLine; + consumedStartS0Col = headSrcRangeStartCol; + } + consumedEndS0Line = splitPosLine; + consumedEndS0Col = splitPosCol; + + headSrcRangeStartLine = splitPosLine; + headSrcRangeStartCol = splitPosCol; + + headLengthLine = remainingLenLine; + headLengthCol = remainingLenCol; + } + currentPosInS1Line = r2End.lineNumber; + currentPosInS1Col = r2End.column; + break; + } + + if (consumedStartS0Line === null) { + consumedStartS0Line = headSrcRangeStartLine; + consumedStartS0Col = headSrcRangeStartCol; + } + consumedEndS0Line = headSrcRangeEndLine; + consumedEndS0Col = headSrcRangeEndCol; + + currentPosInS1Line = headEndInS1Line; + currentPosInS1Col = headEndInS1Col; + headHasValue = false; + } + + if (consumedStartS0Line !== null) { + resultReplacements.push(new TextReplacement(new Range(consumedStartS0Line, consumedStartS0Col!, consumedEndS0Line!, consumedEndS0Col!), r2.text)); + } else { + ensureHead(); + const insertPosS0Line = headSrcRangeStartLine; + const insertPosS0Col = headSrcRangeStartCol; + resultReplacements.push(new TextReplacement(new Range(insertPosS0Line, insertPosS0Col, insertPosS0Line, insertPosS0Col), r2.text)); + } + } + + while (true) { + ensureHead(); + if (headIsInfinite) { break; } + if (headText !== null) { + resultReplacements.push(new TextReplacement(new Range(headSrcRangeStartLine, headSrcRangeStartCol, headSrcRangeEndLine, headSrcRangeEndCol), headText)); + } + headHasValue = false; + } + + return new TextEdit(resultReplacements).normalize(); + } + toString(text: AbstractText | string | undefined): string { if (text === undefined) { return this.replacements.map(edit => edit.toString()).join('\n'); @@ -267,7 +642,7 @@ export class TextEdit { } } -export class TextReplacement { +export class TextReplacement implements IEquatable { public static joinReplacements(replacements: TextReplacement[], initialValue: AbstractText): TextReplacement { if (replacements.length === 0) { throw new BugIndicatingError(); } if (replacements.length === 1) { return replacements[0]; } diff --git a/src/vs/editor/common/core/range.ts b/src/vs/editor/common/core/range.ts index 72a1086d98e..b42088e53a1 100644 --- a/src/vs/editor/common/core/range.ts +++ b/src/vs/editor/common/core/range.ts @@ -364,6 +364,9 @@ export class Range { return new Range(this.startLineNumber + lineCount, this.startColumn, this.endLineNumber + lineCount, this.endColumn); } + /** + * Test if this range starts and ends on the same line. + */ public isSingleLine(): boolean { return this.startLineNumber === this.endLineNumber; } diff --git a/src/vs/editor/common/core/ranges/offsetRange.ts b/src/vs/editor/common/core/ranges/offsetRange.ts index d776adc4924..e279b382078 100644 --- a/src/vs/editor/common/core/ranges/offsetRange.ts +++ b/src/vs/editor/common/core/ranges/offsetRange.ts @@ -18,6 +18,10 @@ export class OffsetRange implements IOffsetRange { return new OffsetRange(start, endExclusive); } + public static equals(r1: IOffsetRange, r2: IOffsetRange): boolean { + return r1.start === r2.start && r1.endExclusive === r2.endExclusive; + } + public static addRange(range: OffsetRange, sortedRanges: OffsetRange[]): void { let i = 0; while (i < sortedRanges.length && sortedRanges[i].endExclusive < range.start) { @@ -126,6 +130,10 @@ export class OffsetRange implements IOffsetRange { return Math.max(0, end - start); } + /** + * `a.intersects(b)` iff there exists a number n so that `a.contains(n)` and `b.contains(n)`. + * Warning: If one range is empty, this method returns always false. + */ public intersects(other: OffsetRange): boolean { const start = Math.max(this.start, other.start); const end = Math.min(this.endExclusive, other.endExclusive); @@ -208,6 +216,15 @@ export class OffsetRange implements IOffsetRange { } return new OffsetRange(this.start, range.endExclusive); } + + public withMargin(margin: number): OffsetRange; + public withMargin(marginStart: number, marginEnd: number): OffsetRange; + public withMargin(marginStart: number, marginEnd?: number): OffsetRange { + if (marginEnd === undefined) { + marginEnd = marginStart; + } + return new OffsetRange(this.start - marginStart, this.endExclusive + marginEnd); + } } export class OffsetRangeSet { diff --git a/src/vs/editor/common/core/text/positionToOffsetImpl.ts b/src/vs/editor/common/core/text/positionToOffsetImpl.ts index 75c0ea80ee9..dfb30ff65da 100644 --- a/src/vs/editor/common/core/text/positionToOffsetImpl.ts +++ b/src/vs/editor/common/core/text/positionToOffsetImpl.ts @@ -73,27 +73,43 @@ export function _setPositionOffsetTransformerDependencies(deps: IDeps): void { } export class PositionOffsetTransformer extends PositionOffsetTransformerBase { - private readonly lineStartOffsetByLineIdx: number[]; - private readonly lineEndOffsetByLineIdx: number[]; + private _lineStartOffsetByLineIdx: number[] | undefined; + private _lineEndOffsetByLineIdx: number[] | undefined; constructor(public readonly text: string) { super(); + } + + private get lineStartOffsetByLineIdx(): number[] { + if (!this._lineStartOffsetByLineIdx) { + this._computeLineOffsets(); + } + return this._lineStartOffsetByLineIdx!; + } + + private get lineEndOffsetByLineIdx(): number[] { + if (!this._lineEndOffsetByLineIdx) { + this._computeLineOffsets(); + } + return this._lineEndOffsetByLineIdx!; + } - this.lineStartOffsetByLineIdx = []; - this.lineEndOffsetByLineIdx = []; + private _computeLineOffsets(): void { + this._lineStartOffsetByLineIdx = []; + this._lineEndOffsetByLineIdx = []; - this.lineStartOffsetByLineIdx.push(0); - for (let i = 0; i < text.length; i++) { - if (text.charAt(i) === '\n') { - this.lineStartOffsetByLineIdx.push(i + 1); - if (i > 0 && text.charAt(i - 1) === '\r') { - this.lineEndOffsetByLineIdx.push(i - 1); + this._lineStartOffsetByLineIdx.push(0); + for (let i = 0; i < this.text.length; i++) { + if (this.text.charAt(i) === '\n') { + this._lineStartOffsetByLineIdx.push(i + 1); + if (i > 0 && this.text.charAt(i - 1) === '\r') { + this._lineEndOffsetByLineIdx.push(i - 1); } else { - this.lineEndOffsetByLineIdx.push(i); + this._lineEndOffsetByLineIdx.push(i); } } } - this.lineEndOffsetByLineIdx.push(text.length); + this._lineEndOffsetByLineIdx.push(this.text.length); } override getOffset(position: Position): number { diff --git a/src/vs/editor/common/cursor/cursorTypeEditOperations.ts b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts index e250bb74431..d1290a3e651 100644 --- a/src/vs/editor/common/cursor/cursorTypeEditOperations.ts +++ b/src/vs/editor/common/cursor/cursorTypeEditOperations.ts @@ -664,15 +664,15 @@ export class PasteOperation { } private static _distributePasteToCursors(config: CursorConfiguration, selections: Selection[], text: string, pasteOnNewLine: boolean, multicursorText: string[]): string[] | null { - if (pasteOnNewLine) { - return null; - } if (selections.length === 1) { return null; } if (multicursorText && multicursorText.length === selections.length) { return multicursorText; } + if (pasteOnNewLine) { + return null; + } if (config.multiCursorPaste === 'spread') { // Try to spread the pasted text in case the line count matches the cursor count // Remove trailing \n if present diff --git a/src/vs/editor/common/cursor/cursorWordOperations.ts b/src/vs/editor/common/cursor/cursorWordOperations.ts index 9c7aa6f182a..0a3f7170f97 100644 --- a/src/vs/editor/common/cursor/cursorWordOperations.ts +++ b/src/vs/editor/common/cursor/cursorWordOperations.ts @@ -484,7 +484,7 @@ export class WordOperations { return new Range(lineNumber, column, position.lineNumber, position.column); } - public static deleteInsideWord(wordSeparators: WordCharacterClassifier, model: ITextModel, selection: Selection): Range { + public static deleteInsideWord(wordSeparators: WordCharacterClassifier, model: ITextModel, selection: Selection, onlyWord: boolean = false): Range { if (!selection.isEmpty()) { return selection; } @@ -496,7 +496,7 @@ export class WordOperations { return r; } - return this._deleteInsideWordDetermineDeleteRange(wordSeparators, model, position); + return this._deleteInsideWordDetermineDeleteRange(wordSeparators, model, position, onlyWord); } private static _charAtIsWhitespace(str: string, index: number): boolean { @@ -538,7 +538,7 @@ export class WordOperations { return new Range(position.lineNumber, leftIndex + 1, position.lineNumber, rightIndex + 2); } - private static _deleteInsideWordDetermineDeleteRange(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position): Range { + private static _deleteInsideWordDetermineDeleteRange(wordSeparators: WordCharacterClassifier, model: ICursorSimpleModel, position: Position, onlyWord: boolean): Range { const lineContent = model.getLineContent(position.lineNumber); const lineLength = lineContent.length; if (lineLength === 0) { @@ -566,6 +566,9 @@ export class WordOperations { const deleteWordAndAdjacentWhitespace = (word: IFindWordResult) => { let startColumn = word.start + 1; let endColumn = word.end + 1; + if (onlyWord) { + return createRangeWithPosition(startColumn, endColumn); + } let expandedToTheRight = false; while (endColumn - 1 < lineLength && this._charAtIsWhitespace(lineContent, endColumn - 1)) { expandedToTheRight = true; diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index 4b8e4eafa89..d1e7c0dd95c 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -60,6 +60,9 @@ export namespace EditorContextKeys { export const standaloneColorPickerVisible = new RawContextKey('standaloneColorPickerVisible', false, nls.localize('standaloneColorPickerVisible', "Whether the standalone color picker is visible")); export const standaloneColorPickerFocused = new RawContextKey('standaloneColorPickerFocused', false, nls.localize('standaloneColorPickerFocused', "Whether the standalone color picker is focused")); + + export const isComposing = new RawContextKey('isComposing', false, nls.localize('isComposing', "Whether the editor is in the composition mode")); + /** * A context key that is set when an editor is part of a larger editor, like notebooks or * (future) a diff editor diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 2729d4014e6..25724438958 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -27,6 +27,7 @@ import { localize } from '../../nls.js'; import { ExtensionIdentifier } from '../../platform/extensions/common/extensions.js'; import { IMarkerData } from '../../platform/markers/common/markers.js'; import { EditDeltaInfo } from './textModelEditSource.js'; +import { FontTokensUpdate } from './textModelEvents.js'; /** * @internal @@ -64,6 +65,17 @@ export class TokenizationResult { } } +/** + * @internal + */ +export interface IFontToken { + readonly startIndex: number; + readonly endIndex: number; + readonly fontFamily: string | null; + readonly fontSizeMultiplier: number | null; + readonly lineHeightMultiplier: number | null; +} + /** * @internal */ @@ -78,6 +90,7 @@ export class EncodedTokenizationResult { * */ public readonly tokens: Uint32Array, + public readonly fontInfo: IFontToken[], public readonly endState: IState, ) { } @@ -140,6 +153,8 @@ export interface IBackgroundTokenizer extends IDisposable { export interface IBackgroundTokenizationStore { setTokens(tokens: ContiguousMultilineTokens[]): void; + setFontInfo(changes: FontTokensUpdate): void; + setEndState(lineNumber: number, state: IState): void; /** @@ -738,6 +753,18 @@ export enum InlineCompletionTriggerKind { Explicit = 1, } +/** + * Arbitrary data that the provider can pass when firing {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * This data is passed back to the provider in {@link InlineCompletionContext.changeHint}. + */ +export interface IInlineCompletionChangeHint { + /** + * Arbitrary data that the provider can use to identify what triggered the change. + * This data must be JSON serializable. + */ + readonly data?: unknown; +} + export interface InlineCompletionContext { /** @@ -760,6 +787,22 @@ export interface InlineCompletionContext { readonly includeInlineCompletions: boolean; readonly requestIssuedDateTime: number; readonly earliestShownDateTime: number; + + /** + * The change hint that was passed to {@link InlineCompletionsProvider.onDidChangeInlineCompletions}. + * Only set if this request was triggered by such an event. + */ + readonly changeHint?: IInlineCompletionChangeHint; +} + +export interface IInlineCompletionModelInfo { + models: IInlineCompletionModel[]; + currentModelId: string; +} + +export interface IInlineCompletionModel { + name: string; + id: string; } export class SelectedSuggestionInfo { @@ -837,12 +880,18 @@ export interface InlineCompletion { readonly warning?: InlineCompletionWarning; - readonly hint?: InlineCompletionHint; + readonly hint?: IInlineCompletionHint; + + readonly supportsRename?: boolean; /** * Used for telemetry. */ readonly correlationId?: string | undefined; + + readonly jumpToPosition?: IPosition; + + readonly doNotLog?: boolean; } export interface InlineCompletionWarning { @@ -855,12 +904,11 @@ export enum InlineCompletionHintStyle { Label = 2 } -export interface InlineCompletionHint { +export interface IInlineCompletionHint { /** Refers to the current document. */ range: IRange; style: InlineCompletionHintStyle; content: string; - jumpToEdit: boolean; } // TODO: add `| URI | { light: URI; dark: URI }`. @@ -916,7 +964,12 @@ export interface InlineCompletionsProvider; + /** + * Fired when the provider wants to trigger a new completion request. + * The event can pass a {@link IInlineCompletionChangeHint} which will be + * included in the {@link InlineCompletionContext} of the subsequent request. + */ + onDidChangeInlineCompletions?: Event; /** * Only used for {@link yieldsToGroupIds}. @@ -939,6 +992,10 @@ export interface InlineCompletionsProvider; + setModelId?(modelId: string): Promise; + toString?(): string; } @@ -1014,6 +1071,7 @@ export enum InlineCompletionEndOfLifeReasonKind { export type InlineCompletionEndOfLifeReason = { kind: InlineCompletionEndOfLifeReasonKind.Accepted; // User did an explicit action to accept + alternativeAction: boolean; // Whether the user performed an alternative action. } | { kind: InlineCompletionEndOfLifeReasonKind.Rejected; // User did an explicit action to reject } | { @@ -1033,6 +1091,7 @@ export type LifetimeSummary = { shownDuration: number; shownDurationUncollapsed: number; timeUntilShown: number | undefined; + timeUntilActuallyShown: number | undefined; timeUntilProviderRequest: number; timeUntilProviderResponse: number; notShownReason: string | undefined; @@ -1041,6 +1100,7 @@ export type LifetimeSummary = { preceeded: boolean; languageId: string; requestReason: string; + performanceMarkers?: string; cursorColumnDistance?: number; cursorLineDistance?: number; lineCountOriginal?: number; @@ -1053,6 +1113,16 @@ export type LifetimeSummary = { typingIntervalCharacterCount: number; selectedSuggestionInfo: boolean; availableProviders: string; + skuPlan: string | undefined; + skuType: string | undefined; + renameCreated: boolean | undefined; + renameDuration: number | undefined; + renameTimedOut: boolean | undefined; + renameDroppedOtherEdits: number | undefined; + renameDroppedRenameEdits: number | undefined; + editKind: string | undefined; + longDistanceHintVisible?: boolean; + longDistanceHintDistance?: number; }; export interface CodeAction { @@ -2283,6 +2353,7 @@ export interface Comment { readonly commentReactions?: CommentReaction[]; readonly label?: string; readonly mode?: CommentMode; + readonly state?: CommentState; readonly timestamp?: string; } diff --git a/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts b/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts index b269c27a4d1..34936e31acd 100644 --- a/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts +++ b/src/vs/editor/common/languages/defaultDocumentColorsComputer.ts @@ -100,8 +100,8 @@ function _findMatches(model: IDocumentColorComputerTarget | string, regex: RegEx function computeColors(model: IDocumentColorComputerTarget): IColorInformation[] { const result: IColorInformation[] = []; - // Early validation for RGB and HSL - const initialValidationRegex = /\b(rgb|rgba|hsl|hsla)(\([0-9\s,.\%]*\))|^(#)([A-Fa-f0-9]{3})\b|^(#)([A-Fa-f0-9]{4})\b|^(#)([A-Fa-f0-9]{6})\b|^(#)([A-Fa-f0-9]{8})\b|(?<=['"\s])(#)([A-Fa-f0-9]{3})\b|(?<=['"\s])(#)([A-Fa-f0-9]{4})\b|(?<=['"\s])(#)([A-Fa-f0-9]{6})\b|(?<=['"\s])(#)([A-Fa-f0-9]{8})\b/gm; + // Early validation for RGB and HSL (including CSS Level 4 syntax with / separator) + const initialValidationRegex = /\b(rgb|rgba|hsl|hsla)(\([0-9\s,.\%\/]*\))|^(#)([A-Fa-f0-9]{3})\b|^(#)([A-Fa-f0-9]{4})\b|^(#)([A-Fa-f0-9]{6})\b|^(#)([A-Fa-f0-9]{8})\b|(?<=['"\s])(#)([A-Fa-f0-9]{3})\b|(?<=['"\s])(#)([A-Fa-f0-9]{4})\b|(?<=['"\s])(#)([A-Fa-f0-9]{6})\b|(?<=['"\s])(#)([A-Fa-f0-9]{8})\b/gm; const initialValidationMatches = _findMatches(model, initialValidationRegex); // Potential colors have been found, validate the parameters @@ -115,16 +115,19 @@ function computeColors(model: IDocumentColorComputerTarget): IColorInformation[] } let colorInformation; if (colorScheme === 'rgb') { - const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*\)$/gm; + // Supports both comma-separated (rgb(255, 0, 0)) and CSS Level 4 space-separated syntax (rgb(255 0 0)) + const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*\)$/gm; colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false); } else if (colorScheme === 'rgba') { - const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(0[.][0-9]+|[.][0-9]+|[01][.]|[01])\s*\)$/gm; + // Supports both comma-separated (rgba(255, 0, 0, 0.5)) and CSS Level 4 syntax (rgba(255 0 0 / 0.5)) + const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*(?:[\s,]|[\s]*\/)\s*(0[.][0-9]+|[.][0-9]+|[01][.]|[01])\s*\)$/gm; colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true); } else if (colorScheme === 'hsl') { - const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*\)$/gm; + const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*\)$/gm; colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false); } else if (colorScheme === 'hsla') { - const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(0[.][0-9]+|[.][0-9]+|[01][.]0*|[01])\s*\)$/gm; + // Supports both comma-separated (hsla(253, 100%, 50%, 0.5)) and CSS Level 4 syntax (hsla(253 100% 50% / 0.5)) + const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*(?:[\s,]|[\s]*\/)\s*(0[.][0-9]+|[.][0-9]+|[01][.]0*|[01])\s*\)$/gm; colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true); } else if (colorScheme === '#') { colorInformation = _findHexColorInformation(_findRange(model, initialMatch), colorScheme + colorParameters); diff --git a/src/vs/editor/common/languages/nullTokenize.ts b/src/vs/editor/common/languages/nullTokenize.ts index 8966ab8b734..2ed15d199fe 100644 --- a/src/vs/editor/common/languages/nullTokenize.ts +++ b/src/vs/editor/common/languages/nullTokenize.ts @@ -30,5 +30,5 @@ export function nullTokenizeEncoded(languageId: LanguageId, state: IState | null | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) ) >>> 0; - return new EncodedTokenizationResult(tokens, state === null ? NullState : state); + return new EncodedTokenizationResult(tokens, [], state === null ? NullState : state); } diff --git a/src/vs/editor/common/languages/supports/tokenization.ts b/src/vs/editor/common/languages/supports/tokenization.ts index f6322a09dda..0545b34945d 100644 --- a/src/vs/editor/common/languages/supports/tokenization.ts +++ b/src/vs/editor/common/languages/supports/tokenization.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Color } from '../../../../base/common/color.js'; +import { IFontTokenOptions } from '../../../../platform/theme/common/themeService.js'; import { LanguageId, FontStyle, ColorId, StandardTokenType, MetadataConsts } from '../../encodedTokenAttributes.js'; export interface ITokenThemeRule { @@ -422,3 +423,46 @@ export function generateTokensCSSForColorMap(colorMap: readonly Color[]): string rules.push('.mtks.mtku { text-decoration: underline line-through; text-underline-position: under; }'); return rules.join('\n'); } + +export function generateTokensCSSForFontMap(fontMap: readonly IFontTokenOptions[]): string { + const rules: string[] = []; + const fonts = new Set(); + for (let i = 1, len = fontMap.length; i < len; i++) { + const font = fontMap[i]; + if (!font.fontFamily && !font.fontSizeMultiplier) { + continue; + } + const className = classNameForFontTokenDecorations(font.fontFamily ?? '', font.fontSizeMultiplier ?? 0); + if (fonts.has(className)) { + continue; + } + fonts.add(className); + let rule = `.${className} {`; + if (font.fontFamily) { + rule += `font-family: ${font.fontFamily};`; + } + if (font.fontSizeMultiplier) { + rule += `font-size: calc(var(--editor-font-size)*${font.fontSizeMultiplier});`; + } + rule += `}`; + rules.push(rule); + } + return rules.join('\n'); +} + +export function classNameForFontTokenDecorations(fontFamily: string, fontSize: number): string { + const safeFontFamily = sanitizeFontFamilyForClassName(fontFamily); + return cleanClassName(`font-decoration-${safeFontFamily}-${fontSize}`); +} + +function sanitizeFontFamilyForClassName(fontFamily: string): string { + const normalized = fontFamily.toLowerCase().trim(); + if (!normalized) { + return 'default'; + } + return cleanClassName(normalized); +} + +function cleanClassName(className: string): string { + return className.replace(/[^a-z0-9_-]/gi, '-'); +} diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 28862df0b5d..c70fa95a387 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -222,7 +222,7 @@ export interface IModelDecorationOptions { */ glyphMargin?: IModelDecorationGlyphMarginOptions | null; /** - * If set, the decoration will override the line height of the lines it spans. Maximum value is 300px. + * If set, the decoration will override the line height of the lines it spans. This value is a multiplier to the default line height. */ lineHeight?: number | null; /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts index 2b431cb64bd..66e78d57cb0 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.ts @@ -42,7 +42,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen //#endregion - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { if (onlyMinimapDecorations) { // Bracket pair colorization decorations are not rendered in the minimap return []; @@ -70,7 +70,7 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return result; } - getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[] { if (ownerId === undefined) { return []; } @@ -80,7 +80,8 @@ export class ColorizedBracketPairsDecorationProvider extends Disposable implemen return this.getDecorationsInRange( new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, - filterOutValidation + filterOutValidation, + filterFontDecorations ); } } diff --git a/src/vs/editor/common/model/decorationProvider.ts b/src/vs/editor/common/model/decorationProvider.ts index e3c146831de..e28ef209de8 100644 --- a/src/vs/editor/common/model/decorationProvider.ts +++ b/src/vs/editor/common/model/decorationProvider.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from '../../../base/common/event.js'; import { Range } from '../core/range.js'; import { IModelDecoration } from '../model.js'; @@ -16,14 +15,40 @@ export interface DecorationProvider { * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). * @return An array with the decorations */ - getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean): IModelDecoration[]; + getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean): IModelDecoration[]; /** * Gets all the decorations as an array. * @param ownerId If set, it will ignore decorations belonging to other owners. * @param filterOutValidation If set, it will ignore decorations specific to validation (i.e. warnings, errors). */ - getAllDecorations(ownerId?: number, filterOutValidation?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; + getAllDecorations(ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[]; - readonly onDidChange: Event; +} + +export class LineHeightChangingDecoration { + + public static toKey(obj: LineHeightChangingDecoration): string { + return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; + } + + constructor( + public readonly ownerId: number, + public readonly decorationId: string, + public readonly lineNumber: number, + public readonly lineHeight: number | null + ) { } +} + +export class LineFontChangingDecoration { + + public static toKey(obj: LineFontChangingDecoration): string { + return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; + } + + constructor( + public readonly ownerId: number, + public readonly decorationId: string, + public readonly lineNumber: number + ) { } } diff --git a/src/vs/editor/common/model/indentationGuesser.ts b/src/vs/editor/common/model/indentationGuesser.ts index ba6a7f64089..b3fb3ce0ac2 100644 --- a/src/vs/editor/common/model/indentationGuesser.ts +++ b/src/vs/editor/common/model/indentationGuesser.ts @@ -192,10 +192,7 @@ export function guessIndentation(source: ITextBuffer, defaultTabSize: number, de // Guess tabSize only if inserting spaces... if (insertSpaces) { - let tabSizeScore = (insertSpaces ? 0 : 0.1 * linesCount); - - // console.log("score threshold: " + tabSizeScore); - + let tabSizeScore = 0; ALLOWED_TAB_SIZE_GUESSES.forEach((possibleTabSize) => { const possibleTabSizeScore = spacesDiffCount[possibleTabSize]; if (possibleTabSizeScore > tabSizeScore) { @@ -204,14 +201,14 @@ export function guessIndentation(source: ITextBuffer, defaultTabSize: number, de } }); - // Let a tabSize of 2 win even if it is not the maximum - // (only in case 4 was guessed) - if (tabSize === 4 && spacesDiffCount[4] > 0 && spacesDiffCount[2] > 0 && spacesDiffCount[2] >= spacesDiffCount[4] / 2) { + // Let a tabSize of 2 win over 4 only if it has at least 2/3 of the occurrences of 4 + // This helps detect 2-space indentation in cases like YAML files where there might be + // some 4-space diffs from deeper nesting, while still preferring 4 when it's clearly predominant + if (tabSize === 4 && spacesDiffCount[4] > 0 && spacesDiffCount[2] > 0 && spacesDiffCount[2] >= spacesDiffCount[4] * 2 / 3) { tabSize = 2; } } - // console.log('--------------------------'); // console.log('linesIndentedWithTabsCount: ' + linesIndentedWithTabsCount + ', linesIndentedWithSpacesCount: ' + linesIndentedWithSpacesCount); // console.log('spacesDiffCount: ' + spacesDiffCount); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 342ddc740e0..f38bd4218d3 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -5,6 +5,8 @@ import { ArrayQueue, pushMany } from '../../../base/common/arrays.js'; import { VSBuffer, VSBufferReadableStream } from '../../../base/common/buffer.js'; +import { CharCode } from '../../../base/common/charCode.js'; +import { SetWithKey } from '../../../base/common/collections.js'; import { Color } from '../../../base/common/color.js'; import { BugIndicatingError, illegalArgument, onUnexpectedError } from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; @@ -15,19 +17,30 @@ import * as strings from '../../../base/common/strings.js'; import { ThemeColor } from '../../../base/common/themables.js'; import { Constants } from '../../../base/common/uint.js'; import { URI } from '../../../base/common/uri.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { isDark } from '../../../platform/theme/common/theme.js'; +import { IColorTheme } from '../../../platform/theme/common/themeService.js'; +import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js'; import { ISingleEditOperation } from '../core/editOperation.js'; +import { TextEdit } from '../core/edits/textEdit.js'; import { countEOL } from '../core/misc/eolCounter.js'; import { normalizeIndentation } from '../core/misc/indentation.js'; +import { EDITOR_MODEL_DEFAULTS } from '../core/misc/textModelDefaults.js'; import { IPosition, Position } from '../core/position.js'; import { IRange, Range } from '../core/range.js'; import { Selection } from '../core/selection.js'; import { TextChange } from '../core/textChange.js'; -import { EDITOR_MODEL_DEFAULTS } from '../core/misc/textModelDefaults.js'; import { IWordAtPosition } from '../core/wordHelper.js'; import { FormattingOptions } from '../languages.js'; import { ILanguageSelection, ILanguageService } from '../languages/language.js'; import { ILanguageConfigurationService } from '../languages/languageConfigurationRegistry.js'; import * as model from '../model.js'; +import { IBracketPairsTextModelPart } from '../textModelBracketPairs.js'; +import { EditSources, TextModelEditSource } from '../textModelEditSource.js'; +import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, LineInjectedText, ModelFontChanged, ModelFontChangedEvent, ModelInjectedTextChangedEvent, ModelLineHeightChanged, ModelLineHeightChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted } from '../textModelEvents.js'; +import { IGuidesTextModelPart } from '../textModelGuides.js'; +import { ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; +import { TokenArray } from '../tokens/lineTokens.js'; import { BracketPairsTextModelPart } from './bracketPairsTextModelPart/bracketPairsImpl.js'; import { ColorizedBracketPairsDecorationProvider } from './bracketPairsTextModelPart/colorizedBracketPairsDecorationProvider.js'; import { EditStack } from './editStack.js'; @@ -37,20 +50,10 @@ import { IntervalNode, IntervalTree, recomputeMaxEnd } from './intervalTree.js'; import { PieceTreeTextBuffer } from './pieceTreeTextBuffer/pieceTreeTextBuffer.js'; import { PieceTreeTextBufferBuilder } from './pieceTreeTextBuffer/pieceTreeTextBufferBuilder.js'; import { SearchParams, TextModelSearch } from './textModelSearch.js'; -import { TokenizationTextModelPart } from './tokens/tokenizationTextModelPart.js'; import { AttachedViews } from './tokens/abstractSyntaxTokenBackend.js'; -import { IBracketPairsTextModelPart } from '../textModelBracketPairs.js'; -import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptionsChangedEvent, InternalModelContentChangeEvent, ModelInjectedTextChangedEvent, ModelRawChange, ModelRawContentChangedEvent, ModelRawEOLChanged, ModelRawFlush, ModelRawLineChanged, ModelRawLinesDeleted, ModelRawLinesInserted, ModelLineHeightChangedEvent, ModelLineHeightChanged, ModelFontChangedEvent, ModelFontChanged, LineInjectedText } from '../textModelEvents.js'; -import { IGuidesTextModelPart } from '../textModelGuides.js'; -import { ITokenizationTextModelPart } from '../tokenizationTextModelPart.js'; -import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; -import { IColorTheme } from '../../../platform/theme/common/themeService.js'; -import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js'; -import { TokenArray } from '../tokens/lineTokens.js'; -import { SetWithKey } from '../../../base/common/collections.js'; -import { EditSources, TextModelEditSource } from '../textModelEditSource.js'; -import { TextEdit } from '../core/edits/textEdit.js'; -import { isDark } from '../../../platform/theme/common/theme.js'; +import { TokenizationFontDecorationProvider } from './tokens/tokenizationFontDecorationsProvider.js'; +import { LineFontChangingDecoration, LineHeightChangingDecoration } from './decorationProvider.js'; +import { TokenizationTextModelPart } from './tokens/tokenizationTextModelPart.js'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -291,6 +294,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati private _decorations: { [decorationId: string]: IntervalNode }; private _decorationsTree: DecorationsTrees; private readonly _decorationProvider: ColorizedBracketPairsDecorationProvider; + private readonly _fontTokenDecorationsProvider: TokenizationFontDecorationProvider; //#endregion private readonly _tokenizationTextModelPart: TokenizationTextModelPart; @@ -365,6 +369,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati languageId, this._attachedViews ); + this._fontTokenDecorationsProvider = this._register(new TokenizationFontDecorationProvider(this, this._tokenizationTextModelPart)); this._isTooLargeForSyncing = (bufferTextLength > TextModel._MODEL_SYNC_LIMIT); @@ -391,6 +396,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati this._onDidChangeDecorations.fire(); this._onDidChangeDecorations.endDeferredEmit(); })); + this._register(this._fontTokenDecorationsProvider.onDidChangeLineHeight((affectedLineHeights) => { + this._onDidChangeDecorations.beginDeferredEmit(); + this._onDidChangeDecorations.fire(); + this._fireOnDidChangeLineHeight(affectedLineHeights); + this._onDidChangeDecorations.endDeferredEmit(); + })); + this._register(this._fontTokenDecorationsProvider.onDidChangeFont((affectedFontLines) => { + this._onDidChangeDecorations.beginDeferredEmit(); + this._onDidChangeDecorations.fire(); + this._fireOnDidChangeFont(affectedFontLines); + this._onDidChangeDecorations.endDeferredEmit(); + })); this._languageService.requestRichLanguageFeatures(languageId); @@ -453,6 +470,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } this._tokenizationTextModelPart.handleDidChangeContent(change); this._bracketPairs.handleDidChangeContent(change); + this._fontTokenDecorationsProvider.handleDidChangeContent(change); this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change)); } @@ -1271,10 +1289,29 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (rawOperation instanceof model.ValidAnnotatedEditOperation) { return rawOperation; } + + const validatedRange = this.validateRange(rawOperation.range); + + // Normalize edit when replacement text ends with lone CR + // and the range ends right before a CRLF in the buffer. + // We strip the trailing CR from the replacement text. + let opText = rawOperation.text; + if (opText) { + const endsWithLoneCR = ( + opText.length > 0 && opText.charCodeAt(opText.length - 1) === CharCode.CarriageReturn + ); + const removeTrailingCR = ( + this.getEOL() === '\r\n' && endsWithLoneCR && validatedRange.endColumn === this.getLineMaxColumn(validatedRange.endLineNumber) + ); + if (removeTrailingCR) { + opText = opText.substring(0, opText.length - 1); + } + } + return new model.ValidAnnotatedEditOperation( rawOperation.identifier || null, - this.validateRange(rawOperation.range), - rawOperation.text, + validatedRange, + opText, rawOperation.forceMoveMarkers || false, rawOperation.isAutoWhitespaceEdit || false, rawOperation._isTracked || false @@ -1610,11 +1647,19 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const lineChangeEvents = affectedLines.map(lineNumber => new ModelRawLineChanged(lineNumber, this.getLineContent(lineNumber), this._getInjectedTextInLine(lineNumber))); this._onDidChangeInjectedText.fire(new ModelInjectedTextChangedEvent(lineChangeEvents)); } + this._fireOnDidChangeLineHeight(affectedLineHeights); + this._fireOnDidChangeFont(affectedFontLines); + } + + private _fireOnDidChangeLineHeight(affectedLineHeights: Set | null): void { if (affectedLineHeights && affectedLineHeights.size > 0) { const affectedLines = Array.from(affectedLineHeights); const lineHeightChangeEvent = affectedLines.map(specialLineHeightChange => new ModelLineHeightChanged(specialLineHeightChange.ownerId, specialLineHeightChange.decorationId, specialLineHeightChange.lineNumber, specialLineHeightChange.lineHeight)); this._onDidChangeLineHeight.fire(new ModelLineHeightChangedEvent(lineHeightChangeEvent)); } + } + + private _fireOnDidChangeFont(affectedFontLines: Set | null): void { if (affectedFontLines && affectedFontLines.size > 0) { const affectedLines = Array.from(affectedFontLines); const fontChangeEvent = affectedLines.map(fontChange => new ModelFontChanged(fontChange.ownerId, fontChange.lineNumber)); @@ -1774,7 +1819,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const range = new Range(startLineNumber, 1, endLineNumber, endColumn); const decorations = this._getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(range, ownerId, filterOutValidation, filterFontDecorations)); return decorations; } @@ -1782,7 +1828,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati const validatedRange = this.validateRange(range); const decorations = this._getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMarginDecorations); - pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, onlyMinimapDecorations)); + pushMany(decorations, this._decorationProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); + pushMany(decorations, this._fontTokenDecorationsProvider.getDecorationsInRange(validatedRange, ownerId, filterOutValidation, filterFontDecorations, onlyMinimapDecorations)); return decorations; } @@ -1795,7 +1842,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati } public getCustomLineHeightsDecorations(ownerId: number = 0): model.IModelDecoration[] { - return this._decorationsTree.getAllCustomLineHeights(this, ownerId); + const decs = this._decorationsTree.getAllCustomLineHeights(this, ownerId); + pushMany(decs, this._fontTokenDecorationsProvider.getAllDecorations(ownerId)); + return decs; } private _getInjectedTextInLine(lineNumber: number): LineInjectedText[] { @@ -1815,6 +1864,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati public getAllDecorations(ownerId: number = 0, filterOutValidation: boolean = false, filterFontDecorations: boolean = false): model.IModelDecoration[] { let result = this._decorationsTree.getAll(this, ownerId, filterOutValidation, filterFontDecorations, false, false); result = result.concat(this._decorationProvider.getAllDecorations(ownerId, filterOutValidation)); + result = result.concat(this._fontTokenDecorationsProvider.getAllDecorations(ownerId, filterOutValidation)); return result; } @@ -2490,32 +2540,6 @@ function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorat return ModelDecorationOptions.createDynamic(options); } -class LineHeightChangingDecoration { - - public static toKey(obj: LineHeightChangingDecoration): string { - return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; - } - - constructor( - public readonly ownerId: number, - public readonly decorationId: string, - public readonly lineNumber: number, - public readonly lineHeight: number | null - ) { } -} - -class LineFontChangingDecoration { - - public static toKey(obj: LineFontChangingDecoration): string { - return `${obj.ownerId};${obj.decorationId};${obj.lineNumber}`; - } - - constructor( - public readonly ownerId: number, - public readonly decorationId: string, - public readonly lineNumber: number - ) { } -} class DidChangeDecorationsEmitter extends Disposable { diff --git a/src/vs/editor/common/model/tokens/abstractSyntaxTokenBackend.ts b/src/vs/editor/common/model/tokens/abstractSyntaxTokenBackend.ts index c9ca2c46377..2daa1d88fc6 100644 --- a/src/vs/editor/common/model/tokens/abstractSyntaxTokenBackend.ts +++ b/src/vs/editor/common/model/tokens/abstractSyntaxTokenBackend.ts @@ -12,11 +12,11 @@ import { StandardTokenType } from '../../encodedTokenAttributes.js'; import { ILanguageIdCodec } from '../../languages.js'; import { IAttachedView } from '../../model.js'; import { TextModel } from '../textModel.js'; -import { IModelContentChangedEvent, IModelTokensChangedEvent } from '../../textModelEvents.js'; +import { IModelContentChangedEvent, IModelTokensChangedEvent, IModelFontTokensChangedEvent } from '../../textModelEvents.js'; import { BackgroundTokenizationState } from '../../tokenizationTextModelPart.js'; import { LineTokens } from '../../tokens/lineTokens.js'; import { derivedOpts, IObservable, ISettableObservable, observableSignal, observableValueOpts } from '../../../../base/common/observable.js'; -import { equalsIfDefined, itemEquals, itemsEquals } from '../../../../base/common/equals.js'; +import { equalsIfDefinedC, thisEqualsC, arrayEqualsC } from '../../../../base/common/equals.js'; /** * @internal @@ -33,7 +33,7 @@ export class AttachedViews { constructor() { this.visibleLineRanges = derivedOpts({ owner: this, - equalsFn: itemsEquals(itemEquals()) + equalsFn: arrayEqualsC(thisEqualsC()) }, reader => { this._viewsChanged.read(reader); const ranges = LineRange.joinMany( @@ -89,7 +89,7 @@ class AttachedViewImpl implements IAttachedView { constructor( private readonly handleStateChange: (state: AttachedViewState) => void ) { - this._state = observableValueOpts({ owner: this, equalsFn: equalsIfDefined((a, b) => a.equals(b)) }, undefined); + this._state = observableValueOpts({ owner: this, equalsFn: equalsIfDefinedC((a, b) => a.equals(b)) }, undefined); } setVisibleLines(visibleLines: { startLineNumber: number; endLineNumber: number }[], stabilized: boolean): void { @@ -145,6 +145,10 @@ export abstract class AbstractSyntaxTokenBackend extends Disposable { /** @internal, should not be exposed by the text model! */ public readonly onDidChangeTokens: Event = this._onDidChangeTokens.event; + protected readonly _onDidChangeFontTokens: Emitter = this._register(new Emitter()); + /** @internal, should not be exposed by the text model! */ + public readonly onDidChangeFontTokens: Event = this._onDidChangeFontTokens.event; + constructor( protected readonly _languageIdCodec: ILanguageIdCodec, protected readonly _textModel: TextModel, diff --git a/src/vs/editor/common/model/tokens/annotations.ts b/src/vs/editor/common/model/tokens/annotations.ts new file mode 100644 index 00000000000..cf3943442dc --- /dev/null +++ b/src/vs/editor/common/model/tokens/annotations.ts @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { binarySearch2 } from '../../../../base/common/arrays.js'; +import { StringEdit } from '../../core/edits/stringEdit.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; + +export interface IAnnotation { + range: OffsetRange; + annotation: T; +} + +export interface IAnnotatedString { + /** + * Set annotations for a specific line. + * Annotations should be sorted and non-overlapping. + */ + setAnnotations(annotations: AnnotationsUpdate): void; + /** + * Return annotations intersecting with the given offset range. + */ + getAnnotationsIntersecting(range: OffsetRange): IAnnotation[]; + /** + * Get all the annotations. Method is used for testing. + */ + getAllAnnotations(): IAnnotation[]; + /** + * Apply a string edit to the annotated string. + * @returns The annotations that were deleted (became empty) as a result of the edit. + */ + applyEdit(edit: StringEdit): IAnnotation[]; + /** + * Clone the annotated string. + */ + clone(): IAnnotatedString; +} + +export class AnnotatedString implements IAnnotatedString { + + /** + * Annotations are non intersecting and contiguous in the array. + */ + private _annotations: IAnnotation[] = []; + + constructor(annotations: IAnnotation[] = []) { + this._annotations = annotations; + } + + /** + * Set annotations for a specific range. + * Annotations should be sorted and non-overlapping. + * If the annotation value is undefined, the annotation is removed. + */ + public setAnnotations(annotations: AnnotationsUpdate): void { + for (const annotation of annotations.annotations) { + const startIndex = this._getStartIndexOfIntersectingAnnotation(annotation.range.start); + const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(annotation.range.endExclusive); + if (annotation.annotation !== undefined) { + this._annotations.splice(startIndex, endIndexExclusive - startIndex, { range: annotation.range, annotation: annotation.annotation }); + } else { + this._annotations.splice(startIndex, endIndexExclusive - startIndex); + } + } + } + + /** + * Returns all annotations that intersect with the given offset range. + */ + public getAnnotationsIntersecting(range: OffsetRange): IAnnotation[] { + const startIndex = this._getStartIndexOfIntersectingAnnotation(range.start); + const endIndexExclusive = this._getEndIndexOfIntersectingAnnotation(range.endExclusive); + return this._annotations.slice(startIndex, endIndexExclusive); + } + + private _getStartIndexOfIntersectingAnnotation(offset: number): number { + // Find index to the left of the offset + const startIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => { + return this._annotations[index].range.start - offset; + }); + let startIndex: number; + if (startIndexWhereToReplace >= 0) { + startIndex = startIndexWhereToReplace; + } else { + const candidate = this._annotations[- (startIndexWhereToReplace + 2)]?.range; + if (candidate && offset >= candidate.start && offset < candidate.endExclusive) { + startIndex = - (startIndexWhereToReplace + 2); + } else { + startIndex = - (startIndexWhereToReplace + 1); + } + } + return startIndex; + } + + private _getEndIndexOfIntersectingAnnotation(offset: number): number { + // Find index to the right of the offset + const endIndexWhereToReplace = binarySearch2(this._annotations.length, (index) => { + return this._annotations[index].range.endExclusive - offset; + }); + let endIndexExclusive: number; + if (endIndexWhereToReplace >= 0) { + endIndexExclusive = endIndexWhereToReplace + 1; + } else { + const candidate = this._annotations[-(endIndexWhereToReplace + 1)]?.range; + if (candidate && offset > candidate.start && offset <= candidate.endExclusive) { + endIndexExclusive = - endIndexWhereToReplace; + } else { + endIndexExclusive = - (endIndexWhereToReplace + 1); + } + } + return endIndexExclusive; + } + + /** + * Returns a copy of all annotations. + */ + public getAllAnnotations(): IAnnotation[] { + return this._annotations.slice(); + } + + /** + * Applies a string edit to the annotated string, updating annotation ranges accordingly. + * @param edit The string edit to apply. + * @returns The annotations that were deleted (became empty) as a result of the edit. + */ + public applyEdit(edit: StringEdit): IAnnotation[] { + const annotations = this._annotations.slice(); + + // treat edits as deletion of the replace range and then as insertion that extends the first range + const finalAnnotations: IAnnotation[] = []; + const deletedAnnotations: IAnnotation[] = []; + + let offset = 0; + + for (const e of edit.replacements) { + while (true) { + // ranges before the current edit + const annotation = annotations[0]; + if (!annotation) { + break; + } + const range = annotation.range; + if (range.endExclusive >= e.replaceRange.start) { + break; + } + annotations.shift(); + const newAnnotation = { range: range.delta(offset), annotation: annotation.annotation }; + if (!newAnnotation.range.isEmpty) { + finalAnnotations.push(newAnnotation); + } else { + deletedAnnotations.push(newAnnotation); + } + } + + const intersecting: IAnnotation[] = []; + while (true) { + const annotation = annotations[0]; + if (!annotation) { + break; + } + const range = annotation.range; + if (!range.intersectsOrTouches(e.replaceRange)) { + break; + } + annotations.shift(); + intersecting.push(annotation); + } + + for (let i = intersecting.length - 1; i >= 0; i--) { + const annotation = intersecting[i]; + let r = annotation.range; + + // Inserted text will extend the first intersecting annotation, if the edit truly overlaps it + const shouldExtend = i === 0 && (e.replaceRange.endExclusive > r.start) && (e.replaceRange.start < r.endExclusive); + // Annotation shrinks by the overlap then grows with the new text length + const overlap = r.intersect(e.replaceRange)!.length; + r = r.deltaEnd(-overlap + (shouldExtend ? e.newText.length : 0)); + + // If the annotation starts after the edit start, shift left to the edit start position + const rangeAheadOfReplaceRange = r.start - e.replaceRange.start; + if (rangeAheadOfReplaceRange > 0) { + r = r.delta(-rangeAheadOfReplaceRange); + } + + // If annotation shouldn't be extended AND it is after or on edit start, move it after the newly inserted text + if (!shouldExtend && rangeAheadOfReplaceRange >= 0) { + r = r.delta(e.newText.length); + } + + // We already took our offset into account. + // Because we add r back to the queue (which then adds offset again), + // we have to remove it here so as to not double count it. + r = r.delta(-(e.newText.length - e.replaceRange.length)); + + annotations.unshift({ annotation: annotation.annotation, range: r }); + } + + offset += e.newText.length - e.replaceRange.length; + } + + while (true) { + const annotation = annotations[0]; + if (!annotation) { + break; + } + annotations.shift(); + const newAnnotation = { annotation: annotation.annotation, range: annotation.range.delta(offset) }; + if (!newAnnotation.range.isEmpty) { + finalAnnotations.push(newAnnotation); + } else { + deletedAnnotations.push(newAnnotation); + } + } + this._annotations = finalAnnotations; + return deletedAnnotations; + } + + /** + * Creates a shallow clone of this annotated string. + */ + public clone(): IAnnotatedString { + return new AnnotatedString(this._annotations.slice()); + } +} + +export interface IAnnotationUpdate { + range: OffsetRange; + annotation: T | undefined; +} + +type DefinedValue = object | string | number | boolean; + +export type ISerializedAnnotation = { + range: { start: number; endExclusive: number }; + annotation: TSerializedProperty | undefined; +}; + +export class AnnotationsUpdate { + + public static create(annotations: IAnnotationUpdate[]): AnnotationsUpdate { + return new AnnotationsUpdate(annotations); + } + + private _annotations: IAnnotationUpdate[]; + + private constructor(annotations: IAnnotationUpdate[]) { + this._annotations = annotations; + } + + get annotations(): IAnnotationUpdate[] { + return this._annotations; + } + + public rebase(edit: StringEdit): void { + const annotatedString = new AnnotatedString(this._annotations); + annotatedString.applyEdit(edit); + this._annotations = annotatedString.getAllAnnotations(); + } + + public serialize(serializingFunc: (annotation: T) => TSerializedProperty): ISerializedAnnotation[] { + return this._annotations.map(annotation => { + const range = { start: annotation.range.start, endExclusive: annotation.range.endExclusive }; + if (!annotation.annotation) { + return { range, annotation: undefined }; + } + return { range, annotation: serializingFunc(annotation.annotation) }; + }); + } + + static deserialize(serializedAnnotations: ISerializedAnnotation[], deserializingFunc: (annotation: TSerializedProperty) => T): AnnotationsUpdate { + const annotations: IAnnotationUpdate[] = serializedAnnotations.map(serializedAnnotation => { + const range = new OffsetRange(serializedAnnotation.range.start, serializedAnnotation.range.endExclusive); + if (!serializedAnnotation.annotation) { + return { range, annotation: undefined }; + } + return { range, annotation: deserializingFunc(serializedAnnotation.annotation) }; + }); + return new AnnotationsUpdate(annotations); + } +} diff --git a/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts new file mode 100644 index 00000000000..ef7e2d3bfb4 --- /dev/null +++ b/src/vs/editor/common/model/tokens/tokenizationFontDecorationsProvider.ts @@ -0,0 +1,160 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IModelDecoration, ITextModel } from '../../model.js'; +import { TokenizationTextModelPart } from './tokenizationTextModelPart.js'; +import { Range } from '../../core/range.js'; +import { DecorationProvider, LineFontChangingDecoration, LineHeightChangingDecoration } from '../decorationProvider.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { IFontTokenOption, IModelContentChangedEvent } from '../../textModelEvents.js'; +import { classNameForFontTokenDecorations } from '../../languages/supports/tokenization.js'; +import { Position } from '../../core/position.js'; +import { AnnotatedString, AnnotationsUpdate, IAnnotatedString, IAnnotationUpdate } from './annotations.js'; +import { OffsetRange } from '../../core/ranges/offsetRange.js'; +import { offsetEditFromContentChanges } from '../textModelStringEdit.js'; + +export interface IFontTokenAnnotation { + decorationId: string; + fontToken: IFontTokenOption; +} + +export class TokenizationFontDecorationProvider extends Disposable implements DecorationProvider { + + private static DECORATION_COUNT = 0; + + private readonly _onDidChangeLineHeight = new Emitter>(); + public readonly onDidChangeLineHeight = this._onDidChangeLineHeight.event; + + private readonly _onDidChangeFont = new Emitter>(); + public readonly onDidChangeFont = this._onDidChangeFont.event; + + private _fontAnnotatedString: IAnnotatedString = new AnnotatedString(); + + constructor( + private readonly textModel: ITextModel, + private readonly tokenizationTextModelPart: TokenizationTextModelPart + ) { + super(); + this._register(this.tokenizationTextModelPart.onDidChangeFontTokens(fontChanges => { + + const linesChanged = new Set(); + const fontTokenAnnotations: IAnnotationUpdate[] = []; + + const affectedLineHeights = new Set(); + const affectedLineFonts = new Set(); + + for (const annotation of fontChanges.changes.annotations) { + + const startPosition = this.textModel.getPositionAt(annotation.range.start); + const lineNumber = startPosition.lineNumber; + + let fontTokenAnnotation: IAnnotationUpdate; + if (annotation.annotation === undefined) { + fontTokenAnnotation = { + range: annotation.range, + annotation: undefined + }; + } else { + const decorationId = `tokenization-font-decoration-${TokenizationFontDecorationProvider.DECORATION_COUNT}`; + const fontTokenDecoration: IFontTokenAnnotation = { + fontToken: annotation.annotation, + decorationId + }; + fontTokenAnnotation = { + range: annotation.range, + annotation: fontTokenDecoration + }; + TokenizationFontDecorationProvider.DECORATION_COUNT++; + + if (annotation.annotation.lineHeightMultiplier) { + affectedLineHeights.add(new LineHeightChangingDecoration(0, decorationId, lineNumber, annotation.annotation.lineHeightMultiplier)); + } + affectedLineFonts.add(new LineFontChangingDecoration(0, decorationId, lineNumber)); + + } + fontTokenAnnotations.push(fontTokenAnnotation); + + if (!linesChanged.has(lineNumber)) { + // Signal the removal of the font tokenization decorations on the line number + const lineNumberStartOffset = this.textModel.getOffsetAt(new Position(lineNumber, 1)); + const lineNumberEndOffset = this.textModel.getOffsetAt(new Position(lineNumber, this.textModel.getLineMaxColumn(lineNumber))); + const lineOffsetRange = new OffsetRange(lineNumberStartOffset, lineNumberEndOffset); + const lineAnnotations = this._fontAnnotatedString.getAnnotationsIntersecting(lineOffsetRange); + for (const annotation of lineAnnotations) { + const decorationId = annotation.annotation.decorationId; + affectedLineHeights.add(new LineHeightChangingDecoration(0, decorationId, lineNumber, null)); + affectedLineFonts.add(new LineFontChangingDecoration(0, decorationId, lineNumber)); + } + linesChanged.add(lineNumber); + } + } + this._fontAnnotatedString.setAnnotations(AnnotationsUpdate.create(fontTokenAnnotations)); + this._onDidChangeLineHeight.fire(affectedLineHeights); + this._onDidChangeFont.fire(affectedLineFonts); + })); + } + + public handleDidChangeContent(change: IModelContentChangedEvent) { + const edits = offsetEditFromContentChanges(change.changes); + const deletedAnnotations = this._fontAnnotatedString.applyEdit(edits); + if (deletedAnnotations.length === 0) { + return; + } + /* We should fire line and font change events if decorations have been added or removed + * No decorations are added on edit, but they can be removed */ + const affectedLineHeights = new Set(); + const affectedLineFonts = new Set(); + for (const deletedAnnotation of deletedAnnotations) { + const startPosition = this.textModel.getPositionAt(deletedAnnotation.range.start); + const lineNumber = startPosition.lineNumber; + const decorationId = deletedAnnotation.annotation.decorationId; + affectedLineHeights.add(new LineHeightChangingDecoration(0, decorationId, lineNumber, null)); + affectedLineFonts.add(new LineFontChangingDecoration(0, decorationId, lineNumber)); + } + this._onDidChangeLineHeight.fire(affectedLineHeights); + this._onDidChangeFont.fire(affectedLineFonts); + } + + public getDecorationsInRange(range: Range, ownerId?: number, filterOutValidation?: boolean, filterFontDecorations?: boolean, onlyMinimapDecorations?: boolean): IModelDecoration[] { + const startOffsetOfRange = this.textModel.getOffsetAt(range.getStartPosition()); + const endOffsetOfRange = this.textModel.getOffsetAt(range.getEndPosition()); + const annotations = this._fontAnnotatedString.getAnnotationsIntersecting(new OffsetRange(startOffsetOfRange, endOffsetOfRange)); + + const decorations: IModelDecoration[] = []; + for (const annotation of annotations) { + const anno = annotation.annotation; + const affectsFont = !!(anno.fontToken.fontFamily || anno.fontToken.fontSizeMultiplier); + if (!(affectsFont && filterFontDecorations)) { + const annotationStartPosition = this.textModel.getPositionAt(annotation.range.start); + const annotationEndPosition = this.textModel.getPositionAt(annotation.range.endExclusive); + const range = Range.fromPositions(annotationStartPosition, annotationEndPosition); + const anno = annotation.annotation; + const className = classNameForFontTokenDecorations(anno.fontToken.fontFamily ?? '', anno.fontToken.fontSizeMultiplier ?? 0); + const id = anno.decorationId; + decorations.push({ + id: id, + options: { + description: 'FontOptionDecoration', + inlineClassName: className, + lineHeight: anno.fontToken.lineHeightMultiplier, + affectsFont + }, + ownerId: 0, + range + }); + } + } + return decorations; + } + + public getAllDecorations(ownerId?: number, filterOutValidation?: boolean): IModelDecoration[] { + return this.getDecorationsInRange( + new Range(1, 1, this.textModel.getLineCount(), this.textModel.getLineMaxColumn(this.textModel.getLineCount())), + ownerId, + filterOutValidation + ); + } +} diff --git a/src/vs/editor/common/model/tokens/tokenizationTextModelPart.ts b/src/vs/editor/common/model/tokens/tokenizationTextModelPart.ts index ab162dca21c..e04f159946e 100644 --- a/src/vs/editor/common/model/tokens/tokenizationTextModelPart.ts +++ b/src/vs/editor/common/model/tokens/tokenizationTextModelPart.ts @@ -18,7 +18,7 @@ import { TextModel } from '../textModel.js'; import { TextModelPart } from '../textModelPart.js'; import { AbstractSyntaxTokenBackend, AttachedViews } from './abstractSyntaxTokenBackend.js'; import { TreeSitterSyntaxTokenBackend } from './treeSitter/treeSitterSyntaxTokenBackend.js'; -import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent } from '../../textModelEvents.js'; +import { IModelContentChangedEvent, IModelLanguageChangedEvent, IModelLanguageConfigurationChangedEvent, IModelTokensChangedEvent, IModelFontTokensChangedEvent } from '../../textModelEvents.js'; import { ITokenizationTextModelPart } from '../../tokenizationTextModelPart.js'; import { LineTokens } from '../../tokens/lineTokens.js'; import { SparseMultilineTokens } from '../../tokens/sparseMultilineTokens.js'; @@ -40,6 +40,9 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz private readonly _onDidChangeTokens: Emitter; public readonly onDidChangeTokens: Event; + private readonly _onDidChangeFontTokens: Emitter = this._register(new Emitter()); + public readonly onDidChangeFontTokens: Event = this._onDidChangeFontTokens.event; + public readonly tokens: IObservable; private readonly _useTreeSitter: IObservable; private readonly _languageIdObs: ISettableObservable; @@ -80,6 +83,11 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz reader.store.add(tokens.onDidChangeTokens(e => { this._emitModelTokensChangedEvent(e); })); + reader.store.add(tokens.onDidChangeFontTokens(e => { + if (!this._textModel._isDisposing()) { + this._onDidChangeFontTokens.fire(e); + } + })); reader.store.add(tokens.onDidChangeBackgroundTokenizationState(e => { this._bracketPairsTextModelPart.handleDidChangeBackgroundTokenizationState(); @@ -104,9 +112,13 @@ export class TokenizationTextModelPart extends TextModelPart implements ITokeniz this.onDidChangeLanguageConfiguration = this._onDidChangeLanguageConfiguration.event; this._onDidChangeTokens = this._register(new Emitter()); this.onDidChangeTokens = this._onDidChangeTokens.event; + this._onDidChangeFontTokens = this._register(new Emitter()); + this.onDidChangeFontTokens = this._onDidChangeFontTokens.event; } _hasListeners(): boolean { + // Note: _onDidChangeFontTokens is intentionally excluded because it's an internal event + // that TokenizationFontDecorationProvider subscribes to during TextModel construction return (this._onDidChangeLanguage.hasListeners() || this._onDidChangeLanguageConfiguration.hasListeners() || this._onDidChangeTokens.hasListeners()); diff --git a/src/vs/editor/common/model/tokens/tokenizerSyntaxTokenBackend.ts b/src/vs/editor/common/model/tokens/tokenizerSyntaxTokenBackend.ts index 004accbdbcd..176bb35fc27 100644 --- a/src/vs/editor/common/model/tokens/tokenizerSyntaxTokenBackend.ts +++ b/src/vs/editor/common/model/tokens/tokenizerSyntaxTokenBackend.ts @@ -12,7 +12,7 @@ import { LineRange } from '../../core/ranges/lineRange.js'; import { StandardTokenType } from '../../encodedTokenAttributes.js'; import { IBackgroundTokenizer, IState, ILanguageIdCodec, TokenizationRegistry, ITokenizationSupport, IBackgroundTokenizationStore } from '../../languages.js'; import { IAttachedView } from '../../model.js'; -import { IModelContentChangedEvent } from '../../textModelEvents.js'; +import { FontTokensUpdate, IModelContentChangedEvent } from '../../textModelEvents.js'; import { BackgroundTokenizationState } from '../../tokenizationTextModelPart.js'; import { ContiguousMultilineTokens } from '../../tokens/contiguousMultilineTokens.js'; import { ContiguousMultilineTokensBuilder } from '../../tokens/contiguousMultilineTokensBuilder.js'; @@ -123,6 +123,9 @@ export class TokenizerSyntaxTokenBackend extends AbstractSyntaxTokenBackend { setTokens: (tokens) => { this.setTokens(tokens); }, + setFontInfo: (changes: FontTokensUpdate) => { + this.setFontInfo(changes); + }, backgroundTokenizationFinished: () => { if (this._backgroundTokenizationState === BackgroundTokenizationState.Completed) { // We already did a full tokenization and don't go back to progressing. @@ -159,6 +162,9 @@ export class TokenizerSyntaxTokenBackend extends AbstractSyntaxTokenBackend { setTokens: (tokens) => { this._debugBackgroundTokens?.setMultilineTokens(tokens, this._textModel); }, + setFontInfo: (changes: FontTokensUpdate) => { + this.setFontInfo(changes); + }, backgroundTokenizationFinished() { // NO OP }, @@ -210,6 +216,10 @@ export class TokenizerSyntaxTokenBackend extends AbstractSyntaxTokenBackend { return { changes: changes }; } + private setFontInfo(changes: FontTokensUpdate): void { + this._onDidChangeFontTokens.fire({ changes }); + } + private refreshAllVisibleLineTokens(): void { const ranges = LineRange.joinMany([...this._attachedViewStates].map(([_, s]) => s.lineRanges)); this.refreshRanges(ranges); diff --git a/src/vs/editor/common/services/completionsEnablement.ts b/src/vs/editor/common/services/completionsEnablement.ts new file mode 100644 index 00000000000..b113f24da41 --- /dev/null +++ b/src/vs/editor/common/services/completionsEnablement.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import product from '../../../platform/product/common/product.js'; +import { isObject } from '../../../base/common/types.js'; +import { IConfigurationService } from '../../../platform/configuration/common/configuration.js'; +import { ITextResourceConfigurationService } from './textResourceConfiguration.js'; +import { URI } from '../../../base/common/uri.js'; + +/** + * Get the completions enablement setting name from product configuration. + */ +function getCompletionsEnablementSettingName(): string | undefined { + return product.defaultChatAgent?.completionsEnablementSetting; +} + +/** + * Checks if completions (e.g., Copilot) are enabled for a given language ID + * using `IConfigurationService`. + * + * @param configurationService The configuration service to read settings from. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabled(configurationService: IConfigurationService, modeId: string = '*'): boolean { + const settingName = getCompletionsEnablementSettingName(); + if (!settingName) { + return false; + } + + return isCompletionsEnabledFromObject( + configurationService.getValue>(settingName), + modeId + ); +} + +/** + * Checks if completions (e.g., Copilot) are enabled for a given language ID + * using `ITextResourceConfigurationService`. + * + * @param configurationService The text resource configuration service to read settings from. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabledWithTextResourceConfig(configurationService: ITextResourceConfigurationService, resource: URI, modeId: string = '*'): boolean { + const settingName = getCompletionsEnablementSettingName(); + if (!settingName) { + return false; + } + + // Pass undefined as resource to get the global setting + return isCompletionsEnabledFromObject( + configurationService.getValue>(resource, settingName), + modeId + ); +} + +/** + * Checks if completions are enabled for a given language ID using a pre-fetched + * completions enablement object. + * + * @param completionsEnablementObject The object containing per-language enablement settings. + * @param modeId The language ID to check. Defaults to '*' which checks the global setting. + * @returns `true` if completions are enabled for the language, `false` otherwise. + */ +export function isCompletionsEnabledFromObject(completionsEnablementObject: Record | undefined, modeId: string = '*'): boolean { + if (!isObject(completionsEnablementObject)) { + return false; // default to disabled if setting is not available + } + + if (typeof completionsEnablementObject[modeId] !== 'undefined') { + return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined + } + + return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise +} diff --git a/src/vs/editor/common/services/languagesAssociations.ts b/src/vs/editor/common/services/languagesAssociations.ts index 2bfe45a8746..367fa9d603e 100644 --- a/src/vs/editor/common/services/languagesAssociations.ts +++ b/src/vs/editor/common/services/languagesAssociations.ts @@ -8,7 +8,7 @@ import { Mimes } from '../../../base/common/mime.js'; import { Schemas } from '../../../base/common/network.js'; import { basename, posix } from '../../../base/common/path.js'; import { DataUri } from '../../../base/common/resources.js'; -import { startsWithUTF8BOM } from '../../../base/common/strings.js'; +import { endsWithIgnoreCase, equals, startsWithUTF8BOM } from '../../../base/common/strings.js'; import { URI } from '../../../base/common/uri.js'; import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js'; @@ -23,9 +23,7 @@ export interface ILanguageAssociation { interface ILanguageAssociationItem extends ILanguageAssociation { readonly userConfigured: boolean; - readonly filenameLowercase?: string; - readonly extensionLowercase?: string; - readonly filepatternLowercase?: ParsedPattern; + readonly filepatternParsed?: ParsedPattern; readonly filepatternOnPath?: boolean; } @@ -97,9 +95,7 @@ function toLanguageAssociationItem(association: ILanguageAssociation, userConfig filepattern: association.filepattern, firstline: association.firstline, userConfigured: userConfigured, - filenameLowercase: association.filename ? association.filename.toLowerCase() : undefined, - extensionLowercase: association.extension ? association.extension.toLowerCase() : undefined, - filepatternLowercase: association.filepattern ? parse(association.filepattern.toLowerCase()) : undefined, + filepatternParsed: association.filepattern ? parse(association.filepattern, { ignoreCase: true }) : undefined, filepatternOnPath: association.filepattern ? association.filepattern.indexOf(posix.sep) >= 0 : false }; } @@ -203,7 +199,7 @@ function getAssociationByPath(path: string, filename: string, associations: ILan const association = associations[i]; // First exact name match - if (filename === association.filenameLowercase) { + if (equals(filename, association.filename, true)) { filenameMatch = association; break; // take it! } @@ -212,7 +208,7 @@ function getAssociationByPath(path: string, filename: string, associations: ILan if (association.filepattern) { if (!patternMatch || association.filepattern.length > patternMatch.filepattern!.length) { const target = association.filepatternOnPath ? path : filename; // match on full path if pattern contains path separator - if (association.filepatternLowercase?.(target)) { + if (association.filepatternParsed?.(target)) { patternMatch = association; } } @@ -221,7 +217,7 @@ function getAssociationByPath(path: string, filename: string, associations: ILan // Longest extension match if (association.extension) { if (!extensionMatch || association.extension.length > extensionMatch.extension!.length) { - if (filename.endsWith(association.extensionLowercase!)) { + if (endsWithIgnoreCase(filename, association.extension)) { extensionMatch = association; } } @@ -248,7 +244,7 @@ function getAssociationByPath(path: string, filename: string, associations: ILan function getAssociationByFirstline(firstLine: string): ILanguageAssociationItem | undefined { if (startsWithUTF8BOM(firstLine)) { - firstLine = firstLine.substr(1); + firstLine = firstLine.substring(1); } if (firstLine.length > 0) { diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index f15817953a8..092a0769fc0 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -34,6 +34,7 @@ export namespace AccessibilityHelpNLS { export const showAccessibilityHelpAction = nls.localize("showAccessibilityHelpAction", "Show Accessibility Help"); export const listSignalSounds = nls.localize("listSignalSoundsCommand", "Run the command: List Signal Sounds for an overview of all sounds and their current status."); export const listAlerts = nls.localize("listAnnouncementsCommand", "Run the command: List Signal Announcements for an overview of announcements and their current status."); + export const announceCursorPosition = nls.localize("announceCursorPosition", "Run the command: Announce Cursor Position{0} to hear the current line and column.", ''); export const quickChat = nls.localize("quickChatCommand", "Toggle quick chat{0} to open or close a chat session.", ''); export const startInlineChat = nls.localize("startInlineChatCommand", "Start inline chat{0} to create an in editor chat session.", ''); export const startDebugging = nls.localize('debug.startDebugging', "The Debug: Start Debugging command{0} will start a debug session.", ''); @@ -51,6 +52,7 @@ export namespace InspectTokensNLS { export namespace GoToLineNLS { export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line/Column..."); + export const gotoOffsetActionLabel = nls.localize('gotoOffsetActionLabel', "Go to Offset..."); } export namespace QuickHelpNLS { diff --git a/src/vs/editor/common/textModelEditSource.ts b/src/vs/editor/common/textModelEditSource.ts index 7296773cc51..d0ba71d2c35 100644 --- a/src/vs/editor/common/textModelEditSource.ts +++ b/src/vs/editor/common/textModelEditSource.ts @@ -98,7 +98,7 @@ export const EditSources = { } as const); }, - rename: () => createEditSource({ source: 'rename' } as const), + rename: (oldName: string | undefined, newName: string) => createEditSource({ source: 'rename', $$$oldName: oldName, $$$newName: newName } as const), chatApplyEdits(data: { modelId: string | undefined; @@ -125,22 +125,24 @@ export const EditSources = { chatUndoEdits: () => createEditSource({ source: 'Chat.undoEdits' } as const), chatReset: () => createEditSource({ source: 'Chat.reset' } as const), - inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId }) { + inlineCompletionAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined }) { return createEditSource({ source: 'inlineCompletionAccept', $nes: data.nes, ...toProperties(data.providerId), + $$correlationId: data.correlationId, $$requestUuid: data.requestUuid, $$languageId: data.languageId, } as const); }, - inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; type: 'word' | 'line' }) { + inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; languageId: string; providerId?: ProviderId; correlationId: string | undefined; type: 'word' | 'line' }) { return createEditSource({ source: 'inlineCompletionPartialAccept', type: data.type, $nes: data.nes, ...toProperties(data.providerId), + $$correlationId: data.correlationId, $$requestUuid: data.requestUuid, $$languageId: data.languageId, } as const); diff --git a/src/vs/editor/common/textModelEvents.ts b/src/vs/editor/common/textModelEvents.ts index 945bb35b73b..b25c00aae8a 100644 --- a/src/vs/editor/common/textModelEvents.ts +++ b/src/vs/editor/common/textModelEvents.ts @@ -8,6 +8,7 @@ import { IRange, Range } from './core/range.js'; import { Selection } from './core/selection.js'; import { IModelDecoration, InjectedTextOptions } from './model.js'; import { IModelContentChange } from './model/mirrorTextModel.js'; +import { AnnotationsUpdate } from './model/tokens/annotations.js'; import { TextModelEditSource } from './textModelEditSource.js'; /** @@ -150,6 +151,63 @@ export interface IModelTokensChangedEvent { }[]; } +/** + * @internal + */ +export interface IFontTokenOption { + /** + * Font family of the token. + */ + readonly fontFamily?: string; + /** + * Font size of the token. + */ + readonly fontSizeMultiplier?: number; + /** + * Line height of the token. + */ + readonly lineHeightMultiplier?: number; +} + +/** + * An event describing a token font change event + * @internal + */ +export interface IModelFontTokensChangedEvent { + changes: FontTokensUpdate; +} + +/** + * @internal + */ +export type FontTokensUpdate = AnnotationsUpdate; + +/** + * @internal + */ +export function serializeFontTokenOptions(): (options: IFontTokenOption) => IFontTokenOption { + return (annotation: IFontTokenOption) => { + return { + fontFamily: annotation.fontFamily ?? '', + fontSizeMultiplier: annotation.fontSizeMultiplier ?? 0, + lineHeightMultiplier: annotation.lineHeightMultiplier ?? 0 + }; + }; +} + +/** + * @internal + */ +export function deserializeFontTokenOptions(): (options: IFontTokenOption) => IFontTokenOption { + return (annotation: IFontTokenOption) => { + return { + fontFamily: annotation.fontFamily ? String(annotation.fontFamily) : undefined, + fontSizeMultiplier: annotation.fontSizeMultiplier ? Number(annotation.fontSizeMultiplier) : undefined, + lineHeightMultiplier: annotation.lineHeightMultiplier ? Number(annotation.lineHeightMultiplier) : undefined + }; + }; +} + export interface IModelOptionsChangedEvent { readonly tabSize: boolean; readonly indentSize: boolean; @@ -290,13 +348,13 @@ export class ModelLineHeightChanged { /** * The line height on the line. */ - public readonly lineHeight: number | null; + public readonly lineHeightMultiplier: number | null; - constructor(ownerId: number, decorationId: string, lineNumber: number, lineHeight: number | null) { + constructor(ownerId: number, decorationId: string, lineNumber: number, lineHeightMultiplier: number | null) { this.ownerId = ownerId; this.decorationId = decorationId; this.lineNumber = lineNumber; - this.lineHeight = lineHeight; + this.lineHeightMultiplier = lineHeightMultiplier; } } diff --git a/src/vs/editor/common/viewLayout/lineDecorations.ts b/src/vs/editor/common/viewLayout/lineDecorations.ts index 7641e62c859..3439b945aac 100644 --- a/src/vs/editor/common/viewLayout/lineDecorations.ts +++ b/src/vs/editor/common/viewLayout/lineDecorations.ts @@ -28,7 +28,7 @@ export class LineDecoration { ); } - public static equalsArr(a: LineDecoration[], b: LineDecoration[]): boolean { + public static equalsArr(a: readonly LineDecoration[], b: readonly LineDecoration[]): boolean { const aLen = a.length; const bLen = b.length; if (aLen !== bLen) { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 5aa420249f9..11aca36bbb9 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -1018,7 +1018,7 @@ function _renderLine(input: ResolvedRenderLineInput, sb: StringBuilder): RenderL sb.appendString(' = derived(this, reader => { + const widget = this._lightBulbWidget.rawValue; + if (!widget) { + return undefined; + } + return widget.lightBulbInfo.read(reader); + }); + constructor( editor: ICodeEditor, @IMarkerService markerService: IMarkerService, diff --git a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts index 39dd29629f5..3de0481ef73 100644 --- a/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts +++ b/src/vs/editor/contrib/codeAction/browser/lightBulbWidget.ts @@ -8,6 +8,7 @@ import { Gesture } from '../../../../base/browser/touch.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, derived, IObservable, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import './lightBulbWidget.css'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from '../../../browser/editorBrowser.js'; @@ -29,6 +30,15 @@ const GUTTER_LIGHTBULB_AIFIX_ICON = registerIcon('gutter-lightbulb-sparkle', Cod const GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON = registerIcon('gutter-lightbulb-aifix-auto-fix', Codicon.lightbulbSparkleAutofix, nls.localize('gutterLightbulbAIFixAutoFixWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); const GUTTER_SPARKLE_FILLED_ICON = registerIcon('gutter-lightbulb-sparkle-filled', Codicon.sparkleFilled, nls.localize('gutterLightbulbSparkleFilledWidget', 'Icon which spawns code actions menu from the gutter when there is no space in the editor and an AI fix and a quick fix is available.')); +export interface LightBulbInfo { + readonly actions: CodeActionSet; + readonly trigger: CodeActionTrigger; + readonly icon: ThemeIcon; + readonly autoRun: boolean; + readonly title: string; + readonly isGutter: boolean; +} + namespace LightBulbState { export const enum Type { @@ -71,8 +81,23 @@ export class LightBulbWidget extends Disposable implements IContentWidget { private readonly _onClick = this._register(new Emitter<{ readonly x: number; readonly y: number; readonly actions: CodeActionSet; readonly trigger: CodeActionTrigger }>()); public readonly onClick = this._onClick.event; - private _state: LightBulbState.State = LightBulbState.Hidden; - private _gutterState: LightBulbState.State = LightBulbState.Hidden; + private readonly _state = observableValue(this, LightBulbState.Hidden); + private readonly _gutterState = observableValue(this, LightBulbState.Hidden); + + private readonly _combinedInfo = derived(this, reader => { + const gutterState = this._gutterState.read(reader); + if (gutterState.type === LightBulbState.Type.Showing) { + return LightBulbWidget._computeLightBulbInfo(gutterState, true, this._preferredKbLabel.read(reader), this._quickFixKbLabel.read(reader)); + } + const state = this._state.read(reader); + if (state.type === LightBulbState.Type.Showing) { + return LightBulbWidget._computeLightBulbInfo(state, false, this._preferredKbLabel.read(reader), this._quickFixKbLabel.read(reader)); + } + return undefined; + }); + + public readonly lightBulbInfo: IObservable = this._combinedInfo; + private _iconClasses: string[] = []; private readonly lightbulbClasses = [ @@ -83,11 +108,50 @@ export class LightBulbWidget extends Disposable implements IContentWidget { 'codicon-' + GUTTER_SPARKLE_FILLED_ICON.id ]; - private _preferredKbLabel?: string; - private _quickFixKbLabel?: string; + private readonly _preferredKbLabel = observableValue(this, undefined); + private readonly _quickFixKbLabel = observableValue(this, undefined); private gutterDecoration: ModelDecorationOptions = LightBulbWidget.GUTTER_DECORATION; + private static _computeLightBulbInfo(state: LightBulbState.State, forGutter: boolean, preferredKbLabel: string | undefined, quickFixKbLabel: string | undefined): LightBulbInfo | undefined { + if (state.type !== LightBulbState.Type.Showing) { + return undefined; + } + + const { actions, trigger } = state; + let icon: ThemeIcon; + let autoRun = false; + if (actions.allAIFixes) { + icon = forGutter ? GUTTER_SPARKLE_FILLED_ICON : Codicon.sparkleFilled; + if (actions.validActions.length === 1) { + autoRun = true; + } + } else if (actions.hasAutoFix) { + if (actions.hasAIFix) { + icon = forGutter ? GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON : Codicon.lightbulbSparkleAutofix; + } else { + icon = forGutter ? GUTTER_LIGHTBULB_AUTO_FIX_ICON : Codicon.lightbulbAutofix; + } + } else if (actions.hasAIFix) { + icon = forGutter ? GUTTER_LIGHTBULB_AIFIX_ICON : Codicon.lightbulbSparkle; + } else { + icon = forGutter ? GUTTER_LIGHTBULB_ICON : Codicon.lightBulb; + } + + let title: string; + if (autoRun) { + title = nls.localize('codeActionAutoRun', "Run: {0}", actions.validActions[0].action.title); + } else if (actions.hasAutoFix && preferredKbLabel) { + title = nls.localize('preferredcodeActionWithKb', "Show Code Actions. Preferred Quick Fix Available ({0})", preferredKbLabel); + } else if (!actions.hasAutoFix && quickFixKbLabel) { + title = nls.localize('codeActionWithKb', "Show Code Actions ({0})", quickFixKbLabel); + } else { + title = nls.localize('codeAction', "Show Code Actions"); + } + + return { actions, trigger, icon, autoRun, title, isGutter: forGutter }; + } + constructor( private readonly _editor: ICodeEditor, @IKeybindingService private readonly _keybindingService: IKeybindingService @@ -103,17 +167,20 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._register(this._editor.onDidChangeModelContent(_ => { // cancel when the line in question has been removed const editorModel = this._editor.getModel(); - if (this.state.type !== LightBulbState.Type.Showing || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) { + const state = this._state.get(); + if (state.type !== LightBulbState.Type.Showing || !editorModel || state.editorPosition.lineNumber >= editorModel.getLineCount()) { this.hide(); } - if (this.gutterState.type !== LightBulbState.Type.Showing || !editorModel || this.gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) { + const gutterState = this._gutterState.get(); + if (gutterState.type !== LightBulbState.Type.Showing || !editorModel || gutterState.editorPosition.lineNumber >= editorModel.getLineCount()) { this.gutterHide(); } })); this._register(dom.addStandardDisposableGenericMouseDownListener(this._domNode, e => { - if (this.state.type !== LightBulbState.Type.Showing) { + const state = this._state.get(); + if (state.type !== LightBulbState.Type.Showing) { return; } @@ -127,15 +194,15 @@ export class LightBulbWidget extends Disposable implements IContentWidget { const lineHeight = this._editor.getOption(EditorOption.lineHeight); let pad = Math.floor(lineHeight / 3); - if (this.state.widgetPosition.position !== null && this.state.widgetPosition.position.lineNumber < this.state.editorPosition.lineNumber) { + if (state.widgetPosition.position !== null && state.widgetPosition.position.lineNumber < state.editorPosition.lineNumber) { pad += lineHeight; } this._onClick.fire({ x: e.posx, y: top + height + pad, - actions: this.state.actions, - trigger: this.state.trigger, + actions: state.actions, + trigger: state.trigger, }); })); @@ -150,9 +217,15 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._register(Event.runAndSubscribe(this._keybindingService.onDidUpdateKeybindings, () => { - this._preferredKbLabel = this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined; - this._quickFixKbLabel = this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined; - this._updateLightBulbTitleAndIcon(); + this._preferredKbLabel.set(this._keybindingService.lookupKeybinding(autoFixCommandId)?.getLabel() ?? undefined, undefined); + this._quickFixKbLabel.set(this._keybindingService.lookupKeybinding(quickFixCommandId)?.getLabel() ?? undefined, undefined); + })); + + // Autorun to update the DOM based on state changes + this._register(autorun(reader => { + const info = this._combinedInfo.read(reader); + this._updateLightBulbTitleAndIcon(info); + this._updateGutterDecorationOptions(info); })); this._register(this._editor.onMouseDown(async (e: IEditorMouseEvent) => { @@ -161,7 +234,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { return; } - if (this.gutterState.type !== LightBulbState.Type.Showing) { + const gutterState = this._gutterState.get(); + if (gutterState.type !== LightBulbState.Type.Showing) { return; } @@ -174,15 +248,15 @@ export class LightBulbWidget extends Disposable implements IContentWidget { const lineHeight = this._editor.getOption(EditorOption.lineHeight); let pad = Math.floor(lineHeight / 3); - if (this.gutterState.widgetPosition.position !== null && this.gutterState.widgetPosition.position.lineNumber < this.gutterState.editorPosition.lineNumber) { + if (gutterState.widgetPosition.position !== null && gutterState.widgetPosition.position.lineNumber < gutterState.editorPosition.lineNumber) { pad += lineHeight; } this._onClick.fire({ x: e.event.posx, y: top + height + pad, - actions: this.gutterState.actions, - trigger: this.gutterState.trigger, + actions: gutterState.actions, + trigger: gutterState.trigger, }); })); } @@ -204,7 +278,8 @@ export class LightBulbWidget extends Disposable implements IContentWidget { } getPosition(): IContentWidgetPosition | null { - return this._state.type === LightBulbState.Type.Showing ? this._state.widgetPosition : null; + const state = this._state.get(); + return state.type === LightBulbState.Type.Showing ? state.widgetPosition : null; } public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) { @@ -276,10 +351,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { // check above and below. if both are blocked, display lightbulb in the gutter. if (!nextLineEmptyOrIndented && !prevLineEmptyOrIndented && !hasDecoration) { - this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + this._gutterState.set(new LightBulbState.Showing(actions, trigger, atPosition, { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: LightBulbWidget._posPref - }); + }), undefined); this.renderGutterLightbub(); return this.hide(); } else if (prevLineEmptyOrIndented || endLine || (prevLineEmptyOrIndented && !currLineEmptyOrIndented)) { @@ -289,10 +364,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { } } else if (lineNumber === 1 && (lineNumber === model.getLineCount() || !isLineEmptyOrIndented(lineNumber + 1) && !isLineEmptyOrIndented(lineNumber))) { // special checks for first line blocked vs. not blocked. - this.gutterState = new LightBulbState.Showing(actions, trigger, atPosition, { + this._gutterState.set(new LightBulbState.Showing(actions, trigger, atPosition, { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: LightBulbWidget._posPref - }); + }), undefined); if (hasDecoration) { this.gutterHide(); @@ -310,10 +385,10 @@ export class LightBulbWidget extends Disposable implements IContentWidget { effectiveColumnNumber = /^\S\s*$/.test(model.getLineContent(effectiveLineNumber)) ? 2 : 1; } - this.state = new LightBulbState.Showing(actions, trigger, atPosition, { + this._state.set(new LightBulbState.Showing(actions, trigger, atPosition, { position: { lineNumber: effectiveLineNumber, column: effectiveColumnNumber }, preference: LightBulbWidget._posPref - }); + }), undefined); if (this._gutterDecorationID) { this._removeGutterDecoration(this._gutterDecorationID); @@ -331,16 +406,16 @@ export class LightBulbWidget extends Disposable implements IContentWidget { } public hide(): void { - if (this.state === LightBulbState.Hidden) { + if (this._state.get() === LightBulbState.Hidden) { return; } - this.state = LightBulbState.Hidden; + this._state.set(LightBulbState.Hidden, undefined); this._editor.layoutContentWidget(this); } public gutterHide(): void { - if (this.gutterState === LightBulbState.Hidden) { + if (this._gutterState.get() === LightBulbState.Hidden) { return; } @@ -348,84 +423,31 @@ export class LightBulbWidget extends Disposable implements IContentWidget { this._removeGutterDecoration(this._gutterDecorationID); } - this.gutterState = LightBulbState.Hidden; + this._gutterState.set(LightBulbState.Hidden, undefined); } - private get state(): LightBulbState.State { return this._state; } - - private set state(value) { - this._state = value; - this._updateLightBulbTitleAndIcon(); - } - - private get gutterState(): LightBulbState.State { return this._gutterState; } - - private set gutterState(value) { - this._gutterState = value; - this._updateGutterLightBulbTitleAndIcon(); - } - - private _updateLightBulbTitleAndIcon(): void { + private _updateLightBulbTitleAndIcon(info: LightBulbInfo | undefined): void { this._domNode.classList.remove(...this._iconClasses); this._iconClasses = []; - if (this.state.type !== LightBulbState.Type.Showing) { + if (!info || info.isGutter) { return; } - let icon: ThemeIcon; - let autoRun = false; - if (this.state.actions.allAIFixes) { - icon = Codicon.sparkleFilled; - if (this.state.actions.validActions.length === 1) { - autoRun = true; - } - } else if (this.state.actions.hasAutoFix) { - if (this.state.actions.hasAIFix) { - icon = Codicon.lightbulbSparkleAutofix; - } else { - icon = Codicon.lightbulbAutofix; - } - } else if (this.state.actions.hasAIFix) { - icon = Codicon.lightbulbSparkle; - } else { - icon = Codicon.lightBulb; - } - this._updateLightbulbTitle(this.state.actions.hasAutoFix, autoRun); - this._iconClasses = ThemeIcon.asClassNameArray(icon); + this._domNode.title = info.title; + this._iconClasses = ThemeIcon.asClassNameArray(info.icon); this._domNode.classList.add(...this._iconClasses); } - private _updateGutterLightBulbTitleAndIcon(): void { - if (this.gutterState.type !== LightBulbState.Type.Showing) { + private _updateGutterDecorationOptions(info: LightBulbInfo | undefined): void { + if (!info || !info.isGutter) { return; } - let icon: ThemeIcon; - let autoRun = false; - if (this.gutterState.actions.allAIFixes) { - icon = GUTTER_SPARKLE_FILLED_ICON; - if (this.gutterState.actions.validActions.length === 1) { - autoRun = true; - } - } else if (this.gutterState.actions.hasAutoFix) { - if (this.gutterState.actions.hasAIFix) { - icon = GUTTER_LIGHTBULB_AIFIX_AUTO_FIX_ICON; - } else { - icon = GUTTER_LIGHTBULB_AUTO_FIX_ICON; - } - } else if (this.gutterState.actions.hasAIFix) { - icon = GUTTER_LIGHTBULB_AIFIX_ICON; - } else { - icon = GUTTER_LIGHTBULB_ICON; - } - this._updateLightbulbTitle(this.gutterState.actions.hasAutoFix, autoRun); - const GUTTER_DECORATION = ModelDecorationOptions.register({ + this.gutterDecoration = ModelDecorationOptions.register({ description: 'codicon-gutter-lightbulb-decoration', - glyphMarginClassName: ThemeIcon.asClassName(icon), + glyphMarginClassName: ThemeIcon.asClassName(info.icon), glyphMargin: { position: GlyphMarginLane.Left }, stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, }); - - this.gutterDecoration = GUTTER_DECORATION; } /* Gutter Helper Functions */ @@ -462,22 +484,5 @@ export class LightBulbWidget extends Disposable implements IContentWidget { }); } - private _updateLightbulbTitle(autoFix: boolean, autoRun: boolean): void { - if (this.state.type !== LightBulbState.Type.Showing) { - return; - } - if (autoRun) { - this.title = nls.localize('codeActionAutoRun', "Run: {0}", this.state.actions.validActions[0].action.title); - } else if (autoFix && this._preferredKbLabel) { - this.title = nls.localize('preferredcodeActionWithKb', "Show Code Actions. Preferred Quick Fix Available ({0})", this._preferredKbLabel); - } else if (!autoFix && this._quickFixKbLabel) { - this.title = nls.localize('codeActionWithKb', "Show Code Actions ({0})", this._quickFixKbLabel); - } else if (!autoFix) { - this.title = nls.localize('codeAction', "Show Code Actions"); - } - } - private set title(value: string) { - this._domNode.title = value; - } } diff --git a/src/vs/editor/contrib/codelens/browser/codelens.ts b/src/vs/editor/contrib/codelens/browser/codelens.ts index c91c3092043..fdbcf259f18 100644 --- a/src/vs/editor/contrib/codelens/browser/codelens.ts +++ b/src/vs/editor/contrib/codelens/browser/codelens.ts @@ -112,7 +112,7 @@ CommandsRegistry.registerCommand('_executeCodeLensProvider', function (accessor, return getCodeLensModel(codeLensProvider, model, CancellationToken.None).then(value => { disposables.add(value); - const resolve: Promise[] = []; + const resolve: Promise[] = []; for (const item of value.lenses) { if (itemResolveCount === undefined || itemResolveCount === null || Boolean(item.symbol.command)) { diff --git a/src/vs/editor/contrib/codelens/browser/codelensController.ts b/src/vs/editor/contrib/codelens/browser/codelensController.ts index f34e88a4d24..3a3877a2664 100644 --- a/src/vs/editor/contrib/codelens/browser/codelensController.ts +++ b/src/vs/editor/contrib/codelens/browser/codelensController.ts @@ -42,7 +42,7 @@ export class CodeLensContribution implements IEditorContribution { private _getCodeLensModelPromise: CancelablePromise | undefined; private readonly _oldCodeLensModels = new DisposableStore(); private _currentCodeLensModel: CodeLensModel | undefined; - private _resolveCodeLensesPromise: CancelablePromise | undefined; + private _resolveCodeLensesPromise: CancelablePromise | undefined; constructor( private readonly _editor: ICodeEditor, diff --git a/src/vs/editor/contrib/colorPicker/browser/defaultDocumentColorProvider.ts b/src/vs/editor/contrib/colorPicker/browser/defaultDocumentColorProvider.ts index 1359b5babf4..e1914f3031c 100644 --- a/src/vs/editor/contrib/colorPicker/browser/defaultDocumentColorProvider.ts +++ b/src/vs/editor/contrib/colorPicker/browser/defaultDocumentColorProvider.ts @@ -24,12 +24,11 @@ export class DefaultDocumentColorProvider implements DocumentColorProvider { provideColorPresentations(_model: ITextModel, colorInfo: IColorInformation, _token: CancellationToken): IColorPresentation[] { const range = colorInfo.range; const colorFromInfo: IColor = colorInfo.color; - const alpha = colorFromInfo.alpha; - const color = new Color(new RGBA(Math.round(255 * colorFromInfo.red), Math.round(255 * colorFromInfo.green), Math.round(255 * colorFromInfo.blue), alpha)); + const color = new Color(new RGBA(Math.round(255 * colorFromInfo.red), Math.round(255 * colorFromInfo.green), Math.round(255 * colorFromInfo.blue), colorFromInfo.alpha)); - const rgb = alpha ? Color.Format.CSS.formatRGBA(color) : Color.Format.CSS.formatRGB(color); - const hsl = alpha ? Color.Format.CSS.formatHSLA(color) : Color.Format.CSS.formatHSL(color); - const hex = alpha ? Color.Format.CSS.formatHexA(color) : Color.Format.CSS.formatHex(color); + const rgb = Color.Format.CSS.formatRGB(color); + const hsl = Color.Format.CSS.formatHSL(color); + const hex = Color.Format.CSS.formatHexA(color, true); const colorPresentations: IColorPresentation[] = []; colorPresentations.push({ label: rgb, textEdit: { range: range, text: rgb } }); diff --git a/src/vs/editor/contrib/colorPicker/test/browser/defaultDocumentColorProvider.test.ts b/src/vs/editor/contrib/colorPicker/test/browser/defaultDocumentColorProvider.test.ts new file mode 100644 index 00000000000..0124b6ad790 --- /dev/null +++ b/src/vs/editor/contrib/colorPicker/test/browser/defaultDocumentColorProvider.test.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Range } from '../../../../common/core/range.js'; +import { IColorInformation } from '../../../../common/languages.js'; +import { DefaultDocumentColorProvider } from '../../browser/defaultDocumentColorProvider.js'; + +suite('DefaultDocumentColorProvider', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('Color presentations should not include alpha channel when alpha is 1', () => { + const provider = new DefaultDocumentColorProvider(null!); + + // Test case 1: Fully opaque color (alpha = 1) should not include alpha channel + const opaqueColorInfo: IColorInformation = { + range: new Range(1, 1, 1, 10), + color: { + red: 1, + green: 0, + blue: 0, + alpha: 1 + } + }; + + const opaquePresentations = provider.provideColorPresentations(null!, opaqueColorInfo, CancellationToken.None); + assert.strictEqual(opaquePresentations[0].label, 'rgb(255, 0, 0)', 'RGB should not include alpha when alpha is 1'); + assert.strictEqual(opaquePresentations[1].label, 'hsl(0, 100%, 50%)', 'HSL should not include alpha when alpha is 1'); + assert.strictEqual(opaquePresentations[2].label, '#ff0000', 'HEX should not include alpha when alpha is 1'); + }); + + test('Color presentations should include alpha channel when alpha is not 1', () => { + const provider = new DefaultDocumentColorProvider(null!); + + // Test case 2: Transparent color (alpha = 0) should include alpha channel + const transparentColorInfo: IColorInformation = { + range: new Range(1, 1, 1, 10), + color: { + red: 0, + green: 0, + blue: 0, + alpha: 0 + } + }; + + const transparentPresentations = provider.provideColorPresentations(null!, transparentColorInfo, CancellationToken.None); + assert.strictEqual(transparentPresentations[0].label, 'rgba(0, 0, 0, 0)', 'RGB should include alpha when alpha is 0'); + assert.strictEqual(transparentPresentations[1].label, 'hsla(0, 0%, 0%, 0.00)', 'HSL should include alpha when alpha is 0'); + assert.strictEqual(transparentPresentations[2].label, '#00000000', 'HEX should include alpha when alpha is 0'); + }); + + test('Color presentations should include alpha channel when alpha is between 0 and 1', () => { + const provider = new DefaultDocumentColorProvider(null!); + + // Test case 3: Semi-transparent color (alpha = 0.67) should include alpha channel + const semiTransparentColorInfo: IColorInformation = { + range: new Range(1, 1, 1, 10), + color: { + red: 0.67, + green: 0, + blue: 0, + alpha: 0.67 + } + }; + + const semiTransparentPresentations = provider.provideColorPresentations(null!, semiTransparentColorInfo, CancellationToken.None); + assert.strictEqual(semiTransparentPresentations[0].label, 'rgba(171, 0, 0, 0.67)', 'RGB should include alpha when alpha is 0.67'); + assert.strictEqual(semiTransparentPresentations[1].label, 'hsla(0, 100%, 34%, 0.67)', 'HSL should include alpha when alpha is 0.67'); + assert.strictEqual(semiTransparentPresentations[2].label, '#ab0000ab', 'HEX should include alpha when alpha is 0.67'); + }); + + test('Regression test for issue #243746: opacity should be preserved when switching to hex format', () => { + // Original bug: When switching from rgba/hsla with opacity to hex format, + // the opacity was being lost because alpha was falsy (0 or less than 1) + const provider = new DefaultDocumentColorProvider(null!); + + const colorWithOpacity: IColorInformation = { + range: new Range(1, 1, 1, 10), + color: { + red: 0.5, + green: 0.5, + blue: 0.5, + alpha: 0.5 + } + }; + + const presentations = provider.provideColorPresentations(null!, colorWithOpacity, CancellationToken.None); + + // Hex format should preserve the opacity by including alpha channel + assert.strictEqual(presentations[2].label, '#80808080', 'HEX format should preserve opacity (issue #243746)'); + }); + + test('Regression test for issue #256853: fully opaque colors should not add unnecessary alpha suffix', () => { + // Bug introduced by fix for #243746: When alpha was 1 (fully opaque), + // the hex format would incorrectly add 'ff' suffix + const provider = new DefaultDocumentColorProvider(null!); + + const fullyOpaqueColor: IColorInformation = { + range: new Range(1, 1, 1, 10), + color: { + red: 0.58, // #935ba5 example from issue + green: 0.36, + blue: 0.65, + alpha: 1 + } + }; + + const presentations = provider.provideColorPresentations(null!, fullyOpaqueColor, CancellationToken.None); + + // Hex format should NOT include alpha when it's 1 (fully opaque) + // The actual hex value is #945ca6 (after rounding 0.58*255, 0.36*255, 0.65*255) + assert.strictEqual(presentations[2].label, '#945ca6', 'HEX format should not add ff suffix when fully opaque (issue #256853)'); + }); +}); diff --git a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts index 0d8bd9f4151..8c4e0b6dce2 100644 --- a/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts +++ b/src/vs/editor/contrib/comment/test/browser/lineCommentCommand.test.ts @@ -1145,7 +1145,7 @@ suite('Editor Contrib - Line Comment in mixed modes', () => { (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) | (encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) ); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } })); } diff --git a/src/vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution.ts b/src/vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution.ts index ee9be220430..300ea4bba1e 100644 --- a/src/vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution.ts +++ b/src/vs/editor/contrib/diffEditorBreadcrumbs/browser/contribution.ts @@ -55,6 +55,17 @@ class DiffEditorBreadcrumbsSource extends Disposable implements IDiffEditorBread symbols.sort(reverseOrder(compareBy(s => s.range.endLineNumber - s.range.startLineNumber, numberComparator))); return symbols.map(s => ({ name: s.name, kind: s.kind, startLineNumber: s.range.startLineNumber })); } + + public getAt(lineNumber: number, reader: IReader): { name: string; kind: SymbolKind; startLineNumber: number }[] { + const m = this._currentModel.read(reader); + if (!m) { return []; } + const symbols = m.asListOfDocumentSymbols() + .filter(s => new LineRange(s.range.startLineNumber, s.range.endLineNumber).contains(lineNumber)); + if (symbols.length === 0) { return []; } + symbols.sort(reverseOrder(compareBy(s => s.range.endLineNumber - s.range.startLineNumber, numberComparator))); + + return symbols.map(s => ({ name: s.name, kind: s.kind, startLineNumber: s.range.startLineNumber })); + } } HideUnchangedRegionsFeature.setBreadcrumbsSourceFactory((textModel, instantiationService) => { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts index 3d524982981..79e5132ede9 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/copyPasteController.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener } from '../../../../base/browser/dom.js'; import { IAction } from '../../../../base/common/actions.js'; import { coalesce } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; @@ -13,7 +12,6 @@ import { isCancellationError } from '../../../../base/common/errors.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Mimes } from '../../../../base/common/mime.js'; -import * as platform from '../../../../base/common/platform.js'; import { upcast } from '../../../../base/common/types.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; @@ -25,12 +23,10 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; -import { ClipboardEventUtils, InMemoryClipboardMetadataManager } from '../../../browser/controller/editContext/clipboardUtils.js'; -import { toExternalVSDataTransfer, toVSDataTransfer } from '../../../browser/dataTransfer.js'; +import { IClipboardCopyEvent, IClipboardPasteEvent, IWritableClipboardData } from '../../../browser/controller/editContext/clipboardUtils.js'; import { ICodeEditor, PastePayload } from '../../../browser/editorBrowser.js'; import { IBulkEditService } from '../../../browser/services/bulkEditService.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; -import { IRange, Range } from '../../../common/core/range.js'; import { Selection } from '../../../common/core/selection.js'; import { Handler, IEditorContribution } from '../../../common/editorCommon.js'; import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteTriggerKind } from '../../../common/languages.js'; @@ -130,10 +126,9 @@ export class CopyPasteController extends Disposable implements IEditorContributi this._editor = editor; - const container = editor.getContainerDomNode(); - this._register(addDisposableListener(container, 'copy', e => this.handleCopy(e))); - this._register(addDisposableListener(container, 'cut', e => this.handleCopy(e))); - this._register(addDisposableListener(container, 'paste', e => this.handlePaste(e), true)); + this._register(editor.onWillCopy(e => this.handleCopy(e))); + this._register(editor.onWillCut(e => this.handleCopy(e))); + this._register(editor.onWillPaste(e => this.handlePaste(e))); this._pasteProgressManager = this._register(new InlineProgressManager('pasteIntoEditor', editor, instantiationService)); @@ -171,16 +166,8 @@ export class CopyPasteController extends Disposable implements IEditorContributi await this._currentPasteOperation; } - private handleCopy(e: ClipboardEvent) { - let id: string | null = null; - if (e.clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - const storedMetadata = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - id = storedMetadata?.id || null; - this._logService.trace('CopyPasteController#handleCopy for id : ', id, ' with text.length : ', text.length); - } else { - this._logService.trace('CopyPasteController#handleCopy'); - } + private handleCopy(e: IClipboardCopyEvent) { + this._logService.trace('CopyPasteController#handleCopy'); if (!this._editor.hasTextFocus()) { return; } @@ -190,34 +177,20 @@ export class CopyPasteController extends Disposable implements IEditorContributi // This means the resources clipboard is not properly updated when copying from the editor. this._clipboardService.clearInternalState?.(); - if (!e.clipboardData || !this.isPasteAsEnabled()) { + if (!this.isPasteAsEnabled()) { return; } const model = this._editor.getModel(); + const viewModel = this._editor._getViewModel(); const selections = this._editor.getSelections(); - if (!model || !selections?.length) { + if (!model || !viewModel || !selections?.length) { return; } - const enableEmptySelectionClipboard = this._editor.getOption(EditorOption.emptySelectionClipboard); - - let ranges: readonly IRange[] = selections; - const wasFromEmptySelection = selections.length === 1 && selections[0].isEmpty(); - if (wasFromEmptySelection) { - if (!enableEmptySelectionClipboard) { - return; - } - - ranges = [new Range(ranges[0].startLineNumber, 1, ranges[0].startLineNumber, 1 + model.getLineLength(ranges[0].startLineNumber))]; - } - - const toCopy = this._editor._getViewModel()?.getPlainTextToCopy(selections, enableEmptySelectionClipboard, platform.isWindows); - const multicursorText = Array.isArray(toCopy) ? toCopy : null; - const defaultPastePayload = { - multicursorText, - pasteOnNewLine: wasFromEmptySelection, + multicursorText: e.dataToCopy.multicursorText ?? null, + pasteOnNewLine: e.dataToCopy.isFromEmptySelection, mode: null }; @@ -229,11 +202,11 @@ export class CopyPasteController extends Disposable implements IEditorContributi return; } - const dataTransfer = toVSDataTransfer(e.clipboardData); + const dataTransfer = new VSDataTransfer(); const providerCopyMimeTypes = providers.flatMap(x => x.copyMimeTypes ?? []); // Save off a handle pointing to data that VS Code maintains. - const handle = id ?? generateUuid(); + const handle = generateUuid(); this.setCopyMetadata(e.clipboardData, { id: handle, providerCopyMimeTypes, @@ -244,7 +217,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi return { providerMimeTypes: provider.copyMimeTypes, operation: createCancelablePromise(token => - provider.prepareDocumentPaste!(model, ranges, dataTransfer, token) + provider.prepareDocumentPaste!(model, e.dataToCopy.sourceRanges, dataTransfer, token) .catch(err => { console.error(err); return undefined; @@ -256,17 +229,18 @@ export class CopyPasteController extends Disposable implements IEditorContributi CopyPasteController._currentCopyOperation = { handle, operations }; } - private async handlePaste(e: ClipboardEvent) { - if (e.clipboardData) { - const [text, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - const metadataComputed = metadata || InMemoryClipboardMetadataManager.INSTANCE.get(text); - this._logService.trace('CopyPasteController#handlePaste for id : ', metadataComputed?.id); - } else { - this._logService.trace('CopyPasteController#handlePaste'); + private async handlePaste(e: IClipboardPasteEvent) { + this._logService.trace('CopyPasteController#handlePaste for id : ', e.metadata?.id); + + if (!this._editor.hasTextFocus()) { + return; } - if (!e.clipboardData || !this._editor.hasTextFocus()) { + + const dataTransfer = e.toExternalVSDataTransfer(); + if (!dataTransfer) { return; } + dataTransfer.delete(vscodeClipboardMime); MessageController.get(this._editor)?.closeMessage(); this._currentPasteOperation?.cancel(); @@ -287,8 +261,6 @@ export class CopyPasteController extends Disposable implements IEditorContributi const metadata = this.fetchCopyMetadata(e); this._logService.trace('CopyPasteController#handlePaste with metadata : ', metadata?.id, ' and text.length : ', e.clipboardData.getData('text/plain').length); - const dataTransfer = toExternalVSDataTransfer(e.clipboardData); - dataTransfer.delete(vscodeClipboardMime); const fileTypes = Array.from(e.clipboardData.files).map(file => file.type); @@ -321,8 +293,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi this.showPasteAsNoEditMessage(selections, this._pasteAsActionContext.preferred); // Also prevent default paste from applying - e.preventDefault(); - e.stopImmediatePropagation(); + e.setHandled(); } return; } @@ -330,13 +301,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi // Prevent the editor's default paste handler from running. // Note that after this point, we are fully responsible for handling paste. // If we can't provider a paste for any reason, we need to explicitly delegate pasting back to the editor. - e.preventDefault(); - e.stopImmediatePropagation(); + e.setHandled(); if (this._pasteAsActionContext) { this.showPasteAsPick(this._pasteAsActionContext.preferred, allProviders, selections, dataTransfer, metadata); } else { - this.doPasteInline(allProviders, selections, dataTransfer, metadata, e); + this.doPasteInline(allProviders, selections, dataTransfer, metadata, e.browserEvent); } } @@ -350,7 +320,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi MessageController.get(this._editor)?.showMessage(localize('pasteAsError', "No paste edits for '{0}' found", kindLabel), selections[0].getStartPosition()); } - private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent): void { + private doPasteInline(allProviders: readonly DocumentPasteEditProvider[], selections: readonly Selection[], dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, clipboardEvent: ClipboardEvent | undefined): void { this._logService.trace('CopyPasteController#doPasteInline'); const editor = this._editor; if (!editor.hasModel()) { @@ -557,16 +527,13 @@ export class CopyPasteController extends Disposable implements IEditorContributi }, () => p); } - private setCopyMetadata(dataTransfer: DataTransfer, metadata: CopyMetadata) { + private setCopyMetadata(clipboardData: IWritableClipboardData, metadata: CopyMetadata) { this._logService.trace('CopyPasteController#setCopyMetadata new id : ', metadata.id); - dataTransfer.setData(vscodeClipboardMime, JSON.stringify(metadata)); + clipboardData.setData(vscodeClipboardMime, JSON.stringify(metadata)); } - private fetchCopyMetadata(e: ClipboardEvent): CopyMetadata | undefined { + private fetchCopyMetadata(e: IClipboardPasteEvent): CopyMetadata | undefined { this._logService.trace('CopyPasteController#fetchCopyMetadata'); - if (!e.clipboardData) { - return; - } // Prefer using the clipboard data we saved off const rawMetadata = e.clipboardData.getData(vscodeClipboardMime); @@ -578,14 +545,12 @@ export class CopyPasteController extends Disposable implements IEditorContributi } } - // Otherwise try to extract the generic text editor metadata - const [_, metadata] = ClipboardEventUtils.getTextData(e.clipboardData); - if (metadata) { + if (e.metadata) { return { defaultPastePayload: { - mode: metadata.mode, - multicursorText: metadata.multicursorText ?? null, - pasteOnNewLine: !!metadata.isFromEmptySelection, + mode: e.metadata.mode, + multicursorText: e.metadata.multicursorText ?? null, + pasteOnNewLine: !!e.metadata.isFromEmptySelection, }, }; } @@ -657,7 +622,7 @@ export class CopyPasteController extends Disposable implements IEditorContributi }; } - private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent) { + private async applyDefaultPasteHandler(dataTransfer: VSDataTransfer, metadata: CopyMetadata | undefined, token: CancellationToken, clipboardEvent: ClipboardEvent | undefined) { const textDataTransfer = dataTransfer.get(Mimes.text) ?? dataTransfer.get('text'); const text = (await textDataTransfer?.asString()) ?? ''; if (token.isCancellationRequested) { diff --git a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts index 0b8a7b23edd..735768eb175 100644 --- a/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts +++ b/src/vs/editor/contrib/dropOrPasteInto/browser/postEditWidget.ts @@ -88,8 +88,7 @@ class PostEditWidget extends Dis } private _updateButtonTitle() { - const binding = this._keybindingService.lookupKeybinding(this.showCommand.id)?.getLabel(); - this.button.element.title = this.showCommand.label + (binding ? ` (${binding})` : ''); + this.button.element.title = this._keybindingService.appendKeybinding(this.showCommand.label, this.showCommand.id); } private create(): void { diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 806cb3a6e5b..e7fbab43228 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -766,7 +766,7 @@ export class MoveToMatchFindAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { + public run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { const controller = CommonFindController.get(editor); if (!controller) { return; @@ -1107,7 +1107,7 @@ registerEditorCommand(new FindCommand({ handler: x => x.replace(), kbOpts: { weight: KeybindingWeight.EditorContrib + 5, - kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED), + kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED, EditorContextKeys.isComposing.negate()), primary: KeyCode.Enter } })); diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts index 57bb5c1c951..dab491acd42 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts @@ -120,11 +120,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { } private _keybindingLabelFor(actionId: string): string { - const kb = this._keybindingService.lookupKeybinding(actionId); - if (!kb) { - return ''; - } - return ` (${kb.getLabel()})`; + return this._keybindingService.appendKeybinding('', actionId); } public override dispose(): void { diff --git a/src/vs/editor/contrib/find/browser/findWidget.css b/src/vs/editor/contrib/find/browser/findWidget.css index a8f93698513..f1dd6191b95 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.css +++ b/src/vs/editor/contrib/find/browser/findWidget.css @@ -11,7 +11,7 @@ overflow: hidden; line-height: 19px; transition: transform 200ms linear; - padding: 0 4px; + padding: 0 4px 0 9px; box-sizing: border-box; transform: translateY(calc(-100% - 10px)); /* shadow (10px) */ box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); @@ -151,8 +151,7 @@ } .monaco-editor .find-widget .button.left { - margin-left: 0; - margin-right: 3px; + margin: 4px 0 4px 5px; } .monaco-editor .find-widget .button.wide { @@ -164,10 +163,10 @@ .monaco-editor .find-widget .button.toggle { position: absolute; top: 0; - left: 3px; + left: 0; width: 18px; - height: 100%; - border-radius: 0; + height: -webkit-fill-available; + border-radius: var(--vscode-cornerRadius-small); box-sizing: border-box; } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index b6724abfa60..25efe745fe0 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -910,11 +910,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL // ----- initialization private _keybindingLabelFor(actionId: string): string { - const kb = this._keybindingService.lookupKeybinding(actionId); - if (!kb) { - return ''; - } - return ` (${kb.getLabel()})`; + return this._keybindingService.appendKeybinding('', actionId); } private _buildDomNode(): void { diff --git a/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts b/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts index c065053c4f5..414d3f51bc4 100644 --- a/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts +++ b/src/vs/editor/contrib/find/browser/findWidgetSearchHistory.ts @@ -53,7 +53,7 @@ export class FindWidgetSearchHistory implements IHistory { this.save(); } - forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: any): void { + forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: unknown): void { // fetch latest from storage this.load(); return this.inMemoryValues.forEach(callbackfn); diff --git a/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts b/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts index a570cc7b9e2..45440ed2909 100644 --- a/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts +++ b/src/vs/editor/contrib/find/browser/replaceWidgetHistory.ts @@ -53,7 +53,7 @@ export class ReplaceWidgetHistory implements IHistory { this.save(); } - forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: any): void { + forEach(callbackfn: (value: string, value2: string, set: Set) => void, thisArg?: unknown): void { // fetch latest from storage this.load(); return this.inMemoryValues.forEach(callbackfn); diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css index ea9e47d9e2a..422e073e5e7 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.css @@ -4,41 +4,49 @@ *--------------------------------------------------------------------------------------------*/ .floating-menu-overlay-widget { - padding: 0px; - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - border-radius: 2px; + padding: 2px 4px; + color: var(--vscode-foreground); + background-color: var(--vscode-editorWidget-background); + border-radius: 6px; border: 1px solid var(--vscode-contrastBorder); display: flex; align-items: center; + justify-content: center; + gap: 4px; 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; + .actions-container { + gap: 4px; } - .action-item > .action-label.codicon { - color: var(--vscode-button-foreground); + .action-item > .action-label { + padding: 4px 6px; + font-size: 11px; + line-height: 14px; + border-radius: 4px; } .action-item > .action-label.codicon:not(.separator) { - padding-top: 6px; - padding-bottom: 6px; + color: var(--vscode-foreground); + width: 22px; + height: 22px; + padding: 0; + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; } - .action-item:first-child > .action-label { - padding-left: 7px; - } - - .action-item:last-child > .action-label { - padding-right: 7px; + .action-item.primary > .action-label, + .action-item.primary > .action-label.action-label.codicon:not(.separator) { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); } - .action-item .action-label.separator { - background-color: var(--vscode-menu-separatorBackground); + .action-item.primary > .action-label:hover { + background-color: var(--vscode-button-hoverBackground) !important; } } diff --git a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts index 56f8117ef61..1a530186e66 100644 --- a/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts +++ b/src/vs/editor/contrib/floatingMenu/browser/floatingMenu.ts @@ -4,11 +4,13 @@ *--------------------------------------------------------------------------------------------*/ 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 { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { getActionBarActions, MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { autorun, constObservable, derived, IObservable, observableFromEvent } from '../../../../base/common/observable.js'; +import { URI } from '../../../../base/common/uri.js'; import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ICodeEditor, OverlayWidgetPositionPreference } from '../../../browser/editorBrowser.js'; @@ -27,38 +29,101 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu super(); const editorObs = this._register(observableCodeEditor(editor)); + const editorUriObs = derived(reader => editorObs.model.read(reader)?.uri); - const menu = this._register(menuService.createMenu(MenuId.EditorContent, editor.contextKeyService)); - const menuIsEmptyObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions().length === 0); + // Widget + const widget = this._register(instantiationService.createInstance( + FloatingEditorToolbarWidget, + MenuId.EditorContent, + editor.contextKeyService, + editorUriObs)); + // Render widget this._register(autorun(reader => { - const menuIsEmpty = menuIsEmptyObs.read(reader); - if (menuIsEmpty) { + const hasActions = widget.hasActions.read(reader); + if (!hasActions) { return; } - const container = h('div.floating-menu-overlay-widget'); + // Overlay widget + reader.store.add(editorObs.createOverlayWidget({ + allowEditorOverflow: false, + domNode: widget.element, + minContentWidthInPx: constObservable(0), + position: constObservable({ + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER + }) + })); + })); + } +} + +export class FloatingEditorToolbarWidget extends Disposable { + readonly element: HTMLElement; + readonly hasActions: IObservable; + + constructor( + _menuId: MenuId, + _scopedContextKeyService: IContextKeyService, + _toolbarContext: IObservable, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IMenuService menuService: IMenuService + ) { + super(); + + const menu = this._register(menuService.createMenu(_menuId, _scopedContextKeyService)); + const menuGroupsObs = observableFromEvent(this, menu.onDidChange, () => menu.getActions()); + + const menuPrimaryActionIdObs = derived(reader => { + const menuGroups = menuGroupsObs.read(reader); + + const { primary } = getActionBarActions(menuGroups, () => true); + return primary.length > 0 ? primary[0].id : undefined; + }); + + this.hasActions = derived(reader => menuGroupsObs.read(reader).length > 0); - // 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'; + this.element = h('div.floating-menu-overlay-widget').root; + this._register(toDisposable(() => this.element.remove())); + + // Set height explicitly to ensure that the floating menu element + // is rendered in the lower right corner at the correct position. + this.element.style.height = '26px'; + + this._register(autorun(reader => { + const hasActions = this.hasActions.read(reader); + const menuPrimaryActionId = menuPrimaryActionIdObs.read(reader); + + if (!hasActions) { + return; + } // Toolbar - const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, container.root, MenuId.EditorContent, { + const toolbar = instantiationService.createInstance(MenuWorkbenchToolBar, this.element, _menuId, { 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 { + override render(container: HTMLElement): void { + super.render(container); + + // Highlight primary action + if (action.id === menuPrimaryActionId) { + this.element?.classList.add('primary'); + } + } + protected override updateLabel(): void { + const keybinding = keybindingService.lookupKeybinding(action.id); + const keybindingLabel = keybinding ? keybinding.getLabel() : undefined; + if (this.options.label && this.label) { - this.label.textContent = `${this._commandAction.label} (${keybinding.getLabel()})`; + this.label.textContent = keybindingLabel + ? `${this._commandAction.label} (${keybindingLabel})` + : this._commandAction.label; } } }, action, { ...options, keybindingNotRenderedWithLabel: true }); @@ -76,18 +141,8 @@ export class FloatingEditorToolbar extends Disposable implements IEditorContribu 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 - }) + const context = _toolbarContext.read(reader); + toolbar.context = context; })); })); } diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 6c4a72cc721..53bfa66fcf1 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -24,7 +24,7 @@ import { ITextModel } from '../../../common/model.js'; import { IModelContentChangedEvent } from '../../../common/textModelEvents.js'; import { FoldingRange, FoldingRangeKind, FoldingRangeProvider } from '../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; -import { CollapseMemento, FoldingModel, getNextFoldLine, getParentFoldLine as getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateForType, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp, toggleCollapseState } from './foldingModel.js'; +import { CollapseMemento, FoldingModel, getNextFoldLine, getParentFoldLine, getPreviousFoldLine, setCollapseStateAtLevel, setCollapseStateForMatchingLines, setCollapseStateForRest, setCollapseStateForType, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateUp, toggleCollapseState } from './foldingModel.js'; import { HiddenRangeModel } from './hiddenRangeModel.js'; import { IndentRangeProvider } from './indentRangeProvider.js'; import * as nls from '../../../../nls.js'; @@ -613,7 +613,7 @@ interface FoldingArguments { selectionLines?: number[]; } -function foldingArgumentsConstraint(args: any) { +function foldingArgumentsConstraint(args: unknown) { if (!types.isUndefined(args)) { if (!types.isObject(args)) { return false; diff --git a/src/vs/editor/contrib/format/browser/format.ts b/src/vs/editor/contrib/format/browser/format.ts index 2e5842846d5..b7fad3cd0fd 100644 --- a/src/vs/editor/contrib/format/browser/format.ts +++ b/src/vs/editor/contrib/format/browser/format.ts @@ -7,7 +7,7 @@ import { asArray, isNonEmptyArray } from '../../../../base/common/arrays.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Iterable } from '../../../../base/common/iterator.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, IReference } from '../../../../base/common/lifecycle.js'; import { LinkedList } from '../../../../base/common/linkedList.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -21,7 +21,7 @@ import { ScrollType } from '../../../common/editorCommon.js'; import { ITextModel } from '../../../common/model.js'; import { DocumentFormattingEditProvider, DocumentRangeFormattingEditProvider, FormattingOptions, TextEdit } from '../../../common/languages.js'; import { IEditorWorkerService } from '../../../common/services/editorWorker.js'; -import { ITextModelService } from '../../../common/services/resolverService.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../common/services/resolverService.js'; import { FormattingEdit } from './formattingEdit.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { ExtensionIdentifierSet } from '../../../../platform/extensions/common/extensions.js'; @@ -470,14 +470,13 @@ CommandsRegistry.registerCommand('_executeFormatRangeProvider', async function ( const [resource, range, options] = args; assertType(URI.isUri(resource)); assertType(Range.isIRange(range)); - assertType(isFormattingOptions(options)); const resolverService = accessor.get(ITextModelService); const workerService = accessor.get(IEditorWorkerService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const reference = await resolverService.createModelReference(resource); try { - return getDocumentRangeFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, Range.lift(range), options, CancellationToken.None); + return getDocumentRangeFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, Range.lift(range), ensureFormattingOptions(options, reference), CancellationToken.None); } finally { reference.dispose(); } @@ -486,14 +485,13 @@ CommandsRegistry.registerCommand('_executeFormatRangeProvider', async function ( CommandsRegistry.registerCommand('_executeFormatDocumentProvider', async function (accessor, ...args) { const [resource, options] = args; assertType(URI.isUri(resource)); - assertType(isFormattingOptions(options)); const resolverService = accessor.get(ITextModelService); const workerService = accessor.get(IEditorWorkerService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const reference = await resolverService.createModelReference(resource); try { - return getDocumentFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, options, CancellationToken.None); + return getDocumentFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, ensureFormattingOptions(options, reference), CancellationToken.None); } finally { reference.dispose(); } @@ -504,15 +502,29 @@ CommandsRegistry.registerCommand('_executeFormatOnTypeProvider', async function assertType(URI.isUri(resource)); assertType(Position.isIPosition(position)); assertType(typeof ch === 'string'); - assertType(isFormattingOptions(options)); const resolverService = accessor.get(ITextModelService); const workerService = accessor.get(IEditorWorkerService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); const reference = await resolverService.createModelReference(resource); try { - return getOnTypeFormattingEdits(workerService, languageFeaturesService, reference.object.textEditorModel, Position.lift(position), ch, options, CancellationToken.None); + return getOnTypeFormattingEdits(workerService, languageFeaturesService, reference.object.textEditorModel, Position.lift(position), ch, ensureFormattingOptions(options, reference), CancellationToken.None); } finally { reference.dispose(); } }); +function ensureFormattingOptions(options: unknown, reference: IReference): FormattingOptions { + let validatedOptions: FormattingOptions; + if (isFormattingOptions(options)) { + validatedOptions = options; + } else { + const modelOptions = reference.object.textEditorModel.getOptions(); + validatedOptions = { + tabSize: modelOptions.tabSize, + insertSpaces: modelOptions.insertSpaces + }; + } + + return validatedOptions; +} + diff --git a/src/vs/editor/contrib/gotoError/browser/gotoError.ts b/src/vs/editor/contrib/gotoError/browser/gotoError.ts index eea7aa4931a..6c71c87435a 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoError.ts @@ -190,7 +190,7 @@ class MarkerNavigationAction extends EditorAction { } export class NextMarkerAction extends MarkerNavigationAction { - static ID: string = 'editor.action.marker.next'; + static readonly ID = 'editor.action.marker.next'; static LABEL = nls.localize2('markerAction.next.label', "Go to Next Problem (Error, Warning, Info)"); constructor() { super(true, false, { @@ -213,8 +213,8 @@ export class NextMarkerAction extends MarkerNavigationAction { } } -class PrevMarkerAction extends MarkerNavigationAction { - static ID: string = 'editor.action.marker.prev'; +export class PrevMarkerAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.prev'; static LABEL = nls.localize2('markerAction.previous.label', "Go to Previous Problem (Error, Warning, Info)"); constructor() { super(false, false, { @@ -237,10 +237,11 @@ class PrevMarkerAction extends MarkerNavigationAction { } } -class NextMarkerInFilesAction extends MarkerNavigationAction { +export class NextMarkerInFilesAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.nextInFiles'; constructor() { super(true, true, { - id: 'editor.action.marker.nextInFiles', + id: NextMarkerInFilesAction.ID, label: nls.localize2('markerAction.nextInFiles.label', "Go to Next Problem in Files (Error, Warning, Info)"), precondition: undefined, kbOpts: { @@ -258,10 +259,11 @@ class NextMarkerInFilesAction extends MarkerNavigationAction { } } -class PrevMarkerInFilesAction extends MarkerNavigationAction { +export class PrevMarkerInFilesAction extends MarkerNavigationAction { + static readonly ID = 'editor.action.marker.prevInFiles'; constructor() { super(false, true, { - id: 'editor.action.marker.prevInFiles', + id: PrevMarkerInFilesAction.ID, label: nls.localize2('markerAction.previousInFiles.label', "Go to Previous Problem in Files (Error, Warning, Info)"), precondition: undefined, kbOpts: { diff --git a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts index 3d2538512e7..b8394a1e72c 100644 --- a/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/browser/gotoErrorWidget.ts @@ -358,7 +358,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { } this._icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(this._severity))}`; - this.editor.revealPositionNearTop(position, ScrollType.Smooth); + this.editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth); this.editor.focus(); } diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 0cf939159b7..ba0e7d0b161 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -17,12 +17,12 @@ import { IKeybindingService } from '../../../../platform/keybinding/common/keybi import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js'; import { HoverVerbosityAction } from '../../../common/languages.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { isMousePositionWithinElement } from './hoverUtils.js'; +import { isMousePositionWithinElement, shouldShowHover, isTriggerModifierPressed } from './hoverUtils.js'; import { ContentHoverWidgetWrapper } from './contentHoverWidgetWrapper.js'; import './hover.css'; import { Emitter } from '../../../../base/common/event.js'; import { isOnColorDecorator } from '../../colorPicker/browser/hoverColorPicker/hoverColorPicker.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { isModifierKey, KeyCode } from '../../../../base/common/keyCodes.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; // sticky hover widget which doesn't disappear on focus out and such @@ -31,7 +31,7 @@ const _sticky = false ; interface IHoverSettings { - readonly enabled: boolean; + readonly enabled: 'on' | 'off' | 'onKeyboardModifier'; readonly sticky: boolean; readonly hidingDelay: number; } @@ -98,7 +98,7 @@ export class ContentHoverController extends Disposable implements IEditorContrib sticky: hoverOpts.sticky, hidingDelay: hoverOpts.hidingDelay }; - if (!hoverOpts.enabled) { + if (hoverOpts.enabled === 'off') { this._cancelSchedulerAndHide(); } this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); @@ -249,7 +249,11 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _reactToEditorMouseMove(mouseEvent: IEditorMouseEvent): void { - if (this._hoverSettings.enabled) { + if (shouldShowHover( + this._hoverSettings.enabled, + this._editor.getOption(EditorOption.multiCursorModifier), + mouseEvent + )) { const contentWidget: ContentHoverWidgetWrapper = this._getOrCreateContentWidget(); if (contentWidget.showsOrWillShow(mouseEvent)) { return; @@ -262,14 +266,21 @@ export class ContentHoverController extends Disposable implements IEditorContrib } private _onKeyDown(e: IKeyboardEvent): void { - if (this._ignoreMouseEvents) { + if (this._ignoreMouseEvents || !this._contentWidget) { return; } - if (!this._contentWidget) { + + if (this._hoverSettings.enabled === 'onKeyboardModifier' + && isTriggerModifierPressed(this._editor.getOption(EditorOption.multiCursorModifier), e) + && this._mouseMoveEvent) { + if (!this._contentWidget.isVisible) { + this._contentWidget.showsOrWillShow(this._mouseMoveEvent); + } return; } + const isPotentialKeyboardShortcut = this._isPotentialKeyboardShortcut(e); - const isModifierKeyPressed = this._isModifierKeyPressed(e); + const isModifierKeyPressed = isModifierKey(e.keyCode); if (isPotentialKeyboardShortcut || isModifierKeyPressed) { return; } @@ -293,13 +304,6 @@ export class ContentHoverController extends Disposable implements IEditorContrib return moreChordsAreNeeded || isHoverAction; } - private _isModifierKeyPressed(e: IKeyboardEvent): boolean { - return e.keyCode === KeyCode.Ctrl - || e.keyCode === KeyCode.Alt - || e.keyCode === KeyCode.Meta - || e.keyCode === KeyCode.Shift; - } - public hideContentHover(): void { if (_sticky) { return; diff --git a/src/vs/editor/contrib/hover/browser/glyphHoverController.ts b/src/vs/editor/contrib/hover/browser/glyphHoverController.ts index 9048716322a..c8dbfa9ec3d 100644 --- a/src/vs/editor/contrib/hover/browser/glyphHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/glyphHoverController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { isModifierKey } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { ICodeEditor, IEditorMouseEvent, IPartialEditorMouseEvent } from '../../../browser/editorBrowser.js'; import { ConfigurationChangedEvent, EditorOption } from '../../../common/config/editorOptions.js'; @@ -12,7 +12,7 @@ import { IEditorContribution, IScrollEvent } from '../../../common/editorCommon. import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IHoverWidget } from './hoverTypes.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; -import { isMousePositionWithinElement } from './hoverUtils.js'; +import { isMousePositionWithinElement, isTriggerModifierPressed, shouldShowHover } from './hoverUtils.js'; import './hover.css'; import { GlyphHoverWidget } from './glyphHoverWidget.js'; @@ -22,7 +22,7 @@ const _sticky = false ; interface IHoverSettings { - readonly enabled: boolean; + readonly enabled: 'on' | 'off' | 'onKeyboardModifier'; readonly sticky: boolean; readonly hidingDelay: number; } @@ -80,7 +80,7 @@ export class GlyphHoverController extends Disposable implements IEditorContribut hidingDelay: hoverOpts.hidingDelay }; - if (hoverOpts.enabled) { + if (hoverOpts.enabled !== 'off') { this._listenersStore.add(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e))); this._listenersStore.add(this._editor.onMouseUp(() => this._onEditorMouseUp())); this._listenersStore.add(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e))); @@ -176,6 +176,17 @@ export class GlyphHoverController extends Disposable implements IEditorContribut if (!mouseEvent) { return; } + if (!shouldShowHover( + this._hoverSettings.enabled, + this._editor.getOption(EditorOption.multiCursorModifier), + mouseEvent + )) { + if (_sticky) { + return; + } + this.hideGlyphHover(); + return; + } const glyphWidgetShowsOrWillShow = this._tryShowHoverWidget(mouseEvent); if (glyphWidgetShowsOrWillShow) { return; @@ -195,10 +206,15 @@ export class GlyphHoverController extends Disposable implements IEditorContribut if (!this._editor.hasModel()) { return; } - if (e.keyCode === KeyCode.Ctrl - || e.keyCode === KeyCode.Alt - || e.keyCode === KeyCode.Meta - || e.keyCode === KeyCode.Shift) { + + if (this._hoverSettings.enabled === 'onKeyboardModifier' + && isTriggerModifierPressed(this._editor.getOption(EditorOption.multiCursorModifier), e) + && this._mouseMoveEvent) { + this._tryShowHoverWidget(this._mouseMoveEvent); + return; + } + + if (isModifierKey(e.keyCode)) { // Do not hide hover when a modifier key is pressed return; } diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index b1cc6eee631..aedcb6944b3 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -15,7 +15,7 @@ .monaco-editor .monaco-resizable-hover > .monaco-hover { border: none; - border-radius: none; + border-radius: unset; } .monaco-editor .monaco-hover { diff --git a/src/vs/editor/contrib/hover/browser/hoverUtils.ts b/src/vs/editor/contrib/hover/browser/hoverUtils.ts index 7f4a74956b6..997d4512c1a 100644 --- a/src/vs/editor/contrib/hover/browser/hoverUtils.ts +++ b/src/vs/editor/contrib/hover/browser/hoverUtils.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; +import { IEditorMouseEvent } from '../../../browser/editorBrowser.js'; export function isMousePositionWithinElement(element: HTMLElement, posx: number, posy: number): boolean { const elementRect = dom.getDomNodePagePosition(element); @@ -15,3 +16,40 @@ export function isMousePositionWithinElement(element: HTMLElement, posx: number, } return true; } +/** + * Determines whether hover should be shown based on the hover setting and current keyboard modifiers. + * When `hoverEnabled` is 'onKeyboardModifier', hover is shown when the user presses the opposite + * modifier key from the multi-cursor modifier (e.g., if multi-cursor uses Alt, hover shows on Ctrl/Cmd). + * + * @param hoverEnabled - The hover enabled setting + * @param multiCursorModifier - The modifier key used for multi-cursor operations + * @param mouseEvent - The current mouse event containing modifier key states + * @returns true if hover should be shown, false otherwise + */ +export function shouldShowHover( + hoverEnabled: 'on' | 'off' | 'onKeyboardModifier', + multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey', + mouseEvent: IEditorMouseEvent +): boolean { + if (hoverEnabled === 'on') { + return true; + } + if (hoverEnabled === 'off') { + return false; + } + return isTriggerModifierPressed(multiCursorModifier, mouseEvent.event); +} + +/** + * Returns true if the trigger modifier (inverse of multi-cursor modifier) is pressed. + * This works with both mouse and keyboard events by relying only on the modifier flags. + */ +export function isTriggerModifierPressed( + multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey', + event: { ctrlKey: boolean; metaKey: boolean; altKey: boolean } +): boolean { + if (multiCursorModifier === 'altKey') { + return event.ctrlKey || event.metaKey; + } + return event.altKey; // multiCursorModifier is ctrlKey or metaKey +} diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index cff1168f829..e289a3dbca0 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -321,7 +321,7 @@ class MarkdownRenderedHoverParts implements IRenderedHoverParts { const isActionIncrease = action === HoverVerbosityAction.Increase; const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon))); actionElement.tabIndex = 0; - const hoverDelegate = new WorkbenchHoverDelegate('mouse', undefined, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService); + const hoverDelegate = store.add(new WorkbenchHoverDelegate('mouse', undefined, { target: container, position: { hoverPosition: HoverPosition.LEFT } }, this._configurationService, this._hoverService)); store.add(this._hoverService.setupManagedHover(hoverDelegate, actionElement, labelForHoverVerbosityAction(this._keybindingService, action))); if (!actionEnabled) { actionElement.classList.add('disabled'); @@ -527,17 +527,9 @@ function renderMarkdown( export function labelForHoverVerbosityAction(keybindingService: IKeybindingService, action: HoverVerbosityAction): string { switch (action) { - case HoverVerbosityAction.Increase: { - const kb = keybindingService.lookupKeybinding(INCREASE_HOVER_VERBOSITY_ACTION_ID); - return kb ? - nls.localize('increaseVerbosityWithKb', "Increase Hover Verbosity ({0})", kb.getLabel()) : - nls.localize('increaseVerbosity', "Increase Hover Verbosity"); - } - case HoverVerbosityAction.Decrease: { - const kb = keybindingService.lookupKeybinding(DECREASE_HOVER_VERBOSITY_ACTION_ID); - return kb ? - nls.localize('decreaseVerbosityWithKb', "Decrease Hover Verbosity ({0})", kb.getLabel()) : - nls.localize('decreaseVerbosity', "Decrease Hover Verbosity"); - } + case HoverVerbosityAction.Increase: + return keybindingService.appendKeybinding(nls.localize('increaseVerbosity', "Increase Hover Verbosity"), INCREASE_HOVER_VERBOSITY_ACTION_ID); + case HoverVerbosityAction.Decrease: + return keybindingService.appendKeybinding(nls.localize('decreaseVerbosity', "Decrease Hover Verbosity"), DECREASE_HOVER_VERBOSITY_ACTION_ID); } } diff --git a/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts b/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts index 4b7495ca9a8..ceecd7d6adc 100644 --- a/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts +++ b/src/vs/editor/contrib/hover/test/browser/hoverCopyButton.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; diff --git a/src/vs/editor/contrib/hover/test/browser/hoverUtils.test.ts b/src/vs/editor/contrib/hover/test/browser/hoverUtils.test.ts new file mode 100644 index 00000000000..e40987aeefe --- /dev/null +++ b/src/vs/editor/contrib/hover/test/browser/hoverUtils.test.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { isMousePositionWithinElement, isTriggerModifierPressed, shouldShowHover } from '../../browser/hoverUtils.js'; +import { IEditorMouseEvent } from '../../../../browser/editorBrowser.js'; + +suite('Hover Utils', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('shouldShowHover', () => { + + function createMockMouseEvent(ctrlKey: boolean, altKey: boolean, metaKey: boolean): IEditorMouseEvent { + return { + event: { + ctrlKey, + altKey, + metaKey, + shiftKey: false, + } + } as IEditorMouseEvent; + } + + test('returns true when enabled is "on"', () => { + const mouseEvent = createMockMouseEvent(false, false, false); + const result = shouldShowHover('on', 'altKey', mouseEvent); + assert.strictEqual(result, true); + }); + + test('returns false when enabled is "off"', () => { + const mouseEvent = createMockMouseEvent(false, false, false); + const result = shouldShowHover('off', 'altKey', mouseEvent); + assert.strictEqual(result, false); + }); + + test('returns true with ctrl pressed when multiCursorModifier is altKey', () => { + const mouseEvent = createMockMouseEvent(true, false, false); + const result = shouldShowHover('onKeyboardModifier', 'altKey', mouseEvent); + assert.strictEqual(result, true); + }); + + test('returns false without ctrl pressed when multiCursorModifier is altKey', () => { + const mouseEvent = createMockMouseEvent(false, false, false); + const result = shouldShowHover('onKeyboardModifier', 'altKey', mouseEvent); + assert.strictEqual(result, false); + }); + + test('returns true with metaKey pressed when multiCursorModifier is altKey', () => { + const mouseEvent = createMockMouseEvent(false, false, true); + const result = shouldShowHover('onKeyboardModifier', 'altKey', mouseEvent); + assert.strictEqual(result, true); + }); + + test('returns true with alt pressed when multiCursorModifier is ctrlKey', () => { + const mouseEvent = createMockMouseEvent(false, true, false); + const result = shouldShowHover('onKeyboardModifier', 'ctrlKey', mouseEvent); + assert.strictEqual(result, true); + }); + + test('returns false without alt pressed when multiCursorModifier is ctrlKey', () => { + const mouseEvent = createMockMouseEvent(false, false, false); + const result = shouldShowHover('onKeyboardModifier', 'ctrlKey', mouseEvent); + assert.strictEqual(result, false); + }); + + test('returns true with alt pressed when multiCursorModifier is metaKey', () => { + const mouseEvent = createMockMouseEvent(false, true, false); + const result = shouldShowHover('onKeyboardModifier', 'metaKey', mouseEvent); + assert.strictEqual(result, true); + }); + + test('ignores alt when multiCursorModifier is altKey', () => { + const mouseEvent = createMockMouseEvent(false, true, false); + const result = shouldShowHover('onKeyboardModifier', 'altKey', mouseEvent); + assert.strictEqual(result, false); + }); + + test('ignores ctrl when multiCursorModifier is ctrlKey', () => { + const mouseEvent = createMockMouseEvent(true, false, false); + const result = shouldShowHover('onKeyboardModifier', 'ctrlKey', mouseEvent); + assert.strictEqual(result, false); + }); + }); + + suite('isMousePositionWithinElement', () => { + + function createMockElement(left: number, top: number, width: number, height: number): HTMLElement { + const element = document.createElement('div'); + // Mock getDomNodePagePosition by setting up the element's bounding rect + element.getBoundingClientRect = () => ({ + left, + top, + width, + height, + right: left + width, + bottom: top + height, + x: left, + y: top, + toJSON: () => { } + }); + return element; + } + + test('returns true when mouse is inside element bounds', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 150, 150), true); + assert.strictEqual(isMousePositionWithinElement(element, 200, 150), true); + assert.strictEqual(isMousePositionWithinElement(element, 250, 180), true); + }); + + test('returns true when mouse is on element edges', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 100, 100), true); // top-left corner + assert.strictEqual(isMousePositionWithinElement(element, 300, 100), true); // top-right corner + assert.strictEqual(isMousePositionWithinElement(element, 100, 200), true); // bottom-left corner + assert.strictEqual(isMousePositionWithinElement(element, 300, 200), true); // bottom-right corner + }); + + test('returns false when mouse is left of element', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 99, 150), false); + assert.strictEqual(isMousePositionWithinElement(element, 50, 150), false); + }); + + test('returns false when mouse is right of element', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 301, 150), false); + assert.strictEqual(isMousePositionWithinElement(element, 400, 150), false); + }); + + test('returns false when mouse is above element', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 200, 99), false); + assert.strictEqual(isMousePositionWithinElement(element, 200, 50), false); + }); + + test('returns false when mouse is below element', () => { + const element = createMockElement(100, 100, 200, 100); + assert.strictEqual(isMousePositionWithinElement(element, 200, 201), false); + assert.strictEqual(isMousePositionWithinElement(element, 200, 300), false); + }); + + test('handles element at origin (0,0)', () => { + const element = createMockElement(0, 0, 100, 100); + assert.strictEqual(isMousePositionWithinElement(element, 0, 0), true); + assert.strictEqual(isMousePositionWithinElement(element, 50, 50), true); + assert.strictEqual(isMousePositionWithinElement(element, 100, 100), true); + assert.strictEqual(isMousePositionWithinElement(element, 101, 101), false); + }); + + test('handles small elements (1x1)', () => { + const element = createMockElement(100, 100, 1, 1); + assert.strictEqual(isMousePositionWithinElement(element, 100, 100), true); + assert.strictEqual(isMousePositionWithinElement(element, 101, 101), true); + assert.strictEqual(isMousePositionWithinElement(element, 102, 102), false); + }); + }); + + suite('isTriggerModifierPressed', () => { + + function createModifierEvent(ctrlKey: boolean, altKey: boolean, metaKey: boolean) { + return { ctrlKey, altKey, metaKey }; + } + + test('returns true with ctrl pressed when multiCursorModifier is altKey', () => { + const event = createModifierEvent(true, false, false); + assert.strictEqual(isTriggerModifierPressed('altKey', event), true); + }); + + test('returns true with metaKey pressed when multiCursorModifier is altKey', () => { + const event = createModifierEvent(false, false, true); + assert.strictEqual(isTriggerModifierPressed('altKey', event), true); + }); + + test('returns true with both ctrl and metaKey pressed when multiCursorModifier is altKey', () => { + const event = createModifierEvent(true, false, true); + assert.strictEqual(isTriggerModifierPressed('altKey', event), true); + }); + + test('returns false without ctrl or metaKey when multiCursorModifier is altKey', () => { + const event = createModifierEvent(false, false, false); + assert.strictEqual(isTriggerModifierPressed('altKey', event), false); + }); + + test('returns false with alt pressed when multiCursorModifier is altKey', () => { + const event = createModifierEvent(false, true, false); + assert.strictEqual(isTriggerModifierPressed('altKey', event), false); + }); + + test('returns true with alt pressed when multiCursorModifier is ctrlKey', () => { + const event = createModifierEvent(false, true, false); + assert.strictEqual(isTriggerModifierPressed('ctrlKey', event), true); + }); + + test('returns false without alt pressed when multiCursorModifier is ctrlKey', () => { + const event = createModifierEvent(false, false, false); + assert.strictEqual(isTriggerModifierPressed('ctrlKey', event), false); + }); + + test('returns false with ctrl pressed when multiCursorModifier is ctrlKey', () => { + const event = createModifierEvent(true, false, false); + assert.strictEqual(isTriggerModifierPressed('ctrlKey', event), false); + }); + + test('returns true with alt pressed when multiCursorModifier is metaKey', () => { + const event = createModifierEvent(false, true, false); + assert.strictEqual(isTriggerModifierPressed('metaKey', event), true); + }); + + test('returns false without alt pressed when multiCursorModifier is metaKey', () => { + const event = createModifierEvent(false, false, false); + assert.strictEqual(isTriggerModifierPressed('metaKey', event), false); + }); + + test('returns false with metaKey pressed when multiCursorModifier is metaKey', () => { + const event = createModifierEvent(false, false, true); + assert.strictEqual(isTriggerModifierPressed('metaKey', event), false); + }); + }); +}); diff --git a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts index 0dcfd898c47..4359d30fffa 100644 --- a/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts +++ b/src/vs/editor/contrib/indentation/test/browser/indentation.test.ts @@ -18,8 +18,8 @@ import { NullState } from '../../../../common/languages/nullTokenize.js'; import { AutoIndentOnPaste, IndentationToSpacesCommand, IndentationToTabsCommand } from '../../browser/indentation.js'; import { withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { testCommand } from '../../../../test/browser/testCommand.js'; -import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules } from '../../../../test/common/modes/supports/indentationRules.js'; -import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules } from '../../../../test/common/modes/supports/onEnterRules.js'; +import { goIndentationRules, htmlIndentationRules, javascriptIndentationRules, latexIndentationRules, luaIndentationRules, phpIndentationRules, rubyIndentationRules, vbIndentationRules } from '../../../../test/common/modes/supports/indentationRules.js'; +import { cppOnEnterRules, htmlOnEnterRules, javascriptOnEnterRules, phpOnEnterRules, vbOnEnterRules } from '../../../../test/common/modes/supports/onEnterRules.js'; import { TypeOperations } from '../../../../common/cursor/cursorTypeOperations.js'; import { cppBracketRules, goBracketRules, htmlBracketRules, latexBracketRules, luaBracketRules, phpBracketRules, rubyBracketRules, typescriptBracketRules, vbBracketRules } from '../../../../test/common/modes/supports/bracketRules.js'; import { javascriptAutoClosingPairsRules, latexAutoClosingPairsRules } from '../../../../test/common/modes/supports/autoClosingPairsRules.js'; @@ -94,6 +94,8 @@ export function registerLanguageConfiguration(languageConfigurationService: ILan case Language.VB: return languageConfigurationService.register(language, { brackets: vbBracketRules, + indentationRules: vbIndentationRules, + onEnterRules: vbOnEnterRules, }); case Language.Latex: return languageConfigurationService.register(language, { @@ -132,7 +134,7 @@ export function registerTokenizationSupport(instantiationService: TestInstantiat | (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET) ); } - return new EncodedTokenizationResult(result, state); + return new EncodedTokenizationResult(result, [], state); } }; return TokenizationRegistry.register(languageId, tokenizationSupport); @@ -1737,14 +1739,14 @@ suite('Auto Indent On Type - Visual Basic', () => { assert.ok(true); }); - test.skip('issue #118932: no indentation in visual basic files', () => { + test('issue #118932: no indentation in visual basic files', () => { // https://github.com/microsoft/vscode/issues/118932 const model = createTextModel([ - 'if True then', + 'If True Then', ' Some code', - ' end i', + ' End I', ].join('\n'), languageId, {}); disposables.add(model); @@ -1752,12 +1754,603 @@ suite('Auto Indent On Type - Visual Basic', () => { editor.setSelection(new Selection(3, 10, 3, 10)); viewModel.type('f', 'keyboard'); assert.strictEqual(model.getValue(), [ - 'if True then', + 'If True Then', ' Some code', - 'end if', + 'End If', ].join('\n')); }); }); + + test('issue #118932: indent after Module declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + viewModel.type('Module Test'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Sub declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Sub Main()'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Sub', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello")', + ' End Su', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 15, 4, 15)); + viewModel.type('b', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello")', + ' End Sub', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Module', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // When End Module is typed right after Module (no nested blocks), it dedents correctly + + const model = createTextModel([ + 'Module Test', + ' Private x As Integer', + ' End Modul', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Private x As Integer', + 'End Module', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Function declaration', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Function Add(a As Integer, b As Integer) As Integer'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Function Add(a As Integer, b As Integer) As Integer', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Function', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Module Test', + ' Function Add(a, b)', + ' Return a + b', + ' End Functio', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 20, 4, 20)); + viewModel.type('n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Function Add(a, b)', + ' Return a + b', + ' End Function', + ].join('\n')); + }); + }); + + test('issue #118932: indent after If Then', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('If x > 0 Then'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: indent after ElseIf Then', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' ElseIf x < 0 Then', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 22, 4, 22)); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' ElseIf x < 0 Then', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Else', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' Els', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' If x > 0 Then', + ' DoSomething()', + ' Else', + ].join('\n')); + }); + }); + + test('issue #118932: indent after While', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('While x > 0'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' While x > 0', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End While', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' While x > 0', + ' x = x - 1', + ' End Whil', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 17, 4, 17)); + viewModel.type('e', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' While x > 0', + ' x = x - 1', + ' End While', + ].join('\n')); + }); + }); + + test('issue #118932: indent after For', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('For i = 1 To 10'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' For i = 1 To 10', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on Next', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' For i = 1 To 10', + ' DoSomething(i)', + ' Nex', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('t', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' For i = 1 To 10', + ' DoSomething(i)', + ' Next', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Do', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Do'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Do', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on Loop', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Do', + ' x = x + 1', + ' Loo', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 12, 4, 12)); + viewModel.type('p', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Do', + ' x = x + 1', + ' Loop', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Select Case', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Select Case x'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Select Case x', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Select', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // When End Select is typed, it dedents to match Select Case level + + const model = createTextModel([ + 'Sub Test()', + ' Select Case x', + ' End Selec', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 18, 3, 18)); + viewModel.type('t', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Select Case x', + ' End Select', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Try', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' ', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(2, 5, 2, 5)); + viewModel.type('Try'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Catch', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catc', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(4, 13, 4, 13)); + viewModel.type('h', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ].join('\n')); + }); + }); + + test('issue #118932: dedent and indent on Finally', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finall', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(6, 15, 6, 15)); + viewModel.type('y', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Try', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ' Cleanup()', + ' End Tr', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(8, 15, 8, 15)); + viewModel.type('y', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Sub Test()', + ' Try', + ' DoSomething()', + ' Catch', + ' HandleError()', + ' Finally', + ' Cleanup()', + ' End Try', + ].join('\n')); + }); + }); + + test('issue #118932: indent after Class', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + viewModel.type('Class MyClass'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Class MyClass', + ' ', + ].join('\n')); + }); + }); + + test('issue #118932: dedent on End Class', () => { + + // https://github.com/microsoft/vscode/issues/118932 + + const model = createTextModel([ + 'Class MyClass', + ' Private x As Integer', + ' End Clas', + ].join('\n'), languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + editor.setSelection(new Selection(3, 14, 3, 14)); + viewModel.type('s', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Class MyClass', + ' Private x As Integer', + 'End Class', + ].join('\n')); + }); + }); + + test('issue #118932: full program indentation flow', () => { + + // https://github.com/microsoft/vscode/issues/118932 + // Verify the complete flow as described in the verification comment + // Note: Auto-indent only triggers on typing the last character that completes a keyword + // and only decreases by one indentation level per keyword completion + + const model = createTextModel('', languageId, {}); + disposables.add(model); + + withTestCodeEditor(model, { autoIndent: 'full', serviceCollection }, (editor, viewModel) => { + // Type Module Test and press Enter + viewModel.type('Module Test'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' ', + ].join('\n'), 'After Module Test'); + + // Type Sub Main() and press Enter + viewModel.type('Sub Main()'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' ', + ].join('\n'), 'After Sub Main()'); + + // Type Console.WriteLine and press Enter + viewModel.type('Console.WriteLine("Hello, World!")'); + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' ', + ].join('\n'), 'After Console.WriteLine'); + + // Type End Su then 'b' to complete End Sub (auto-indent triggers on last char) + viewModel.type('End Su'); + viewModel.type('b', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' End Sub', + ].join('\n'), 'After End Sub'); + + // Press Enter - should maintain same indent level after End Sub + viewModel.type('\n', 'keyboard'); + assert.strictEqual(model.getValue(), [ + 'Module Test', + ' Sub Main()', + ' Console.WriteLine("Hello, World!")', + ' End Sub', + ' ', + ].join('\n'), 'After Enter after End Sub'); + }); + }); }); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 29a814e2336..6c1c58c4f8a 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -264,7 +264,9 @@ export class InlayHintsController implements IEditorContribution { })); } } + store.add(inlayHints); + store.add(toDisposable(() => watchedProviders.clear())); this._updateHintsDecorators(inlayHints.ranges, inlayHints.items); this._cacheHintsForFastRestore(model); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts index 4902ea81c66..e9401c5a1fc 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts @@ -5,6 +5,8 @@ export const inlineSuggestCommitId = 'editor.action.inlineSuggest.commit'; +export const inlineSuggestCommitAlternativeActionId = 'editor.action.inlineSuggest.commitAlternativeAction'; + export const showPreviousInlineSuggestionActionId = 'editor.action.inlineSuggest.showPrevious'; export const showNextInlineSuggestionActionId = 'editor.action.inlineSuggest.showNext'; @@ -14,3 +16,5 @@ export const jumpToNextInlineEditId = 'editor.action.inlineSuggest.jump'; export const hideInlineCompletionId = 'editor.action.inlineSuggest.hide'; export const toggleShowCollapsedId = 'editor.action.inlineSuggest.toggleShowCollapsed'; + +export const renameSymbolCommandId = 'editor.action.inlineSuggest.renameSymbol'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index 10702eca570..97392ce8d7e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -6,7 +6,7 @@ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; import { asyncTransaction, transaction } from '../../../../../base/common/observable.js'; import { splitLines } from '../../../../../base/common/strings.js'; -import { vBoolean, vObj, vOptionalProp, vString, vUndefined, vUnion, vWithJsonSchemaRef } from '../../../../../base/common/validation.js'; +import { vBoolean, vObj, vOptionalProp, vString, vUnchecked, vUndefined, vUnion, vWithJsonSchemaRef } from '../../../../../base/common/validation.js'; import * as nls from '../../../../../nls.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js'; import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -21,7 +21,7 @@ import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; import { InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; -import { hideInlineCompletionId, inlineSuggestCommitId, jumpToNextInlineEditId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId, toggleShowCollapsedId } from './commandIds.js'; +import { hideInlineCompletionId, inlineSuggestCommitAlternativeActionId, inlineSuggestCommitId, jumpToNextInlineEditId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId, toggleShowCollapsedId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; import { InlineCompletionsController } from './inlineCompletionsController.js'; @@ -80,6 +80,7 @@ const argsValidator = vUnion(vObj({ showNoResultNotification: vOptionalProp(vBoolean()), providerId: vOptionalProp(vWithJsonSchemaRef(providerIdSchemaUri, vString())), explicit: vOptionalProp(vBoolean()), + changeHintData: vOptionalProp(vUnchecked()), }), vUndefined()); export class TriggerInlineSuggestionAction extends EditorAction { @@ -118,6 +119,7 @@ export class TriggerInlineSuggestionAction extends EditorAction { await controller?.model.get()?.trigger(tx, { provider: provider, explicit: validatedArgs?.explicit ?? true, + changeHint: validatedArgs?.changeHintData ? { data: validatedArgs.changeHintData } : undefined, }); controller?.playAccessibilitySignal(tx); }); @@ -243,6 +245,37 @@ KeybindingsRegistry.registerKeybindingRule({ when: ContextKeyExpr.and(InlineCompletionContextKeys.inInlineEditsPreviewEditor) }); +export class AcceptInlineCompletionAlternativeAction extends EditorAction { + constructor() { + super({ + id: inlineSuggestCommitAlternativeActionId, + label: nls.localize2('action.inlineSuggest.acceptAlternativeAction', "Accept Inline Suggestion Alternative Action"), + precondition: ContextKeyExpr.and(InlineCompletionContextKeys.inlineSuggestionAlternativeActionVisible, InlineCompletionContextKeys.inlineEditVisible), + menuOpts: [], + kbOpts: [ + { + primary: KeyMod.Shift | KeyCode.Tab, + weight: 203, + } + ], + }); + } + + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = InlineCompletionsController.getInFocusedEditorOrParent(accessor); + if (controller) { + controller.model.get()?.accept(controller.editor, true); + controller.editor.focus(); + } + } +} +KeybindingsRegistry.registerKeybindingRule({ + id: inlineSuggestCommitAlternativeActionId, + weight: 203, + primary: KeyMod.Shift | KeyCode.Tab, + when: ContextKeyExpr.and(InlineCompletionContextKeys.inInlineEditsPreviewEditor) +}); + export class JumpToNextInlineEdit extends EditorAction { constructor() { super({ diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/common.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/common.ts new file mode 100644 index 00000000000..8fcb15ce0da --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/common.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import type { InlineCompletionsController } from './inlineCompletionsController.js'; + +let _getInlineCompletionsController: ((editor: ICodeEditor) => InlineCompletionsController | null) | undefined; + +export function getInlineCompletionsController(editor: ICodeEditor): InlineCompletionsController | null { + return _getInlineCompletionsController?.(editor) ?? null; +} + +export function setInlineCompletionsControllerGetter(getter: (editor: ICodeEditor) => InlineCompletionsController | null): void { + _getInlineCompletionsController = getter; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts index 75f3a002e1d..5311c33018c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.ts @@ -10,6 +10,7 @@ import * as nls from '../../../../../nls.js'; export abstract class InlineCompletionContextKeys { public static readonly inlineSuggestionVisible = new RawContextKey('inlineSuggestionVisible', false, localize('inlineSuggestionVisible', "Whether an inline suggestion is visible")); + public static readonly inlineSuggestionAlternativeActionVisible = new RawContextKey('inlineSuggestionAlternativeActionVisible', false, localize('inlineSuggestionAlternativeActionVisible', "Whether an alternative action for the inline suggestion is visible.")); public static readonly inlineSuggestionHasIndentation = new RawContextKey('inlineSuggestionHasIndentation', false, localize('inlineSuggestionHasIndentation', "Whether the inline suggestion starts with whitespace")); public static readonly inlineSuggestionHasIndentationLessThanTabSize = new RawContextKey('inlineSuggestionHasIndentationLessThanTabSize', true, localize('inlineSuggestionHasIndentationLessThanTabSize', "Whether the inline suggestion starts with whitespace that is less than what would be inserted by tab")); public static readonly suppressSuggestions = new RawContextKey('inlineSuggestionSuppressSuggestions', undefined, localize('suppressSuggestions', "Whether suggestions should be suppressed for the current suggestion")); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index d5a51839367..035f8b38421 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -32,16 +32,20 @@ import { CursorChangeReason } from '../../../../common/cursorEvents.js'; import { ILanguageFeatureDebounceService } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { FIND_IDS } from '../../../find/browser/findModel.js'; +import { NextMarkerAction, NextMarkerInFilesAction, PrevMarkerAction, PrevMarkerInFilesAction } from '../../../gotoError/browser/gotoError.js'; import { InsertLineAfterAction, InsertLineBeforeAction } from '../../../linesOperations/browser/linesOperations.js'; import { InlineSuggestionHintsContentWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js'; import { TextModelChangeRecorder } from '../model/changeRecorder.js'; import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { ObservableSuggestWidgetAdapter } from '../model/suggestWidgetAdapter.js'; import { ObservableContextKeyService } from '../utils.js'; -import { InlineCompletionsView } from '../view/inlineCompletionsView.js'; +import { InlineSuggestionsView } from '../view/inlineSuggestionsView.js'; import { inlineSuggestCommitId } from './commandIds.js'; +import { setInlineCompletionsControllerGetter } from './common.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; +setInlineCompletionsControllerGetter((editor) => InlineCompletionsController.get(editor)); + export class InlineCompletionsController extends Disposable { private static readonly _instances = new Set(); @@ -71,22 +75,50 @@ export class InlineCompletionsController extends Disposable { private readonly _enabledInConfig; private readonly _isScreenReaderEnabled; private readonly _editorDictationInProgress; - private readonly _enabled; + private readonly _enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); private readonly _debounceValue; - private readonly _focusIsInMenu; - private readonly _focusIsInEditorOrMenu; - - private readonly _cursorIsInIndentation; - - public readonly model; + private readonly _focusIsInMenu = observableValue(this, false); + private readonly _focusIsInEditorOrMenu = derived(this, reader => { + const editorHasFocus = this._editorObs.isFocused.read(reader); + const menuHasFocus = this._focusIsInMenu.read(reader); + return editorHasFocus || menuHasFocus; + }); + + private readonly _cursorIsInIndentation = derived(this, reader => { + const cursorPos = this._editorObs.cursorPosition.read(reader); + if (cursorPos === null) { return false; } + const model = this._editorObs.model.read(reader); + if (!model) { return false; } + this._editorObs.versionId.read(reader); + const indentMaxColumn = model.getLineIndentColumn(cursorPos.lineNumber); + return cursorPos.column <= indentMaxColumn; + }); + + public readonly model = derivedDisposable(this, reader => { + if (this._editorObs.isReadonly.read(reader)) { return undefined; } + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const model: InlineCompletionsModel = this._instantiationService.createInstance( + InlineCompletionsModel, + textModel, + this._suggestWidgetAdapter.selectedItem, + this._editorObs.versionId, + this._positions, + this._debounceValue, + this._enabled, + this.editor, + ); + return model; + }); - private readonly _playAccessibilitySignal; + private readonly _playAccessibilitySignal = observableSignal(this); private readonly _hideInlineEditOnSelectionChange; - protected readonly _view; + protected readonly _view = derived(reader => reader.store.add(this._instantiationService.createInstance(InlineSuggestionsView.hot.read(reader), this.editor, this.model, this._focusIsInMenu))); constructor( public readonly editor: ICodeEditor, @@ -98,7 +130,7 @@ export class InlineCompletionsController extends Disposable { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService ) { super(); this._editorObs = observableCodeEditor(this.editor); @@ -114,47 +146,16 @@ export class InlineCompletionsController extends Disposable { this._contextKeyService.onDidChangeContext, () => this._contextKeyService.getContext(this.editor.getDomNode()).getValue('editorDictation.inProgress') === true ); - this._enabled = derived(this, reader => this._enabledInConfig.read(reader) && (!this._isScreenReaderEnabled.read(reader) || !this._editorDictationInProgress.read(reader))); + this._debounceValue = this._debounceService.for( this._languageFeaturesService.inlineCompletionsProvider, 'InlineCompletionsDebounce', { min: 50, max: 50 } ); - this._focusIsInMenu = observableValue(this, false); - this._focusIsInEditorOrMenu = derived(this, reader => { - const editorHasFocus = this._editorObs.isFocused.read(reader); - const menuHasFocus = this._focusIsInMenu.read(reader); - return editorHasFocus || menuHasFocus; - }); - this._cursorIsInIndentation = derived(this, reader => { - const cursorPos = this._editorObs.cursorPosition.read(reader); - if (cursorPos === null) { return false; } - const model = this._editorObs.model.read(reader); - if (!model) { return false; } - this._editorObs.versionId.read(reader); - const indentMaxColumn = model.getLineIndentColumn(cursorPos.lineNumber); - return cursorPos.column <= indentMaxColumn; - }); - this.model = derivedDisposable(this, reader => { - if (this._editorObs.isReadonly.read(reader)) { return undefined; } - const textModel = this._editorObs.model.read(reader); - if (!textModel) { return undefined; } - - const model: InlineCompletionsModel = this._instantiationService.createInstance( - InlineCompletionsModel, - textModel, - this._suggestWidgetAdapter.selectedItem, - this._editorObs.versionId, - this._positions, - this._debounceValue, - this._enabled, - this.editor, - ); - return model; - }).recomputeInitiallyAndOnChange(this._store); - this._playAccessibilitySignal = observableSignal(this); + this.model.recomputeInitiallyAndOnChange(this._store); this._hideInlineEditOnSelectionChange = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => true); - this._view = this._register(this._instantiationService.createInstance(InlineCompletionsView, this.editor, this.model, this._focusIsInMenu)); + + this._view.recomputeInitiallyAndOnChange(this._store); InlineCompletionsController._instances.add(this); this._register(toDisposable(() => InlineCompletionsController._instances.delete(this))); @@ -227,6 +228,10 @@ export class InlineCompletionsController extends Disposable { InsertLineAfterAction.ID, InsertLineBeforeAction.ID, FIND_IDS.NextMatchFindAction, + NextMarkerAction.ID, + PrevMarkerAction.ID, + NextMarkerInFilesAction.ID, + PrevMarkerInFilesAction.ID, ...TriggerInlineEditCommandsRegistry.getRegisteredCommands(), ]); this._register(this._commandService.onDidExecuteCommand((e) => { @@ -283,7 +288,7 @@ export class InlineCompletionsController extends Disposable { } if (!model) { return; } - if (model.state.read(undefined)?.inlineCompletion?.isFromExplicitRequest && model.inlineEditAvailable.read(undefined)) { + if (model.state.read(undefined)?.inlineSuggestion?.isFromExplicitRequest && model.inlineEditAvailable.read(undefined)) { // dont hide inline edits on blur when requested explicitly return; } @@ -315,7 +320,7 @@ export class InlineCompletionsController extends Disposable { if (this._suggestWidgetAdapter.selectedItem.get()) { return last; } - return state?.inlineCompletion?.semanticId; + return state?.inlineSuggestion?.semanticId; }); this._register(runOnChangeWithStore(derived(reader => { this._playAccessibilitySignal.read(reader); @@ -370,12 +375,18 @@ export class InlineCompletionsController extends Disposable { this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.suppressSuggestions, reader => { const model = this.model.read(reader); const state = model?.inlineCompletionState.read(reader); - return state?.primaryGhostText && state?.inlineCompletion ? state.inlineCompletion.source.inlineSuggestions.suppressSuggestions : undefined; + return state?.primaryGhostText && state?.inlineSuggestion ? state.inlineSuggestion.source.inlineSuggestions.suppressSuggestions : undefined; + })); + this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineSuggestionAlternativeActionVisible, reader => { + const model = this.model.read(reader); + const state = model?.inlineEditState.read(reader); + const action = state?.inlineSuggestion.action; + return action && action.kind === 'edit' && action.alternativeAction !== undefined; })); this._register(contextKeySvcObs.bind(InlineCompletionContextKeys.inlineSuggestionVisible, reader => { const model = this.model.read(reader); const state = model?.inlineCompletionState.read(reader); - return !!state?.inlineCompletion && state?.primaryGhostText !== undefined && !state?.primaryGhostText.isEmpty(); + return !!state?.inlineSuggestion && state?.primaryGhostText !== undefined && !state?.primaryGhostText.isEmpty(); })); const firstGhostTextPos = derived(this, reader => { const model = this.model.read(reader); @@ -425,7 +436,7 @@ export class InlineCompletionsController extends Disposable { } public shouldShowHoverAtViewZone(viewZoneId: string): boolean { - return this._view.shouldShowHoverAtViewZone(viewZoneId); + return this._view.get().shouldShowHoverAtViewZone(viewZoneId); } public reject(): void { @@ -436,7 +447,7 @@ export class InlineCompletionsController extends Disposable { // Only if this controller is in focus can we cancel others. if (this._focusIsInEditorOrMenu.get()) { for (const ctrl of InlineCompletionsController._instances) { - if (ctrl !== this) { + if (ctrl !== this && !ctrl._focusIsInEditorOrMenu.get()) { ctrl.model.get()?.stop('automatic', tx); } } @@ -451,8 +462,4 @@ export class InlineCompletionsController extends Disposable { m.jump(); } } - - public testOnlyDisableUi() { - this._view.dispose(); - } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts index 52b393c1949..3ecd2a1daa1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts @@ -143,12 +143,7 @@ export class InlineSuggestionHintsContentWidget extends Disposable implements IC true, () => this._commandService.executeCommand(commandId), ); - const kb = this.keybindingService.lookupKeybinding(commandId, this._contextKeyService); - let tooltip = label; - if (kb) { - tooltip = localize({ key: 'content', comment: ['A label', 'A keybinding'] }, '{0} ({1})', label, kb.getLabel()); - } - action.tooltip = tooltip; + action.tooltip = this.keybindingService.appendKeybinding(label, commandId, this._contextKeyService); return action; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts index 1885a412fb2..ab3e5126ef3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletions.contribution.ts @@ -8,7 +8,7 @@ import { registerAction2 } from '../../../../platform/actions/common/actions.js' import { wrapInHotClass1 } from '../../../../platform/observable/common/wrapInHotClass.js'; import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from '../../../browser/editorExtensions.js'; import { HoverParticipantRegistry } from '../../hover/browser/hoverTypes.js'; -import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, DevExtractReproSample, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, TriggerInlineSuggestionAction, ToggleInlineCompletionShowCollapsed } from './controller/commands.js'; +import { AcceptInlineCompletion, AcceptNextLineOfInlineCompletion, AcceptNextWordOfInlineCompletion, DevExtractReproSample, HideInlineCompletion, JumpToNextInlineEdit, ShowNextInlineSuggestionAction, ShowPreviousInlineSuggestionAction, ToggleAlwaysShowInlineSuggestionToolbar, TriggerInlineSuggestionAction, ToggleInlineCompletionShowCollapsed, AcceptInlineCompletionAlternativeAction } from './controller/commands.js'; import { InlineCompletionsController } from './controller/inlineCompletionsController.js'; import { InlineCompletionsHoverParticipant } from './hintsWidget/hoverParticipant.js'; import { InlineCompletionsAccessibleView } from './inlineCompletionsAccessibleView.js'; @@ -22,6 +22,7 @@ registerEditorAction(ShowPreviousInlineSuggestionAction); registerEditorAction(AcceptNextWordOfInlineCompletion); registerEditorAction(AcceptNextLineOfInlineCompletion); registerEditorAction(AcceptInlineCompletion); +registerEditorAction(AcceptInlineCompletionAlternativeAction); registerEditorAction(ToggleInlineCompletionShowCollapsed); registerEditorAction(HideInlineCompletion); registerEditorAction(JumpToNextInlineEdit); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/InlineSuggestAlternativeAction.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/InlineSuggestAlternativeAction.ts new file mode 100644 index 00000000000..41fafa762f7 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/InlineSuggestAlternativeAction.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { Command } from '../../../../common/languages.js'; + +export type InlineSuggestAlternativeAction = { + label: string; + icon: ThemeIcon; + command: Command; + count: Promise; +}; + +export namespace InlineSuggestAlternativeAction { + export function toString(action: InlineSuggestAlternativeAction | undefined): string | undefined { + return action?.command.id ?? undefined; + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/editKind.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/editKind.ts new file mode 100644 index 00000000000..b998fc18838 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/editKind.ts @@ -0,0 +1,292 @@ +/*--------------------------------------------------------------------------------------------- + * 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 '../../../../common/core/position.js'; +import { StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js'; +import { ITextModel } from '../../../../common/model.js'; + +const syntacticalChars = new Set([';', ',', '=', '+', '-', '*', '/', '{', '}', '(', ')', '[', ']', '<', '>', ':', '.', '!', '?', '&', '|', '^', '%', '@', '#', '~', '`', '\\', '\'', '"', '$']); + +function isSyntacticalChar(char: string): boolean { + return syntacticalChars.has(char); +} + +function isIdentifierChar(char: string): boolean { + return /[a-zA-Z0-9_]/.test(char); +} + +function isWhitespaceChar(char: string): boolean { + return char === ' ' || char === '\t'; +} + +type SingleCharacterKind = 'syntactical' | 'identifier' | 'whitespace'; + +interface SingleLineTextShape { + readonly kind: 'singleLine'; + readonly isSingleCharacter: boolean; + readonly singleCharacterKind: SingleCharacterKind | undefined; + readonly isWord: boolean; + readonly isMultipleWords: boolean; + readonly isMultipleWhitespace: boolean; + readonly hasDuplicatedWhitespace: boolean; +} + +interface MultiLineTextShape { + readonly kind: 'multiLine'; + readonly lineCount: number; +} + +type TextShape = SingleLineTextShape | MultiLineTextShape; + +function analyzeTextShape(text: string): TextShape { + const lines = text.split(/\r\n|\r|\n/); + if (lines.length > 1) { + return { + kind: 'multiLine', + lineCount: lines.length, + }; + } + + const isSingleChar = text.length === 1; + let singleCharKind: SingleCharacterKind | undefined; + if (isSingleChar) { + if (isSyntacticalChar(text)) { + singleCharKind = 'syntactical'; + } else if (isIdentifierChar(text)) { + singleCharKind = 'identifier'; + } else if (isWhitespaceChar(text)) { + singleCharKind = 'whitespace'; + } + } + + // Analyze whitespace patterns + const whitespaceMatches = text.match(/[ \t]+/g) || []; + const isMultipleWhitespace = whitespaceMatches.some(ws => ws.length > 1); + const hasDuplicatedWhitespace = whitespaceMatches.some(ws => + (ws.includes(' ') || ws.includes('\t\t')) + ); + + // Analyze word patterns + const words = text.split(/\s+/).filter(w => w.length > 0); + const isWord = words.length === 1 && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(words[0]); + const isMultipleWords = words.length > 1; + + return { + kind: 'singleLine', + isSingleCharacter: isSingleChar, + singleCharacterKind: singleCharKind, + isWord, + isMultipleWords, + isMultipleWhitespace, + hasDuplicatedWhitespace, + }; +} + +type InsertLocationShape = 'endOfLine' | 'emptyLine' | 'startOfLine' | 'middleOfLine'; + +interface InsertLocationRelativeToCursor { + readonly atCursor: boolean; + readonly beforeCursorOnSameLine: boolean; + readonly afterCursorOnSameLine: boolean; + readonly linesAbove: number | undefined; + readonly linesBelow: number | undefined; +} + +export interface InsertProperties { + readonly textShape: TextShape; + readonly locationShape: InsertLocationShape; + readonly relativeToCursor: InsertLocationRelativeToCursor | undefined; +} + +export interface DeleteProperties { + readonly textShape: TextShape; + readonly isAtEndOfLine: boolean; + readonly deletesEntireLineContent: boolean; +} + +export interface ReplaceProperties { + readonly isWordToWordReplacement: boolean; + readonly isAdditive: boolean; + readonly isSubtractive: boolean; + readonly isSingleLineToSingleLine: boolean; + readonly isSingleLineToMultiLine: boolean; + readonly isMultiLineToSingleLine: boolean; +} + +type EditOperation = 'insert' | 'delete' | 'replace'; + +interface IInlineSuggestionEditKindEdit { + readonly operation: EditOperation; + readonly properties: InsertProperties | DeleteProperties | ReplaceProperties; + readonly charactersInserted: number; + readonly charactersDeleted: number; + readonly linesInserted: number; + readonly linesDeleted: number; +} +export class InlineSuggestionEditKind { + constructor(readonly edits: IInlineSuggestionEditKindEdit[]) { } + toString(): string { + return JSON.stringify({ edits: this.edits }); + } +} + +export function computeEditKind(edit: StringEdit, textModel: ITextModel, cursorPosition?: Position): InlineSuggestionEditKind | undefined { + if (edit.replacements.length === 0) { + // Empty edit - return undefined as there's no edit to classify + return undefined; + } + + return new InlineSuggestionEditKind(edit.replacements.map(rep => computeSingleEditKind(rep, textModel, cursorPosition))); +} + +function countLines(text: string): number { + if (text.length === 0) { + return 0; + } + return text.split(/\r\n|\r|\n/).length - 1; +} + +function computeSingleEditKind(replacement: StringReplacement, textModel: ITextModel, cursorPosition?: Position): IInlineSuggestionEditKindEdit { + const replaceRange = replacement.replaceRange; + const newText = replacement.newText; + const deletedLength = replaceRange.length; + const insertedLength = newText.length; + const linesInserted = countLines(newText); + + const kind = replaceRange.isEmpty ? 'insert' : (newText.length === 0 ? 'delete' : 'replace'); + switch (kind) { + case 'insert': + return { + operation: 'insert', + properties: computeInsertProperties(replaceRange.start, newText, textModel, cursorPosition), + charactersInserted: insertedLength, + charactersDeleted: 0, + linesInserted, + linesDeleted: 0, + }; + case 'delete': { + const deletedText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive); + return { + operation: 'delete', + properties: computeDeleteProperties(replaceRange.start, replaceRange.endExclusive, textModel), + charactersInserted: 0, + charactersDeleted: deletedLength, + linesInserted: 0, + linesDeleted: countLines(deletedText), + }; + } + case 'replace': { + const oldText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive); + return { + operation: 'replace', + properties: computeReplaceProperties(oldText, newText), + charactersInserted: insertedLength, + charactersDeleted: deletedLength, + linesInserted, + linesDeleted: countLines(oldText), + }; + } + } +} + +function computeInsertProperties(offset: number, newText: string, textModel: ITextModel, cursorPosition?: Position): InsertProperties { + const textShape = analyzeTextShape(newText); + const insertPosition = textModel.getPositionAt(offset); + const lineContent = textModel.getLineContent(insertPosition.lineNumber); + const lineLength = lineContent.length; + + // Determine location shape + let locationShape: InsertLocationShape; + const isLineEmpty = lineContent.trim().length === 0; + const isAtEndOfLine = insertPosition.column > lineLength; + const isAtStartOfLine = insertPosition.column === 1; + + if (isLineEmpty) { + locationShape = 'emptyLine'; + } else if (isAtEndOfLine) { + locationShape = 'endOfLine'; + } else if (isAtStartOfLine) { + locationShape = 'startOfLine'; + } else { + locationShape = 'middleOfLine'; + } + + // Compute relative to cursor if cursor position is provided + let relativeToCursor: InsertLocationRelativeToCursor | undefined; + if (cursorPosition) { + const cursorLine = cursorPosition.lineNumber; + const insertLine = insertPosition.lineNumber; + const cursorColumn = cursorPosition.column; + const insertColumn = insertPosition.column; + + const atCursor = cursorLine === insertLine && cursorColumn === insertColumn; + const beforeCursorOnSameLine = cursorLine === insertLine && insertColumn < cursorColumn; + const afterCursorOnSameLine = cursorLine === insertLine && insertColumn > cursorColumn; + const linesAbove = insertLine < cursorLine ? cursorLine - insertLine : undefined; + const linesBelow = insertLine > cursorLine ? insertLine - cursorLine : undefined; + + relativeToCursor = { + atCursor, + beforeCursorOnSameLine, + afterCursorOnSameLine, + linesAbove, + linesBelow, + }; + } + + return { + textShape, + locationShape, + relativeToCursor, + }; +} + +function computeDeleteProperties(startOffset: number, endOffset: number, textModel: ITextModel): DeleteProperties { + const deletedText = textModel.getValue().substring(startOffset, endOffset); + const textShape = analyzeTextShape(deletedText); + + const startPosition = textModel.getPositionAt(startOffset); + const endPosition = textModel.getPositionAt(endOffset); + + // Check if delete is at end of line + const lineContent = textModel.getLineContent(endPosition.lineNumber); + const isAtEndOfLine = endPosition.column > lineContent.length; + + // Check if entire line content is deleted + const deletesEntireLineContent = + startPosition.lineNumber === endPosition.lineNumber && + startPosition.column === 1 && + endPosition.column > lineContent.length; + + return { + textShape, + isAtEndOfLine, + deletesEntireLineContent, + }; +} + +function computeReplaceProperties(oldText: string, newText: string): ReplaceProperties { + const oldShape = analyzeTextShape(oldText); + const newShape = analyzeTextShape(newText); + + const oldIsWord = oldShape.kind === 'singleLine' && oldShape.isWord; + const newIsWord = newShape.kind === 'singleLine' && newShape.isWord; + const isWordToWordReplacement = oldIsWord && newIsWord; + + const isAdditive = newText.length > oldText.length; + const isSubtractive = newText.length < oldText.length; + + const isSingleLineToSingleLine = oldShape.kind === 'singleLine' && newShape.kind === 'singleLine'; + const isSingleLineToMultiLine = oldShape.kind === 'singleLine' && newShape.kind === 'multiLine'; + const isMultiLineToSingleLine = oldShape.kind === 'multiLine' && newShape.kind === 'singleLine'; + + return { + isWordToWordReplacement, + isAdditive, + isSubtractive, + isSingleLineToSingleLine, + isSingleLineToMultiLine, + isMultiLineToSingleLine, + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionIsVisible.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionIsVisible.ts new file mode 100644 index 00000000000..d42b1b28c48 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionIsVisible.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { matchesSubString } from '../../../../../base/common/filters.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; +import { Position } from '../../../../common/core/position.js'; +import { Range } from '../../../../common/core/range.js'; +import { ITextModel, EndOfLinePreference } from '../../../../common/model.js'; +import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; + +export function inlineCompletionIsVisible(singleTextEdit: TextReplacement, originalRange: Range | undefined, model: ITextModel, cursorPosition: Position): boolean { + const minimizedReplacement = singleTextRemoveCommonPrefix(singleTextEdit, model); + const editRange = singleTextEdit.range; + if (!editRange + || (originalRange && !originalRange.getStartPosition().equals(editRange.getStartPosition())) + || cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber + || minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible + ) { + return false; + } + + // We might consider comparing by .toLowerText, but this requires GhostTextReplacement + const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF); + const filterText = minimizedReplacement.text; + + const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn); + + let filterTextBefore = filterText.substring(0, cursorPosIndex); + let filterTextAfter = filterText.substring(cursorPosIndex); + + let originalValueBefore = originalValue.substring(0, cursorPosIndex); + let originalValueAfter = originalValue.substring(cursorPosIndex); + + const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber); + if (minimizedReplacement.range.startColumn <= originalValueIndent) { + // Remove indentation + originalValueBefore = originalValueBefore.trimStart(); + if (originalValueBefore.length === 0) { + originalValueAfter = originalValueAfter.trimStart(); + } + filterTextBefore = filterTextBefore.trimStart(); + if (filterTextBefore.length === 0) { + filterTextAfter = filterTextAfter.trimStart(); + } + } + + return filterTextBefore.startsWith(originalValueBefore) + && !!matchesSubString(originalValueAfter, filterTextAfter); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index defa17938d1..dc04e89e4cf 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { mapFindFirst } from '../../../../../base/common/arraysFind.js'; -import { itemsEquals } from '../../../../../base/common/equals.js'; +import { arrayEqualsC } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, constObservable, derived, derivedHandleChanges, derivedOpts, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; import { firstNonWhitespaceIndex } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; @@ -25,7 +25,7 @@ import { Selection } from '../../../../common/core/selection.js'; import { TextReplacement, TextEdit } from '../../../../common/core/edits/textEdit.js'; import { TextLength } from '../../../../common/core/text/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; -import { InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js'; +import { IInlineCompletionChangeHint, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionTriggerKind, PartialAcceptTriggerKind, InlineCompletionsProvider, InlineCompletionCommand } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { EndOfLinePreference, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; @@ -38,9 +38,8 @@ import { AnimatedValue, easeOutCubic, ObservableAnimatedValue } from './animatio import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; import { InlineCompletionsSource } from './inlineCompletionsSource.js'; -import { InlineEdit } from './inlineEdit.js'; import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem } from './inlineSuggestionItem.js'; -import { InlineCompletionContextWithoutUuid, InlineCompletionEditorType, InlineSuggestRequestInfo } from './provideInlineCompletions.js'; +import { InlineCompletionContextWithoutUuid, InlineCompletionEditorType, InlineSuggestRequestInfo, InlineSuggestSku } from './provideInlineCompletions.js'; import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; import { SuggestItemInfo } from './suggestWidgetAdapter.js'; import { TextModelEditSource, EditSources } from '../../../../common/textModelEditSource.js'; @@ -51,6 +50,10 @@ import { TypingInterval } from './typingSpeed.js'; import { StringReplacement } from '../../../../common/core/edits/stringEdit.js'; import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { URI } from '../../../../../base/common/uri.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { getInlineCompletionsController } from '../controller/common.js'; export class InlineCompletionsModel extends Disposable { private readonly _source; @@ -59,21 +62,23 @@ export class InlineCompletionsModel extends Disposable { private readonly _forceUpdateExplicitlySignal = observableSignal(this); private readonly _noDelaySignal = observableSignal(this); - private readonly _fetchSpecificProviderSignal = observableSignal(this); + private readonly _fetchSpecificProviderSignal = observableSignal<{ provider: InlineCompletionsProvider; changeHint?: IInlineCompletionChangeHint } | undefined>(this); // We use a semantic id to keep the same inline completion selected even if the provider reorders the completions. private readonly _selectedInlineCompletionId = observableValue(this, undefined); public readonly primaryPosition = derived(this, reader => this._positions.read(reader)[0] ?? new Position(1, 1)); public readonly allPositions = derived(this, reader => this._positions.read(reader)); + private readonly sku = observableValue(this, undefined); + private _isAcceptingPartially = false; private readonly _appearedInsideViewport = derived(this, reader => { const state = this.state.read(reader); - if (!state || !state.inlineCompletion) { + if (!state || !state.inlineSuggestion) { return false; } - return isSuggestionInViewport(this._editor, state.inlineCompletion); + return isSuggestionInViewport(this._editor, state.inlineSuggestion); }); public get isAcceptingPartially() { return this._isAcceptingPartially; } @@ -96,6 +101,10 @@ export class InlineCompletionsModel extends Disposable { private readonly _suppressInSnippetMode; private readonly _isInSnippetMode; + get editor() { + return this._editor; + } + constructor( public readonly textModel: ITextModel, private readonly _selectedSuggestItem: IObservable, @@ -110,7 +119,8 @@ export class InlineCompletionsModel extends Disposable { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, - @IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService + @IInlineCompletionsService private readonly _inlineCompletionsService: IInlineCompletionsService, + @IDefaultAccountService defaultAccountService: IDefaultAccountService, ) { super(); this._source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue, this.primaryPosition)); @@ -135,6 +145,9 @@ export class InlineCompletionsModel extends Disposable { const snippetController = SnippetController2.get(this._editor); this._isInSnippetMode = snippetController?.isInSnippetObservable ?? constObservable(false); + defaultAccountService.getDefaultAccount().then(createDisposableCb(account => this.sku.set(skuFromAccount(account), undefined), this._store)); + this._register(defaultAccountService.onDidChangeDefaultAccount(account => this.sku.set(skuFromAccount(account), undefined))); + this._typing = this._register(new TypingInterval(this.textModel)); this._register(this._inlineCompletionsService.onDidChangeIsSnoozing((isSnoozing) => { @@ -144,7 +157,7 @@ export class InlineCompletionsModel extends Disposable { })); { // Determine editor type - const isNotebook = this.textModel.uri.scheme === 'vscode-notebook-cell'; + const isNotebook = this.textModel.uri.scheme === Schemas.vscodeNotebookCell; const [diffEditor] = this._codeEditorService.listDiffEditors() .filter(d => d.getOriginalEditor().getId() === this._editor.getId() || @@ -157,8 +170,8 @@ export class InlineCompletionsModel extends Disposable { } this._register(recomputeInitiallyAndOnChange(this.state, (s) => { - if (s && s.inlineCompletion) { - this._inlineCompletionsService.reportNewCompletion(s.inlineCompletion.requestUuid); + if (s && s.inlineSuggestion) { + this._inlineCompletionsService.reportNewCompletion(s.inlineSuggestion.requestUuid); } })); @@ -176,7 +189,14 @@ export class InlineCompletionsModel extends Disposable { } })); - const inlineEditSemanticId = this.inlineEditState.map(s => s?.inlineCompletion.semanticId); + this._register(autorun(reader => { + const inlineSuggestion = this.state.map(s => s?.inlineSuggestion).read(reader); + if (inlineSuggestion) { + inlineSuggestion.addPerformanceMarker('activeSuggestion'); + } + })); + + const inlineEditSemanticId = this.inlineEditState.map(s => s?.inlineSuggestion.semanticId); this._register(autorun(reader => { const id = inlineEditSemanticId.read(reader); @@ -184,7 +204,7 @@ export class InlineCompletionsModel extends Disposable { this._editor.pushUndoStop(); this._lastShownInlineCompletionInfo = { alternateTextModelVersionId: this.textModel.getAlternativeVersionId(), - inlineCompletion: this.state.get()!.inlineCompletion!, + inlineCompletion: this.state.get()!.inlineSuggestion!, }; } })); @@ -196,7 +216,7 @@ export class InlineCompletionsModel extends Disposable { return; } - store.add(provider.onDidChangeInlineCompletions(() => { + store.add(provider.onDidChangeInlineCompletions(changeHint => { if (!this._enabled.get()) { return; } @@ -216,12 +236,12 @@ export class InlineCompletionsModel extends Disposable { // If there is an active suggestion from a different provider, we ignore the update const activeState = this.state.get(); - if (activeState && (activeState.inlineCompletion || activeState.edits) && activeState.inlineCompletion?.source.provider !== provider) { + if (activeState && (activeState.inlineSuggestion || activeState.edits) && activeState.inlineSuggestion?.source.provider !== provider) { return; } transaction(tx => { - this._fetchSpecificProviderSignal.trigger(tx, provider); + this._fetchSpecificProviderSignal.trigger(tx, { provider, changeHint: changeHint ?? undefined }); this.trigger(tx); }); @@ -315,6 +335,7 @@ export class InlineCompletionsModel extends Disposable { onlyRequestInlineEdits: false, shouldDebounce: true, provider: undefined as InlineCompletionsProvider | undefined, + changeHint: undefined as IInlineCompletionChangeHint | undefined, textChange: false, changeReason: '', }), @@ -335,11 +356,13 @@ export class InlineCompletionsModel extends Disposable { } else if (ctx.didChange(this._onlyRequestInlineEditsSignal)) { changeSummary.onlyRequestInlineEdits = true; } else if (ctx.didChange(this._fetchSpecificProviderSignal)) { - changeSummary.provider = ctx.change; + changeSummary.provider = ctx.change?.provider; + changeSummary.changeHint = ctx.change?.changeHint; } return true; }, }, + }, (reader, changeSummary) => { this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation this._noDelaySignal.read(reader); @@ -395,6 +418,7 @@ export class InlineCompletionsModel extends Disposable { typingInterval: typingInterval.averageInterval, typingIntervalCharacterCount: typingInterval.characterCount, availableProviders: [], + sku: this.sku.read(undefined), }; let context: InlineCompletionContextWithoutUuid = { @@ -404,6 +428,7 @@ export class InlineCompletionsModel extends Disposable { includeInlineEdits: this._inlineEditsEnabled.read(reader), requestIssuedDateTime: requestInfo.startTime, earliestShownDateTime: requestInfo.startTime + (changeSummary.inlineCompletionTriggerKind === InlineCompletionTriggerKind.Explicit || this.inAcceptFlow.read(undefined) ? 0 : this._minShowDelay.read(undefined)), + changeHint: changeSummary.changeHint, }; if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) { @@ -454,7 +479,7 @@ export class InlineCompletionsModel extends Disposable { return availableProviders; } - public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean } = {}): Promise { + public async trigger(tx?: ITransaction, options: { onlyFetchInlineEdits?: boolean; noDelay?: boolean; provider?: InlineCompletionsProvider; explicit?: boolean; changeHint?: IInlineCompletionChangeHint } = {}): Promise { subtransaction(tx, tx => { if (options.onlyFetchInlineEdits) { this._onlyRequestInlineEditsSignal.trigger(tx); @@ -469,7 +494,7 @@ export class InlineCompletionsModel extends Disposable { this._forceUpdateExplicitlySignal.trigger(tx); } if (options.provider) { - this._fetchSpecificProviderSignal.trigger(tx, options.provider); + this._fetchSpecificProviderSignal.trigger(tx, { provider: options.provider, changeHint: options.changeHint }); } }); await this._fetchInlineCompletionsPromise.get(); @@ -482,7 +507,7 @@ export class InlineCompletionsModel extends Disposable { public stop(stopReason: 'explicitCancel' | 'automatic' = 'automatic', tx?: ITransaction): void { subtransaction(tx, tx => { if (stopReason === 'explicitCancel') { - const inlineCompletion = this.state.get()?.inlineCompletion; + const inlineCompletion = this.state.get()?.inlineSuggestion; if (inlineCompletion) { inlineCompletion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Rejected }); } @@ -520,7 +545,7 @@ export class InlineCompletionsModel extends Disposable { }; }); - private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: itemsEquals() }, reader => { + private readonly _filteredInlineCompletionItems = derivedOpts({ owner: this, equalsFn: arrayEqualsC() }, reader => { const c = this._inlineCompletionItems.read(reader); return c?.inlineCompletions ?? []; }); @@ -544,7 +569,7 @@ export class InlineCompletionsModel extends Disposable { return filteredCompletions[idx]; }); - public readonly activeCommands = derivedOpts({ owner: this, equalsFn: itemsEquals() }, + public readonly activeCommands = derivedOpts({ owner: this, equalsFn: arrayEqualsC() }, r => this.selectedInlineCompletion.read(r)?.source.inlineSuggestions.commands ?? [] ); @@ -584,12 +609,11 @@ export class InlineCompletionsModel extends Disposable { primaryGhostText: GhostTextOrReplacement; ghostTexts: readonly GhostTextOrReplacement[]; suggestItem: SuggestItemInfo | undefined; - inlineCompletion: InlineCompletionItem | undefined; + inlineSuggestion: InlineCompletionItem | undefined; } | { kind: 'inlineEdit'; edits: readonly TextReplacement[]; - inlineEdit: InlineEdit; - inlineCompletion: InlineEditItem; + inlineSuggestion: InlineEditItem; cursorAtInlineEdit: IObservable; nextEditUri: URI | undefined; } | undefined>({ @@ -599,10 +623,10 @@ export class InlineCompletionsModel extends Disposable { if (a.kind === 'ghostText' && b.kind === 'ghostText') { return ghostTextsOrReplacementsEqual(a.ghostTexts, b.ghostTexts) - && a.inlineCompletion === b.inlineCompletion + && a.inlineSuggestion === b.inlineSuggestion && a.suggestItem === b.suggestItem; } else if (a.kind === 'inlineEdit' && b.kind === 'inlineEdit') { - return a.inlineEdit.equals(b.inlineEdit); + return a.inlineSuggestion === b.inlineSuggestion; } return false; } @@ -619,20 +643,17 @@ export class InlineCompletionsModel extends Disposable { if (this._hasVisiblePeekWidgets.read(reader)) { return undefined; } - let edit = inlineEditResult.getSingleTextEdit(); - edit = singleTextRemoveCommonPrefix(edit, model); - const cursorAtInlineEdit = this.primaryPosition.map(cursorPos => LineRange.fromRangeInclusive(inlineEditResult.targetRange).addMargin(1, 1).contains(cursorPos.lineNumber)); + const stringEdit = inlineEditResult.action?.kind === 'edit' ? inlineEditResult.action.stringEdit : undefined; + const replacements = stringEdit ? TextEdit.fromStringEdit(stringEdit, new TextModelText(this.textModel)).replacements : []; - const commands = inlineEditResult.source.inlineSuggestions.commands; - const inlineEdit = new InlineEdit(edit, commands ?? [], inlineEditResult); - - const edits = inlineEditResult.updatedEdit; - const e = edits ? TextEdit.fromStringEdit(edits, new TextModelText(this.textModel)).replacements : [edit]; - const nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') && + let nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') && // eslint-disable-next-line local/code-no-any-casts item.inlineEdit?.command.arguments?.length ? URI.from(item.inlineEdit?.command.arguments[0]) : undefined; - return { kind: 'inlineEdit', inlineEdit, inlineCompletion: inlineEditResult, edits: e, cursorAtInlineEdit, nextEditUri }; + if (!inlineEditResult.originalTextRef.targets(this.textModel)) { + nextEditUri = inlineEditResult.originalTextRef.uri; + } + return { kind: 'inlineEdit', inlineSuggestion: inlineEditResult, edits: replacements, cursorAtInlineEdit, nextEditUri }; } const suggestItem = this._selectedSuggestItem.read(reader); @@ -655,13 +676,13 @@ export class InlineCompletionsModel extends Disposable { const edits = validEditsAndGhostTexts.map(({ edit }) => edit!); const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!); const primaryGhostText = ghostTexts[0] ?? new GhostText(fullEdit.range.endLineNumber, []); - return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineCompletion: augmentation?.completion, suggestItem }; + return { kind: 'ghostText', edits, primaryGhostText, ghostTexts, inlineSuggestion: augmentation?.completion, suggestItem }; } else { if (!this._isActive.read(reader)) { return undefined; } - const inlineCompletion = this.selectedInlineCompletion.read(reader); - if (!inlineCompletion) { return undefined; } + const inlineSuggestion = this.selectedInlineCompletion.read(reader); + if (!inlineSuggestion) { return undefined; } - const replacement = inlineCompletion.getSingleTextEdit(); + const replacement = inlineSuggestion.getSingleTextEdit(); const mode = this._inlineSuggestMode.read(reader); const positions = this._positions.read(reader); const allPotentialEdits = [replacement, ...getSecondaryEdits(this.textModel, positions, replacement)]; @@ -671,7 +692,7 @@ export class InlineCompletionsModel extends Disposable { const edits = validEditsAndGhostTexts.map(({ edit }) => edit!); const ghostTexts = validEditsAndGhostTexts.map(({ ghostText }) => ghostText!); if (!ghostTexts[0]) { return undefined; } - return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineCompletion, suggestItem: undefined }; + return { kind: 'ghostText', edits, primaryGhostText: ghostTexts[0], ghostTexts, inlineSuggestion, suggestItem: undefined }; } }); @@ -728,7 +749,7 @@ export class InlineCompletionsModel extends Disposable { } public readonly warning = derived(this, reader => { - return this.inlineCompletionState.read(reader)?.inlineCompletion?.warning; + return this.inlineCompletionState.read(reader)?.inlineSuggestion?.warning; }); public readonly ghostTexts = derivedOpts({ owner: this, equalsFn: ghostTextsOrReplacementsEqual }, reader => { @@ -753,13 +774,13 @@ export class InlineCompletionsModel extends Disposable { return false; } - if (state.inlineCompletion.hint) { + if (state.inlineSuggestion.hint || state.inlineSuggestion.action?.kind === 'jumpTo') { return false; } - const isCurrentModelVersion = state.inlineCompletion.updatedEditModelVersion === this._textModelVersionId.read(reader); + const isCurrentModelVersion = state.inlineSuggestion.updatedEditModelVersion === this._textModelVersionId.read(reader); return (this._inlineEditsShowCollapsedEnabled.read(reader) || !isCurrentModelVersion) - && this._jumpedToId.read(reader) !== state.inlineCompletion.semanticId + && this._jumpedToId.read(reader) !== state.inlineSuggestion.semanticId && !this._inAcceptFlow.read(reader); }); @@ -799,11 +820,16 @@ export class InlineCompletionsModel extends Disposable { return false; } + + if (s.inlineSuggestion.action?.kind === 'jumpTo') { + return true; + } + if (this.showCollapsed.read(reader)) { return true; } - if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader) && !s.inlineCompletion.hint?.jumpToEdit) { + if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) { return false; } @@ -815,19 +841,22 @@ export class InlineCompletionsModel extends Disposable { if (!s) { return false; } + if (s.inlineSuggestion.action?.kind === 'jumpTo') { + return false; + } if (this.showCollapsed.read(reader)) { return false; } if (this._tabShouldIndent.read(reader)) { return false; } - if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader) && !s.inlineCompletion.hint?.jumpToEdit) { + if (this._inAcceptFlow.read(reader) && this._appearedInsideViewport.read(reader)) { return true; } - if (s.inlineCompletion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { + if (s.inlineSuggestion.targetRange.startLineNumber === this._editorObs.cursorLineNumber.read(reader)) { return true; } - if (this._jumpedToId.read(reader) === s.inlineCompletion.semanticId) { + if (this._jumpedToId.read(reader) === s.inlineSuggestion.semanticId) { return true; } @@ -862,18 +891,20 @@ export class InlineCompletionsModel extends Disposable { providerId: completion.source.provider.providerId, languageId, type, + correlationId: completion.getSourceCompletion().correlationId, }); } else { return EditSources.inlineCompletionAccept({ nes: completion.isInlineEdit, requestUuid: completion.requestUuid, + correlationId: completion.getSourceCompletion().correlationId, providerId: completion.source.provider.providerId, languageId }); } } - public async accept(editor: ICodeEditor = this._editor): Promise { + public async accept(editor: ICodeEditor = this._editor, alternativeAction: boolean = false): Promise { if (editor.getModel() !== this.textModel) { throw new BugIndicatingError(); } @@ -882,12 +913,12 @@ export class InlineCompletionsModel extends Disposable { let isNextEditUri = false; const state = this.state.get(); if (state?.kind === 'ghostText') { - if (!state || state.primaryGhostText.isEmpty() || !state.inlineCompletion) { + if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) { return; } - completion = state.inlineCompletion; + completion = state.inlineSuggestion; } else if (state?.kind === 'inlineEdit') { - completion = state.inlineCompletion; + completion = state.inlineSuggestion; isNextEditUri = !!state.nextEditUri; } else { return; @@ -897,44 +928,65 @@ export class InlineCompletionsModel extends Disposable { completion.addRef(); try { + let followUpTrigger = false; editor.pushUndoStop(); - if (isNextEditUri) { - // Do nothing - } else if (completion.snippetInfo) { - const mainEdit = TextReplacement.delete(completion.editRange); - const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? '')); - const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]); - editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId())); - - editor.setPosition(completion.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept'); - SnippetController2.get(editor)?.insert(completion.snippetInfo.snippet, { undoStopBefore: false }); - } else { - const edits = state.edits; - - // The cursor should move to the end of the edit, not the end of the range provided by the extension - // Inline Edit diffs (human readable) the suggestion from the extension so it already removes common suffix/prefix - // Inline Completions does diff the suggestion so it may contain common suffix - let minimalEdits = edits; - if (state.kind === 'ghostText') { - minimalEdits = removeTextReplacementCommonSuffixPrefix(edits, this.textModel); + + if (!completion.originalTextRef.targets(this.textModel)) { + // The edit targets a different document, open it and transplant the completion + const targetEditor = await this._codeEditorService.openCodeEditor({ resource: completion.originalTextRef.uri }, this._editor); + if (targetEditor) { + const controller = getInlineCompletionsController(targetEditor); + const m = controller?.model.get(); + targetEditor.focus(); + m?.transplantCompletion(completion); + targetEditor.revealLineInCenter(completion.targetRange.startLineNumber); } - const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p)); + } else if (isNextEditUri) { + // Do nothing + } else if (completion.action?.kind === 'edit') { + const action = completion.action; + if (alternativeAction && action.alternativeAction) { + followUpTrigger = true; + const altCommand = action.alternativeAction.command; + await this._commandService + .executeCommand(altCommand.id, ...(altCommand.arguments || [])) + .then(undefined, onUnexpectedExternalError); + } else if (action.snippetInfo) { + const mainEdit = TextReplacement.delete(action.textReplacement.range); + const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? '')); + const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]); + editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId())); + + editor.setPosition(action.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept'); + SnippetController2.get(editor)?.insert(action.snippetInfo.snippet, { undoStopBefore: false }); + } else { + const edits = state.edits; + + // The cursor should move to the end of the edit, not the end of the range provided by the extension + // Inline Edit diffs (human readable) the suggestion from the extension so it already removes common suffix/prefix + // Inline Completions does diff the suggestion so it may contain common suffix + let minimalEdits = edits; + if (state.kind === 'ghostText') { + minimalEdits = removeTextReplacementCommonSuffixPrefix(edits, this.textModel); + } + const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p)); - const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? '')); - const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]); + const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? '')); + const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]); - editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId())); + editor.edit(edit, this._getMetadata(completion, this.textModel.getLanguageId())); - if (completion.hint === undefined) { - // do not move the cursor when the completion is displayed in a different location - editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept'); - } + if (completion.hint === undefined) { + // do not move the cursor when the completion is displayed in a different location + editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept'); + } - if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) { - const editRanges = edit.getNewRanges(); - const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => { - this._store.delete(dec); - })); + if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) { + const editRanges = edit.getNewRanges(); + const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => { + this._store.delete(dec); + })); + } } } @@ -949,7 +1001,12 @@ export class InlineCompletionsModel extends Disposable { .then(undefined, onUnexpectedExternalError); } - completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted }); + // TODO: how can we make alternative actions to retrigger? + if (followUpTrigger) { + this.trigger(undefined); + } + + completion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction }); } finally { completion.removeRef(); this._inAcceptFlow.set(true, undefined); @@ -1002,11 +1059,11 @@ export class InlineCompletionsModel extends Disposable { } const state = this.inlineCompletionState.get(); - if (!state || state.primaryGhostText.isEmpty() || !state.inlineCompletion) { + if (!state || state.primaryGhostText.isEmpty() || !state.inlineSuggestion) { return; } const ghostText = state.primaryGhostText; - const completion = state.inlineCompletion; + const completion = state.inlineSuggestion; if (completion.snippetInfo) { // not in WYSIWYG mode, partial commit might change completion, thus it is not supported @@ -1041,7 +1098,7 @@ export class InlineCompletionsModel extends Disposable { editor.edit(TextEdit.fromParallelReplacementsUnsorted(edits), this._getMetadata(completion, type)); editor.setSelections(selections, 'inlineCompletionPartialAccept'); - editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate); + editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Smooth); } finally { this._isAcceptingPartially = false; } @@ -1082,7 +1139,7 @@ export class InlineCompletionsModel extends Disposable { public extractReproSample(): Repro { const value = this.textModel.getValue(); - const item = this.state.get()?.inlineCompletion; + const item = this.state.get()?.inlineSuggestion; return { documentValue: value, inlineCompletion: item?.getSourceCompletion(), @@ -1097,30 +1154,62 @@ export class InlineCompletionsModel extends Disposable { const s = this.inlineEditState.get(); if (!s) { return; } - transaction(tx => { - this._jumpedToId.set(s.inlineCompletion.semanticId, tx); - this.dontRefetchSignal.trigger(tx); - const targetRange = s.inlineCompletion.targetRange; - const targetPosition = targetRange.getStartPosition(); - this._editor.setPosition(targetPosition, 'inlineCompletions.jump'); - - // TODO: consider using view information to reveal it - const isSingleLineChange = targetRange.isSingleLine() && (s.inlineCompletion.hint || !s.inlineCompletion.insertText.includes('\n')); - if (isSingleLineChange) { - this._editor.revealPosition(targetPosition); - } else { - const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1); - this._editor.revealRange(revealRange, ScrollType.Immediate); - } + const suggestion = s.inlineSuggestion; + + if (!suggestion.originalTextRef.targets(this.textModel)) { + this.accept(this._editor); + return; + } - s.inlineCompletion.identity.setJumpTo(tx); - this._editor.focus(); - }); + suggestion.addRef(); + try { + transaction(tx => { + if (suggestion.action?.kind === 'jumpTo') { + this.stop(undefined, tx); + suggestion.reportEndOfLife({ kind: InlineCompletionEndOfLifeReasonKind.Accepted, alternativeAction: false }); + } + + this._jumpedToId.set(s.inlineSuggestion.semanticId, tx); + this.dontRefetchSignal.trigger(tx); + const targetRange = s.inlineSuggestion.targetRange; + const targetPosition = targetRange.getStartPosition(); + this._editor.setPosition(targetPosition, 'inlineCompletions.jump'); + + // TODO: consider using view information to reveal it + const isSingleLineChange = targetRange.isSingleLine() && (s.inlineSuggestion.hint || (s.inlineSuggestion.action?.kind === 'edit' && !s.inlineSuggestion.action.textReplacement.text.includes('\n'))); + if (isSingleLineChange || s.inlineSuggestion.action?.kind === 'jumpTo') { + this._editor.revealPosition(targetPosition, ScrollType.Smooth); + } else { + const revealRange = new Range(targetRange.startLineNumber - 1, 1, targetRange.endLineNumber + 1, 1); + this._editor.revealRange(revealRange, ScrollType.Smooth); + } + + s.inlineSuggestion.identity.setJumpTo(tx); + + this._editor.focus(); + }); + } finally { + suggestion.removeRef(); + } } - public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData): Promise { - await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData); + public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, timeWhenShown: number): Promise { + await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData, this.textModel, timeWhenShown); + } + + /** + * Transplants an inline completion from another model to this one. + * Used for cross-file inline edits. + */ + public transplantCompletion(item: InlineSuggestionItem): void { + item.addRef(); + transaction(tx => { + this._source.seedWithCompletion(item, tx); + this._isActive.set(true, tx); + this._inAcceptFlow.set(true, tx); + this.dontRefetchSignal.trigger(tx); + }); } } @@ -1206,13 +1295,51 @@ class FadeoutDecoration extends Disposable { } } -function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem): boolean { +export function isSuggestionInViewport(editor: ICodeEditor, suggestion: InlineSuggestionItem, reader: IReader | undefined = undefined): boolean { const targetRange = suggestion.targetRange; + + // TODO make getVisibleRanges reactive! + observableCodeEditor(editor).scrollTop.read(reader); const visibleRanges = editor.getVisibleRanges(); + if (visibleRanges.length < 1) { return false; } - const viewportRange = new Range(visibleRanges[0].startLineNumber, visibleRanges[0].startColumn, visibleRanges[visibleRanges.length - 1].endLineNumber, visibleRanges[visibleRanges.length - 1].endColumn); + const viewportRange = new Range( + visibleRanges[0].startLineNumber, + visibleRanges[0].startColumn, + visibleRanges[visibleRanges.length - 1].endLineNumber, + visibleRanges[visibleRanges.length - 1].endColumn + ); return viewportRange.containsRange(targetRange); } + +function skuFromAccount(account: IDefaultAccount | null): InlineSuggestSku | undefined { + if (account?.entitlementsData?.access_type_sku && account?.entitlementsData?.copilot_plan) { + return { type: account.entitlementsData.access_type_sku, plan: account.entitlementsData.copilot_plan }; + } + return undefined; +} + +class DisposableCallback { + private _cb: ((e: T) => void) | undefined; + + constructor(cb: (e: T) => void) { + this._cb = cb; + } + + dispose(): void { + this._cb = undefined; + } + + readonly handler = (val: T) => { + return this._cb?.(val); + }; +} + +function createDisposableCb(cb: (e: T) => void, store: DisposableStore): (e: T) => void { + const dcb = new DisposableCallback(cb); + store.add(dcb); + return dcb.handler; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 80f78e37d59..352af77e87f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -7,9 +7,10 @@ import { booleanComparator, compareBy, compareUndefinedSmallest, numberComparato import { findLastMax } from '../../../../../base/common/arraysFind.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.js'; +import { equalsIfDefined, thisEqualsC } from '../../../../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, transaction } from '../../../../../base/common/observable.js'; +import { cloneAndChange } from '../../../../../base/common/objects.js'; +import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, runOnChange, transaction } from '../../../../../base/common/observable.js'; // eslint-disable-next-line local/code-no-deep-import-of-internal import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js'; import { isDefined, isObject } from '../../../../../base/common/types.js'; @@ -22,17 +23,22 @@ import { observableConfigValue } from '../../../../../platform/observable/common import product from '../../../../../platform/product/common/product.js'; import { StringEdit } from '../../../../common/core/edits/stringEdit.js'; import { Position } from '../../../../common/core/position.js'; -import { InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; +import { Range } from '../../../../common/core/range.js'; +import { Command, InlineCompletionEndOfLifeReasonKind, InlineCompletionTriggerKind, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js'; +import { isCompletionsEnabledFromObject } from '../../../../common/services/completionsEnablement.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; +import { ITextModelService } from '../../../../common/services/resolverService.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js'; import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry } from '../telemetry.js'; import { wait } from '../utils.js'; import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js'; import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js'; +import { RenameSymbolProcessor } from './renameSymbolProcessor.js'; +import { TextModelValueReference } from './textModelValueReference.js'; export class InlineCompletionsSource extends Disposable { private static _requestId = 0; @@ -75,6 +81,8 @@ export class InlineCompletionsSource extends Disposable { public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); + private readonly _renameProcessor: RenameSymbolProcessor; + private _completionsEnabled: Record | undefined = undefined; constructor( @@ -87,6 +95,7 @@ export class InlineCompletionsSource extends Disposable { @IConfigurationService private readonly _configurationService: IConfigurationService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ITextModelService private readonly _textModelService: ITextModelService, ) { super(); this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store); @@ -98,6 +107,8 @@ export class InlineCompletionsSource extends Disposable { 'editor.inlineSuggest.logFetch.commandId' )); + this._renameProcessor = this._store.add(this._instantiationService.createInstance(RenameSymbolProcessor)); + this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store); const enablementSetting = product.defaultChatAgent?.completionsEnablementSetting ?? undefined; @@ -225,7 +236,7 @@ export class InlineCompletionsSource extends Disposable { let shouldStopEarly = false; let producedSuggestion = false; - const suggestions: InlineSuggestionItem[] = []; + const providerSuggestions: InlineSuggestionItem[] = []; for await (const list of providerResult.lists) { if (!list) { continue; @@ -244,8 +255,33 @@ export class InlineCompletionsSource extends Disposable { continue; } - const i = InlineSuggestionItem.create(item, this._textModel); - suggestions.push(i); + item.addPerformanceMarker('providerReturned'); + + const targetUri = item.action?.uri; + let targetModel: ITextModel; + let disposable: IDisposable | undefined; + + if (targetUri && targetUri.toString() !== this._textModel.uri.toString()) { + const modelRef = await this._textModelService.createModelReference(targetUri); + targetModel = modelRef.object.textEditorModel; + disposable = modelRef; + } else { + targetModel = this._textModel; + disposable = undefined; + } + + const ref = TextModelValueReference.snapshot(targetModel); + + const i = InlineSuggestionItem.create(item, ref); + if (disposable) { + const s = runOnChange(i.identity.onDispose, () => { + disposable?.dispose(); + s.dispose(); + }); + } + + item.addPerformanceMarker('itemCreated'); + providerSuggestions.push(i); // Stop after first visible inline completion if (!i.isInlineEdit && !i.showInlineEditMenu && context.triggerKind === InlineCompletionTriggerKind.Automatic) { if (i.isVisible(this._textModel, this._cursorPosition.get())) { @@ -259,6 +295,14 @@ export class InlineCompletionsSource extends Disposable { } } + providerSuggestions.forEach(s => s.addPerformanceMarker('providersResolved')); + + const suggestions: InlineSuggestionItem[] = await Promise.all(providerSuggestions.map(async s => { + return this._renameProcessor.proposeRenameRefactoring(this._textModel, s, context); + })); + + suggestions.forEach(s => s.addPerformanceMarker('renameProcessed')); + providerResult.cancelAndDispose({ kind: 'lostRace' }); if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) { @@ -267,14 +311,46 @@ export class InlineCompletionsSource extends Disposable { if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) { error = 'canceled'; } - const result = suggestions.map(c => ({ - range: c.editRange.toString(), - text: c.insertText, - hint: c.hint, - isInlineEdit: c.isInlineEdit, - showInlineEditMenu: c.showInlineEditMenu, - providerId: c.source.provider.providerId?.toString(), - })); + const result = suggestions.map(c => { + const comp = c.getSourceCompletion(); + if (comp.doNotLog) { + return undefined; + } + const obj = { + insertText: comp.insertText, + range: comp.range, + additionalTextEdits: comp.additionalTextEdits, + uri: comp.uri, + command: comp.command, + gutterMenuLinkAction: comp.gutterMenuLinkAction, + shownCommand: comp.shownCommand, + completeBracketPairs: comp.completeBracketPairs, + isInlineEdit: comp.isInlineEdit, + showInlineEditMenu: comp.showInlineEditMenu, + showRange: comp.showRange, + warning: comp.warning, + hint: comp.hint, + supportsRename: comp.supportsRename, + correlationId: comp.correlationId, + jumpToPosition: comp.jumpToPosition, + }; + return { + ...(cloneAndChange(obj, v => { + if (Range.isIRange(v)) { + return Range.lift(v).toString(); + } + if (Position.isIPosition(v)) { + return Position.lift(v).toString(); + } + if (Command.is(v)) { + return { $commandId: v.id }; + } + return v; + }) as object), + $providerId: c.source.provider.providerId?.toString(), + }; + }).filter(result => result !== undefined); + this._log({ sourceId: 'InlineCompletions.fetch', kind: 'end', requestId, durationMs: (Date.now() - startTime.getTime()), error, result, time: Date.now(), didAllProvidersReturn }); } @@ -298,6 +374,8 @@ export class InlineCompletionsSource extends Disposable { await wait(remainingTimeToWait, source.token); } + suggestions.forEach(s => s.addPerformanceMarker('minShowDelayPassed')); + if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId || userJumpedToActiveCompletion.get() /* In the meantime the user showed interest for the active completion so dont hide it */) { const notShownReason = @@ -350,6 +428,9 @@ export class InlineCompletionsSource extends Disposable { } public clear(tx: ITransaction): void { + if (this._store.isDisposed) { + return; + } this._updateOperation.clear(); const v = this._state.get(); this._state.set({ @@ -382,6 +463,20 @@ export class InlineCompletionsSource extends Disposable { }); } + /** + * Seeds the inline completions with an external inline completion item. + * Used when transplanting a completion from one model to another (cross-file edits). + */ + public seedWithCompletion(item: InlineSuggestionItem, tx: ITransaction): void { + const s = this._state.get(); + this._state.set({ + inlineCompletions: new InlineCompletionsState([item], undefined), + suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(), + }, tx); + s.inlineCompletions.dispose(); + s.suggestWidgetInlineCompletions.dispose(); + } + private sendInlineCompletionsRequestTelemetry( requestResponseInfo: RequestResponseData ): void { @@ -394,7 +489,7 @@ export class InlineCompletionsSource extends Disposable { } - if (!isCompletionsEnabled(this._completionsEnabled, this._textModel.getLanguageId())) { + if (!isCompletionsEnabledFromObject(this._completionsEnabled, this._textModel.getLanguageId())) { return; } @@ -409,6 +504,8 @@ export class InlineCompletionsSource extends Disposable { extensionVersion: '0.0.0', groupId: 'empty', shown: false, + skuPlan: requestResponseInfo.requestInfo.sku?.plan, + skuType: requestResponseInfo.requestInfo.sku?.type, editorType: requestResponseInfo.requestInfo.editorType, requestReason: requestResponseInfo.requestInfo.reason, typingInterval: requestResponseInfo.requestInfo.typingInterval, @@ -423,6 +520,7 @@ export class InlineCompletionsSource extends Disposable { preceeded: undefined, superseded: undefined, reason: undefined, + acceptedAlternativeAction: undefined, correlationId: undefined, shownDuration: undefined, shownDurationUncollapsed: undefined, @@ -439,7 +537,16 @@ export class InlineCompletionsSource extends Disposable { characterCountModified: undefined, disjointReplacements: undefined, sameShapeReplacements: undefined, + longDistanceHintVisible: undefined, + longDistanceHintDistance: undefined, notShownReason: undefined, + renameCreated: false, + renameDuration: undefined, + renameTimedOut: false, + renameDroppedOtherEdits: undefined, + renameDroppedRenameEdits: undefined, + performanceMarkers: undefined, + editKind: undefined, }; const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService); @@ -468,7 +575,7 @@ class UpdateRequest { public satisfies(other: UpdateRequest): boolean { return this.position.equals(other.position) - && equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, itemEquals()) + && equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, thisEqualsC()) && (other.context.triggerKind === InlineCompletionTriggerKind.Automatic || this.context.triggerKind === InlineCompletionTriggerKind.Explicit) && this.versionId === other.versionId @@ -508,18 +615,6 @@ function isSubset(set1: Set, set2: Set): boolean { return [...set1].every(item => set2.has(item)); } -function isCompletionsEnabled(completionsEnablementObject: Record | undefined, modeId: string = '*'): boolean { - if (completionsEnablementObject === undefined) { - return false; // default to disabled if setting is not available - } - - if (typeof completionsEnablementObject[modeId] !== 'undefined') { - return Boolean(completionsEnablementObject[modeId]); // go with setting if explicitly defined - } - - return Boolean(completionsEnablementObject['*']); // fallback to global setting otherwise -} - class UpdateOperation implements IDisposable { constructor( public readonly request: UpdateRequest, @@ -542,12 +637,12 @@ class InlineCompletionsState extends Disposable { public readonly inlineCompletions: readonly InlineSuggestionItem[], public readonly request: UpdateRequest | undefined, ) { - for (const inlineCompletion of inlineCompletions) { + super(); + + for (const inlineCompletion of this.inlineCompletions) { inlineCompletion.addRef(); } - super(); - this._register({ dispose: () => { for (const inlineCompletion of this.inlineCompletions) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts deleted file mode 100644 index 6a8a76e9d6f..00000000000 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineEdit.ts +++ /dev/null @@ -1,29 +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 { TextReplacement } from '../../../../common/core/edits/textEdit.js'; -import { InlineCompletionCommand } from '../../../../common/languages.js'; -import { InlineSuggestionItem } from './inlineSuggestionItem.js'; - -export class InlineEdit { - constructor( - public readonly edit: TextReplacement, - public readonly commands: readonly InlineCompletionCommand[], - public readonly inlineCompletion: InlineSuggestionItem, - ) { } - - public get range() { - return this.edit.range; - } - - public get text() { - return this.edit.text; - } - - public equals(other: InlineEdit): boolean { - return this.edit.equals(other.edit) - && this.inlineCompletion === other.inlineCompletion; - } -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index 6444e2040b2..02165506d6a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -4,10 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../../../base/common/errors.js'; -import { matchesSubString } from '../../../../../base/common/filters.js'; import { IObservable, ITransaction, observableSignal, observableValue } from '../../../../../base/common/observable.js'; import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js'; -import { URI } from '../../../../../base/common/uri.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; import { ISingleEditOperation } from '../../../../common/core/editOperation.js'; import { applyEditsToRanges, StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js'; @@ -20,34 +18,76 @@ import { getPositionOffsetTransformerFromTextModel } from '../../../../common/co import { PositionOffsetTransformerBase } from '../../../../common/core/text/positionToOffset.js'; import { TextLength } from '../../../../common/core/text/textLength.js'; import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; -import { Command, InlineCompletion, InlineCompletionHintStyle, InlineCompletionEndOfLifeReason, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo, InlineCompletionHint } from '../../../../common/languages.js'; -import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; +import { Command, IInlineCompletionHint, InlineCompletion, InlineCompletionEndOfLifeReason, InlineCompletionHintStyle, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo } from '../../../../common/languages.js'; +import { ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js'; -import { InlineSuggestData, InlineSuggestionList, PartialAcceptance, SnippetInfo } from './provideInlineCompletions.js'; -import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; +import { computeEditKind, InlineSuggestionEditKind } from './editKind.js'; +import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js'; +import { IInlineSuggestDataAction, IInlineSuggestDataActionEdit, InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js'; +import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js'; +import { TextModelValueReference } from './textModelValueReference.js'; export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem; export namespace InlineSuggestionItem { export function create( data: InlineSuggestData, - textModel: ITextModel, + textModel: TextModelValueReference, + shouldDiffEdit: boolean = true, // TODO@benibenj it should only be created once and hence not meeded to be passed here ): InlineSuggestionItem { - if (!data.isInlineEdit && !data.uri) { - return InlineCompletionItem.create(data, textModel); + if (!data.isInlineEdit && !data.action?.uri && data.action?.kind === 'edit') { + return InlineCompletionItem.create(data, textModel, data.action); } else { - return InlineEditItem.create(data, textModel); + return InlineEditItem.create(data, textModel, shouldDiffEdit); } } } +export type InlineSuggestionAction = IInlineSuggestionActionEdit | IInlineSuggestionActionJumpTo; + +export interface IInlineSuggestionActionEdit { + kind: 'edit'; + textReplacement: TextReplacement; + snippetInfo: SnippetInfo | undefined; + stringEdit: StringEdit; + target: TextModelValueReference; + alternativeAction: InlineSuggestAlternativeAction | undefined; +} + +export interface IInlineSuggestionActionJumpTo { + kind: 'jumpTo'; + position: Position; + offset: number; + target: TextModelValueReference; +} + +function hashInlineSuggestionAction(action: InlineSuggestionAction | undefined): string { + const obj = action?.kind === 'edit' ? { + ...action, alternativeAction: InlineSuggestAlternativeAction.toString(action.alternativeAction), + target: action?.target.uri.toString(), + } : { + ...action, + target: action?.target.uri.toString(), + }; + + return JSON.stringify(obj); +} + abstract class InlineSuggestionItemBase { constructor( protected readonly _data: InlineSuggestData, public readonly identity: InlineSuggestionIdentity, - public readonly hint: InlineSuggestHint | undefined - ) { } + public readonly hint: InlineSuggestHint | undefined, + /** + * Reference to the text model this item targets. + * For cross-file edits, this may differ from the current editor's model. + */ + public readonly originalTextRef: TextModelValueReference, + ) { + } + + public abstract get action(): InlineSuggestionAction | undefined; /** * A reference to the original inline completion list this inline completion has been constructed from. @@ -57,19 +97,27 @@ abstract class InlineSuggestionItemBase { public get isFromExplicitRequest(): boolean { return this._data.context.triggerKind === InlineCompletionTriggerKind.Explicit; } public get forwardStable(): boolean { return this.source.inlineSuggestions.enableForwardStability ?? false; } - public get editRange(): Range { return this.getSingleTextEdit().range; } - public get targetRange(): Range { return this.hint?.range && !this.hint.jumpToEdit ? this.hint?.range : this.editRange; } - public get insertText(): string { return this.getSingleTextEdit().text; } + + public get targetRange(): Range { + if (this.hint) { + return this.hint.range; + } + if (this.action?.kind === 'edit') { + return this.action.textReplacement.range; + } else if (this.action?.kind === 'jumpTo') { + return Range.fromPositions(this.action.position); + } + throw new BugIndicatingError('InlineSuggestionItem: Either hint or action must be set'); + } + public get semanticId(): string { return this.hash; } - public get action(): Command | undefined { return this._sourceInlineCompletion.gutterMenuLinkAction; } + public get gutterMenuLinkAction(): Command | undefined { return this._sourceInlineCompletion.gutterMenuLinkAction; } public get command(): Command | undefined { return this._sourceInlineCompletion.command; } + public get supportsRename(): boolean { return this._data.supportsRename; } public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } - public get hash() { - return JSON.stringify([ - this.getSingleTextEdit().text, - this.getSingleTextEdit().range.getStartPosition().toString() - ]); + public get hash(): string { + return hashInlineSuggestionAction(this.action); } /** @deprecated */ public get shownCommand(): Command | undefined { return this._sourceInlineCompletion.shownCommand; } @@ -85,13 +133,12 @@ abstract class InlineSuggestionItemBase { private get _sourceInlineCompletion(): InlineCompletion { return this._data.sourceInlineCompletion; } - public abstract getSingleTextEdit(): TextReplacement; - public abstract withEdit(userEdit: StringEdit, textModel: ITextModel): InlineSuggestionItem | undefined; public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem; public abstract canBeReused(model: ITextModel, position: Position): boolean; + public abstract computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined; public addRef(): void { this.identity.addRef(); @@ -103,8 +150,9 @@ abstract class InlineSuggestionItemBase { this.source.removeRef(); } - public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData) { - this._data.reportInlineEditShown(commandService, this.insertText, viewKind, viewData); + public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, model: ITextModel, timeWhenShown: number) { + const insertText = this.action?.kind === 'edit' ? this.action.textReplacement.text : ''; // TODO@hediet support insertText === undefined + this._data.reportInlineEditShown(commandService, insertText, viewKind, viewData, this.computeEditKind(model), timeWhenShown); } public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo, partialAcceptance: PartialAcceptance) { @@ -133,6 +181,18 @@ abstract class InlineSuggestionItemBase { public getSourceCompletion(): InlineCompletion { return this._sourceInlineCompletion; } + + public setRenameProcessingInfo(info: RenameInfo): void { + this._data.setRenameProcessingInfo(info); + } + + public withAction(action: IInlineSuggestDataAction): InlineSuggestData { + return this._data.withAction(action); + } + + public addPerformanceMarker(marker: string): void { + this._data.addPerformanceMarker(marker); + } } export class InlineSuggestionIdentity { @@ -145,7 +205,7 @@ export class InlineSuggestionIdentity { return this._jumpedTo; } - private _refCount = 1; + private _refCount = 0; public readonly id = 'InlineCompletionIdentity' + InlineSuggestionIdentity.idCounter++; addRef() { @@ -166,12 +226,11 @@ export class InlineSuggestionIdentity { export class InlineSuggestHint { - public static create(displayLocation: InlineCompletionHint) { + public static create(hint: IInlineCompletionHint) { return new InlineSuggestHint( - Range.lift(displayLocation.range), - displayLocation.content, - displayLocation.style, - displayLocation.jumpToEdit + Range.lift(hint.range), + hint.content, + hint.style, ); } @@ -179,7 +238,6 @@ export class InlineSuggestHint { public readonly range: Range, public readonly content: string, public readonly style: InlineCompletionHintStyle, - public readonly jumpToEdit: boolean, ) { } public withEdit(edit: StringEdit, positionOffsetTransformer: PositionOffsetTransformerBase): InlineSuggestHint | undefined { @@ -195,27 +253,28 @@ export class InlineSuggestHint { const newRange = positionOffsetTransformer.getRange(newOffsetRange); - return new InlineSuggestHint(newRange, this.content, this.style, this.jumpToEdit); + return new InlineSuggestHint(newRange, this.content, this.style); } } export class InlineCompletionItem extends InlineSuggestionItemBase { public static create( data: InlineSuggestData, - textModel: ITextModel, + textModel: TextModelValueReference, + action: IInlineSuggestDataActionEdit, ): InlineCompletionItem { const identity = new InlineSuggestionIdentity(); - const transformer = getPositionOffsetTransformerFromTextModel(textModel); + const transformer = textModel.getTransformer(); - const insertText = data.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL()); + const insertText = action.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL()); - const edit = reshapeInlineCompletion(new StringReplacement(transformer.getOffsetRange(data.range), insertText), textModel); + const edit = reshapeInlineCompletion(new StringReplacement(transformer.getOffsetRange(action.range), insertText), textModel); const trimmedEdit = edit.removeCommonSuffixAndPrefix(textModel.getValue()); const textEdit = transformer.getTextReplacement(edit); const displayLocation = data.hint ? InlineSuggestHint.create(data.hint) : undefined; - return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, data.snippetInfo, data.additionalTextEdits, data, identity, displayLocation); + return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, action.snippetInfo, data.additionalTextEdits, data, identity, displayLocation, textModel); } public readonly isInlineEdit = false; @@ -231,15 +290,27 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { data: InlineSuggestData, identity: InlineSuggestionIdentity, displayLocation: InlineSuggestHint | undefined, + originalTextRef: TextModelValueReference, ) { - super(data, identity, displayLocation); + super(data, identity, displayLocation, originalTextRef); + } + + override get action(): IInlineSuggestionActionEdit { + return { + kind: 'edit', + textReplacement: this.getSingleTextEdit(), + snippetInfo: this.snippetInfo, + stringEdit: new StringEdit([this._trimmedEdit]), + alternativeAction: undefined, + target: this.originalTextRef, + }; } override get hash(): string { return JSON.stringify(this._trimmedEdit.toJson()); } - override getSingleTextEdit(): TextReplacement { return this._textEdit; } + getSingleTextEdit(): TextReplacement { return this._textEdit; } override withIdentity(identity: InlineSuggestionIdentity): InlineCompletionItem { return new InlineCompletionItem( @@ -251,11 +322,17 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this.additionalTextEdits, this._data, identity, - this.hint + this.hint, + this.originalTextRef ); } override withEdit(textModelEdit: StringEdit, textModel: ITextModel): InlineCompletionItem | undefined { + // If the edit is to a different model than our target, it's a noop + if (!this.originalTextRef.targets(textModel)) { + return this; // unchanged + } + const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit); if (newEditRange.length === 0) { return undefined; @@ -283,7 +360,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { this.additionalTextEdits, this._data, this.identity, - newDisplayLocation + newDisplayLocation, + this.originalTextRef ); } @@ -301,66 +379,60 @@ export class InlineCompletionItem extends InlineSuggestionItemBase { const singleTextEdit = this.getSingleTextEdit(); return inlineCompletionIsVisible(singleTextEdit, this._originalRange, model, cursorPosition); } -} - -export function inlineCompletionIsVisible(singleTextEdit: TextReplacement, originalRange: Range | undefined, model: ITextModel, cursorPosition: Position): boolean { - const minimizedReplacement = singleTextRemoveCommonPrefix(singleTextEdit, model); - const editRange = singleTextEdit.range; - if (!editRange - || (originalRange && !originalRange.getStartPosition().equals(editRange.getStartPosition())) - || cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber - || minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible - ) { - return false; - } - - // We might consider comparing by .toLowerText, but this requires GhostTextReplacement - const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF); - const filterText = minimizedReplacement.text; - - const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn); - - let filterTextBefore = filterText.substring(0, cursorPosIndex); - let filterTextAfter = filterText.substring(cursorPosIndex); - - let originalValueBefore = originalValue.substring(0, cursorPosIndex); - let originalValueAfter = originalValue.substring(cursorPosIndex); - const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber); - if (minimizedReplacement.range.startColumn <= originalValueIndent) { - // Remove indentation - originalValueBefore = originalValueBefore.trimStart(); - if (originalValueBefore.length === 0) { - originalValueAfter = originalValueAfter.trimStart(); - } - filterTextBefore = filterTextBefore.trimStart(); - if (filterTextBefore.length === 0) { - filterTextAfter = filterTextAfter.trimStart(); - } + override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined { + return computeEditKind(new StringEdit([this._edit]), model); } - return filterTextBefore.startsWith(originalValueBefore) - && !!matchesSubString(originalValueAfter, filterTextAfter); + public get editRange(): Range { return this.getSingleTextEdit().range; } + public get insertText(): string { return this.getSingleTextEdit().text; } } export class InlineEditItem extends InlineSuggestionItemBase { public static create( data: InlineSuggestData, - textModel: ITextModel, + textModel: TextModelValueReference, + shouldDiffEdit: boolean = true, ): InlineEditItem { - const offsetEdit = getStringEdit(textModel, data.range, data.insertText); - const text = new TextModelText(textModel); - const textEdit = TextEdit.fromStringEdit(offsetEdit, text); - const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(text); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing + let action: InlineSuggestionAction | undefined; + let edits: SingleUpdatedNextEdit[] = []; + if (data.action?.kind === 'edit') { + const offsetEdit = shouldDiffEdit ? getDiffedStringEdit(textModel, data.action.range, data.action.insertText) : getStringEdit(textModel, data.action.range, data.action.insertText); // TODO compute async + const textEdit = TextEdit.fromStringEdit(offsetEdit, textModel); + const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(textModel); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing + + edits = offsetEdit.replacements.map(edit => { + const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getTransformer().getPosition(edit.replaceRange.endExclusive)); + const replacedText = textModel.getValueInRange(replacedRange); + return SingleUpdatedNextEdit.create(edit, replacedText); + }); + + action = { + kind: 'edit', + snippetInfo: data.action.snippetInfo, + stringEdit: offsetEdit, + textReplacement: singleTextEdit, + alternativeAction: data.action.alternativeAction, + target: textModel, + }; + } else if (data.action?.kind === 'jumpTo') { + action = { + kind: 'jumpTo', + position: data.action.position, + offset: textModel.getTransformer().getOffset(data.action.position), + target: textModel, + }; + } else { + action = undefined; + if (!data.hint) { + throw new BugIndicatingError('InlineEditItem: action is undefined and no hint is provided'); + } + } + const identity = new InlineSuggestionIdentity(); - const edits = offsetEdit.replacements.map(edit => { - const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getPositionAt(edit.replaceRange.endExclusive)); - const replacedText = textModel.getValueInRange(replacedRange); - return SingleUpdatedNextEdit.create(edit, replacedText); - }); const hint = data.hint ? InlineSuggestHint.create(data.hint) : undefined; - return new InlineEditItem(offsetEdit, singleTextEdit, data.uri, data, identity, edits, hint, false, textModel.getVersionId()); + return new InlineEditItem(action, data, identity, edits, hint, false, textModel.getVersionId(), textModel); } public readonly snippetInfo: SnippetInfo | undefined = undefined; @@ -368,9 +440,7 @@ export class InlineEditItem extends InlineSuggestionItemBase { public readonly isInlineEdit = true; private constructor( - private readonly _edit: StringEdit, - private readonly _textEdit: TextReplacement, - public readonly uri: URI | undefined, + private readonly _action: InlineSuggestionAction | undefined, data: InlineSuggestData, @@ -379,28 +449,28 @@ export class InlineEditItem extends InlineSuggestionItemBase { hint: InlineSuggestHint | undefined, private readonly _lastChangePartOfInlineEdit = false, private readonly _inlineEditModelVersion: number, + originalTextRef: TextModelValueReference, ) { - super(data, identity, hint); + super(data, identity, hint, originalTextRef); } public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; } - public get updatedEdit(): StringEdit { return this._edit; } + // public get updatedEdit(): StringEdit { return this._edit; } - override getSingleTextEdit(): TextReplacement { - return this._textEdit; + override get action(): InlineSuggestionAction | undefined { + return this._action; } override withIdentity(identity: InlineSuggestionIdentity): InlineEditItem { return new InlineEditItem( - this._edit, - this._textEdit, - this.uri, + this._action, this._data, identity, this._edits, this.hint, this._lastChangePartOfInlineEdit, this._inlineEditModelVersion, + this.originalTextRef, ); } @@ -410,37 +480,74 @@ export class InlineEditItem extends InlineSuggestionItemBase { } override withEdit(textModelChanges: StringEdit, textModel: ITextModel): InlineEditItem | undefined { + // If the edit is to a different model than our target, it's a noop + if (!this.originalTextRef.targets(textModel)) { + return this; // unchanged + } + const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel); return edit; } private _applyTextModelChanges(textModelChanges: StringEdit, edits: readonly SingleUpdatedNextEdit[], textModel: ITextModel): InlineEditItem | undefined { - edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges)); + const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); - if (edits.some(edit => edit.edit === undefined)) { - return undefined; // change is invalid, so we will have to drop the completion - } + let lastChangePartOfInlineEdit = false; + let inlineEditModelVersion = this._inlineEditModelVersion; + let newAction: InlineSuggestionAction | undefined; - const newTextModelVersion = textModel.getVersionId(); + if (this.action?.kind === 'edit') { // TODO What about rename? + edits = edits.map(innerEdit => innerEdit.applyTextModelChanges(textModelChanges)); - let inlineEditModelVersion = this._inlineEditModelVersion; - const lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit); - if (lastChangePartOfInlineEdit) { - inlineEditModelVersion = newTextModelVersion ?? -1; - } + if (edits.some(edit => edit.edit === undefined)) { + return undefined; // change is invalid, so we will have to drop the completion + } - if (newTextModelVersion === null || inlineEditModelVersion + 20 < newTextModelVersion) { - return undefined; // the completion has been ignored for a while, remove it - } - edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty); - if (edits.length === 0) { - return undefined; // the completion has been typed by the user - } + const newTextModelVersion = textModel.getVersionId(); + lastChangePartOfInlineEdit = edits.some(edit => edit.lastChangeUpdatedEdit); + if (lastChangePartOfInlineEdit) { + inlineEditModelVersion = newTextModelVersion ?? -1; + } - const newEdit = new StringEdit(edits.map(edit => edit.edit!)); - const positionOffsetTransformer = getPositionOffsetTransformerFromTextModel(textModel); - const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toReplacement(new TextModelText(textModel)); + if (newTextModelVersion === null || inlineEditModelVersion + 20 < newTextModelVersion) { + return undefined; // the completion has been ignored for a while, remove it + } + + edits = edits.filter(innerEdit => !innerEdit.edit!.isEmpty); + if (edits.length === 0) { + return undefined; // the completion has been typed by the user + } + + const newEdit = new StringEdit(edits.map(edit => edit.edit!)); + + const newTextEdit = positionOffsetTransformer.getTextEdit(newEdit).toReplacement(new TextModelText(textModel)); + + newAction = { + kind: 'edit', + textReplacement: newTextEdit, + snippetInfo: this.snippetInfo, + stringEdit: newEdit, + alternativeAction: this.action.alternativeAction, + target: this.originalTextRef, + }; + } else if (this.action?.kind === 'jumpTo') { + const jumpToOffset = this.action.offset; + const newJumpToOffset = textModelChanges.applyToOffsetOrUndefined(jumpToOffset); + if (newJumpToOffset === undefined) { + return undefined; + } + const newJumpToPosition = positionOffsetTransformer.getPosition(newJumpToOffset); + + newAction = { + kind: 'jumpTo', + position: newJumpToPosition, + offset: newJumpToOffset, + target: this.originalTextRef, + }; + } else { + newAction = undefined; + } let newDisplayLocation = this.hint; if (newDisplayLocation) { @@ -451,22 +558,29 @@ export class InlineEditItem extends InlineSuggestionItemBase { } return new InlineEditItem( - newEdit, - newTextEdit, - this.uri, + newAction, this._data, this.identity, edits, newDisplayLocation, lastChangePartOfInlineEdit, inlineEditModelVersion, + this.originalTextRef, ); } + + override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined { + const edit = this.action?.kind === 'edit' ? this.action.stringEdit : undefined; + if (!edit) { + return undefined; + } + return computeEditKind(edit, model); + } } -function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit { +function getDiffedStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit { const eol = textModel.getEOL(); - const editOriginalText = textModel.getValueInRange(editRange); + const editOriginalText = textModel.getValueOfRange(editRange); const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol); const diffAlgorithm = linesDiffComputers.getDefault(); @@ -477,7 +591,7 @@ function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: str ignoreTrimWhitespace: false, computeMoves: false, extendToSubwords: true, - maxComputationTimeMs: 500, + maxComputationTimeMs: 50, } ); @@ -493,12 +607,12 @@ function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: str const offsetEdit = new StringEdit( innerChanges.map(c => { const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange); - const originalRange = getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(rangeInModel); + const originalRange = textModel.getTransformer().getOffsetRange(rangeInModel); const replaceText = modifiedText.getValueOfRange(c.modifiedRange); const edit = new StringReplacement(originalRange, replaceText); - const originalText = textModel.getValueInRange(rangeInModel); + const originalText = textModel.getValueOfRange(rangeInModel); return reshapeInlineEdit(edit, originalText, innerChanges.length, textModel); }) ); @@ -506,6 +620,13 @@ function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: str return offsetEdit; } +function getStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit { + return new StringEdit([new StringReplacement( + textModel.getTransformer().getOffsetRange(editRange), + replaceText + )]); +} + class SingleUpdatedNextEdit { public static create( edit: StringReplacement, @@ -546,7 +667,7 @@ class SingleUpdatedNextEdit { } private _applyTextModelChanges(textModelChanges: StringEdit) { - this._lastChangeUpdatedEdit = false; + this._lastChangeUpdatedEdit = false; // TODO @benibenj make immutable if (!this._edit) { throw new BugIndicatingError('UpdatedInnerEdits: No edit to apply changes to'); @@ -579,7 +700,7 @@ class SingleUpdatedNextEdit { if (isInsertion && !shouldPreserveEditShape && change.replaceRange.start === editStart && editReplaceText.startsWith(change.newText)) { editStart += change.newText.length; editReplaceText = editReplaceText.substring(change.newText.length); - editEnd = Math.max(editStart, editEnd); + editEnd += change.newText.length; editHasChanged = true; continue; } @@ -634,7 +755,7 @@ class SingleUpdatedNextEdit { } } -function reshapeInlineCompletion(edit: StringReplacement, textModel: ITextModel): StringReplacement { +function reshapeInlineCompletion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement { // If the insertion is a multi line insertion starting on the next line // Move it forwards so that the multi line insertion starts on the current line const eol = textModel.getEOL(); @@ -645,7 +766,7 @@ function reshapeInlineCompletion(edit: StringReplacement, textModel: ITextModel) return edit; } -function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: ITextModel): StringReplacement { +function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: TextModelValueReference): StringReplacement { // TODO: EOL are not properly trimmed by the diffAlgorithm #12680 const eol = textModel.getEOL(); if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) { @@ -656,7 +777,7 @@ function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalI // If the insertion ends with a new line and is inserted at the start of a line which has text, // we move the insertion to the end of the previous line if possible if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) { - const startPosition = textModel.getPositionAt(edit.replaceRange.start); + const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start); const hasTextOnInsertionLine = textModel.getLineLength(startPosition.lineNumber) !== 0; if (hasTextOnInsertionLine) { edit = reshapeMultiLineInsertion(edit, textModel); @@ -683,7 +804,7 @@ function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalI return edit; } -function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextModel): StringReplacement { +function reshapeMultiLineInsertion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement { if (!edit.replaceRange.isEmpty) { throw new BugIndicatingError('Unexpected original range'); } @@ -693,7 +814,7 @@ function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextMode } const eol = textModel.getEOL(); - const startPosition = textModel.getPositionAt(edit.replaceRange.start); + const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start); const startColumn = startPosition.column; const startLineNumber = startPosition.lineNumber; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 2a8813dca58..84c14a69639 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -6,7 +6,7 @@ import { assertNever } from '../../../../../base/common/assert.js'; import { AsyncIterableProducer } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { onUnexpectedExternalError } from '../../../../../base/common/errors.js'; +import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { prefixedUuid } from '../../../../../base/common/uuid.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -16,20 +16,22 @@ import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; -import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, PartialAcceptInfo, InlineCompletionsDisposeReason, LifetimeSummary, ProviderId, InlineCompletionHint } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, PartialAcceptInfo, InlineCompletionsDisposeReason, LifetimeSummary, ProviderId, IInlineCompletionHint } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; import { SnippetParser, Text } from '../../../snippet/browser/snippetParser.js'; -import { getReadonlyEmptyArray } from '../utils.js'; +import { ErrorResult, getReadonlyEmptyArray } from '../utils.js'; import { groupByMap } from '../../../../../base/common/collections.js'; import { DirectedGraph } from './graph.js'; import { CachedFunction } from '../../../../../base/common/cache.js'; import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js'; import { isDefined } from '../../../../../base/common/types.js'; -import { inlineCompletionIsVisible } from './inlineSuggestionItem.js'; +import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js'; import { EditDeltaInfo } from '../../../../common/textModelEditSource.js'; import { URI } from '../../../../../base/common/uri.js'; +import { InlineSuggestionEditKind } from './editKind.js'; +import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js'; export type InlineCompletionContextWithoutUuid = Omit; @@ -115,7 +117,12 @@ export function provideInlineCompletions( } for (const item of result.items) { - data.push(toInlineSuggestData(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid, requestInfo, { startTime: providerStartTime, endTime: providerEndTime })); + const r = toInlineSuggestData(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid, requestInfo, { startTime: providerStartTime, endTime: providerEndTime }); + if (ErrorResult.is(r)) { + r.logError(); + continue; + } + data.push(r); } return list; @@ -173,85 +180,106 @@ function toInlineSuggestData( context: InlineCompletionContext, requestInfo: InlineSuggestRequestInfo, providerRequestInfo: InlineSuggestProviderRequestInfo, -): InlineSuggestData { - let insertText: string; - let snippetInfo: SnippetInfo | undefined; - let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange; - - if (typeof inlineCompletion.insertText === 'string') { - insertText = inlineCompletion.insertText; - - if (languageConfigurationService && inlineCompletion.completeBracketPairs) { - insertText = closeBrackets( - insertText, - range.getStartPosition(), - textModel, - languageConfigurationService - ); - - // Modify range depending on if brackets are added or removed - const diff = insertText.length - inlineCompletion.insertText.length; - if (diff !== 0) { - range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); +): InlineSuggestData | ErrorResult { + + let action: IInlineSuggestDataAction | undefined; + const uri = inlineCompletion.uri ? URI.revive(inlineCompletion.uri) : undefined; + + if (inlineCompletion.jumpToPosition !== undefined) { + action = { + kind: 'jumpTo', + position: Position.lift(inlineCompletion.jumpToPosition), + uri, + }; + } else if (inlineCompletion.insertText !== undefined) { + let insertText: string; + let snippetInfo: SnippetInfo | undefined; + let range = inlineCompletion.range ? Range.lift(inlineCompletion.range) : defaultReplaceRange; + + if (typeof inlineCompletion.insertText === 'string') { + insertText = inlineCompletion.insertText; + + if (languageConfigurationService && inlineCompletion.completeBracketPairs) { + insertText = closeBrackets( + insertText, + range.getStartPosition(), + textModel, + languageConfigurationService + ); + + // Modify range depending on if brackets are added or removed + const diff = insertText.length - inlineCompletion.insertText.length; + if (diff !== 0) { + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); + } } - } - snippetInfo = undefined; - } else if (inlineCompletion.insertText === undefined) { - insertText = ''; // TODO use undefined - snippetInfo = undefined; - range = new Range(1, 1, 1, 1); - } else if ('snippet' in inlineCompletion.insertText) { - const preBracketCompletionLength = inlineCompletion.insertText.snippet.length; - - if (languageConfigurationService && inlineCompletion.completeBracketPairs) { - inlineCompletion.insertText.snippet = closeBrackets( - inlineCompletion.insertText.snippet, - range.getStartPosition(), - textModel, - languageConfigurationService - ); - - // Modify range depending on if brackets are added or removed - const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength; - if (diff !== 0) { - range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); + snippetInfo = undefined; + } else if ('snippet' in inlineCompletion.insertText) { + const preBracketCompletionLength = inlineCompletion.insertText.snippet.length; + + if (languageConfigurationService && inlineCompletion.completeBracketPairs) { + inlineCompletion.insertText.snippet = closeBrackets( + inlineCompletion.insertText.snippet, + range.getStartPosition(), + textModel, + languageConfigurationService + ); + + // Modify range depending on if brackets are added or removed + const diff = inlineCompletion.insertText.snippet.length - preBracketCompletionLength; + if (diff !== 0) { + range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + diff); + } } - } - const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet); - - if (snippet.children.length === 1 && snippet.children[0] instanceof Text) { - insertText = snippet.children[0].value; - snippetInfo = undefined; + const snippet = new SnippetParser().parse(inlineCompletion.insertText.snippet); + + if (snippet.children.length === 1 && snippet.children[0] instanceof Text) { + insertText = snippet.children[0].value; + snippetInfo = undefined; + } else { + insertText = snippet.toString(); + snippetInfo = { + snippet: inlineCompletion.insertText.snippet, + range: range + }; + } } else { - insertText = snippet.toString(); - snippetInfo = { - snippet: inlineCompletion.insertText.snippet, - range: range - }; + assertNever(inlineCompletion.insertText); } + action = { + kind: 'edit', + range, + insertText, + snippetInfo, + uri, + alternativeAction: undefined, + }; } else { - assertNever(inlineCompletion.insertText); + action = undefined; + if (!inlineCompletion.hint) { + return ErrorResult.message('Inline completion has no insertText, jumpToPosition nor hint.'); + } } return new InlineSuggestData( - range, - insertText, - snippetInfo, - URI.revive(inlineCompletion.uri), + action, inlineCompletion.hint, inlineCompletion.additionalTextEdits || getReadonlyEmptyArray(), inlineCompletion, source, context, inlineCompletion.isInlineEdit ?? false, + inlineCompletion.supportsRename ?? false, requestInfo, providerRequestInfo, inlineCompletion.correlationId, ); } +export type InlineSuggestSku = { type: string; plan: string }; + export type InlineSuggestRequestInfo = { startTime: number; editorType: InlineCompletionEditorType; @@ -260,6 +288,7 @@ export type InlineSuggestRequestInfo = { typingInterval: number; typingIntervalCharacterCount: number; availableProviders: ProviderId[]; + sku: InlineSuggestSku | undefined; }; export type InlineSuggestProviderRequestInfo = { @@ -273,15 +302,41 @@ export type PartialAcceptance = { ratio: number; }; +export type RenameInfo = { + createdRename: boolean; + duration: number; + timedOut?: boolean; + droppedOtherEdits?: number; + droppedRenameEdits?: number; +}; + export type InlineSuggestViewData = { editorType: InlineCompletionEditorType; renderData?: InlineCompletionViewData; viewKind?: InlineCompletionViewKind; }; +export type IInlineSuggestDataAction = IInlineSuggestDataActionEdit | IInlineSuggestDataActionJumpTo; + +export interface IInlineSuggestDataActionEdit { + kind: 'edit'; + range: Range; + insertText: string; + snippetInfo: SnippetInfo | undefined; + uri: URI | undefined; + alternativeAction: InlineSuggestAlternativeAction | undefined; +} + +export interface IInlineSuggestDataActionJumpTo { + kind: 'jumpTo'; + position: Position; + uri: URI | undefined; +} + export class InlineSuggestData { private _didShow = false; private _timeUntilShown: number | undefined = undefined; + private _timeUntilActuallyShown: number | undefined = undefined; private _showStartTime: number | undefined = undefined; private _shownDuration: number = 0; private _showUncollapsedStartTime: number | undefined = undefined; @@ -294,20 +349,22 @@ export class InlineSuggestData { private _isPreceeded = false; private _partiallyAcceptedCount = 0; private _partiallyAcceptedSinceOriginal: PartialAcceptance = { characters: 0, ratio: 0, count: 0 }; + private _renameInfo: RenameInfo | undefined = undefined; + private _editKind: InlineSuggestionEditKind | undefined = undefined; + + get action(): IInlineSuggestDataAction | undefined { + return this._action; + } constructor( - public readonly range: Range, - public readonly insertText: string, - public readonly snippetInfo: SnippetInfo | undefined, - public readonly uri: URI | undefined, - public readonly hint: InlineCompletionHint | undefined, + private _action: IInlineSuggestDataAction | undefined, + public readonly hint: IInlineCompletionHint | undefined, public readonly additionalTextEdits: readonly ISingleEditOperation[], - public readonly sourceInlineCompletion: InlineCompletion, public readonly source: InlineSuggestionList, public readonly context: InlineCompletionContext, public readonly isInlineEdit: boolean, - + public readonly supportsRename: boolean, private readonly _requestInfo: InlineSuggestRequestInfo, private readonly _providerRequestInfo: InlineSuggestProviderRequestInfo, private readonly _correlationId: string | undefined, @@ -319,20 +376,20 @@ export class InlineSuggestData { public get partialAccepts(): PartialAcceptance { return this._partiallyAcceptedSinceOriginal; } - public getSingleTextEdit() { - return new TextReplacement(this.range, this.insertText); - } - public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData): Promise { + public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, editKind: InlineSuggestionEditKind | undefined, timeWhenShown: number): Promise { this.updateShownDuration(viewKind); - if (this._didShow) { + if (this._didShow || this._didReportEndOfLife) { return; } + this.addPerformanceMarker('shown'); this._didShow = true; + this._editKind = editKind; this._viewData.viewKind = viewKind; this._viewData.renderData = viewData; - this._timeUntilShown = Date.now() - this._requestInfo.startTime; + this._timeUntilShown = timeWhenShown - this._requestInfo.startTime; + this._timeUntilActuallyShown = Date.now() - this._requestInfo.startTime; const editDeltaInfo = new EditDeltaInfo(viewData.lineCountModified, viewData.lineCountOriginal, viewData.characterCountModified, viewData.characterCountOriginal); this.source.provider.handleItemDidShow?.(this.source.inlineSuggestions, this.sourceInlineCompletion, updatedInsertText, editDeltaInfo); @@ -372,6 +429,12 @@ export class InlineSuggestData { reason = this._lastSetEndOfLifeReason ?? { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; } + // A suggestion can only be "rejected" if it was actually shown to the user. + // If the suggestion was never shown, downgrade to "ignored". + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && !this._didShow) { + reason = { kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: undefined }; + } + if (reason.kind === InlineCompletionEndOfLifeReasonKind.Rejected && this.source.provider.handleRejection) { this.source.provider.handleRejection(this.source.inlineSuggestions, this.sourceInlineCompletion); } @@ -388,8 +451,10 @@ export class InlineSuggestData { shown: this._didShow, shownDuration: this._shownDuration, shownDurationUncollapsed: this._showUncollapsedDuration, + editKind: this._editKind?.toString(), preceeded: this._isPreceeded, timeUntilShown: this._timeUntilShown, + timeUntilActuallyShown: this._timeUntilActuallyShown, timeUntilProviderRequest: this._providerRequestInfo.startTime - this._requestInfo.startTime, timeUntilProviderResponse: this._providerRequestInfo.endTime - this._requestInfo.startTime, editorType: this._viewData.editorType, @@ -397,10 +462,18 @@ export class InlineSuggestData { requestReason: this._requestInfo.reason, viewKind: this._viewData.viewKind, notShownReason: this._notShownReason, + performanceMarkers: this.performance.toString(), + renameCreated: this._renameInfo?.createdRename, + renameDuration: this._renameInfo?.duration, + renameTimedOut: this._renameInfo?.timedOut, + renameDroppedOtherEdits: this._renameInfo?.droppedOtherEdits, + renameDroppedRenameEdits: this._renameInfo?.droppedRenameEdits, typingInterval: this._requestInfo.typingInterval, typingIntervalCharacterCount: this._requestInfo.typingIntervalCharacterCount, + skuPlan: this._requestInfo.sku?.plan, + skuType: this._requestInfo.sku?.type, availableProviders: this._requestInfo.availableProviders.map(p => p.toString()).join(','), - ...this._viewData.renderData, + ...this._viewData.renderData?.getData(), }; this.source.provider.handleEndOfLifetime(this.source.inlineSuggestions, this.sourceInlineCompletion, reason, summary); } @@ -457,6 +530,43 @@ export class InlineSuggestData { this._showUncollapsedDuration += timeNow - this._showUncollapsedStartTime; this._showUncollapsedStartTime = undefined; } + + public setRenameProcessingInfo(info: RenameInfo): void { + if (this._renameInfo) { + throw new BugIndicatingError('Rename info has already been set.'); + } + this._renameInfo = info; + } + + public withAction(action: IInlineSuggestDataAction): InlineSuggestData { + this._action = action; + return this; + } + + private performance = new InlineSuggestionsPerformance(); + public addPerformanceMarker(marker: string): void { + this.performance.mark(marker); + } +} + +class InlineSuggestionsPerformance { + private markers: { name: string; timeStamp: number }[] = []; + constructor() { + this.markers.push({ name: 'start', timeStamp: Date.now() }); + } + + mark(marker: string): void { + this.markers.push({ name: marker, timeStamp: Date.now() }); + } + + toString(): string { + const deltas = []; + for (let i = 1; i < this.markers.length; i++) { + const delta = this.markers[i].timeStamp - this.markers[i - 1].timeStamp; + deltas.push({ [this.markers[i].name]: delta }); + } + return JSON.stringify(deltas); + } } export interface SnippetInfo { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts new file mode 100644 index 00000000000..25ab7287379 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts @@ -0,0 +1,587 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceTimeout } from '../../../../../base/common/async.js'; +import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { LcsDiff, StringDiffSequence } from '../../../../../base/common/diff/diff.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { ServicesAccessor } from '../../../../browser/editorExtensions.js'; +import { IBulkEditService, ResourceTextEdit } from '../../../../browser/services/bulkEditService.js'; +import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; +import { Position } from '../../../../common/core/position.js'; +import { Range, type IRange } from '../../../../common/core/range.js'; +import { StandardTokenType } from '../../../../common/encodedTokenAttributes.js'; +import { Command, type Rejection, type WorkspaceEdit } from '../../../../common/languages.js'; +import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; +import { ITextModel } from '../../../../common/model.js'; +import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; +import { EditSources, TextModelEditSource } from '../../../../common/textModelEditSource.js'; +import { hasProvider, rawRename } from '../../../rename/browser/rename.js'; +import { renameSymbolCommandId } from '../controller/commandIds.js'; +import { InlineSuggestionItem } from './inlineSuggestionItem.js'; +import { IInlineSuggestDataActionEdit, InlineCompletionContextWithoutUuid } from './provideInlineCompletions.js'; +import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { IRenameSymbolTrackerService } from '../../../../browser/services/renameSymbolTrackerService.js'; +import type { URI } from '../../../../../base/common/uri.js'; +import type { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js'; +import { TextModelValueReference } from './textModelValueReference.js'; + +enum RenameKind { + no = 'no', + yes = 'yes', + maybe = 'maybe' +} + +namespace RenameKind { + export function fromString(value: string): RenameKind { + switch (value) { + case 'no': return RenameKind.no; + case 'yes': return RenameKind.yes; + case 'maybe': return RenameKind.maybe; + default: return RenameKind.no; + } + } +} + +export namespace PrepareNesRenameResult { + export type Yes = { + canRename: RenameKind.yes; + oldName: string; + onOldState: boolean; + }; + export type Maybe = { + canRename: RenameKind.maybe; + oldName: string; + onOldState: boolean; + }; + export type No = { + canRename: RenameKind.no; + timedOut: boolean; + reason?: string; + }; +} + +export type PrepareNesRenameResult = PrepareNesRenameResult.Yes | PrepareNesRenameResult.Maybe | PrepareNesRenameResult.No; + +export type TextChange = { + range: { start: { line: number; character: number }; end: { line: number; character: number } }; + newText?: string; +}; + +export type RenameGroup = { + file: URI; + changes: TextChange[]; +}; + +export type RenameEdits = { + renames: { edits: TextReplacement[]; position: Position; oldName: string; newName: string }; + others: { edits: TextReplacement[] }; +}; + +export class RenameInferenceEngine { + + public constructor() { + } + + public inferRename(textModel: ITextModel, editRange: Range, insertText: string, wordDefinition: RegExp): RenameEdits | undefined { + + // Extend the edit range to full lines to capture prefix/suffix renames + const extendedRange = new Range(editRange.startLineNumber, 1, editRange.endLineNumber, textModel.getLineMaxColumn(editRange.endLineNumber)); + const startDiff = editRange.startColumn - extendedRange.startColumn; + const endDiff = extendedRange.endColumn - editRange.endColumn; + + const originalText = textModel.getValueInRange(extendedRange); + const modifiedText = + textModel.getValueInRange(new Range(extendedRange.startLineNumber, extendedRange.startColumn, extendedRange.startLineNumber, extendedRange.startColumn + startDiff)) + + insertText + + textModel.getValueInRange(new Range(extendedRange.endLineNumber, extendedRange.endColumn - endDiff, extendedRange.endLineNumber, extendedRange.endColumn)); + + // console.log(`Original: ${originalText} \nmodified: ${modifiedText}`); + const others: TextReplacement[] = []; + const renames: TextReplacement[] = []; + let oldName: string | undefined = undefined; + let newName: string | undefined = undefined; + let position: Position | undefined = undefined; + + const nesOffset = textModel.getOffsetAt(extendedRange.getStartPosition()); + + const { changes: originalChanges } = (new LcsDiff(new StringDiffSequence(originalText), new StringDiffSequence(modifiedText))).ComputeDiff(true); + if (originalChanges.length === 0) { + return undefined; + } + + // Fold the changes to larger changes if the gap between two changes is a full word. This covers cases like renaming + // `foo` to `abcfoobar` + const changes: typeof originalChanges = []; + for (const change of originalChanges) { + if (changes.length === 0) { + changes.push(change); + continue; + } + + const lastChange = changes[changes.length - 1]; + const gapOriginalLength = change.originalStart - (lastChange.originalStart + lastChange.originalLength); + + if (gapOriginalLength > 0) { + const gapStartOffset = nesOffset + lastChange.originalStart + lastChange.originalLength; + const gapStartPos = textModel.getPositionAt(gapStartOffset); + const wordRange = textModel.getWordAtPosition(gapStartPos); + + if (wordRange) { + const wordStartOffset = textModel.getOffsetAt(new Position(gapStartPos.lineNumber, wordRange.startColumn)); + const wordEndOffset = textModel.getOffsetAt(new Position(gapStartPos.lineNumber, wordRange.endColumn)); + const gapEndOffset = gapStartOffset + gapOriginalLength; + + if (wordStartOffset <= gapStartOffset && gapEndOffset <= wordEndOffset && wordStartOffset <= gapEndOffset && gapEndOffset <= wordEndOffset) { + lastChange.originalLength = (change.originalStart + change.originalLength) - lastChange.originalStart; + lastChange.modifiedLength = (change.modifiedStart + change.modifiedLength) - lastChange.modifiedStart; + continue; + } + } + } + + changes.push(change); + } + + let tokenDiff: number = 0; + for (const change of changes) { + const originalTextSegment = originalText.substring(change.originalStart, change.originalStart + change.originalLength); + const insertedTextSegment = modifiedText.substring(change.modifiedStart, change.modifiedStart + change.modifiedLength); + + const startOffset = nesOffset + change.originalStart; + const startPos = textModel.getPositionAt(startOffset); + + const endOffset = startOffset + change.originalLength; + const endPos = textModel.getPositionAt(endOffset); + + const range = Range.fromPositions(startPos, endPos); + + const diff = insertedTextSegment.length - change.originalLength; + + // If the original text segment contains a whitespace character we don't consider this a rename since + // identifiers in programming languages can't contain whitespace characters usually + if (/\s/.test(originalTextSegment)) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + if (originalTextSegment.length > 0) { + wordDefinition.lastIndex = 0; + const match = wordDefinition.exec(originalTextSegment); + if (match === null || match.index !== 0 || match[0].length !== originalTextSegment.length) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + } + // If the inserted text contains a whitespace character we don't consider this a rename since identifiers in + // programming languages can't contain whitespace characters usually + if (/\s/.test(insertedTextSegment)) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + if (insertedTextSegment.length > 0) { + wordDefinition.lastIndex = 0; + const match = wordDefinition.exec(insertedTextSegment); + if (match === null || match.index !== 0 || match[0].length !== insertedTextSegment.length) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + } + + const wordRange = textModel.getWordAtPosition(startPos); + // If we don't have a word range at the start position of the current document then we + // don't treat it as a rename assuming that the rename refactoring will fail as well since + // there can't be an identifier at that position. + if (wordRange === null) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + const originalStartColumn = change.originalStart + 1; + const isInsertion = change.originalLength === 0 && change.modifiedLength > 0; + let tokenInfo: { type: StandardTokenType; range: Range }; + // Word info is left aligned whereas token info is right aligned for insertions. + // We prefer a suffix insertion for renames so we take the word range for the token info. + if (isInsertion && originalStartColumn === wordRange.endColumn && wordRange.endColumn > wordRange.startColumn) { + tokenInfo = this.getTokenAtPosition(textModel, new Position(startPos.lineNumber, wordRange.startColumn)); + } else { + tokenInfo = this.getTokenAtPosition(textModel, startPos); + } + if (wordRange.startColumn !== tokenInfo.range.startColumn || wordRange.endColumn !== tokenInfo.range.endColumn) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + if (tokenInfo.type === StandardTokenType.Other) { + + let identifier = textModel.getValueInRange(tokenInfo.range); + if (identifier.length === 0) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + if (oldName === undefined) { + oldName = identifier; + } else if (oldName !== identifier) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + + // We assume that the new name starts at the same position as the old name from a token range perspective. + const tokenStartPos = textModel.getOffsetAt(tokenInfo.range.getStartPosition()) - nesOffset + tokenDiff; + const tokenEndPos = textModel.getOffsetAt(tokenInfo.range.getEndPosition()) - nesOffset + tokenDiff; + identifier = modifiedText.substring(tokenStartPos, tokenEndPos + diff); + if (identifier.length === 0) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + if (newName === undefined) { + newName = identifier; + } else if (newName !== identifier) { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += diff; + continue; + } + + if (position === undefined) { + position = tokenInfo.range.getStartPosition(); + } + + if (oldName !== undefined && newName !== undefined && oldName.length > 0 && newName.length > 0 && oldName !== newName) { + renames.push(new TextReplacement(tokenInfo.range, newName)); + } else { + renames.push(new TextReplacement(range, insertedTextSegment)); + } + tokenDiff += diff; + } else { + others.push(new TextReplacement(range, insertedTextSegment)); + tokenDiff += insertedTextSegment.length - change.originalLength; + } + } + + if (oldName === undefined || newName === undefined || position === undefined || oldName.length === 0 || newName.length === 0 || oldName === newName) { + return undefined; + } + + wordDefinition.lastIndex = 0; + let match = wordDefinition.exec(oldName); + if (match === null || match.index !== 0 || match[0].length !== oldName.length) { + return undefined; + } + + wordDefinition.lastIndex = 0; + match = wordDefinition.exec(newName); + if (match === null || match.index !== 0 || match[0].length !== newName.length) { + return undefined; + } + + return { + renames: { edits: renames, position, oldName, newName }, + others: { edits: others } + }; + } + + + protected getTokenAtPosition(textModel: ITextModel, position: Position): { type: StandardTokenType; range: Range } { + textModel.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = textModel.tokenization.getLineTokens(position.lineNumber); + const idx = tokens.findTokenIndexAtOffset(position.column - 1); + return { + type: tokens.getStandardTokenType(idx), + range: new Range(position.lineNumber, 1 + tokens.getStartOffset(idx), position.lineNumber, 1 + tokens.getEndOffset(idx)) + }; + } +} + +class EditorState { + + public static create(codeEditorService: ICodeEditorService, textModel: ITextModel): EditorState | undefined { + const editor = codeEditorService.getFocusedCodeEditor(); + if (editor === null) { + return undefined; + } + + if (editor.getModel() !== textModel) { + return undefined; + } + + return new EditorState(editor, textModel.getVersionId()); + } + + private constructor( + private readonly editor: ICodeEditor, + private readonly versionId: number, + ) { } + + public equals(other: EditorState | undefined): boolean { + if (other === undefined) { + return false; + } + return this.editor === other.editor && this.versionId === other.versionId; + } +} + +class RenameSymbolRunnable { + + private readonly _commandService: ICommandService; + private readonly _requestUuid: string; + private readonly _textModel: ITextModel; + private readonly _state: EditorState; + private readonly _cancellationTokenSource: CancellationTokenSource; + private readonly _promise: Promise; + private _result: WorkspaceEdit & Rejection | undefined = undefined; + + constructor(languageFeaturesService: ILanguageFeaturesService, commandService: ICommandService, requestUuid: string, textModel: ITextModel, state: EditorState, position: Position, newName: string, lastSymbolRename: IRange | undefined, oldName: string | undefined) { + this._commandService = commandService; + this._textModel = textModel; + this._state = state; + this._requestUuid = requestUuid; + this._cancellationTokenSource = new CancellationTokenSource(); + if (lastSymbolRename === undefined || oldName === undefined) { + this._promise = rawRename(languageFeaturesService.renameProvider, textModel, position, newName, this._cancellationTokenSource.token); + return; + } else { + this._promise = this.sendNesRenameRequest(textModel, position, oldName, newName, lastSymbolRename); + } + } + + public get requestUuid(): string { + return this._requestUuid; + } + + public isValid(codeEditorService: ICodeEditorService): boolean { + return this._state.equals(EditorState.create(codeEditorService, this._textModel)); + } + + public cancel(): void { + this._cancellationTokenSource.cancel(); + } + + public async getCount(): Promise { + if (this._cancellationTokenSource.token.isCancellationRequested) { + return 0; + } + const result = await this.getResult(); + if (result === undefined || this._cancellationTokenSource.token.isCancellationRequested) { + return 0; + } + + return result.edits.length; + } + + public async getWorkspaceEdit(): Promise { + return this.getResult(); + } + + private async getResult(): Promise { + if (this._cancellationTokenSource.token.isCancellationRequested) { + return undefined; + } + if (this._result === undefined) { + this._result = await this._promise; + } + if (this._result.rejectReason || this._cancellationTokenSource.token.isCancellationRequested) { + return undefined; + } + return this._result; + } + + private async sendNesRenameRequest(textModel: ITextModel, position: Position, oldName: string, newName: string, lastSymbolRename: IRange | undefined): Promise { + try { + const result = await this._commandService.executeCommand('github.copilot.nes.postRename', textModel.uri, position, oldName, newName, lastSymbolRename); + if (result === undefined) { + return { rejectReason: 'Rename failed', edits: [] }; + } + const edits: ResourceTextEdit[] = []; + for (const item of result) { + for (const change of item.changes) { + const range = new Range(change.range.start.line + 1, change.range.start.character + 1, change.range.end.line + 1, change.range.end.character + 1); + const edit = new ResourceTextEdit(item.file, new TextReplacement(range, change.newText ?? newName)); + edits.push(edit); + } + } + return { edits }; + } catch (error) { + return { rejectReason: 'Rename failed', edits: [] }; + } + } +} + +export class RenameSymbolProcessor extends Disposable { + + private readonly _renameInferenceEngine = new RenameInferenceEngine(); + + private _renameRunnable: RenameSymbolRunnable | undefined = undefined; + + constructor( + @ICommandService private readonly _commandService: ICommandService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, + @IBulkEditService bulkEditService: IBulkEditService, + @IRenameSymbolTrackerService private readonly _renameSymbolTrackerService: IRenameSymbolTrackerService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + ) { + super(); + this._register(CommandsRegistry.registerCommand(renameSymbolCommandId, async (_: ServicesAccessor, source: TextModelEditSource, renameRunnable: RenameSymbolRunnable | undefined) => { + if (renameRunnable === undefined || !renameRunnable.isValid(this._codeEditorService)) { + return; + } + + try { + const workspaceEdit = await renameRunnable.getWorkspaceEdit(); + if (workspaceEdit === undefined) { + return; + } + bulkEditService.apply(workspaceEdit, { reason: source }); + } finally { + if (this._renameRunnable === renameRunnable) { + this._renameRunnable = undefined; + } + } + })); + } + + public async proposeRenameRefactoring(textModel: ITextModel, suggestItem: InlineSuggestionItem, context: InlineCompletionContextWithoutUuid): Promise { + if (!suggestItem.supportsRename || suggestItem.action?.kind !== 'edit' || context.selectedSuggestionInfo) { + return suggestItem; + } + + if (!hasProvider(this._languageFeaturesService.renameProvider, textModel)) { + return suggestItem; + } + + const state = EditorState.create(this._codeEditorService, textModel); + if (state === undefined) { + return suggestItem; + } + + const start = Date.now(); + const edit = suggestItem.action.textReplacement; + const languageConfiguration = this._languageConfigurationService.getLanguageConfiguration(textModel.getLanguageId()); + + // Check synchronously if a rename is possible + const edits = this._renameInferenceEngine.inferRename(textModel, edit.range, edit.text, languageConfiguration.wordDefinition); + if (edits === undefined || edits.renames.edits.length === 0) { + return suggestItem; + } + + const { oldName, newName, position, edits: renameEdits } = edits.renames; + + const trackedWord = this._renameSymbolTrackerService.trackedWord.get(); + let lastSymbolRename: IRange | undefined = undefined; + if (trackedWord !== undefined && trackedWord.model === textModel && trackedWord.originalWord === oldName && trackedWord.currentWord === newName) { + lastSymbolRename = trackedWord.currentRange; + } + + // Check asynchronously if a rename is possible + let timedOut = false; + const check = await raceTimeout(this.checkRenamePrecondition(suggestItem, textModel, position, oldName, newName, lastSymbolRename), 100, () => { timedOut = true; }); + const renamePossible = this.isRenamePossible(suggestItem, check, state, textModel); + + suggestItem.setRenameProcessingInfo({ + createdRename: renamePossible, + duration: Date.now() - start, + timedOut, + droppedOtherEdits: renamePossible ? edits.others.edits.length : undefined, + droppedRenameEdits: renamePossible ? renameEdits.length - 1 : undefined, + }); + + if (!renamePossible) { + return suggestItem; + } + + // Prepare the rename edits + if (this._renameRunnable === undefined) { + this._renameRunnable = new RenameSymbolRunnable(this._languageFeaturesService, this._commandService, suggestItem.requestUuid, textModel, state, position, newName, lastSymbolRename, lastSymbolRename !== undefined ? oldName : undefined); + } + + // Create alternative action + const source = EditSources.inlineCompletionAccept({ + nes: suggestItem.isInlineEdit, + requestUuid: suggestItem.requestUuid, + providerId: suggestItem.source.provider.providerId, + languageId: textModel.getLanguageId(), + correlationId: suggestItem.getSourceCompletion().correlationId, + }); + const command: Command = { + id: renameSymbolCommandId, + title: localize('rename', "Rename"), + arguments: [source, this._renameRunnable], + }; + const alternativeAction: InlineSuggestAlternativeAction = { + label: localize('rename', "Rename"), + icon: Codicon.replaceAll, + command, + count: this._renameRunnable.getCount(), + }; + const renameAction: IInlineSuggestDataActionEdit = { + kind: 'edit', + range: renameEdits[0].range, + insertText: renameEdits[0].text, + snippetInfo: suggestItem.snippetInfo, + alternativeAction, + uri: textModel.uri + }; + + const ref = TextModelValueReference.snapshot(textModel); + return InlineSuggestionItem.create(suggestItem.withAction(renameAction), ref, false); + } + + private async checkRenamePrecondition(suggestItem: InlineSuggestionItem, textModel: ITextModel, position: Position, oldName: string, newName: string, lastSymbolRename: IRange | undefined): Promise { + const no: PrepareNesRenameResult.No = { canRename: RenameKind.no, timedOut: false }; + try { + const result = await this._commandService.executeCommand('github.copilot.nes.prepareRename', textModel.uri, position, oldName, newName, suggestItem.requestUuid, lastSymbolRename); + if (result === undefined) { + return no; + } else if (typeof result === 'string') { + const canRename = RenameKind.fromString(result); + if (canRename === RenameKind.yes || canRename === RenameKind.maybe) { + return { + canRename, + oldName, + onOldState: false, + }; + } else { + return { + canRename, + timedOut: false, + }; + } + } else { + return result; + } + } catch (error) { + return no; + } + } + + private isRenamePossible(suggestItem: InlineSuggestionItem, check: PrepareNesRenameResult | undefined, state: EditorState, textModel: ITextModel): boolean { + if (check === undefined || check.canRename === RenameKind.no) { + return false; + } + if (!state.equals(EditorState.create(this._codeEditorService, textModel))) { + return false; + } + if (this._renameRunnable === undefined) { + return true; + } + if (this._renameRunnable.requestUuid === suggestItem.requestUuid) { + return false; + } else { + this._renameRunnable.cancel(); + this._renameRunnable = undefined; + return true; + } + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference.ts new file mode 100644 index 00000000000..1a973830d85 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/textModelValueReference.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from '../../../../../base/common/errors.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Position } from '../../../../common/core/position.js'; +import { Range } from '../../../../common/core/range.js'; +import { AbstractText } from '../../../../common/core/text/abstractText.js'; +import { TextLength } from '../../../../common/core/text/textLength.js'; +import { ITextModel } from '../../../../common/model.js'; + +/** + * An immutable view of a text model at a specific version. + * Like TextModelText but throws if the underlying model has changed. + * This ensures data read from the reference is consistent with + * the version at construction time. + */ +export class TextModelValueReference extends AbstractText { + private readonly _version: number; + + static snapshot(textModel: ITextModel): TextModelValueReference { + return new TextModelValueReference(textModel); + } + + private constructor(private readonly _textModel: ITextModel) { + super(); + this._version = _textModel.getVersionId(); + } + + get uri(): URI { + return this._textModel.uri; + } + + get version(): number { + return this._version; + } + + private _assertValid(): void { + if (this._textModel.getVersionId() !== this._version) { + onUnexpectedError(new Error(`TextModel has changed: expected version ${this._version}, got ${this._textModel.getVersionId()}`)); + // TODO: throw here! + } + } + + targets(textModel: ITextModel): boolean { + return this._textModel.uri.toString() === textModel.uri.toString(); + } + + override getValueOfRange(range: Range): string { + this._assertValid(); + return this._textModel.getValueInRange(range); + } + + override getLineLength(lineNumber: number): number { + this._assertValid(); + return this._textModel.getLineLength(lineNumber); + } + + get length(): TextLength { + this._assertValid(); + const lastLineNumber = this._textModel.getLineCount(); + const lastLineLen = this._textModel.getLineLength(lastLineNumber); + return new TextLength(lastLineNumber - 1, lastLineLen); + } + + getEOL(): string { + this._assertValid(); + return this._textModel.getEOL(); + } + + getPositionAt(offset: number): Position { + this._assertValid(); + return this._textModel.getPositionAt(offset); + } + + getValueInRange(range: Range): string { + this._assertValid(); + return this._textModel.getValueInRange(range); + } + + getVersionId(): number { + return this._version; + } + + dangerouslyGetUnderlyingModel(): ITextModel { + return this._textModel; + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts index b8f1ca93c6c..b486803836d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts @@ -19,6 +19,8 @@ export type InlineCompletionEndOfLifeEvent = { typingIntervalCharacterCount: number; selectedSuggestionInfo: boolean; availableProviders: string; + skuPlan: string | undefined; + skuType: string | undefined; // response correlationId: string | undefined; extensionId: string; @@ -32,6 +34,7 @@ export type InlineCompletionEndOfLifeEvent = { timeUntilProviderRequest: number | undefined; timeUntilProviderResponse: number | undefined; reason: 'accepted' | 'rejected' | 'ignored' | undefined; + acceptedAlternativeAction: boolean | undefined; partiallyAccepted: number | undefined; partiallyAcceptedCountSinceOriginal: number | undefined; partiallyAcceptedRatioSinceOriginal: number | undefined; @@ -39,6 +42,12 @@ export type InlineCompletionEndOfLifeEvent = { preceeded: boolean | undefined; superseded: boolean | undefined; notShownReason: string | undefined; + renameCreated: boolean | undefined; + renameDuration: number | undefined; + renameTimedOut: boolean | undefined; + renameDroppedOtherEdits: number | undefined; + renameDroppedRenameEdits: number | undefined; + performanceMarkers: string | undefined; // rendering viewKind: string | undefined; cursorColumnDistance: number | undefined; @@ -49,8 +58,12 @@ export type InlineCompletionEndOfLifeEvent = { characterCountModified: number | undefined; disjointReplacements: number | undefined; sameShapeReplacements: boolean | undefined; + longDistanceHintVisible: boolean | undefined; + longDistanceHintDistance: number | undefined; // empty noSuggestionReason: string | undefined; + // shape + editKind: string | undefined; }; type InlineCompletionsEndOfLifeClassification = { @@ -62,6 +75,8 @@ type InlineCompletionsEndOfLifeClassification = { extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension that contributed the inline completion' }; groupId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The group ID of the extension that contributed the inline completion' }; availableProviders: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The list of available inline completion providers at the time of the request' }; + skuPlan: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The the plan the user is subscribed to' }; + skuType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The sku type of the user' }; shown: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the inline completion was shown to the user' }; shownDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration for which the inline completion was shown' }; shownDurationUncollapsed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration for which the inline completion was shown without collapsing' }; @@ -69,6 +84,7 @@ type InlineCompletionsEndOfLifeClassification = { timeUntilProviderRequest: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time it took for the inline completion to be requested from the provider' }; timeUntilProviderResponse: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The time it took for the inline completion to be shown after the request' }; reason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the inline completion ending' }; + acceptedAlternativeAction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user performed an alternative action when accepting the inline completion' }; selectedSuggestionInfo: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the inline completion was requested with a selected suggestion' }; partiallyAccepted: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often the inline completion was partially accepted by the user' }; partiallyAcceptedCountSinceOriginal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often the inline completion was partially accepted since the original request' }; @@ -79,6 +95,11 @@ type InlineCompletionsEndOfLifeClassification = { requestReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the inline completion request' }; typingInterval: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The average typing interval of the user at the moment the inline completion was requested' }; typingIntervalCharacterCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The character count involved in the typing interval calculation' }; + renameCreated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a rename operation was created' }; + renameDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration of the rename processor' }; + renameTimedOut: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the rename prepare operation timed out' }; + renameDroppedOtherEdits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of non-rename edits dropped due to rename processing' }; + renameDroppedRenameEdits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of rename edits dropped due to rename processing' }; superseded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the inline completion was superseded by another one' }; editorType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the editor where the inline completion was shown' }; viewKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of the view where the inline completion was shown' }; @@ -90,6 +111,10 @@ type InlineCompletionsEndOfLifeClassification = { characterCountModified: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of characters in the modified text' }; disjointReplacements: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of inner replacements made by the inline completion' }; sameShapeReplacements: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether all inner replacements are the same shape' }; + longDistanceHintVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a long distance hint was rendered' }; + longDistanceHintDistance: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The distance in lines between the long distance hint and the inline edit' }; noSuggestionReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason why no inline completion was provided' }; notShownReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason why the inline completion was not shown' }; + performanceMarkers: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Performance markers for the inline completion lifecycle' }; + editKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of edit made by the inline completion' }; }; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 44694055aa9..d23250c245f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -108,3 +108,23 @@ export function wait(ms: number, cancellationToken?: CancellationToken): Promise } }); } + +export class ErrorResult { + public static message(message: string): ErrorResult { + return new ErrorResult(undefined, message); + } + + constructor(public readonly error: T, public readonly message: string | undefined = undefined) { } + + public static is(obj: TOther | ErrorResult): obj is ErrorResult { + return obj instanceof ErrorResult; + } + + public logError(): void { + if (this.message) { + console.error(`ErrorResult: ${this.message}`, this.error); + } else { + console.error(`ErrorResult: An unexpected error-case occurred, usually caused by invalid input.`, this.error); + } + } +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css index 16148bd87b0..e46dca6d726 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.css @@ -64,6 +64,13 @@ border-bottom: 4px double var(--vscode-editorWarning-border); } +.monaco-editor .ghost-text-decoration.short-text, +.monaco-editor .ghost-text-decoration-preview.short-text, +.monaco-editor .suggest-preview-text .ghost-text.short-text { + text-decoration: underline dotted var(--vscode-editorGhostText-foreground); + text-underline-position: under; +} + .ghost-text-view-warning-widget-icon { .codicon { color: var(--vscode-editorWarning-foreground) !important; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts index 21235c7f9e0..96053b4e19a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/ghostText/ghostTextView.ts @@ -7,9 +7,8 @@ import { createTrustedTypesPolicy } from '../../../../../../base/browser/trusted import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, constObservable, derived, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +import { IObservable, autorun, autorunWithStore, constObservable, derived, derivedOpts, observableSignalFromEvent, observableValue } from '../../../../../../base/common/observable.js'; import * as strings from '../../../../../../base/common/strings.js'; import { applyFontInfo } from '../../../../../browser/config/domFontInfo.js'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidgetPosition, IViewZoneChangeAccessor, MouseTargetType } from '../../../../../browser/editorBrowser.js'; @@ -19,7 +18,7 @@ import { StringEdit, StringReplacement } from '../../../../../common/core/edits/ import { Position } from '../../../../../common/core/position.js'; import { Range } from '../../../../../common/core/range.js'; import { StringBuilder } from '../../../../../common/core/stringBuilder.js'; -import { IconPath } from '../../../../../common/languages.js'; +import { IconPath, InlineCompletionWarning } from '../../../../../common/languages.js'; import { ILanguageService } from '../../../../../common/languages/language.js'; import { IModelDeltaDecoration, ITextModel, InjectedTextCursorStops, PositionAffinity } from '../../../../../common/model.js'; import { LineTokens } from '../../../../../common/tokens/lineTokens.js'; @@ -35,179 +34,86 @@ import { CodeEditorWidget } from '../../../../../browser/widget/codeEditor/codeE import { TokenWithTextArray } from '../../../../../common/tokens/tokenWithTextArray.js'; import { InlineCompletionViewData } from '../inlineEdits/inlineEditsViewInterface.js'; import { InlineDecorationType } from '../../../../../common/viewModel/inlineDecorations.js'; -import { sum } from '../../../../../../base/common/arrays.js'; +import { equals, sum } from '../../../../../../base/common/arrays.js'; +import { equalsIfDefinedC, IEquatable, thisEqualsC } from '../../../../../../base/common/equals.js'; -export interface IGhostTextWidgetModel { - readonly targetTextModel: IObservable; - readonly ghostText: IObservable; - readonly warning: IObservable<{ icon: IconPath | undefined } | undefined>; - readonly minReservedLineCount: IObservable; +export interface IGhostTextWidgetData { + readonly ghostText: GhostText | GhostTextReplacement; + readonly warning: GhostTextWidgetWarning | undefined; + handleInlineCompletionShown(viewData: InlineCompletionViewData): void; +} - readonly handleInlineCompletionShown: IObservable<(viewData: InlineCompletionViewData) => void>; +export class GhostTextWidgetWarning { + public static from(warning: InlineCompletionWarning | undefined): GhostTextWidgetWarning | undefined { + if (!warning) { + return undefined; + } + return new GhostTextWidgetWarning(warning.icon); + } + + constructor( + public readonly icon: IconPath = Codicon.warning, + ) { } } const USE_SQUIGGLES_FOR_WARNING = true; const GHOST_TEXT_CLASS_NAME = 'ghost-text'; export class GhostTextView extends Disposable { - private readonly _isDisposed; + private readonly _isDisposed = observableValue(this, false); private readonly _editorObs; - public static hot = createHotClass(this); - - private _warningState; - - private readonly _onDidClick; - public readonly onDidClick; + private readonly _warningState = derived(reader => { + const model = this._data.read(reader); + const warning = model?.warning; + if (!model || !warning) { return undefined; } + const gt = model.ghostText; + return { lineNumber: gt.lineNumber, position: new Position(gt.lineNumber, gt.parts[0].column), icon: warning.icon }; + }); + + private readonly _onDidClick = this._register(new Emitter()); + public readonly onDidClick = this._onDidClick.event; + + private readonly _extraClasses: readonly string[]; + private readonly _isClickable: boolean; + private readonly _shouldKeepCursorStable: boolean; + private readonly _minReservedLineCount: IObservable; + private readonly _useSyntaxHighlighting: IObservable; + private readonly _highlightShortText: boolean; constructor( private readonly _editor: ICodeEditor, - private readonly _model: IGhostTextWidgetModel, - private readonly _options: IObservable<{ - extraClasses?: string[]; - syntaxHighlightingEnabled: boolean; - }>, - private readonly _shouldKeepCursorStable: boolean, - private readonly _isClickable: boolean, - @ILanguageService private readonly _languageService: ILanguageService, + private readonly _data: IObservable, + options: { + extraClasses?: readonly string[]; // TODO@benibenj improve + isClickable?: boolean; + shouldKeepCursorStable?: boolean; + minReservedLineCount?: IObservable; + useSyntaxHighlighting?: IObservable; + highlightShortSuggestions?: boolean; + }, + @ILanguageService private readonly _languageService: ILanguageService ) { super(); - this._isDisposed = observableValue(this, false); - this._editorObs = observableCodeEditor(this._editor); - this._warningState = derived(reader => { - const gt = this._model.ghostText.read(reader); - if (!gt) { return undefined; } - const warning = this._model.warning.read(reader); - if (!warning) { return undefined; } - return { lineNumber: gt.lineNumber, position: new Position(gt.lineNumber, gt.parts[0].column), icon: warning.icon }; - }); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; - this._useSyntaxHighlighting = this._options.map(o => o.syntaxHighlightingEnabled); - this._extraClassNames = derived(this, reader => { - const extraClasses = [...this._options.read(reader).extraClasses ?? []]; - if (this._useSyntaxHighlighting.read(reader)) { - extraClasses.push('syntax-highlighted'); - } - if (USE_SQUIGGLES_FOR_WARNING && this._warningState.read(reader)) { - extraClasses.push('warning'); - } - const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); - return extraClassNames; - }); - this.uiState = derived(this, reader => { - if (this._isDisposed.read(reader)) { return undefined; } - const textModel = this._editorObs.model.read(reader); - if (textModel !== this._model.targetTextModel.read(reader)) { return undefined; } - const ghostText = this._model.ghostText.read(reader); - if (!ghostText) { return undefined; } - - const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; - - const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); - const extraClassNames = this._extraClassNames.read(reader); - const { inlineTexts, additionalLines, hiddenRange, additionalLinesOriginalSuffix } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); - - const currentLine = textModel.getLineContent(ghostText.lineNumber); - const edit = new StringEdit(inlineTexts.map(t => StringReplacement.insert(t.column - 1, t.text))); - const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLinesAt(ghostText.lineNumber, [edit.apply(currentLine), ...additionalLines.map(l => l.content)]) : undefined; - const newRanges = edit.getNewRanges(); - const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); - - const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => { - let content = tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec); - if (idx === additionalLines.length - 1 && additionalLinesOriginalSuffix) { - const t = TokenWithTextArray.fromLineTokens(textModel.tokenization.getLineTokens(additionalLinesOriginalSuffix.lineNumber)); - const existingContent = t.slice(additionalLinesOriginalSuffix.columnRange.toZeroBasedOffsetRange()); - content = TokenWithTextArray.fromLineTokens(content).append(existingContent).toLineTokens(content.languageIdCodec); - } - return { - content, - decorations: l.decorations, - }; - }); - - const cursorColumn = this._editor.getSelection()?.getStartPosition().column!; - const disjointInlineTexts = inlineTextsWithTokens.filter(inline => inline.text !== ''); - const hasInsertionOnCurrentLine = disjointInlineTexts.length !== 0; - const renderData: InlineCompletionViewData = { - cursorColumnDistance: (hasInsertionOnCurrentLine ? disjointInlineTexts[0].column : 1) - cursorColumn, - cursorLineDistance: hasInsertionOnCurrentLine ? 0 : (additionalLines.findIndex(line => line.content !== '') + 1), - lineCountOriginal: hasInsertionOnCurrentLine ? 1 : 0, - lineCountModified: additionalLines.length + (hasInsertionOnCurrentLine ? 1 : 0), - characterCountOriginal: 0, - characterCountModified: sum(disjointInlineTexts.map(inline => inline.text.length)) + sum(tokenizedAdditionalLines.map(line => line.content.getTextLength())), - disjointReplacements: disjointInlineTexts.length + (additionalLines.length > 0 ? 1 : 0), - sameShapeReplacements: disjointInlineTexts.length > 1 && tokenizedAdditionalLines.length === 0 ? disjointInlineTexts.every(inline => inline.text === disjointInlineTexts[0].text) : undefined, - }; - this._model.handleInlineCompletionShown.read(reader)?.(renderData); - - return { - replacedRange, - inlineTexts: inlineTextsWithTokens, - additionalLines: tokenizedAdditionalLines, - hiddenRange, - lineNumber: ghostText.lineNumber, - additionalReservedLineCount: this._model.minReservedLineCount.read(reader), - targetTextModel: textModel, - syntaxHighlightingEnabled, - }; - }); - this.decorations = derived(this, reader => { - const uiState = this.uiState.read(reader); - if (!uiState) { return []; } - - const decorations: IModelDeltaDecoration[] = []; - - const extraClassNames = this._extraClassNames.read(reader); - - if (uiState.replacedRange) { - decorations.push({ - range: uiState.replacedRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } - }); - } - - if (uiState.hiddenRange) { - decorations.push({ - range: uiState.hiddenRange.toRange(uiState.lineNumber), - options: { inlineClassName: 'ghost-text-hidden', description: 'ghost-text-hidden', } - }); - } - for (const p of uiState.inlineTexts) { - decorations.push({ - range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), - options: { - description: 'ghost-text-decoration', - after: { - content: p.text, - tokens: p.tokens, - inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') - + (this._isClickable ? ' clickable' : '') - + extraClassNames - + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations - cursorStops: InjectedTextCursorStops.Left, - attachedData: new GhostTextAttachedData(this), - }, - showIfCollapsed: true, - } - }); - } + this._extraClasses = options.extraClasses ?? []; + this._isClickable = options.isClickable ?? false; + this._shouldKeepCursorStable = options.shouldKeepCursorStable ?? false; + this._minReservedLineCount = options.minReservedLineCount ?? constObservable(0); + this._useSyntaxHighlighting = options.useSyntaxHighlighting ?? constObservable(true); + this._highlightShortText = options.highlightShortSuggestions ?? false; - return decorations; - }); + this._editorObs = observableCodeEditor(this._editor); this._additionalLinesWidget = this._register( new AdditionalLinesWidget( this._editor, - derived(reader => { + derivedOpts({ owner: this, equalsFn: equalsIfDefinedC(thisEqualsC()) }, reader => { /** @description lines */ - const uiState = this.uiState.read(reader); - return uiState ? { - lineNumber: uiState.lineNumber, - additionalLines: uiState.additionalLines, - minReservedLineCount: uiState.additionalReservedLineCount, - targetTextModel: uiState.targetTextModel, - } : undefined; + const uiState = this._state.read(reader); + return uiState ? new AdditionalLinesData( + uiState.lineNumber, + uiState.additionalLines, + uiState.additionalReservedLineCount, + ) : undefined; }), this._shouldKeepCursorStable, this._isClickable @@ -219,17 +125,9 @@ export class GhostTextView extends Disposable { p.target.detail.injectedText.options.attachedData.owner === this, this._store ); - this.isHovered = derived(this, reader => { - if (this._isDisposed.read(reader)) { return false; } - return this._isInlineTextHovered.read(reader) || this._additionalLinesWidget.isHovered.read(reader); - }); - this.height = derived(this, reader => { - const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); - return lineHeight + (this._additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); - }); this._register(toDisposable(() => { this._isDisposed.set(true, undefined); })); - this._register(this._editorObs.setDecorations(this.decorations)); + this._register(this._editorObs.setDecorations(this._decorations)); if (this._isClickable) { this._register(this._additionalLinesWidget.onDidClick((e) => this._onDidClick.fire(e))); @@ -244,6 +142,11 @@ export class GhostTextView extends Disposable { })); } + this._register(autorun(reader => { + const state = this._state.read(reader); + state?.handleInlineCompletionShown(state.telemetryViewData); + })); + this._register(autorunWithStore((reader, store) => { if (USE_SQUIGGLES_FOR_WARNING) { return; @@ -284,9 +187,9 @@ export class GhostTextView extends Disposable { alignContent: 'center', alignItems: 'center', } - }, [ - renderIcon((state.icon && 'id' in state.icon) ? state.icon : Codicon.warning), - ]) + }, + [renderIcon(state.icon)] + ) ]).keepUpdated(store).element, })); })); @@ -303,21 +206,155 @@ export class GhostTextView extends Disposable { return undefined; } - private readonly _useSyntaxHighlighting; + private readonly _nonWhitespaceCount = derived(this, reader => { + const data = this._data.read(reader); + if (!data) { return undefined; } + const ghostText = data.ghostText; + const allText = ghostText.parts.map(p => p.lines.map(l => l.line).join('')).join(''); + return allText.replace(/\s/g, '').length; + }); + + private readonly _extraClassNames = derived(this, reader => { + const extraClasses = this._extraClasses.slice(); + if (USE_SQUIGGLES_FOR_WARNING && this._warningState.read(reader)) { + extraClasses.push('warning'); + } + const nonWhitespaceCount = this._nonWhitespaceCount.read(reader); + if (this._highlightShortText && nonWhitespaceCount && nonWhitespaceCount < 3) { + extraClasses.push('short-text'); + } else if (this._useSyntaxHighlighting.read(reader)) { + extraClasses.push('syntax-highlighted'); + } + const extraClassNames = extraClasses.map(c => ` ${c}`).join(''); + return extraClassNames; + }); + + private readonly _state = derived(this, reader => { + if (this._isDisposed.read(reader)) { return undefined; } + + const props = this._data.read(reader); + if (!props) { return undefined; } + + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return undefined; } + + const ghostText = props.ghostText; + const replacedRange = ghostText instanceof GhostTextReplacement ? ghostText.columnRange : undefined; + + const syntaxHighlightingEnabled = this._useSyntaxHighlighting.read(reader); + const extraClassNames = this._extraClassNames.read(reader); + const { inlineTexts, additionalLines, hiddenRange, additionalLinesOriginalSuffix } = computeGhostTextViewData(ghostText, textModel, GHOST_TEXT_CLASS_NAME + extraClassNames); + + const currentLine = textModel.getLineContent(ghostText.lineNumber); + const edit = new StringEdit(inlineTexts.map(t => StringReplacement.insert(t.column - 1, t.text))); + const tokens = syntaxHighlightingEnabled ? textModel.tokenization.tokenizeLinesAt(ghostText.lineNumber, [edit.apply(currentLine), ...additionalLines.map(l => l.content)]) : undefined; + const newRanges = edit.getNewRanges(); + const inlineTextsWithTokens = inlineTexts.map((t, idx) => ({ ...t, tokens: tokens?.[0]?.getTokensInRange(newRanges[idx]) })); + + const tokenizedAdditionalLines: LineData[] = additionalLines.map((l, idx) => { + let content = tokens?.[idx + 1] ?? LineTokens.createEmpty(l.content, this._languageService.languageIdCodec); + if (idx === additionalLines.length - 1 && additionalLinesOriginalSuffix) { + const t = TokenWithTextArray.fromLineTokens(textModel.tokenization.getLineTokens(additionalLinesOriginalSuffix.lineNumber)); + const existingContent = t.slice(additionalLinesOriginalSuffix.columnRange.toZeroBasedOffsetRange()); + content = TokenWithTextArray.fromLineTokens(content).append(existingContent).toLineTokens(content.languageIdCodec); + } + return new LineData( + content, + l.decorations, + ); + }); + + const cursorColumn = this._editor.getSelection()?.getStartPosition().column!; + const disjointInlineTexts = inlineTextsWithTokens.filter(inline => inline.text !== ''); + const hasInsertionOnCurrentLine = disjointInlineTexts.length !== 0; + const telemetryViewData = new InlineCompletionViewData( + (hasInsertionOnCurrentLine ? disjointInlineTexts[0].column : 1) - cursorColumn, + hasInsertionOnCurrentLine ? 0 : (additionalLines.findIndex(line => line.content !== '') + 1), + hasInsertionOnCurrentLine ? 1 : 0, + additionalLines.length + (hasInsertionOnCurrentLine ? 1 : 0), + 0, + sum(disjointInlineTexts.map(inline => inline.text.length)) + sum(tokenizedAdditionalLines.map(line => line.content.getTextLength())), + disjointInlineTexts.length + (additionalLines.length > 0 ? 1 : 0), + disjointInlineTexts.length > 1 && tokenizedAdditionalLines.length === 0 ? disjointInlineTexts.every(inline => inline.text === disjointInlineTexts[0].text) : undefined + ); + + return { + replacedRange, + inlineTexts: inlineTextsWithTokens, + additionalLines: tokenizedAdditionalLines, + hiddenRange, + lineNumber: ghostText.lineNumber, + additionalReservedLineCount: this._minReservedLineCount.read(reader), + targetTextModel: textModel, + syntaxHighlightingEnabled, + telemetryViewData, + handleInlineCompletionShown: props.handleInlineCompletionShown, + }; + }); + + private readonly _decorations = derived(this, reader => { + const uiState = this._state.read(reader); + if (!uiState) { return []; } + + const decorations: IModelDeltaDecoration[] = []; + + const extraClassNames = this._extraClassNames.read(reader); + + if (uiState.replacedRange) { + decorations.push({ + range: uiState.replacedRange.toRange(uiState.lineNumber), + options: { inlineClassName: 'inline-completion-text-to-replace' + extraClassNames, description: 'GhostTextReplacement' } + }); + } - private readonly _extraClassNames; + if (uiState.hiddenRange) { + decorations.push({ + range: uiState.hiddenRange.toRange(uiState.lineNumber), + options: { inlineClassName: 'ghost-text-hidden', description: 'ghost-text-hidden', } + }); + } - private readonly uiState; + for (const p of uiState.inlineTexts) { + let inlineExtraClassNames = ''; + if (this._highlightShortText && p.text.length < 5) { + inlineExtraClassNames += ' short-text'; + } + decorations.push({ + range: Range.fromPositions(new Position(uiState.lineNumber, p.column)), + options: { + description: 'ghost-text-decoration', + after: { + content: p.text, + tokens: p.tokens, + inlineClassName: (p.preview ? 'ghost-text-decoration-preview' : 'ghost-text-decoration') + + (this._isClickable ? ' clickable' : '') + + extraClassNames + + inlineExtraClassNames + + p.lineDecorations.map(d => ' ' + d.className).join(' '), // TODO: take the ranges into account for line decorations + cursorStops: InjectedTextCursorStops.Left, + attachedData: new GhostTextAttachedData(this), + }, + showIfCollapsed: true, + } + }); + } - private readonly decorations; + return decorations; + }); private readonly _additionalLinesWidget; private readonly _isInlineTextHovered; - public readonly isHovered; + public readonly isHovered = derived(this, reader => { + if (this._isDisposed.read(reader)) { return false; } + return this._isInlineTextHovered.read(reader) || this._additionalLinesWidget.isHovered.read(reader); + }); - public readonly height; + public readonly height = derived(this, reader => { + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight).read(reader); + return lineHeight + (this._additionalLinesWidget.viewZoneHeight.read(reader) ?? 0); + }); public ownsViewZone(viewZoneId: string): boolean { return this._additionalLinesWidget.viewZoneId === viewZoneId; @@ -403,6 +440,24 @@ function computeGhostTextViewData(ghostText: GhostText | GhostTextReplacement, t }; } +class AdditionalLinesData implements IEquatable { + constructor( + public readonly lineNumber: number, + public readonly additionalLines: readonly LineData[], + public readonly minReservedLineCount: number, + ) { } + + equals(other: AdditionalLinesData): boolean { + if (this.lineNumber !== other.lineNumber) { + return false; + } + if (this.minReservedLineCount !== other.minReservedLineCount) { + return false; + } + return equals(this.additionalLines, other.additionalLines, thisEqualsC()); + } +} + export class AdditionalLinesWidget extends Disposable { private _viewZoneInfo: { viewZoneId: string; heightInLines: number; lineNumber: number } | undefined; public get viewZoneId(): string | undefined { return this._viewZoneInfo?.viewZoneId; } @@ -423,12 +478,7 @@ export class AdditionalLinesWidget extends Disposable { constructor( private readonly _editor: ICodeEditor, - private readonly _lines: IObservable<{ - targetTextModel: ITextModel; - lineNumber: number; - additionalLines: LineData[]; - minReservedLineCount: number; - } | undefined>, + private readonly _lines: IObservable, private readonly _shouldKeepCursorStable: boolean, private readonly _isClickable: boolean, ) { @@ -484,7 +534,7 @@ export class AdditionalLinesWidget extends Disposable { }); } - private updateLines(lineNumber: number, additionalLines: LineData[], minReservedLineCount: number): void { + private updateLines(lineNumber: number, additionalLines: readonly LineData[], minReservedLineCount: number): void { const textModel = this._editor.getModel(); if (!textModel) { return; @@ -492,31 +542,33 @@ export class AdditionalLinesWidget extends Disposable { const { tabSize } = textModel.getOptions(); - this._editor.changeViewZones((changeAccessor) => { - const store = new DisposableStore(); - - this.removeActiveViewZone(changeAccessor); + observableCodeEditor(this._editor).transaction(_ => { + this._editor.changeViewZones((changeAccessor) => { + const store = new DisposableStore(); + + this.removeActiveViewZone(changeAccessor); + + const heightInLines = Math.max(additionalLines.length, minReservedLineCount); + if (heightInLines > 0) { + const domNode = document.createElement('div'); + renderLines(domNode, tabSize, additionalLines, this._editor.getOptions(), this._isClickable); + + if (this._isClickable) { + store.add(addDisposableListener(domNode, 'mousedown', (e) => { + e.preventDefault(); // This prevents that the editor loses focus + })); + store.add(addDisposableListener(domNode, 'click', (e) => { + if (isTargetGhostText(e.target)) { + this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); + } + })); + } - const heightInLines = Math.max(additionalLines.length, minReservedLineCount); - if (heightInLines > 0) { - const domNode = document.createElement('div'); - renderLines(domNode, tabSize, additionalLines, this._editor.getOptions(), this._isClickable); - - if (this._isClickable) { - store.add(addDisposableListener(domNode, 'mousedown', (e) => { - e.preventDefault(); // This prevents that the editor loses focus - })); - store.add(addDisposableListener(domNode, 'click', (e) => { - if (isTargetGhostText(e.target)) { - this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); - } - })); + this.addViewZone(changeAccessor, lineNumber, heightInLines, domNode); } - this.addViewZone(changeAccessor, lineNumber, heightInLines, domNode); - } - - this._viewZoneListener.value = store; + this._viewZoneListener.value = store; + }); }); } @@ -565,12 +617,21 @@ function isTargetGhostText(target: EventTarget | null): boolean { return isHTMLElement(target) && target.classList.contains(GHOST_TEXT_CLASS_NAME); } -export interface LineData { - content: LineTokens; // Must not contain a linebreak! - decorations: LineDecoration[]; +export class LineData implements IEquatable { + constructor( + public readonly content: LineTokens, // Must not contain a linebreak! + public readonly decorations: readonly LineDecoration[] + ) { } + + equals(other: LineData): boolean { + if (!this.content.equals(other.content)) { + return false; + } + return LineDecoration.equalsArr(this.decorations, other.decorations); + } } -function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], opts: IComputedEditorOptions, isClickable: boolean): void { +function renderLines(domNode: HTMLElement, tabSize: number, lines: readonly LineData[], opts: IComputedEditorOptions, isClickable: boolean): void { const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations); const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter); // To avoid visual confusion, we don't want to render visible whitespace @@ -609,7 +670,7 @@ function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], o containsRTL, 0, lineTokens, - lineData.decorations, + lineData.decorations.slice(), tabSize, 0, fontInfo.spaceWidth, diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.ts deleted file mode 100644 index adc0faf9d00..00000000000 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineCompletionsView.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 { createStyleSheetFromObservable } from '../../../../../base/browser/domStylesheets.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { derived, mapObservableArrayCached, derivedDisposable, constObservable, derivedObservableWithCache, IObservable, ISettableObservable } from '../../../../../base/common/observable.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ICodeEditor } from '../../../../browser/editorBrowser.js'; -import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; -import { EditorOption } from '../../../../common/config/editorOptions.js'; -import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js'; -import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; -import { convertItemsToStableObservables } from '../utils.js'; -import { GhostTextView } from './ghostText/ghostTextView.js'; -import { InlineCompletionViewData, InlineCompletionViewKind } from './inlineEdits/inlineEditsViewInterface.js'; -import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js'; - -export class InlineCompletionsView extends Disposable { - private readonly _ghostTexts; - - private readonly _stablizedGhostTexts; - private readonly _editorObs; - - private readonly _ghostTextWidgets; - - private readonly _inlineEdit; - private readonly _everHadInlineEdit; - protected readonly _inlineEditWidget; - - private readonly _fontFamily; - - constructor( - private readonly _editor: ICodeEditor, - private readonly _model: IObservable, - private readonly _focusIsInMenu: ISettableObservable, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - ) { - super(); - this._ghostTexts = derived(this, (reader) => { - const model = this._model.read(reader); - return model?.ghostTexts.read(reader) ?? []; - }); - this._stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); - this._editorObs = observableCodeEditor(this._editor); - this._ghostTextWidgets = mapObservableArrayCached(this, this._stablizedGhostTexts, (ghostText, store) => derivedDisposable((reader) => this._instantiationService.createInstance( - GhostTextView.hot.read(reader), - this._editor, - { - ghostText: ghostText, - warning: this._model.map((m, reader) => { - const warning = m?.warning?.read(reader); - return warning ? { icon: warning.icon } : undefined; - }), - minReservedLineCount: constObservable(0), - targetTextModel: this._model.map(v => v?.textModel), - handleInlineCompletionShown: this._model.map((model, reader) => { - const inlineCompletion = model?.inlineCompletionState.read(reader)?.inlineCompletion; - if (inlineCompletion) { - return (viewData: InlineCompletionViewData) => model.handleInlineSuggestionShown(inlineCompletion, InlineCompletionViewKind.GhostText, viewData); - } - return () => { }; - }), - }, - this._editorObs.getOption(EditorOption.inlineSuggest).map(v => ({ syntaxHighlightingEnabled: v.syntaxHighlightingEnabled })), - false, - false - ) - ).recomputeInitiallyAndOnChange(store) - ).recomputeInitiallyAndOnChange(this._store); - this._inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineEdit); - this._everHadInlineEdit = derivedObservableWithCache(this, (reader, last) => last || !!this._inlineEdit.read(reader) || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineCompletion?.showInlineEditMenu); - this._inlineEditWidget = derivedDisposable(reader => { - if (!this._everHadInlineEdit.read(reader)) { - return undefined; - } - return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer.hot.read(reader), this._editor, this._inlineEdit, this._model, this._focusIsInMenu); - }) - .recomputeInitiallyAndOnChange(this._store); - this._fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); - - this._register(createStyleSheetFromObservable(derived(reader => { - const fontFamily = this._fontFamily.read(reader); - return ` -.monaco-editor .ghost-text-decoration, -.monaco-editor .ghost-text-decoration-preview, -.monaco-editor .ghost-text { - font-family: ${fontFamily}; -}`; - }))); - - this._register(new InlineCompletionsHintsWidget(this._editor, this._model, this._instantiationService)); - } - - public shouldShowHoverAtViewZone(viewZoneId: string): boolean { - return this._ghostTextWidgets.get()[0]?.get().ownsViewZone(viewZoneId) ?? false; - } -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts index 0e563f96455..52015f27148 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorMenu.ts @@ -26,18 +26,17 @@ import { defaultKeybindingLabelStyles } from '../../../../../../../platform/them import { asCssVariable, descriptionForeground, editorActionListForeground, editorHoverBorder } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; -import { hideInlineCompletionId, inlineSuggestCommitId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; -import { IInlineEditModel } from '../inlineEditsViewInterface.js'; +import { hideInlineCompletionId, inlineSuggestCommitAlternativeActionId, inlineSuggestCommitId, toggleShowCollapsedId } from '../../../controller/commandIds.js'; import { FirstFnArg, } from '../utils/utils.js'; +import { InlineSuggestionGutterMenuData } from './gutterIndicatorView.js'; export class GutterIndicatorMenuContent { - private readonly _inlineEditsShowCollapsed: IObservable; constructor( - private readonly _model: IInlineEditModel, - private readonly _close: (focusEditor: boolean) => void, private readonly _editorObs: ObservableCodeEditor, + private readonly _data: InlineSuggestionGutterMenuData, + private readonly _close: (focusEditor: boolean) => void, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @ICommandService private readonly _commandService: ICommandService, @@ -66,13 +65,13 @@ export class GutterIndicatorMenuContent { }; }; - const title = header(this._model.displayName); + const title = header(this._data.displayName); const gotoAndAccept = option(createOptionArgs({ id: 'gotoAndAccept', - title: `${localize('goto', "Go To")} / ${localize('accept', "Accept")}`, + title: localize('gotoAndAccept', "Go To / Accept"), icon: Codicon.check, - commandId: inlineSuggestCommitId + commandId: inlineSuggestCommitId, })); const reject = option(createOptionArgs({ @@ -82,7 +81,14 @@ export class GutterIndicatorMenuContent { commandId: hideInlineCompletionId })); - const extensionCommands = this._model.extensionCommands.map((c, idx) => option(createOptionArgs({ + const alternativeCommand = this._data.alternativeAction ? option(createOptionArgs({ + id: 'alternativeCommand', + title: this._data.alternativeAction.command.title, + icon: this._data.alternativeAction.icon, + commandId: inlineSuggestCommitAlternativeActionId, + })) : undefined; + + const extensionCommands = this._data.extensionCommands.map((c, idx) => option(createOptionArgs({ id: c.command.id + '_' + idx, title: c.command.title, icon: c.icon ?? Codicon.symbolEvent, @@ -90,6 +96,19 @@ export class GutterIndicatorMenuContent { commandArgs: c.command.arguments }))); + const showModelEnabled = false; + const modelOptions = showModelEnabled ? this._data.modelInfo?.models.map((m: { id: string; name: string }) => option({ + title: m.name, + icon: m.id === this._data.modelInfo?.currentModelId ? Codicon.check : Codicon.circle, + keybinding: constObservable(undefined), + isActive: activeElement.map(v => v === 'model_' + m.id), + onHoverChange: v => activeElement.set(v ? 'model_' + m.id : undefined, undefined), + onAction: () => { + this._close(true); + this._data.setModelId?.(m.id); + }, + })) ?? [] : []; + const toggleCollapsedMode = this._inlineEditsShowCollapsed.map(showCollapsed => showCollapsed ? option(createOptionArgs({ id: 'showExpanded', @@ -120,7 +139,7 @@ export class GutterIndicatorMenuContent { commandArgs: ['@tag:nextEditSuggestions'] })); - const actions = this._model.action ? [this._model.action] : []; + const actions = this._data.action ? [this._data.action] : []; const actionBarFooter = actions.length > 0 ? actionBar( actions.map(action => ({ id: action.id, @@ -136,8 +155,11 @@ export class GutterIndicatorMenuContent { return hoverContent([ title, gotoAndAccept, + alternativeCommand, reject, toggleCollapsedMode, + modelOptions.length ? separator() : undefined, + ...modelOptions, extensionCommands.length ? separator() : undefined, snooze, settings, 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 7eae42f5edb..0c0f5a97e8d 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 @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { n, trackFocus } from '../../../../../../../base/browser/dom.js'; +import { ModifierKeyEmitter, n, trackFocus } from '../../../../../../../base/browser/dom.js'; import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { Codicon } from '../../../../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; @@ -12,7 +12,6 @@ import { IObservable, ISettableObservable, autorun, constObservable, debouncedOb import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { IEditorMouseEvent } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -24,369 +23,105 @@ import { EditorOption, RenderLineNumbersType } from '../../../../../../common/co import { LineRange } from '../../../../../../common/core/ranges/lineRange.js'; import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; -import { IInlineEditModel, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getEditorBlendedColor, inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryBorder, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorsuccessfulBackground, inlineEditIndicatorsuccessfulBorder, inlineEditIndicatorsuccessfulForeground } from '../theme.js'; +import { InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBlendedColor, INLINE_EDITS_BORDER_RADIUS, inlineEditIndicatorBackground, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSecondaryBorder, inlineEditIndicatorSecondaryForeground, inlineEditIndicatorSuccessfulBackground, inlineEditIndicatorSuccessfulBorder, inlineEditIndicatorSuccessfulForeground } from '../theme.js'; import { mapOutFalsy, rectToProps } from '../utils/utils.js'; import { GutterIndicatorMenuContent } from './gutterIndicatorMenu.js'; import { assertNever } from '../../../../../../../base/common/assert.js'; +import { Command, InlineCompletionCommand, IInlineCompletionModelInfo } from '../../../../../../common/languages.js'; +import { InlineSuggestionItem } from '../../../model/inlineSuggestionItem.js'; +import { localize } from '../../../../../../../nls.js'; +import { InlineCompletionsModel } from '../../../model/inlineCompletionsModel.js'; +import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlternativeAction.js'; +import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; + +/** + * Customization options for the gutter indicator appearance and behavior. + */ +export interface GutterIndicatorCustomization { + /** Override the default icon */ + readonly icon?: ThemeIcon; +} -export class InlineEditsGutterIndicator extends Disposable { +export class InlineEditsGutterIndicatorData { + constructor( + readonly gutterMenuData: InlineSuggestionGutterMenuData, + readonly originalRange: LineRange, + readonly model: SimpleInlineSuggestModel, + readonly altAction: InlineSuggestAlternativeAction | undefined, + readonly customization?: GutterIndicatorCustomization, + ) { } +} - private get model() { - const model = this._model.get(); - if (!model) { throw new BugIndicatingError('Inline Edit Model not available'); } - return model; +export class InlineSuggestionGutterMenuData { + public static fromInlineSuggestion(suggestion: InlineSuggestionItem): InlineSuggestionGutterMenuData { + const alternativeAction = suggestion.action?.kind === 'edit' ? suggestion.action.alternativeAction : undefined; + return new InlineSuggestionGutterMenuData( + suggestion.gutterMenuLinkAction, + suggestion.source.provider.displayName ?? localize('inlineSuggestion', "Inline Suggestion"), + suggestion.source.inlineSuggestions.commands ?? [], + alternativeAction, + suggestion.source.provider.modelInfo, + suggestion.source.provider.setModelId?.bind(suggestion.source.provider), + ); } - private readonly _gutterIndicatorStyles; - private readonly _isHoveredOverInlineEditDebounced: IObservable; + constructor( + readonly action: Command | undefined, + readonly displayName: string, + readonly extensionCommands: InlineCompletionCommand[], + readonly alternativeAction: InlineSuggestAlternativeAction | undefined, + readonly modelInfo: IInlineCompletionModelInfo | undefined, + readonly setModelId: ((modelId: string) => Promise) | undefined, + ) { } +} + +// TODO this class does not make that much sense yet. +export class SimpleInlineSuggestModel { + public static fromInlineCompletionModel(model: InlineCompletionsModel): SimpleInlineSuggestModel { + return new SimpleInlineSuggestModel( + () => model.accept(), + () => model.jump(), + ); + } + constructor( + readonly accept: () => void, + readonly jump: () => void, + ) { } +} + +const CODICON_SIZE_PX = 16; +const CODICON_PADDING_PX = 2; + +export class InlineEditsGutterIndicator extends Disposable { constructor( private readonly _editorObs: ObservableCodeEditor, - private readonly _originalRange: IObservable, + private readonly _data: IObservable, + private readonly _tabAction: IObservable, private readonly _verticalOffset: IObservable, - private readonly _model: IObservable, private readonly _isHoveringOverInlineEdit: IObservable, private readonly _focusIsInMenu: ISettableObservable, - @IHoverService private readonly _hoverService: HoverService, + + @IHoverService protected readonly _hoverService: HoverService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IThemeService themeService: IThemeService, + @IThemeService private readonly _themeService: IThemeService ) { super(); - this._tabAction = derived(this, reader => { - const model = this._model.read(reader); - if (!model) { return InlineEditTabAction.Inactive; } - return model.tabAction.read(reader); - }); - - this._hoverVisible = observableValue(this, false); - this.isHoverVisible = this._hoverVisible; - this._isHoveredOverIcon = observableValue(this, false); - this._isHoveredOverIconDebounced = debouncedObservable(this._isHoveredOverIcon, 100); - this.isHoveredOverIcon = this._isHoveredOverIconDebounced; - this._isHoveredOverInlineEditDebounced = debouncedObservable(this._isHoveringOverInlineEdit, 100); + this._originalRangeObs = mapOutFalsy(this._data.map(d => d?.originalRange)); - this._gutterIndicatorStyles = this._tabAction.map(this, (v, reader) => { - switch (v) { - case InlineEditTabAction.Inactive: return { - background: getEditorBlendedColor(inlineEditIndicatorSecondaryBackground, themeService).read(reader).toString(), - foreground: getEditorBlendedColor(inlineEditIndicatorSecondaryForeground, themeService).read(reader).toString(), - border: getEditorBlendedColor(inlineEditIndicatorSecondaryBorder, themeService).read(reader).toString(), - }; - case InlineEditTabAction.Jump: return { - background: getEditorBlendedColor(inlineEditIndicatorPrimaryBackground, themeService).read(reader).toString(), - foreground: getEditorBlendedColor(inlineEditIndicatorPrimaryForeground, themeService).read(reader).toString(), - border: getEditorBlendedColor(inlineEditIndicatorPrimaryBorder, themeService).read(reader).toString() - }; - case InlineEditTabAction.Accept: return { - background: getEditorBlendedColor(inlineEditIndicatorsuccessfulBackground, themeService).read(reader).toString(), - foreground: getEditorBlendedColor(inlineEditIndicatorsuccessfulForeground, themeService).read(reader).toString(), - border: getEditorBlendedColor(inlineEditIndicatorsuccessfulBorder, themeService).read(reader).toString() - }; - default: - assertNever(v); - } - }); - - this._originalRangeObs = mapOutFalsy(this._originalRange); - this._state = derived(this, reader => { - const range = this._originalRangeObs.read(reader); - if (!range) { return undefined; } - return { - range, - lineOffsetRange: this._editorObs.observeLineOffsetRange(range, reader.store), - }; - }); this._stickyScrollController = StickyScrollController.get(this._editorObs.editor); this._stickyScrollHeight = this._stickyScrollController ? observableFromEvent(this._stickyScrollController.onDidChangeStickyScrollHeight, () => this._stickyScrollController!.stickyScrollWidgetHeight) : constObservable(0); - this._lineNumberToRender = derived(this, reader => { - if (this._verticalOffset.read(reader) !== 0) { - return ''; - } - - const lineNumber = this._originalRange.read(reader)?.startLineNumber; - const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); - - if (lineNumber === undefined || lineNumberOptions.renderType === RenderLineNumbersType.Off) { - return ''; - } - - if (lineNumberOptions.renderType === RenderLineNumbersType.Interval) { - const cursorPosition = this._editorObs.cursorPosition.read(reader); - if (lineNumber % 10 === 0 || cursorPosition && cursorPosition.lineNumber === lineNumber) { - return lineNumber.toString(); - } - return ''; - } - - if (lineNumberOptions.renderType === RenderLineNumbersType.Relative) { - const cursorPosition = this._editorObs.cursorPosition.read(reader); - if (!cursorPosition) { - return ''; - } - const relativeLineNumber = Math.abs(lineNumber - cursorPosition.lineNumber); - if (relativeLineNumber === 0) { - return lineNumber.toString(); - } - return relativeLineNumber.toString(); - } - if (lineNumberOptions.renderType === RenderLineNumbersType.Custom) { - if (lineNumberOptions.renderFn) { - return lineNumberOptions.renderFn(lineNumber); - } - return ''; - } - - return lineNumber.toString(); - }); - this._availableWidthForIcon = derived(this, reader => { - const textModel = this._editorObs.editor.getModel(); - const editor = this._editorObs.editor; - const layout = this._editorObs.layoutInfo.read(reader); - const gutterWidth = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft; - - if (!textModel || gutterWidth <= 0) { - return () => 0; - } - - // no glyph margin => the entire gutter width is available as there is no optimal place to put the icon - if (layout.lineNumbersLeft === 0) { - return () => gutterWidth; - } - - const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); - if (lineNumberOptions.renderType === RenderLineNumbersType.Relative || /* likely to flicker */ - lineNumberOptions.renderType === RenderLineNumbersType.Off) { - return () => gutterWidth; - } - - const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const rightOfLineNumber = layout.lineNumbersLeft + layout.lineNumbersWidth; - const totalLines = textModel.getLineCount(); - const totalLinesDigits = (totalLines + 1 /* 0 based to 1 based*/).toString().length; - - const offsetDigits: { - firstLineNumberWithDigitCount: number; - topOfLineNumber: number; - usableWidthLeftOfLineNumber: number; - }[] = []; - - // We only need to pre compute the usable width left of the line number for the first line number with a given digit count - for (let digits = 1; digits <= totalLinesDigits; digits++) { - const firstLineNumberWithDigitCount = 10 ** (digits - 1); - const topOfLineNumber = editor.getTopForLineNumber(firstLineNumberWithDigitCount); - const digitsWidth = digits * w; - const usableWidthLeftOfLineNumber = Math.min(gutterWidth, Math.max(0, rightOfLineNumber - digitsWidth - layout.glyphMarginLeft)); - offsetDigits.push({ firstLineNumberWithDigitCount, topOfLineNumber, usableWidthLeftOfLineNumber }); - } - - return (topOffset: number) => { - for (let i = offsetDigits.length - 1; i >= 0; i--) { - if (topOffset >= offsetDigits[i].topOfLineNumber) { - return offsetDigits[i].usableWidthLeftOfLineNumber; - } - } - throw new BugIndicatingError('Could not find avilable width for icon'); - }; - }); - this._layout = derived(this, reader => { - const s = this._state.read(reader); - if (!s) { return undefined; } - - const layout = this._editorObs.layoutInfo.read(reader); - - const lineHeight = this._editorObs.observeLineHeightForLine(s.range.map(r => r.startLineNumber)).read(reader); - const gutterViewPortPadding = 2; - - // Entire gutter view from top left to bottom right - const gutterWidthWithoutPadding = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - 2 * gutterViewPortPadding; - const gutterHeightWithoutPadding = layout.height - 2 * gutterViewPortPadding; - const gutterViewPortWithStickyScroll = Rect.fromLeftTopWidthHeight(gutterViewPortPadding, gutterViewPortPadding, gutterWidthWithoutPadding, gutterHeightWithoutPadding); - const gutterViewPortWithoutStickyScrollWithoutPaddingTop = gutterViewPortWithStickyScroll.withTop(this._stickyScrollHeight.read(reader)); - const gutterViewPortWithoutStickyScroll = gutterViewPortWithStickyScroll.withTop(gutterViewPortWithoutStickyScrollWithoutPaddingTop.top + gutterViewPortPadding); - - // The glyph margin area across all relevant lines - const verticalEditRange = s.lineOffsetRange.read(reader); - const gutterEditArea = Rect.fromRanges(OffsetRange.fromTo(gutterViewPortWithoutStickyScroll.left, gutterViewPortWithoutStickyScroll.right), verticalEditRange); - - // The gutter view container (pill) - const pillHeight = lineHeight; - const pillOffset = this._verticalOffset.read(reader); - const pillFullyDockedRect = gutterEditArea.withHeight(pillHeight).translateY(pillOffset); - const pillIsFullyDocked = gutterViewPortWithoutStickyScrollWithoutPaddingTop.containsRect(pillFullyDockedRect); - - // The icon which will be rendered in the pill - const iconNoneDocked = this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); - const iconDocked = derived(this, reader => { - if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { - return Codicon.check; - } - if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { - return Codicon.keyboardTab; - } - const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; - const editStartLineNumber = s.range.read(reader).startLineNumber; - return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; - }); - - const idealIconWidth = 22; - const minimalIconWidth = 16; // codicon size - const iconWidth = (pillRect: Rect) => { - const availableWidth = this._availableWidthForIcon.read(undefined)(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPadding; - return Math.max(Math.min(availableWidth, idealIconWidth), minimalIconWidth); - }; - - if (pillIsFullyDocked) { - const pillRect = pillFullyDockedRect; - - let lineNumberWidth; - if (layout.lineNumbersWidth === 0) { - lineNumberWidth = Math.min(Math.max(layout.lineNumbersLeft - gutterViewPortWithStickyScroll.left, 0), pillRect.width - idealIconWidth); - } else { - lineNumberWidth = Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - gutterViewPortWithStickyScroll.left, 0); - } - - const lineNumberRect = pillRect.withWidth(lineNumberWidth); - const iconWidth = Math.max(Math.min(layout.decorationsWidth, idealIconWidth), minimalIconWidth); - const iconRect = pillRect.withWidth(iconWidth).translateX(lineNumberWidth); - - return { - gutterEditArea, - icon: iconDocked, - iconDirection: 'right' as const, - iconRect, - pillRect, - lineNumberRect, - }; - } - - const pillPartiallyDockedPossibleArea = gutterViewPortWithStickyScroll.intersect(gutterEditArea); // The area in which the pill could be partially docked - const pillIsPartiallyDocked = pillPartiallyDockedPossibleArea && pillPartiallyDockedPossibleArea.height >= pillHeight; - - if (pillIsPartiallyDocked) { - // pillFullyDockedRect is outside viewport, move it into the viewport under sticky scroll as we prefer the pill to not be on top of the sticky scroll - // then move it into the possible area which will only cause it to move if it has to be rendered on top of the sticky scroll - const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithoutStickyScroll).moveToBeContainedIn(pillPartiallyDockedPossibleArea); - const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); - const iconRect = pillRect; - - return { - gutterEditArea, - icon: iconDocked, - iconDirection: 'right' as const, - iconRect, - pillRect, - }; - } - - // pillFullyDockedRect is outside viewport, so move it into viewport - const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithStickyScroll); - const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); - const iconRect = pillRect; - - // docked = pill was already in the viewport - const iconDirection = pillRect.top < pillFullyDockedRect.top ? - 'top' as const : - 'bottom' as const; - - return { - gutterEditArea, - icon: iconNoneDocked, - iconDirection, - iconRect, - pillRect, - }; - }); - this._iconRef = n.ref(); - this.isVisible = this._layout.map(l => !!l); - this._indicator = n.div({ - class: 'inline-edits-view-gutter-indicator', - onclick: () => { - const layout = this._layout.get(); - const acceptOnClick = layout?.icon.get() === Codicon.check; - - this._editorObs.editor.focus(); - if (acceptOnClick) { - this.model.accept(); - } else { - this.model.jump(); - } - }, - tabIndex: 0, - style: { - position: 'absolute', - overflow: 'visible', - }, - }, mapOutFalsy(this._layout).map(layout => !layout ? [] : [ - n.div({ - style: { - position: 'absolute', - background: asCssVariable(inlineEditIndicatorBackground), - borderRadius: '4px', - ...rectToProps(reader => layout.read(reader).gutterEditArea), - } - }), - n.div({ - class: 'icon', - ref: this._iconRef, - onmouseenter: () => { - // TODO show hover when hovering ghost text etc. - this._showHover(); - }, - style: { - cursor: 'pointer', - zIndex: '20', - position: 'absolute', - backgroundColor: this._gutterIndicatorStyles.map(v => v.background), - // eslint-disable-next-line local/code-no-any-casts - ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), - border: this._gutterIndicatorStyles.map(v => `1px solid ${v.border}`), - boxSizing: 'border-box', - borderRadius: '4px', - display: 'flex', - justifyContent: 'flex-end', - transition: 'background-color 0.2s ease-in-out, width 0.2s ease-in-out', - ...rectToProps(reader => layout.read(reader).pillRect), - } - }, [ - n.div({ - className: 'line-number', - style: { - lineHeight: layout.map(l => l.lineNumberRect ? l.lineNumberRect.height : 0), - display: layout.map(l => l.lineNumberRect ? 'flex' : 'none'), - alignItems: 'center', - justifyContent: 'flex-end', - width: layout.map(l => l.lineNumberRect ? l.lineNumberRect.width : 0), - height: '100%', - color: this._gutterIndicatorStyles.map(v => v.foreground), - } - }, - this._lineNumberToRender - ), - n.div({ - style: { - rotate: layout.map(l => `${getRotationFromDirection(l.iconDirection)}deg`), - transition: 'rotate 0.2s ease-in-out', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - height: '100%', - marginRight: layout.map(l => l.pillRect.width - l.iconRect.width - (l.lineNumberRect?.width ?? 0)), - width: layout.map(l => l.iconRect.width), - } - }, [ - layout.map((l, reader) => renderIcon(l.icon.read(reader))), - ]) - ]), - ])).keepUpdated(this._store); + const indicator = this._indicator.keepUpdated(this._store); this._register(this._editorObs.createOverlayWidget({ - domNode: this._indicator.element, + domNode: indicator.element, position: constObservable(null), allowEditorOverflow: false, minContentWidthInPx: constObservable(0), @@ -407,6 +142,8 @@ export class InlineEditsGutterIndicator extends Disposable { this._isHoveredOverIcon.set(false, undefined); })); + this._isHoveredOverInlineEditDebounced = debouncedObservable(this._isHoveringOverInlineEdit, 100); + // pulse animation when hovering inline edit this._register(runOnChange(this._isHoveredOverInlineEditDebounced, (isHovering) => { if (isHovering) { @@ -415,13 +152,48 @@ export class InlineEditsGutterIndicator extends Disposable { })); this._register(autorun(reader => { - this._indicator.readEffect(reader); - if (this._indicator.element) { - this._editorObs.editor.applyFontInfo(this._indicator.element); + indicator.readEffect(reader); + if (indicator.element) { + // For the line number + this._editorObs.editor.applyFontInfo(indicator.element); } })); } + private readonly _isHoveredOverInlineEditDebounced: IObservable; + + private readonly _modifierPressed = observableFromEvent(this, ModifierKeyEmitter.getInstance().event, () => ModifierKeyEmitter.getInstance().keyStatus.shiftKey); + private readonly _gutterIndicatorStyles = derived(this, reader => { + let v = this._tabAction.read(reader); + + // TODO: add source of truth for alt action active and key pressed + const altAction = this._data.read(reader)?.altAction; + const modifiedPressed = this._modifierPressed.read(reader); + if (altAction && modifiedPressed) { + v = InlineEditTabAction.Inactive; + } + + switch (v) { + case InlineEditTabAction.Inactive: return { + background: getEditorBlendedColor(inlineEditIndicatorSecondaryBackground, this._themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorSecondaryForeground, this._themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorSecondaryBorder, this._themeService).read(reader).toString(), + }; + case InlineEditTabAction.Jump: return { + background: getEditorBlendedColor(inlineEditIndicatorPrimaryBackground, this._themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorPrimaryForeground, this._themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorPrimaryBorder, this._themeService).read(reader).toString() + }; + case InlineEditTabAction.Accept: return { + background: getEditorBlendedColor(inlineEditIndicatorSuccessfulBackground, this._themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorSuccessfulForeground, this._themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorSuccessfulBorder, this._themeService).read(reader).toString() + }; + default: + assertNever(v); + } + }); + public triggerAnimation(): Promise { if (this._accessibilityService.isMotionReduced()) { return new Animation(null, null).finished; @@ -446,48 +218,265 @@ export class InlineEditsGutterIndicator extends Disposable { private readonly _originalRangeObs; - private readonly _state; + private readonly _state = derived(this, reader => { + const range = this._originalRangeObs.read(reader); + if (!range) { return undefined; } + return { + range, + lineOffsetRange: this._editorObs.observeLineOffsetRange(range, reader.store), + }; + }); private readonly _stickyScrollController; private readonly _stickyScrollHeight; - private readonly _lineNumberToRender; + private readonly _lineNumberToRender = derived(this, reader => { + if (this._verticalOffset.read(reader) !== 0) { + return ''; + } + + const lineNumber = this._data.read(reader)?.originalRange.startLineNumber; + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + + if (lineNumber === undefined || lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Interval) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (lineNumber % 10 === 0 || cursorPosition && cursorPosition.lineNumber === lineNumber) { + return lineNumber.toString(); + } + return ''; + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative) { + const cursorPosition = this._editorObs.cursorPosition.read(reader); + if (!cursorPosition) { + return ''; + } + const relativeLineNumber = Math.abs(lineNumber - cursorPosition.lineNumber); + if (relativeLineNumber === 0) { + return lineNumber.toString(); + } + return relativeLineNumber.toString(); + } + + if (lineNumberOptions.renderType === RenderLineNumbersType.Custom) { + if (lineNumberOptions.renderFn) { + return lineNumberOptions.renderFn(lineNumber); + } + return ''; + } + + return lineNumber.toString(); + }); + + private readonly _availableWidthForIcon = derived(this, reader => { + const textModel = this._editorObs.editor.getModel(); + const editor = this._editorObs.editor; + const layout = this._editorObs.layoutInfo.read(reader); + const gutterWidth = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft; + + if (!textModel || gutterWidth <= 0) { + return () => 0; + } + + // no glyph margin => the entire gutter width is available as there is no optimal place to put the icon + if (layout.lineNumbersLeft === 0) { + return () => gutterWidth; + } - private readonly _availableWidthForIcon; + const lineNumberOptions = this._editorObs.getOption(EditorOption.lineNumbers).read(reader); + if (lineNumberOptions.renderType === RenderLineNumbersType.Relative || /* likely to flicker */ + lineNumberOptions.renderType === RenderLineNumbersType.Off) { + return () => gutterWidth; + } - private readonly _layout; + const w = editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const rightOfLineNumber = layout.lineNumbersLeft + layout.lineNumbersWidth; + const totalLines = textModel.getLineCount(); + const totalLinesDigits = (totalLines + 1 /* 0 based to 1 based*/).toString().length; + + const offsetDigits: { + firstLineNumberWithDigitCount: number; + topOfLineNumber: number; + usableWidthLeftOfLineNumber: number; + }[] = []; + + // We only need to pre compute the usable width left of the line number for the first line number with a given digit count + for (let digits = 1; digits <= totalLinesDigits; digits++) { + const firstLineNumberWithDigitCount = 10 ** (digits - 1); + const topOfLineNumber = editor.getTopForLineNumber(firstLineNumberWithDigitCount); + const digitsWidth = digits * w; + const usableWidthLeftOfLineNumber = Math.min(gutterWidth, Math.max(0, rightOfLineNumber - digitsWidth - layout.glyphMarginLeft)); + offsetDigits.push({ firstLineNumberWithDigitCount, topOfLineNumber, usableWidthLeftOfLineNumber }); + } + return (topOffset: number) => { + for (let i = offsetDigits.length - 1; i >= 0; i--) { + if (topOffset >= offsetDigits[i].topOfLineNumber) { + return offsetDigits[i].usableWidthLeftOfLineNumber; + } + } + throw new BugIndicatingError('Could not find avilable width for icon'); + }; + }); + + private readonly _layout = derived(this, reader => { + const s = this._state.read(reader); + if (!s) { return undefined; } + + const layout = this._editorObs.layoutInfo.read(reader); + + const lineHeight = this._editorObs.observeLineHeightForLine(s.range.map(r => r.startLineNumber)).read(reader); + const gutterViewPortPaddingLeft = 1; + const gutterViewPortPaddingTop = 2; + + // Entire gutter view from top left to bottom right + const gutterWidthWithoutPadding = layout.decorationsLeft + layout.decorationsWidth - layout.glyphMarginLeft - 2 * gutterViewPortPaddingLeft; + const gutterHeightWithoutPadding = layout.height - 2 * gutterViewPortPaddingTop; + const gutterViewPortWithStickyScroll = Rect.fromLeftTopWidthHeight(gutterViewPortPaddingLeft, gutterViewPortPaddingTop, gutterWidthWithoutPadding, gutterHeightWithoutPadding); + const gutterViewPortWithoutStickyScrollWithoutPaddingTop = gutterViewPortWithStickyScroll.withTop(this._stickyScrollHeight.read(reader)); + const gutterViewPortWithoutStickyScroll = gutterViewPortWithStickyScroll.withTop(gutterViewPortWithoutStickyScrollWithoutPaddingTop.top + gutterViewPortPaddingTop); + + // The glyph margin area across all relevant lines + const verticalEditRange = s.lineOffsetRange.read(reader); + const gutterEditArea = Rect.fromRanges(OffsetRange.fromTo(gutterViewPortWithoutStickyScroll.left, gutterViewPortWithoutStickyScroll.right), verticalEditRange); + + // The gutter view container (pill) + const pillHeight = lineHeight; + const pillOffset = this._verticalOffset.read(reader); + const pillFullyDockedRect = gutterEditArea.withHeight(pillHeight).translateY(pillOffset); + const pillIsFullyDocked = gutterViewPortWithoutStickyScrollWithoutPaddingTop.containsRect(pillFullyDockedRect); + + // The icon which will be rendered in the pill + const customIcon = this._data.read(reader)?.customization?.icon; + const iconNoneDocked = customIcon + ? constObservable(customIcon) + : this._tabAction.map(action => action === InlineEditTabAction.Accept ? Codicon.keyboardTab : Codicon.arrowRight); + const iconDocked = customIcon + ? constObservable(customIcon) + : derived(this, reader => { + if (this._isHoveredOverIconDebounced.read(reader) || this._isHoveredOverInlineEditDebounced.read(reader)) { + return Codicon.check; + } + if (this._tabAction.read(reader) === InlineEditTabAction.Accept) { + return Codicon.keyboardTab; + } + const cursorLineNumber = this._editorObs.cursorLineNumber.read(reader) ?? 0; + const editStartLineNumber = s.range.read(reader).startLineNumber; + return cursorLineNumber <= editStartLineNumber ? Codicon.keyboardTabAbove : Codicon.keyboardTabBelow; + }); - private readonly _iconRef; + const idealIconAreaWidth = 22; + const iconWidth = (pillRect: Rect) => { + const availableIconAreaWidth = this._availableWidthForIcon.read(undefined)(pillRect.bottom + this._editorObs.editor.getScrollTop()) - gutterViewPortPaddingLeft; + return Math.max(Math.min(availableIconAreaWidth, idealIconAreaWidth), CODICON_SIZE_PX); + }; - public readonly isVisible; + if (pillIsFullyDocked) { + const pillRect = pillFullyDockedRect; - private readonly _hoverVisible; - public readonly isHoverVisible: IObservable; + let widthUntilLineNumberEnd; + if (layout.lineNumbersWidth === 0) { + widthUntilLineNumberEnd = Math.min(Math.max(layout.lineNumbersLeft - gutterViewPortWithStickyScroll.left, 0), pillRect.width - idealIconAreaWidth); + } else { + widthUntilLineNumberEnd = Math.max(layout.lineNumbersLeft + layout.lineNumbersWidth - gutterViewPortWithStickyScroll.left, 0); + } - private readonly _isHoveredOverIcon; - private readonly _isHoveredOverIconDebounced: IObservable; - public readonly isHoveredOverIcon: IObservable; + const lineNumberRect = pillRect.withWidth(widthUntilLineNumberEnd); + const minimalIconWidthWithPadding = CODICON_SIZE_PX + CODICON_PADDING_PX; + const iconWidth = Math.min(pillRect.width - widthUntilLineNumberEnd, idealIconAreaWidth); + const iconRect = pillRect.withWidth(Math.max(iconWidth, minimalIconWidthWithPadding)).translateX(widthUntilLineNumberEnd); + const iconVisible = iconWidth >= minimalIconWidthWithPadding; - private _showHover(): void { + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + iconVisible, + pillRect, + lineNumberRect, + }; + } + + const pillPartiallyDockedPossibleArea = gutterViewPortWithStickyScroll.intersect(gutterEditArea); // The area in which the pill could be partially docked + const pillIsPartiallyDocked = pillPartiallyDockedPossibleArea && pillPartiallyDockedPossibleArea.height >= pillHeight; + + if (pillIsPartiallyDocked) { + // pillFullyDockedRect is outside viewport, move it into the viewport under sticky scroll as we prefer the pill to not be on top of the sticky scroll + // then move it into the possible area which will only cause it to move if it has to be rendered on top of the sticky scroll + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithoutStickyScroll).moveToBeContainedIn(pillPartiallyDockedPossibleArea); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + return { + gutterEditArea, + icon: iconDocked, + iconDirection: 'right' as const, + iconRect, + pillRect, + iconVisible: true, + }; + } + + // pillFullyDockedRect is outside viewport, so move it into viewport + const pillRectMoved = pillFullyDockedRect.moveToBeContainedIn(gutterViewPortWithStickyScroll); + const pillRect = pillRectMoved.withWidth(iconWidth(pillRectMoved)); + const iconRect = pillRect; + + // docked = pill was already in the viewport + const iconDirection = pillRect.top < pillFullyDockedRect.top ? + 'top' as const : + 'bottom' as const; + + return { + gutterEditArea, + icon: iconNoneDocked, + iconDirection, + iconRect, + pillRect, + iconVisible: true, + }; + }); + + + protected readonly _iconRef = n.ref(); + + public readonly isVisible = this._layout.map(l => !!l); + + protected readonly _hoverVisible = observableValue(this, false); + public readonly isHoverVisible: IObservable = this._hoverVisible; + + private readonly _isHoveredOverIcon = observableValue(this, false); + private readonly _isHoveredOverIconDebounced: IObservable = debouncedObservable(this._isHoveredOverIcon, 100); + public readonly isHoveredOverIcon: IObservable = this._isHoveredOverIconDebounced; + + protected _showHover(): void { if (this._hoverVisible.get()) { return; } + const data = this._data.get(); + if (!data) { + throw new BugIndicatingError('Gutter indicator data not available'); + } const disposableStore = new DisposableStore(); const content = disposableStore.add(this._instantiationService.createInstance( GutterIndicatorMenuContent, - this.model, + this._editorObs, + data.gutterMenuData, (focusEditor) => { if (focusEditor) { this._editorObs.editor.focus(); } h?.dispose(); }, - this._editorObs, ).toDisposableLiveElement()); - const focusTracker = disposableStore.add(trackFocus(content.element)); + const focusTracker = disposableStore.add(trackFocus(content.element)); // TODO@benibenj should this be removed? disposableStore.add(focusTracker.onDidBlur(() => this._focusIsInMenu.set(false, undefined))); disposableStore.add(focusTracker.onDidFocus(() => this._focusIsInMenu.set(true, undefined))); disposableStore.add(toDisposable(() => this._focusIsInMenu.set(false, undefined))); @@ -508,9 +497,94 @@ export class InlineEditsGutterIndicator extends Disposable { } } - private readonly _tabAction; + private readonly _indicator = n.div({ + class: 'inline-edits-view-gutter-indicator', + style: { + position: 'absolute', + overflow: 'visible', + }, + }, mapOutFalsy(this._layout).map(layout => !layout ? [] : [ + n.div({ + style: { + position: 'absolute', + background: asCssVariable(inlineEditIndicatorBackground), + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, + ...rectToProps(reader => layout.read(reader).gutterEditArea), + } + }), + n.div({ + class: 'icon', + ref: this._iconRef, + + tabIndex: 0, + onclick: () => { + const layout = this._layout.get(); + const acceptOnClick = layout?.icon.get() === Codicon.check; - private readonly _indicator; + const data = this._data.get(); + if (!data) { throw new BugIndicatingError('Gutter indicator data not available'); } + + this._editorObs.editor.focus(); + if (acceptOnClick) { + data.model.accept(); + } else { + data.model.jump(); + } + }, + + onmouseenter: () => { + // TODO show hover when hovering ghost text etc. + this._showHover(); + }, + style: { + cursor: 'pointer', + zIndex: '20', + position: 'absolute', + backgroundColor: this._gutterIndicatorStyles.map(v => v.background), + // eslint-disable-next-line local/code-no-any-casts + ['--vscodeIconForeground' as any]: this._gutterIndicatorStyles.map(v => v.foreground), + border: this._gutterIndicatorStyles.map(v => `1px solid ${v.border}`), + boxSizing: 'border-box', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, + display: 'flex', + justifyContent: layout.map(l => l.iconDirection === 'bottom' ? 'flex-start' : 'flex-end'), + transition: this._modifierPressed.map(m => m ? '' : 'background-color 0.2s ease-in-out, width 0.2s ease-in-out'), + ...rectToProps(reader => layout.read(reader).pillRect), + } + }, [ + n.div({ + className: 'line-number', + style: { + lineHeight: layout.map(l => l.lineNumberRect ? l.lineNumberRect.height : 0), + display: layout.map(l => l.lineNumberRect ? 'flex' : 'none'), + alignItems: 'center', + justifyContent: 'flex-end', + width: layout.map(l => l.lineNumberRect ? l.lineNumberRect.width : 0), + height: '100%', + color: this._gutterIndicatorStyles.map(v => v.foreground), + } + }, + this._lineNumberToRender + ), + n.div({ + style: { + transform: layout.map(l => `rotate(${getRotationFromDirection(l.iconDirection)}deg)`), + transition: 'rotate 0.2s ease-in-out, opacity 0.2s ease-in-out', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + opacity: layout.map(l => l.iconVisible ? '1' : '0'), + marginRight: layout.map(l => l.pillRect.width - l.iconRect.width - (l.lineNumberRect?.width ?? 0)), + width: layout.map(l => l.iconRect.width), + position: 'relative', + right: layout.map(l => l.iconDirection === 'top' ? '1px' : '0'), + } + }, [ + layout.map((l, reader) => withStyles(renderIcon(l.icon.read(reader)), { fontSize: toPx(Math.min(l.iconRect.width - CODICON_PADDING_PX, CODICON_SIZE_PX)) })), + ]) + ]), + ])); } function getRotationFromDirection(direction: 'top' | 'bottom' | 'right'): number { @@ -520,3 +594,15 @@ function getRotationFromDirection(direction: 'top' | 'bottom' | 'right'): number case 'right': return 0; } } + +function withStyles(element: T, styles: { [key: string]: string }): T { + for (const key in styles) { + // eslint-disable-next-line local/code-no-any-casts + element.style[key as any] = styles[key]; + } + return element; +} + +function toPx(n: number): string { + return `${n}px`; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts deleted file mode 100644 index 7b54bd6318c..00000000000 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/components/indicatorView.ts +++ /dev/null @@ -1,85 +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 { addDisposableListener, h } from '../../../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { Codicon } from '../../../../../../../base/common/codicons.js'; -import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, constObservable } from '../../../../../../../base/common/observable.js'; -import { localize } from '../../../../../../../nls.js'; -import { buttonBackground, buttonForeground, buttonSeparator } from '../../../../../../../platform/theme/common/colorRegistry.js'; -import { registerColor } from '../../../../../../../platform/theme/common/colorUtils.js'; -import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; -import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; -import { InlineCompletionsModel } from '../../../model/inlineCompletionsModel.js'; - -export interface IInlineEditsIndicatorState { - editTop: number; - showAlways: boolean; -} -export const inlineEditIndicatorForeground = registerColor('inlineEdit.indicator.foreground', buttonForeground, localize('inlineEdit.indicator.foreground', 'Foreground color for the inline edit indicator.')); -export const inlineEditIndicatorBackground = registerColor('inlineEdit.indicator.background', buttonBackground, localize('inlineEdit.indicator.background', 'Background color for the inline edit indicator.')); -export const inlineEditIndicatorBorder = registerColor('inlineEdit.indicator.border', buttonSeparator, localize('inlineEdit.indicator.border', 'Border color for the inline edit indicator.')); - -export class InlineEditsIndicator extends Disposable { - private readonly _indicator = h('div.inline-edits-view-indicator', { - style: { - position: 'absolute', - overflow: 'visible', - cursor: 'pointer', - }, - }, [ - h('div.icon', {}, [ - renderIcon(Codicon.arrowLeft), - ]), - h('div.label', {}, [ - ' inline edit' - ]) - ]); - - public isHoverVisible = constObservable(false); - - constructor( - private readonly _editorObs: ObservableCodeEditor, - private readonly _state: IObservable, - private readonly _model: IObservable, - ) { - super(); - - this._register(addDisposableListener(this._indicator.root, 'click', () => { - this._model.get()?.jump(); - })); - - this._register(this._editorObs.createOverlayWidget({ - domNode: this._indicator.root, - position: constObservable(null), - allowEditorOverflow: false, - minContentWidthInPx: constObservable(0), - })); - - this._register(autorun(reader => { - const state = this._state.read(reader); - if (!state) { - this._indicator.root.style.visibility = 'hidden'; - return; - } - - this._indicator.root.style.visibility = ''; - const i = this._editorObs.layoutInfo.read(reader); - - const range = new OffsetRange(0, i.height - 30); - - const topEdit = state.editTop; - this._indicator.root.classList.toggle('top', topEdit < range.start); - this._indicator.root.classList.toggle('bottom', topEdit > range.endExclusive); - const showAnyway = state.showAlways; - this._indicator.root.classList.toggle('visible', showAnyway); - this._indicator.root.classList.toggle('contained', range.contains(topEdit)); - - this._indicator.root.style.top = `${range.clip(topEdit)}px`; - this._indicator.root.style.right = `${i.minimap.minimapWidth + i.verticalScrollbarWidth}px`; - })); - } -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts index e4fdbbe7448..f532b3420a2 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditWithChanges.ts @@ -7,22 +7,26 @@ import { LineReplacement } from '../../../../../common/core/edits/lineEdit.js'; import { TextEdit } from '../../../../../common/core/edits/textEdit.js'; import { Position } from '../../../../../common/core/position.js'; import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; -import { AbstractText } from '../../../../../common/core/text/abstractText.js'; import { InlineCompletionCommand } from '../../../../../common/languages.js'; -import { InlineSuggestionItem } from '../../model/inlineSuggestionItem.js'; +import { InlineSuggestionAction, InlineSuggestionItem } from '../../model/inlineSuggestionItem.js'; +import { TextModelValueReference } from '../../model/textModelValueReference.js'; export class InlineEditWithChanges { - public get lineEdit() { - if (this.edit.replacements.length === 0) { - return new LineReplacement(new LineRange(1, 1), []); + // TODO@hediet: Move the next 3 fields into the action + public get lineEdit(): LineReplacement { + if (this.action?.kind === 'jumpTo') { + return new LineReplacement(LineRange.ofLength(this.action.position.lineNumber, 0), []); + } else if (this.action?.kind === 'edit') { + return LineReplacement.fromSingleTextEdit(this.edit!.toReplacement(this.originalText), this.originalText); } - return LineReplacement.fromSingleTextEdit(this.edit.toReplacement(this.originalText), this.originalText); + + return new LineReplacement(new LineRange(1, 1), []); } - public get originalLineRange() { return this.lineEdit.lineRange; } - public get modifiedLineRange() { return this.lineEdit.toLineEdit().getNewLineRanges()[0]; } + public get originalLineRange(): LineRange { return this.lineEdit.lineRange; } + public get modifiedLineRange(): LineRange { return this.lineEdit.toLineEdit().getNewLineRanges()[0]; } - public get displayRange() { + public get displayRange(): LineRange { return this.originalText.lineRange.intersect( this.originalLineRange.join( LineRange.ofLength(this.originalLineRange.startLineNumber, this.lineEdit.newLines.length) @@ -31,20 +35,13 @@ export class InlineEditWithChanges { } constructor( - public readonly originalText: AbstractText, - public readonly edit: TextEdit, + public readonly originalText: TextModelValueReference, + public readonly action: InlineSuggestionAction | undefined, + public readonly edit: TextEdit | undefined, public readonly cursorPosition: Position, public readonly multiCursorPositions: readonly Position[], public readonly commands: readonly InlineCompletionCommand[], public readonly inlineCompletion: InlineSuggestionItem, ) { } - - equals(other: InlineEditWithChanges) { - return this.originalText.getValue() === other.originalText.getValue() && - this.edit.equals(other.edit) && - this.cursorPosition.equals(other.cursorPosition) && - this.commands === other.commands && - this.inlineCompletion === other.inlineCompletion; - } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts index b387c46ca1c..70f2ad63a1d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsModel.ts @@ -5,98 +5,53 @@ import { Event } from '../../../../../../base/common/event.js'; import { derived, IObservable } from '../../../../../../base/common/observable.js'; -import { localize } from '../../../../../../nls.js'; -import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; -import { observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; -import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; -import { TextEdit } from '../../../../../common/core/edits/textEdit.js'; -import { StringText } from '../../../../../common/core/text/abstractText.js'; -import { Command, InlineCompletionCommand } from '../../../../../common/languages.js'; -import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; -import { InlineCompletionItem, InlineSuggestHint } from '../../model/inlineSuggestionItem.js'; -import { IInlineEditHost, IInlineEditModel, InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js'; +import { setTimeout0 } from '../../../../../../base/common/platform.js'; +import { InlineCompletionsModel, isSuggestionInViewport } from '../../model/inlineCompletionsModel.js'; +import { InlineSuggestHint } from '../../model/inlineSuggestionItem.js'; +import { InlineCompletionEditorType } from '../../model/provideInlineCompletions.js'; +import { InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; -export class InlineEditModel implements IInlineEditModel { +/** + * Warning: This is not per inline edit id and gets created often. + * @deprecated TODO@hediet remove +*/ +export class ModelPerInlineEdit { - readonly action: Command | undefined; - readonly displayName: string; - readonly extensionCommands: InlineCompletionCommand[]; - readonly isInDiffEditor: boolean; + readonly editorType: InlineCompletionEditorType; readonly displayLocation: InlineSuggestHint | undefined; - readonly showCollapsed: IObservable; + + + /** Determines if the inline suggestion is fully in the view port */ + readonly inViewPort: IObservable; + + readonly onDidAccept: Event; constructor( private readonly _model: InlineCompletionsModel, readonly inlineEdit: InlineEditWithChanges, readonly tabAction: IObservable, ) { - this.action = this.inlineEdit.inlineCompletion.action; - this.displayName = this.inlineEdit.inlineCompletion.source.provider.displayName ?? localize('inlineEdit', "Inline Edit"); - this.extensionCommands = this.inlineEdit.inlineCompletion.source.inlineSuggestions.commands ?? []; - this.isInDiffEditor = this._model.isInDiffEditor; + this.editorType = this._model.editorType; this.displayLocation = this.inlineEdit.inlineCompletion.hint; - this.showCollapsed = this._model.showCollapsed; - } - - accept() { - this._model.accept(); - } - - jump() { - this._model.jump(); - } - - handleInlineEditShown(viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData) { - this._model.handleInlineSuggestionShown(this.inlineEdit.inlineCompletion, viewKind, viewData); - } -} -export class InlineEditHost implements IInlineEditHost { - readonly onDidAccept: Event; - readonly inAcceptFlow: IObservable; - - constructor( - private readonly _model: InlineCompletionsModel, - ) { + this.inViewPort = derived(this, reader => isSuggestionInViewport(this._model.editor, this.inlineEdit.inlineCompletion, reader)); this.onDidAccept = this._model.onDidAccept; - this.inAcceptFlow = this._model.inAcceptFlow; } -} -export class GhostTextIndicator { - - readonly model: InlineEditModel; + accept(alternativeAction?: boolean) { + this._model.accept(undefined, alternativeAction); + } - constructor( - editor: ICodeEditor, - model: InlineCompletionsModel, - readonly lineRange: LineRange, - inlineCompletion: InlineCompletionItem, - ) { - const editorObs = observableCodeEditor(editor); - const tabAction = derived(this, reader => { - if (editorObs.isFocused.read(reader)) { - if (inlineCompletion.showInlineEditMenu) { - return InlineEditTabAction.Accept; - } - } - return InlineEditTabAction.Inactive; + handleInlineEditShownNextFrame(viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData) { + const item = this.inlineEdit.inlineCompletion; + const timeWhenShown = Date.now(); + item.addRef(); + setTimeout0(() => { + this._model.handleInlineSuggestionShown(item, viewKind, viewData, timeWhenShown); + item.removeRef(); }); - - this.model = new InlineEditModel( - model, - new InlineEditWithChanges( - new StringText(''), - new TextEdit([inlineCompletion.getSingleTextEdit()]), - model.primaryPosition.get(), - model.allPositions.get(), - inlineCompletion.source.inlineSuggestions.commands ?? [], - inlineCompletion - ), - tabAction, - ); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts index c6ebf022433..d3ac44df0fe 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsNewUsers.ts @@ -6,11 +6,11 @@ import { timeout } from '../../../../../../base/common/async.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, IObservable, observableValue, runOnChange, runOnChangeWithCancellationToken } from '../../../../../../base/common/observable.js'; +import { autorun, derived, IObservable, observableValue, runOnChange, runOnChangeWithCancellationToken } from '../../../../../../base/common/observable.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; -import { IInlineEditHost, IInlineEditModel } from './inlineEditsViewInterface.js'; +import { ModelPerInlineEdit } from './inlineEditsModel.js'; import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js'; enum UserKind { @@ -38,8 +38,7 @@ export class InlineEditsOnboardingExperience extends Disposable { }); constructor( - private readonly _host: IObservable, - private readonly _model: IObservable, + private readonly _model: IObservable, private readonly _indicator: IObservable, private readonly _collapsedView: InlineEditsCollapsedView, @IStorageService private readonly _storageService: IStorageService, @@ -115,10 +114,10 @@ export class InlineEditsOnboardingExperience extends Disposable { })); // Remember when the user has hovered over the icon - disposableStore.add(autorunWithStore((reader, store) => { + disposableStore.add(autorun((reader) => { const indicator = this._indicator.read(reader); if (!indicator) { return; } - store.add(runOnChange(indicator.isHoveredOverIcon, async (isHovered) => { + reader.store.add(runOnChange(indicator.isHoveredOverIcon, async (isHovered) => { if (isHovered) { userHasHoveredOverIcon = true; } @@ -126,10 +125,10 @@ export class InlineEditsOnboardingExperience extends Disposable { })); // Remember when the user has accepted an inline edit - disposableStore.add(autorunWithStore((reader, store) => { - const host = this._host.read(reader); - if (!host) { return; } - store.add(host.onDidAccept(() => { + disposableStore.add(autorun((reader) => { + const model = this._model.read(reader); + if (!model) { return; } + reader.store.add(model.onDidAccept(() => { inlineEditHasBeenAccepted = true; })); })); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts index 70324757f87..73d400f7ad0 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsView.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { $ } from '../../../../../../base/browser/dom.js'; -import { equalsIfDefined, itemEquals } from '../../../../../../base/common/equals.js'; +import { equals } from '../../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError } from '../../../../../../base/common/errors.js'; import { Event } from '../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, derived, derivedOpts, IObservable, IReader, ISettableObservable, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; +import { autorun, derived, derivedOpts, IObservable, IReader, mapObservableArrayCached, observableValue } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; @@ -20,107 +20,61 @@ import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; import { AbstractText, StringText } from '../../../../../common/core/text/abstractText.js'; import { TextLength } from '../../../../../common/core/text/textLength.js'; import { DetailedLineRangeMapping, lineRangeMappingFromRangeMappings, RangeMapping } from '../../../../../common/diff/rangeMapping.js'; +import { ITextModel } from '../../../../../common/model.js'; import { TextModel } from '../../../../../common/model/textModel.js'; -import { InlineEditItem } from '../../model/inlineSuggestionItem.js'; -import { InlineEditsGutterIndicator } from './components/gutterIndicatorView.js'; +import { InlineSuggestionIdentity } from '../../model/inlineSuggestionItem.js'; +import { InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from './components/gutterIndicatorView.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; -import { GhostTextIndicator, InlineEditHost, InlineEditModel } from './inlineEditsModel.js'; -import { InlineEditsOnboardingExperience } from './inlineEditsNewUsers.js'; -import { IInlineEditModel, InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js'; +import { ModelPerInlineEdit } from './inlineEditsModel.js'; +import { InlineCompletionViewData, InlineCompletionViewKind, InlineEditTabAction } from './inlineEditsViewInterface.js'; import { InlineEditsCollapsedView } from './inlineEditsViews/inlineEditsCollapsedView.js'; import { InlineEditsCustomView } from './inlineEditsViews/inlineEditsCustomView.js'; import { InlineEditsDeletionView } from './inlineEditsViews/inlineEditsDeletionView.js'; import { InlineEditsInsertionView } from './inlineEditsViews/inlineEditsInsertionView.js'; import { InlineEditsLineReplacementView } from './inlineEditsViews/inlineEditsLineReplacementView.js'; +import { ILongDistanceHint, ILongDistanceViewState, InlineEditsLongDistanceHint } from './inlineEditsViews/longDistanceHint/inlineEditsLongDistanceHint.js'; import { InlineEditsSideBySideView } from './inlineEditsViews/inlineEditsSideBySideView.js'; -import { InlineEditsWordReplacementView } from './inlineEditsViews/inlineEditsWordReplacementView.js'; +import { InlineEditsWordReplacementView, WordReplacementsViewData } from './inlineEditsViews/inlineEditsWordReplacementView.js'; import { IOriginalEditorInlineDiffViewState, OriginalEditorInlineDiffView } from './inlineEditsViews/originalEditorInlineDiffView.js'; import { applyEditToModifiedRangeMappings, createReindentEdit } from './utils/utils.js'; import './view.css'; - +import { JumpToView } from './inlineEditsViews/jumpToView.js'; +import { StringEdit } from '../../../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../../../common/core/ranges/offsetRange.js'; +import { getPositionOffsetTransformerFromTextModel } from '../../../../../common/core/text/getPositionOffsetTransformerFromTextModel.js'; +import { InlineCompletionEditorType } from '../../model/provideInlineCompletions.js'; +import { TextModelValueReference } from '../../model/textModelValueReference.js'; +import { URI } from '../../../../../../base/common/uri.js'; export class InlineEditsView extends Disposable { private readonly _editorObs: ObservableCodeEditor; private readonly _useCodeShifting; private readonly _renderSideBySide; - - private readonly _tabAction; + private readonly _tabAction = derived(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive); private _previousView: { // TODO, move into identity id: string; - view: ReturnType; + view: ReturnType; editorWidth: number; timestamp: number; + uri: URI; } | undefined; + private readonly _showLongDistanceHint: IObservable; constructor( private readonly _editor: ICodeEditor, - private readonly _host: IObservable, - private readonly _model: IObservable, - private readonly _ghostTextIndicator: IObservable, - private readonly _focusIsInMenu: ISettableObservable, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + private readonly _model: IObservable, + private readonly _simpleModel: IObservable, + private readonly _inlineSuggestInfo: IObservable, + private readonly _showCollapsed: IObservable, + + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._editorObs = observableCodeEditor(this._editor); - this._tabAction = derived(reader => this._model.read(reader)?.tabAction.read(reader) ?? InlineEditTabAction.Inactive); this._constructorDone = observableValue(this, false); - this._uiState = derived<{ - state: ReturnType; - diff: DetailedLineRangeMapping[]; - edit: InlineEditWithChanges; - newText: string; - newTextLineCount: number; - isInDiffEditor: boolean; - } | undefined>(this, reader => { - const model = this._model.read(reader); - if (!model || !this._constructorDone.read(reader)) { - return undefined; - } - - const inlineEdit = model.inlineEdit; - let mappings = RangeMapping.fromEdit(inlineEdit.edit); - let newText = inlineEdit.edit.apply(inlineEdit.originalText); - let diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); - - let state = this.determineRenderState(model, reader, diff, new StringText(newText)); - if (!state) { - onUnexpectedError(new Error(`unable to determine view: tried to render ${this._previousView?.view}`)); - return undefined; - } - - if (state.kind === InlineCompletionViewKind.SideBySide) { - const indentationAdjustmentEdit = createReindentEdit(newText, inlineEdit.modifiedLineRange, textModel.getOptions().tabSize); - newText = indentationAdjustmentEdit.applyToString(newText); - mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); - diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, new StringText(newText)); - } - - this._previewTextModel.setLanguage(this._editor.getModel()!.getLanguageId()); - - const previousNewText = this._previewTextModel.getValue(); - if (previousNewText !== newText) { - // Only update the model if the text has changed to avoid flickering - this._previewTextModel.setValue(newText); - } - - if (model.showCollapsed.read(reader) && !this._indicator.read(reader)?.isHoverVisible.read(reader)) { - state = { kind: InlineCompletionViewKind.Collapsed as const, viewData: state.viewData }; - } - - model.handleInlineEditShown(state.kind, state.viewData); - - return { - state, - diff, - edit: inlineEdit, - newText, - newTextLineCount: inlineEdit.modifiedLineRange.length, - isInDiffEditor: model.isInDiffEditor, - }; - }); this._previewTextModel = this._register(this._instantiationService.createInstance( TextModel, '', @@ -128,91 +82,14 @@ export class InlineEditsView extends Disposable { { ...TextModel.DEFAULT_CREATION_OPTIONS, bracketPairColorizationOptions: { enabled: true, independentColorPoolPerBracketType: false } }, null )); - this._indicatorCyclicDependencyCircuitBreaker = observableValue(this, false); - this._indicator = derived(this, (reader) => { - if (!this._indicatorCyclicDependencyCircuitBreaker.read(reader)) { - return undefined; - } - - const indicatorDisplayRange = derivedOpts({ owner: this, equalsFn: equalsIfDefined(itemEquals()) }, reader => { - /** @description indicatorDisplayRange */ - const ghostTextIndicator = this._ghostTextIndicator.read(reader); - if (ghostTextIndicator) { - return ghostTextIndicator.lineRange; - } - - const state = this._uiState.read(reader); - if (!state) { return undefined; } - - if (state.state?.kind === 'custom') { - const range = state.state.displayLocation?.range; - if (!range) { - throw new BugIndicatingError('custom view should have a range'); - } - return new LineRange(range.startLineNumber, range.endLineNumber); - } - - if (state.state?.kind === 'insertionMultiLine') { - return this._insertion.originalLines.read(reader); - } - - return state.edit.displayRange; - }); - - const modelWithGhostTextSupport = derived(this, reader => { - /** @description modelWithGhostTextSupport */ - const model = this._model.read(reader); - if (model) { - return model; - } - const ghostTextIndicator = this._ghostTextIndicator.read(reader); - if (ghostTextIndicator) { - return ghostTextIndicator.model; - } - - return model; - }); - - return reader.store.add(this._instantiationService.createInstance( - InlineEditsGutterIndicator, - this._editorObs, - indicatorDisplayRange, - this._gutterIndicatorOffset, - modelWithGhostTextSupport, - this._inlineEditsIsHovered, - this._focusIsInMenu, - )); - }); - this._inlineEditsIsHovered = derived(this, reader => { - return this._sideBySide.isHovered.read(reader) - || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) - || this._deletion.isHovered.read(reader) - || this._inlineDiffView.isHovered.read(reader) - || this._lineReplacementView.isHovered.read(reader) - || this._insertion.isHovered.read(reader) - || this._customView.isHovered.read(reader); - }); - this._gutterIndicatorOffset = derived(this, reader => { - // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone - if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { - return this._insertion.startLineOffset.read(reader); - } - - const ghostTextIndicator = this._ghostTextIndicator.read(reader); - if (ghostTextIndicator) { - return getGhostTextTopOffset(ghostTextIndicator, this._editor); - } - - return 0; - }); this._sideBySide = this._register(this._instantiationService.createInstance(InlineEditsSideBySideView, this._editor, this._model.map(m => m?.inlineEdit), this._previewTextModel, this._uiState.map(s => s && s.state?.kind === InlineCompletionViewKind.SideBySide ? ({ newTextLineCount: s.newTextLineCount, - isInDiffEditor: s.isInDiffEditor, + editorType: s.editorType, }) : undefined), this._tabAction, )); @@ -222,7 +99,7 @@ export class InlineEditsView extends Disposable { this._uiState.map(s => s && s.state?.kind === InlineCompletionViewKind.Deletion ? ({ originalRange: s.state.originalRange, deletions: s.state.deletions, - inDiffEditor: s.isInDiffEditor, + editorType: s.editorType, }) : undefined), this._tabAction, )); @@ -232,14 +109,50 @@ export class InlineEditsView extends Disposable { lineNumber: s.state.lineNumber, startColumn: s.state.column, text: s.state.text, - inDiffEditor: s.isInDiffEditor, + editorType: s.editorType, }) : undefined), this._tabAction, )); + + this._inlineCollapsedView = this._register(this._instantiationService.createInstance(InlineEditsCollapsedView, + this._editor, + this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === InlineCompletionViewKind.Collapsed ? m?.inlineEdit : undefined) + )); + this._customView = this._register(this._instantiationService.createInstance(InlineEditsCustomView, + this._editor, + this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === InlineCompletionViewKind.Custom ? m?.displayLocation : undefined), + this._tabAction, + this._uiState.map(s => s?.editorType ?? InlineCompletionEditorType.TextEditor), + )); + + this._showLongDistanceHint = this._editorObs.getOption(EditorOption.inlineSuggest).map(this, s => s.edits.showLongDistanceHint); + this._longDistanceHint = derived(this, reader => { + if (!this._showLongDistanceHint.read(reader)) { + return undefined; + } + return reader.store.add(this._instantiationService.createInstance(InlineEditsLongDistanceHint, + this._editor, + this._uiState.map((s, reader) => s?.longDistanceHint ? ({ + hint: s.longDistanceHint, + newTextLineCount: s.newTextLineCount, + edit: s.edit, + diff: s.diff, + editorType: s.editorType, + model: this._simpleModel.read(reader)!, + inlineSuggestInfo: this._inlineSuggestInfo.read(reader)!, + nextCursorPosition: s.nextCursorPosition, + target: s.target, + }) : undefined), + this._previewTextModel, + this._tabAction, + )); + }).recomputeInitiallyAndOnChange(this._store); + + this._inlineDiffViewState = derived(this, reader => { const e = this._uiState.read(reader); if (!e || !e.state) { return undefined; } - if (e.state.kind === 'wordReplacements' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed' || e.state.kind === 'custom') { + if (e.state.kind === 'wordReplacements' || e.state.kind === 'insertionMultiLine' || e.state.kind === 'collapsed' || e.state.kind === 'custom' || e.state.kind === 'jumpTo') { return undefined; } return { @@ -247,21 +160,25 @@ export class InlineEditsView extends Disposable { diff: e.diff, mode: e.state.kind, modifiedCodeEditor: this._sideBySide.previewEditor, - isInDiffEditor: e.isInDiffEditor, + editorType: e.editorType, }; }); - this._inlineCollapsedView = this._register(this._instantiationService.createInstance(InlineEditsCollapsedView, - this._editor, - this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'collapsed' ? m?.inlineEdit : undefined) - )); - this._customView = this._register(this._instantiationService.createInstance(InlineEditsCustomView, - this._editor, - this._model.map((m, reader) => this._uiState.read(reader)?.state?.kind === 'custom' ? m?.displayLocation : undefined), - this._tabAction, - )); this._inlineDiffView = this._register(new OriginalEditorInlineDiffView(this._editor, this._inlineDiffViewState, this._previewTextModel)); - this._wordReplacementViews = mapObservableArrayCached(this, this._uiState.map(s => s?.state?.kind === 'wordReplacements' ? s.state.replacements : []), (e, store) => { - return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, e, this._tabAction)); + this._jumpToView = this._register(this._instantiationService.createInstance(JumpToView, this._editorObs, { style: 'label' }, derived(reader => { + const s = this._uiState.read(reader); + if (s?.state?.kind === InlineCompletionViewKind.JumpTo) { + return { jumpToPosition: s.state.position }; + } + return undefined; + }))); + const wordReplacements = derivedOpts({ + equalsFn: equals.arrayC(equals.thisC()) + }, reader => { + const s = this._uiState.read(reader); + return s?.state?.kind === InlineCompletionViewKind.WordReplacements ? s.state.replacements.map(replacement => new WordReplacementsViewData(replacement, s.editorType, s.state?.alternativeAction)) : []; + }); + this._wordReplacementViews = mapObservableArrayCached(this, wordReplacements, (viewData, store) => { + return store.add(this._instantiationService.createInstance(InlineEditsWordReplacementView, this._editorObs, viewData, this._tabAction)); }); this._lineReplacementView = this._register(this._instantiationService.createInstance(InlineEditsLineReplacementView, this._editorObs, @@ -271,44 +188,37 @@ export class InlineEditsView extends Disposable { modifiedLines: s.state.modifiedLines, replacements: s.state.replacements, }) : undefined), - this._uiState.map(s => s?.isInDiffEditor ?? false), + this._uiState.map(s => s?.editorType ?? InlineCompletionEditorType.TextEditor), this._tabAction, )); this._useCodeShifting = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.allowCodeShifting); this._renderSideBySide = this._editorObs.getOption(EditorOption.inlineSuggest).map(s => s.edits.renderSideBySide); - this._register(autorunWithStore((reader, store) => { + this._register(autorun((reader) => { const model = this._model.read(reader); if (!model) { return; } - - store.add( + reader.store.add( Event.any( this._sideBySide.onDidClick, - this._deletion.onDidClick, this._lineReplacementView.onDidClick, this._insertion.onDidClick, ...this._wordReplacementViews.read(reader).map(w => w.onDidClick), this._inlineDiffView.onDidClick, this._customView.onDidClick, - )(e => { + )(clickEvent => { if (this._viewHasBeenShownLongerThan(350)) { - e.preventDefault(); - model.accept(); + clickEvent.event.preventDefault(); + model.accept(clickEvent.alternativeAction); } }) ); })); - this._indicator.recomputeInitiallyAndOnChange(this._store); this._wordReplacementViews.recomputeInitiallyAndOnChange(this._store); - this._indicatorCyclicDependencyCircuitBreaker.set(true, undefined); - - this._register(this._instantiationService.createInstance(InlineEditsOnboardingExperience, this._host, this._model, this._indicator, this._inlineCollapsedView)); - const minEditorScrollHeight = derived(this, reader => { return Math.max( ...this._wordReplacementViews.read(reader).map(v => v.minEditorScrollHeight.read(reader)), @@ -317,16 +227,17 @@ export class InlineEditsView extends Disposable { ); }).recomputeInitiallyAndOnChange(this._store); - const textModel = this._editor.getModel()!; - let viewZoneId: string | undefined; this._register(autorun(reader => { const minScrollHeight = minEditorScrollHeight.read(reader); + const textModel = this._editorObs.model.read(reader); + if (!textModel) { return; } + this._editor.changeViewZones(accessor => { const scrollHeight = this._editor.getScrollHeight(); const viewZoneHeight = minScrollHeight - scrollHeight + 1 /* Add 1px so there is a small gap */; - if (viewZoneHeight !== 0 && viewZoneId) { + if (viewZoneHeight !== 0 && viewZoneId !== undefined) { accessor.removeZone(viewZoneId); viewZoneId = undefined; } @@ -346,19 +257,162 @@ export class InlineEditsView extends Disposable { this._constructorDone.set(true, undefined); // TODO: remove and use correct initialization order } + public readonly displayRange = derived(this, reader => { + const state = this._uiState.read(reader); + if (!state) { return undefined; } + if (state.target.uri.toString() !== this._editorObs.model.read(reader)?.uri.toString()) { + return undefined; + } + + if (state.state?.kind === 'custom') { + const range = state.state.displayLocation?.range; + if (!range) { + throw new BugIndicatingError('custom view should have a range'); + } + return new LineRange(range.startLineNumber, range.endLineNumber); + } + + if (state.state?.kind === 'insertionMultiLine') { + return this._insertion.originalLines.read(reader); + } + + return state.edit.displayRange; + }); + + + private _currentInlineEditCache: { + inlineSuggestionIdentity: InlineSuggestionIdentity; + firstCursorLineNumber: number; + } | undefined = undefined; + + private _getLongDistanceHintState(model: ModelPerInlineEdit, reader: IReader): ILongDistanceHint | undefined { + if (model.inlineEdit.inlineCompletion.identity.jumpedTo.read(reader)) { + return undefined; + } + if (model.inlineEdit.action === undefined) { + return undefined; + } + if (model.inlineEdit.originalText.uri.toString() !== this._editorObs.model.read(reader)?.uri.toString()) { + return { + isVisible: true, + lineNumber: model.inlineEdit.cursorPosition.lineNumber, + }; + } + + if (this._currentInlineEditCache?.inlineSuggestionIdentity !== model.inlineEdit.inlineCompletion.identity) { + this._currentInlineEditCache = { + inlineSuggestionIdentity: model.inlineEdit.inlineCompletion.identity, + firstCursorLineNumber: model.inlineEdit.cursorPosition.lineNumber, + }; + } + return { + lineNumber: this._currentInlineEditCache.firstCursorLineNumber, + isVisible: !model.inViewPort.read(reader), + }; + } + private readonly _constructorDone; - private readonly _uiState; + private readonly _uiState = derived<{ + state: ReturnType; + diff: DetailedLineRangeMapping[]; + edit: InlineEditWithChanges; + newText: string; + newTextLineCount: number; + editorType: InlineCompletionEditorType; + longDistanceHint: ILongDistanceHint | undefined; + nextCursorPosition: Position | null; + target: TextModelValueReference; + } | undefined>(this, reader => { + const model = this._model.read(reader); + const textModel = this._editorObs.model.read(reader); + if (!model || !textModel || !this._constructorDone.read(reader)) { + return undefined; + } - private readonly _previewTextModel; + const inlineEdit = model.inlineEdit; + let diff: DetailedLineRangeMapping[]; + let mappings: RangeMapping[]; + + let newText: AbstractText | undefined = undefined; + + if (inlineEdit.edit) { + mappings = RangeMapping.fromEdit(inlineEdit.edit); + newText = new StringText(inlineEdit.edit.apply(inlineEdit.originalText)); + diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, newText); + } else { + mappings = []; + diff = []; + newText = inlineEdit.originalText; + } + + + let state = this._determineRenderState(model, reader, diff, newText); + if (!state) { + onUnexpectedError(new Error(`unable to determine view: tried to render ${this._previousView?.view}`)); + return undefined; + } + + const longDistanceHint = this._getLongDistanceHintState(model, reader); + + if (longDistanceHint && longDistanceHint.isVisible) { + state.viewData.setLongDistanceViewData(longDistanceHint.lineNumber, inlineEdit.lineEdit.lineRange.startLineNumber); + } + + if (state.kind === InlineCompletionViewKind.SideBySide) { + const indentationAdjustmentEdit = createReindentEdit(newText.getValue(), inlineEdit.modifiedLineRange, textModel.getOptions().tabSize); + newText = new StringText(indentationAdjustmentEdit.applyToString(newText.getValue())); + + mappings = applyEditToModifiedRangeMappings(mappings, indentationAdjustmentEdit); + diff = lineRangeMappingFromRangeMappings(mappings, inlineEdit.originalText, newText); + } + + this._previewTextModel.setLanguage(textModel.getLanguageId()); + + const previousNewText = this._previewTextModel.getValue(); + if (previousNewText !== newText.getValue()) { + this._previewTextModel.setEOL(textModel.getEndOfLineSequence()); + const updateOldValueEdit = StringEdit.replace(new OffsetRange(0, previousNewText.length), newText.getValue()); + const updateOldValueEditSmall = updateOldValueEdit.removeCommonSuffixPrefix(previousNewText); + + const textEdit = getPositionOffsetTransformerFromTextModel(this._previewTextModel).getTextEdit(updateOldValueEditSmall); + this._previewTextModel.edit(textEdit); + } + + if (this._showCollapsed.read(reader)) { + state = { kind: InlineCompletionViewKind.Collapsed as const, viewData: state.viewData }; + } + + model.handleInlineEditShownNextFrame(state.kind, state.viewData); + + const nextCursorPosition = inlineEdit.action?.kind === 'jumpTo' ? inlineEdit.action.position : null; - private readonly _indicatorCyclicDependencyCircuitBreaker; + return { + state, + diff, + edit: inlineEdit, + newText: newText.getValue(), + newTextLineCount: inlineEdit.modifiedLineRange.length, + editorType: model.editorType, + longDistanceHint, + nextCursorPosition: nextCursorPosition, + target: inlineEdit.inlineCompletion.originalTextRef, + }; + }); - protected readonly _indicator; + private readonly _previewTextModel; - private readonly _inlineEditsIsHovered; - private readonly _gutterIndicatorOffset; + public readonly inlineEditsIsHovered = derived(this, reader => { + return this._sideBySide.isHovered.read(reader) + || this._wordReplacementViews.read(reader).some(v => v.isHovered.read(reader)) + || this._deletion.isHovered.read(reader) + || this._inlineDiffView.isHovered.read(reader) + || this._lineReplacementView.isHovered.read(reader) + || this._insertion.isHovered.read(reader) + || this._customView.isHovered.read(reader) + || this._longDistanceHint.map((v, r) => v?.isHovered.read(r) ?? false).read(reader); + }); private readonly _sideBySide; @@ -368,9 +422,10 @@ export class InlineEditsView extends Disposable { private readonly _inlineDiffViewState; - protected readonly _inlineCollapsedView; + public readonly _inlineCollapsedView; - protected readonly _customView; + private readonly _customView; + protected readonly _longDistanceHint; protected readonly _inlineDiffView; @@ -378,14 +433,24 @@ export class InlineEditsView extends Disposable { protected readonly _lineReplacementView; - private getCacheId(model: IInlineEditModel) { + protected readonly _jumpToView; + + public readonly gutterIndicatorOffset = derived(this, reader => { + // TODO: have a better way to tell the gutter indicator view where the edit is inside a viewzone + if (this._uiState.read(reader)?.state?.kind === 'insertionMultiLine') { + return this._insertion.startLineOffset.read(reader); + } + return 0; + }); + + private _getCacheId(model: ModelPerInlineEdit) { return model.inlineEdit.inlineCompletion.identity.id; } - private determineView(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText): InlineCompletionViewKind { + private _determineView(model: ModelPerInlineEdit, reader: IReader, diff: DetailedLineRangeMapping[], newText: AbstractText): InlineCompletionViewKind { // Check if we can use the previous view if it is the same InlineCompletion as previously shown const inlineEdit = model.inlineEdit; - const canUseCache = this._previousView?.id === this.getCacheId(model) && !model.displayLocation?.jumpToEdit; + const canUseCache = this._previousView?.id === this._getCacheId(model) && this._previousView?.uri.toString() === this._editorObs.model.get()!.uri.toString(); const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editorObs.layoutInfoWidth.read(reader) && ( this._previousView?.view === InlineCompletionViewKind.SideBySide || @@ -396,7 +461,14 @@ export class InlineEditsView extends Disposable { return this._previousView!.view; } - if (model.inlineEdit.inlineCompletion instanceof InlineEditItem && model.inlineEdit.inlineCompletion.uri) { + const action = model.inlineEdit.inlineCompletion.action; + if (action?.kind === 'edit' && action.alternativeAction) { + return InlineCompletionViewKind.WordReplacements; + } + + const targetUri = model.inlineEdit.inlineCompletion.originalTextRef.uri; + const currentUri = this._editorObs.model.read(reader)?.uri; + if (currentUri && targetUri.toString() !== currentUri.toString()) { return InlineCompletionViewKind.Custom; } @@ -411,7 +483,7 @@ export class InlineEditsView extends Disposable { const inner = diff.flatMap(d => d.innerChanges ?? []); const isSingleInnerEdit = inner.length === 1; - if (!model.isInDiffEditor) { + if (model.editorType !== InlineCompletionEditorType.DiffEditor) { if ( isSingleInnerEdit && this._useCodeShifting.read(reader) !== 'never' @@ -452,7 +524,7 @@ export class InlineEditsView extends Disposable { } if (numOriginalLines > 0 && numModifiedLines > 0) { - if (numOriginalLines === 1 && numModifiedLines === 1 && !model.isInDiffEditor /* prefer side by side in diff editor */) { + if (numOriginalLines === 1 && numModifiedLines === 1 && model.editorType !== InlineCompletionEditorType.DiffEditor /* prefer side by side in diff editor */) { return InlineCompletionViewKind.LineReplacement; } @@ -463,7 +535,7 @@ export class InlineEditsView extends Disposable { return InlineCompletionViewKind.LineReplacement; } - if (model.isInDiffEditor) { + if (model.editorType === InlineCompletionEditorType.DiffEditor) { if (isDeletion(inner, inlineEdit, newText)) { return InlineCompletionViewKind.Deletion; } @@ -476,11 +548,18 @@ export class InlineEditsView extends Disposable { return InlineCompletionViewKind.SideBySide; } - private determineRenderState(model: IInlineEditModel, reader: IReader, diff: DetailedLineRangeMapping[], newText: StringText) { - const inlineEdit = model.inlineEdit; + private _determineRenderState(model: ModelPerInlineEdit, reader: IReader, diff: DetailedLineRangeMapping[], newText: AbstractText) { + if (model.inlineEdit.action?.kind === 'jumpTo') { + return { + kind: InlineCompletionViewKind.JumpTo as const, + position: model.inlineEdit.action.position, + viewData: emptyViewData, + }; + } - let view = this.determineView(model, reader, diff, newText); + const inlineEdit = model.inlineEdit; + let view = this._determineView(model, reader, diff, newText); if (this._willRenderAboveCursor(reader, inlineEdit, view)) { switch (view) { case InlineCompletionViewKind.LineReplacement: @@ -489,30 +568,18 @@ export class InlineEditsView extends Disposable { break; } } - - this._previousView = { id: this.getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() }; + this._previousView = { id: this._getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now(), uri: this._editorObs.model.get()!.uri }; const inner = diff.flatMap(d => d.innerChanges ?? []); const textModel = this._editor.getModel()!; const stringChanges = inner.map(m => ({ originalRange: m.originalRange, modifiedRange: m.modifiedRange, - original: textModel.getValueInRange(m.originalRange), + original: inlineEdit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); - const cursorPosition = inlineEdit.cursorPosition; - const startsWithEOL = stringChanges.length === 0 ? false : stringChanges[0].modified.startsWith(textModel.getEOL()); - const viewData: InlineCompletionViewData = { - cursorColumnDistance: inlineEdit.edit.replacements.length === 0 ? 0 : inlineEdit.edit.replacements[0].range.getStartPosition().column - cursorPosition.column, - cursorLineDistance: inlineEdit.lineEdit.lineRange.startLineNumber - cursorPosition.lineNumber + (startsWithEOL && inlineEdit.lineEdit.lineRange.startLineNumber >= cursorPosition.lineNumber ? 1 : 0), - lineCountOriginal: inlineEdit.lineEdit.lineRange.length, - lineCountModified: inlineEdit.lineEdit.newLines.length, - characterCountOriginal: stringChanges.reduce((acc, r) => acc + r.original.length, 0), - characterCountModified: stringChanges.reduce((acc, r) => acc + r.modified.length, 0), - disjointReplacements: stringChanges.length, - sameShapeReplacements: stringChanges.every(r => r.original === stringChanges[0].original && r.modified === stringChanges[0].modified), - }; + const viewData = getViewData(inlineEdit, stringChanges, textModel); switch (view) { case InlineCompletionViewKind.InsertionInline: return { kind: InlineCompletionViewKind.InsertionInline as const, viewData }; @@ -548,7 +615,6 @@ export class InlineEditsView extends Disposable { if (view === InlineCompletionViewKind.WordReplacements) { let grownEdits = growEditsToEntireWord(replacements, inlineEdit.originalText); - if (grownEdits.some(e => e.range.isEmpty())) { grownEdits = growEditsUntilWhitespace(replacements, inlineEdit.originalText); } @@ -556,6 +622,7 @@ export class InlineEditsView extends Disposable { return { kind: InlineCompletionViewKind.WordReplacements as const, replacements: grownEdits, + alternativeAction: model.inlineEdit.action?.alternativeAction, viewData, }; } @@ -609,6 +676,27 @@ export class InlineEditsView extends Disposable { } } +const emptyViewData = new InlineCompletionViewData(-1, -1, -1, -1, -1, -1, -1, true); +function getViewData(inlineEdit: InlineEditWithChanges, stringChanges: { originalRange: Range; modifiedRange: Range; original: string; modified: string }[], textModel: ITextModel) { + if (!inlineEdit.edit) { + return emptyViewData; + } + + const cursorPosition = inlineEdit.cursorPosition; + const startsWithEOL = stringChanges.length === 0 ? false : stringChanges[0].modified.startsWith(textModel.getEOL()); + const viewData = new InlineCompletionViewData( + inlineEdit.edit.replacements.length === 0 ? 0 : inlineEdit.edit.replacements[0].range.getStartPosition().column - cursorPosition.column, + inlineEdit.lineEdit.lineRange.startLineNumber - cursorPosition.lineNumber + (startsWithEOL && inlineEdit.lineEdit.lineRange.startLineNumber >= cursorPosition.lineNumber ? 1 : 0), + inlineEdit.lineEdit.lineRange.length, + inlineEdit.lineEdit.newLines.length, + stringChanges.reduce((acc, r) => acc + r.original.length, 0), + stringChanges.reduce((acc, r) => acc + r.modified.length, 0), + stringChanges.length, + stringChanges.every(r => r.original === stringChanges[0].original && r.modified === stringChanges[0].modified) + ); + return viewData; +} + function isSingleLineInsertion(diff: DetailedLineRangeMapping[]) { return diff.every(m => m.innerChanges!.every(r => isWordInsertion(r))); @@ -667,7 +755,7 @@ function isSingleMultiLineInsertion(diff: DetailedLineRangeMapping[]) { return true; } -function isDeletion(inner: RangeMapping[], inlineEdit: InlineEditWithChanges, newText: StringText) { +function isDeletion(inner: RangeMapping[], inlineEdit: InlineEditWithChanges, newText: AbstractText) { const innerValues = inner.map(m => ({ original: inlineEdit.originalText.getValueOfRange(m.originalRange), modified: newText.getValueOfRange(m.modifiedRange) })); return innerValues.every(({ original, modified }) => modified.trim() === '' && original.length > 0 && (original.length > modified.length || original.trim() !== '')); } @@ -727,37 +815,3 @@ function _growEdits(replacements: TextReplacement[], originalText: AbstractText, return result; } - -function getGhostTextTopOffset(ghostTextIndicator: GhostTextIndicator, editor: ICodeEditor): number { - const replacements = ghostTextIndicator.model.inlineEdit.edit.replacements; - if (replacements.length !== 1) { - return 0; - } - - const textModel = editor.getModel(); - if (!textModel) { - return 0; - } - - const EOL = textModel.getEOL(); - const replacement = replacements[0]; - if (replacement.range.isEmpty() && replacement.text.startsWith(EOL)) { - const lineHeight = editor.getLineHeightForPosition(replacement.range.getStartPosition()); - return countPrefixRepeats(replacement.text, EOL) * lineHeight; - } - - return 0; -} - -function countPrefixRepeats(str: string, prefix: string): number { - if (!prefix.length) { - return 0; - } - let count = 0; - let i = 0; - while (str.startsWith(prefix, i)) { - count++; - i += prefix.length; - } - return count; -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts index 3e225b6a7c4..e5712352eff 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { getWindow } from '../../../../../../base/browser/dom.js'; +import { IMouseEvent, StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; import { Event } from '../../../../../../base/common/event.js'; import { IObservable } from '../../../../../../base/common/observable.js'; -import { Command, InlineCompletionCommand } from '../../../../../common/languages.js'; -import { InlineSuggestHint } from '../../model/inlineSuggestionItem.js'; -import { InlineEditWithChanges } from './inlineEditWithChanges.js'; export enum InlineEditTabAction { Jump = 'jump', @@ -16,30 +14,20 @@ export enum InlineEditTabAction { Inactive = 'inactive' } +export class InlineEditClickEvent { + static create(event: PointerEvent | MouseEvent, alternativeAction: boolean = false) { + return new InlineEditClickEvent(new StandardMouseEvent(getWindow(event), event), alternativeAction); + } + constructor( + public readonly event: IMouseEvent, + public readonly alternativeAction: boolean = false + ) { } +} + export interface IInlineEditsView { isHovered: IObservable; minEditorScrollHeight?: IObservable; - readonly onDidClick: Event; -} - -export interface IInlineEditHost { - readonly onDidAccept: Event; - inAcceptFlow: IObservable; -} - -export interface IInlineEditModel { - displayName: string; - action: Command | undefined; - extensionCommands: InlineCompletionCommand[]; - isInDiffEditor: boolean; - inlineEdit: InlineEditWithChanges; - tabAction: IObservable; - showCollapsed: IObservable; - displayLocation: InlineSuggestHint | undefined; - - handleInlineEditShown(viewKind: string, viewData?: InlineCompletionViewData): void; - accept(): void; - jump(): void; + readonly onDidClick: Event; } // TODO: Move this out of here as it is also includes ghosttext @@ -52,16 +40,43 @@ export enum InlineCompletionViewKind { InsertionMultiLine = 'insertionMultiLine', WordReplacements = 'wordReplacements', LineReplacement = 'lineReplacement', - Collapsed = 'collapsed' + Collapsed = 'collapsed', + JumpTo = 'jumpTo' } -export type InlineCompletionViewData = { - cursorColumnDistance: number; - cursorLineDistance: number; - lineCountOriginal: number; - lineCountModified: number; - characterCountOriginal: number; - characterCountModified: number; - disjointReplacements: number; - sameShapeReplacements?: boolean; -}; +export class InlineCompletionViewData { + + public longDistanceHintVisible: boolean | undefined = undefined; + public longDistanceHintDistance: number | undefined = undefined; + + constructor( + public readonly cursorColumnDistance: number, + public readonly cursorLineDistance: number, + public readonly lineCountOriginal: number, + public readonly lineCountModified: number, + public readonly characterCountOriginal: number, + public readonly characterCountModified: number, + public readonly disjointReplacements: number, + public readonly sameShapeReplacements?: boolean + ) { } + + setLongDistanceViewData(lineNumber: number, inlineEditLineNumber: number): void { + this.longDistanceHintVisible = true; + this.longDistanceHintDistance = Math.abs(inlineEditLineNumber - lineNumber); + } + + getData() { + return { + cursorColumnDistance: this.cursorColumnDistance, + cursorLineDistance: this.cursorLineDistance, + lineCountOriginal: this.lineCountOriginal, + lineCountModified: this.lineCountModified, + characterCountOriginal: this.characterCountOriginal, + characterCountModified: this.characterCountModified, + disjointReplacements: this.disjointReplacements, + sameShapeReplacements: this.sameShapeReplacements, + longDistanceHintVisible: this.longDistanceHintVisible, + longDistanceHintDistance: this.longDistanceHintDistance + }; + } +} 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 caa4419eeea..773548f9706 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewProducer.ts @@ -3,54 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createHotClass } from '../../../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../../../base/common/lifecycle.js'; -import { derived, IObservable, ISettableObservable } from '../../../../../../base/common/observable.js'; +import { derived, IObservable } from '../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js'; -import { LineRange } from '../../../../../common/core/ranges/lineRange.js'; import { Range } from '../../../../../common/core/range.js'; import { TextReplacement, TextEdit } from '../../../../../common/core/edits/textEdit.js'; -import { TextModelText } from '../../../../../common/model/textModelText.js'; import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js'; -import { InlineEdit } from '../../model/inlineEdit.js'; import { InlineEditWithChanges } from './inlineEditWithChanges.js'; -import { GhostTextIndicator, InlineEditHost, InlineEditModel } from './inlineEditsModel.js'; +import { ModelPerInlineEdit } from './inlineEditsModel.js'; import { InlineEditsView } from './inlineEditsView.js'; import { InlineEditTabAction } from './inlineEditsViewInterface.js'; +import { InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from './components/gutterIndicatorView.js'; export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This class is no longer a diff producer. Rename it or get rid of it - public static readonly hot = createHotClass(this); - private readonly _editorObs: ObservableCodeEditor; private readonly _inlineEdit = derived(this, (reader) => { const model = this._model.read(reader); if (!model) { return undefined; } - const inlineEdit = this._edit.read(reader); - if (!inlineEdit) { return undefined; } const textModel = this._editor.getModel(); if (!textModel) { return undefined; } - const editOffset = model.inlineEditState.read(undefined)?.inlineCompletion.updatedEdit; - if (!editOffset) { return undefined; } - - const edits = editOffset.replacements.map(e => { - const innerEditRange = Range.fromPositions( - textModel.getPositionAt(e.replaceRange.start), - textModel.getPositionAt(e.replaceRange.endExclusive) - ); - return new TextReplacement(innerEditRange, e.newText); - }); - - const diffEdits = new TextEdit(edits); - const text = new TextModelText(textModel); + const state = model.inlineEditState.read(reader); + if (!state) { return undefined; } + const action = state.inlineSuggestion.action; + + let diffEdits: TextEdit | undefined; + + if (action?.kind === 'edit') { + const editOffset = action.stringEdit; + const t = state.inlineSuggestion.originalTextRef.getTransformer(); + const edits = editOffset.replacements.map(e => { + const innerEditRange = Range.fromPositions( + t.getPosition(e.replaceRange.start), + t.getPosition(e.replaceRange.endExclusive) + ); + return new TextReplacement(innerEditRange, e.newText); + }); + diffEdits = new TextEdit(edits); + } else { + diffEdits = undefined; + } - return new InlineEditWithChanges(text, diffEdits, model.primaryPosition.read(undefined), model.allPositions.read(undefined), inlineEdit.commands, inlineEdit.inlineCompletion); + return new InlineEditWithChanges( + state.inlineSuggestion.originalTextRef, + action, + diffEdits, + model.primaryPosition.read(undefined), + model.allPositions.read(undefined), + state.inlineSuggestion.source.inlineSuggestions.commands ?? [], + state.inlineSuggestion + ); }); - private readonly _inlineEditModel = derived(this, reader => { + public readonly _inlineEditModel = derived(this, reader => { const model = this._model.read(reader); if (!model) { return undefined; } const edit = this._inlineEdit.read(reader); @@ -65,43 +73,25 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c return InlineEditTabAction.Inactive; }); - return new InlineEditModel(model, edit, tabAction); - }); - - private readonly _inlineEditHost = derived(this, reader => { - const model = this._model.read(reader); - if (!model) { return undefined; } - return new InlineEditHost(model); + return new ModelPerInlineEdit(model, edit, tabAction); }); - private readonly _ghostTextIndicator = derived(this, reader => { - const model = this._model.read(reader); - if (!model) { return undefined; } - const state = model.inlineCompletionState.read(reader); - if (!state) { return undefined; } - const inlineCompletion = state.inlineCompletion; - if (!inlineCompletion) { return undefined; } - - if (!inlineCompletion.showInlineEditMenu) { - return undefined; - } - - const lineRange = LineRange.ofLength(state.primaryGhostText.lineNumber, 1); - - return new GhostTextIndicator(this._editor, model, lineRange, inlineCompletion); - }); + public readonly view: InlineEditsView; constructor( private readonly _editor: ICodeEditor, - private readonly _edit: IObservable, private readonly _model: IObservable, - private readonly _focusIsInMenu: ISettableObservable, + private readonly _showCollapsed: IObservable, @IInstantiationService instantiationService: IInstantiationService, ) { super(); this._editorObs = observableCodeEditor(this._editor); - this._register(instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEditHost, this._inlineEditModel, this._ghostTextIndicator, this._focusIsInMenu)); + this.view = this._register(instantiationService.createInstance(InlineEditsView, this._editor, this._inlineEditModel, + this._model.map(model => model ? SimpleInlineSuggestModel.fromInlineCompletionModel(model) : undefined), + this._inlineEdit.map(e => e ? InlineSuggestionGutterMenuData.fromInlineSuggestion(e.inlineCompletion) : undefined), + this._showCollapsed, + )); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts index d74016c28aa..a11ab53bb4d 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/debugVisualization.ts @@ -16,7 +16,15 @@ export function setVisualization(data: object, visualization: IVisualizationEffe (data as any)['$$visualization'] = visualization; } -export function debugLogRects(rects: Record, elem: HTMLElement): object { +export function debugLogRects(rects: Record | Rect[], elem: HTMLElement): object { + if (Array.isArray(rects)) { + const record: Record = {}; + rects.forEach((rect, index) => { + record[index.toString()] = rect; + }); + rects = record; + } + setVisualization(rects, new ManyRectVisualizer(rects, elem)); return rects; } @@ -26,6 +34,24 @@ export function debugLogRect(rect: Rect, elem: HTMLElement, name: string): Rect return rect; } +export function debugLogHorizontalOffsetRange(rect: Rect, elem: HTMLElement, name: string): Rect { + setVisualization(rect, new HtmlHorizontalOffsetRangeVisualizer(rect, elem, name, 0, 'above')); + return rect; +} + +export function debugLogHorizontalOffsetRanges(rects: Record | Rect[], elem: HTMLElement): object { + if (Array.isArray(rects)) { + const record: Record = {}; + rects.forEach((rect, index) => { + record[index.toString()] = rect; + }); + rects = record; + } + + setVisualization(rects, new ManyHorizontalOffsetRangeVisualizer(rects, elem)); + return rects; +} + class ManyRectVisualizer implements IVisualizationEffect { constructor( private readonly _rects: Record, @@ -47,6 +73,132 @@ class ManyRectVisualizer implements IVisualizationEffect { } } +class ManyHorizontalOffsetRangeVisualizer implements IVisualizationEffect { + constructor( + private readonly _rects: Record, + private readonly _elem: HTMLElement + ) { } + + visualize(): IDisposable { + const d: IDisposable[] = []; + const keys = Object.keys(this._rects); + keys.forEach((key, index) => { + // Stagger labels: odd indices go above, even indices go below + const labelPosition = index % 2 === 0 ? 'above' : 'below'; + const v = new HtmlHorizontalOffsetRangeVisualizer(this._rects[key], this._elem, key, index * 12, labelPosition); + d.push(v.visualize()); + }); + + return { + dispose: () => { + d.forEach(d => d.dispose()); + } + }; + } +} + +class HtmlHorizontalOffsetRangeVisualizer implements IVisualizationEffect { + constructor( + private readonly _rect: Rect, + private readonly _elem: HTMLElement, + private readonly _name: string, + private readonly _verticalOffset: number = 0, + private readonly _labelPosition: 'above' | 'below' = 'above' + ) { } + + visualize(): IDisposable { + const container = document.createElement('div'); + container.style.position = 'fixed'; + container.style.pointerEvents = 'none'; + container.style.zIndex = '100000'; + + // Create horizontal line + const horizontalLine = document.createElement('div'); + horizontalLine.style.position = 'absolute'; + horizontalLine.style.height = '2px'; + horizontalLine.style.backgroundColor = 'green'; + horizontalLine.style.top = '50%'; + horizontalLine.style.transform = 'translateY(-50%)'; + + // Create start vertical bar + const startBar = document.createElement('div'); + startBar.style.position = 'absolute'; + startBar.style.width = '2px'; + startBar.style.height = '8px'; + startBar.style.backgroundColor = 'green'; + startBar.style.left = '0'; + startBar.style.top = '50%'; + startBar.style.transform = 'translateY(-50%)'; + + // Create end vertical bar + const endBar = document.createElement('div'); + endBar.style.position = 'absolute'; + endBar.style.width = '2px'; + endBar.style.height = '8px'; + endBar.style.backgroundColor = 'green'; + endBar.style.right = '0'; + endBar.style.top = '50%'; + endBar.style.transform = 'translateY(-50%)'; + + // Create label + const label = document.createElement('div'); + label.textContent = this._name; + label.style.position = 'absolute'; + + // Position label above or below the line to avoid overlaps + if (this._labelPosition === 'above') { + label.style.bottom = '12px'; + } else { + label.style.top = '12px'; + } + + label.style.left = '2px'; // Slight offset from start + label.style.color = 'green'; + label.style.fontSize = '10px'; + label.style.backgroundColor = 'rgba(255, 255, 255, 0.95)'; + label.style.padding = '1px 3px'; + label.style.border = '1px solid green'; + label.style.borderRadius = '2px'; + label.style.whiteSpace = 'nowrap'; + label.style.boxShadow = '0 1px 2px rgba(0,0,0,0.15)'; + label.style.fontFamily = 'monospace'; + + container.appendChild(horizontalLine); + container.appendChild(startBar); + container.appendChild(endBar); + container.appendChild(label); + + const updatePosition = () => { + const elemRect = this._elem.getBoundingClientRect(); + const centerY = this._rect.top + (this._rect.height / 2) + this._verticalOffset; + const left = elemRect.left + this._rect.left; + const width = this._rect.width; + + container.style.left = left + 'px'; + container.style.top = (elemRect.top + centerY) + 'px'; + container.style.width = width + 'px'; + container.style.height = '8px'; + + horizontalLine.style.width = width + 'px'; + }; + + // This is for debugging only + // eslint-disable-next-line no-restricted-syntax + document.body.appendChild(container); + updatePosition(); + + const observer = new ResizeObserver(updatePosition); + observer.observe(this._elem); + + return { + dispose: () => { + observer.disconnect(); + container.remove(); + } + }; + } +} + class HtmlRectVisualizer implements IVisualizationEffect { constructor( private readonly _rect: Rect, @@ -58,6 +210,7 @@ class HtmlRectVisualizer implements IVisualizationEffect { const div = document.createElement('div'); div.style.position = 'fixed'; div.style.border = '1px solid red'; + div.style.boxSizing = 'border-box'; div.style.pointerEvents = 'none'; div.style.zIndex = '100000'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts index 905104b15a2..f2f07d0d4a3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCollapsedView.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; -import { Emitter } from '../../../../../../../base/common/event.js'; +import { Event } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, IObservable } from '../../../../../../../base/common/observable.js'; import { IAccessibilityService } from '../../../../../../../platform/accessibility/common/accessibility.js'; @@ -20,8 +19,7 @@ import { getEditorValidOverlayRect, PathBuilder, rectToProps } from '../utils/ut export class InlineEditsCollapsedView extends Disposable implements IInlineEditsView { - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + readonly onDidClick = Event.None; private readonly _editorObs: ObservableCodeEditor; private readonly _iconRef = n.ref(); @@ -37,7 +35,7 @@ export class InlineEditsCollapsedView extends Disposable implements IInlineEdits this._editorObs = observableCodeEditor(this._editor); - const firstEdit = this._edit.map(inlineEdit => inlineEdit?.edit.replacements[0] ?? null); + const firstEdit = this._edit.map(inlineEdit => inlineEdit?.edit?.replacements[0] ?? null); const startPosition = firstEdit.map(edit => edit ? singleTextRemoveCommonPrefix(edit, this._editor.getModel()!).range.getStartPosition() : null); const observedStartPoint = this._editorObs.observePosition(startPosition, this._store); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts index b6d96623118..10b00eae05f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsCustomView.ts @@ -2,13 +2,10 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow, n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; +import { n } from '../../../../../../../base/browser/dom.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { autorun, constObservable, derived, derivedObservableWithCache, IObservable, IReader, observableValue } from '../../../../../../../base/common/observable.js'; -import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; -import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -20,8 +17,9 @@ import { InlineCompletionHintStyle } from '../../../../../../common/languages.js import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; import { InlineSuggestHint } from '../../../model/inlineSuggestionItem.js'; -import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getEditorBlendedColor, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorsuccessfulBackground } from '../theme.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; +import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBackgroundColor, getEditorBlendedColor, INLINE_EDITS_BORDER_RADIUS, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSuccessfulBackground } from '../theme.js'; import { getContentRenderWidth, maxContentWidthInRange, rectToProps } from '../utils/utils.js'; const MIN_END_OF_LINE_PADDING = 14; @@ -33,7 +31,7 @@ const VERTICAL_OFFSET_WHEN_ABOVE_BELOW = 2; export class InlineEditsCustomView extends Disposable implements IInlineEditsView { - private readonly _onDidClick = this._register(new Emitter()); + private readonly _onDidClick = this._register(new Emitter()); readonly onDidClick = this._onDidClick.event; private readonly _isHovered = observableValue(this, false); @@ -48,6 +46,7 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie private readonly _editor: ICodeEditor, displayLocation: IObservable, tabAction: IObservable, + editorType: IObservable, @IThemeService themeService: IThemeService, @ILanguageService private readonly _languageService: ILanguageService, ) { @@ -60,11 +59,11 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie switch (v) { case InlineEditTabAction.Inactive: border = inlineEditIndicatorSecondaryBackground; break; case InlineEditTabAction.Jump: border = inlineEditIndicatorPrimaryBackground; break; - case InlineEditTabAction.Accept: border = inlineEditIndicatorsuccessfulBackground; break; + case InlineEditTabAction.Accept: border = inlineEditIndicatorSuccessfulBackground; break; } return { border: getEditorBlendedColor(border, themeService).read(reader).toString(), - background: asCssVariable(editorBackground) + background: getEditorBackgroundColor(editorType.read(reader)) }; }); @@ -249,7 +248,7 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie boxSizing: 'border-box', cursor: 'pointer', border: styles.map(s => `1px solid ${s.border}`), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, backgroundColor: styles.map(s => s.background), display: 'flex', @@ -260,7 +259,7 @@ export class InlineEditsCustomView extends Disposable implements IInlineEditsVie onmousedown: e => { e.preventDefault(); // This prevents that the editor loses focus }, - onclick: (e) => { this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); } + onclick: (e) => { this._onDidClick.fire(InlineEditClickEvent.create(e)); } }, [ line ]); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts index ad9f53fa577..50e3ece51c9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsDeletionView.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; -import { Emitter } from '../../../../../../../base/common/event.js'; +import { Event } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, derivedObservableWithCache, IObservable } from '../../../../../../../base/common/observable.js'; -import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -19,20 +17,20 @@ import { Position } from '../../../../../../common/core/position.js'; import { Range } from '../../../../../../common/core/range.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { InlineEditWithChanges } from '../inlineEditWithChanges.js'; -import { getOriginalBorderColor, originalBackgroundColor } from '../theme.js'; +import { getEditorBackgroundColor, getOriginalBorderColor, INLINE_EDITS_BORDER_RADIUS, originalBackgroundColor } from '../theme.js'; import { getPrefixTrim, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; const HORIZONTAL_PADDING = 0; const VERTICAL_PADDING = 0; const BORDER_WIDTH = 1; const WIDGET_SEPARATOR_WIDTH = 1; const WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH = 3; -const BORDER_RADIUS = 4; +const BORDER_RADIUS = INLINE_EDITS_BORDER_RADIUS; export class InlineEditsDeletionView extends Disposable implements IInlineEditsView { - private readonly _onDidClick = this._register(new Emitter()); - readonly onDidClick = this._onDidClick.event; + readonly onDidClick = Event.None; private readonly _editorObs: ObservableCodeEditor; @@ -47,7 +45,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV private readonly _uiState: IObservable<{ originalRange: LineRange; deletions: Range[]; - inDiffEditor: boolean; + editorType: InlineCompletionEditorType; } | undefined>, private readonly _tabAction: IObservable, ) { @@ -161,16 +159,16 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV return rect.intersectHorizontal(new OffsetRange(overlayHider.left, Number.MAX_SAFE_INTEGER)); }); - const separatorWidth = this._uiState.map(s => s?.inDiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH).read(reader); + const separatorWidth = this._uiState.map(s => s?.editorType === InlineCompletionEditorType.DiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH).read(reader); const separatorRect = overlayRect.map(rect => rect.withMargin(separatorWidth, separatorWidth)); - + const editorBackground = getEditorBackgroundColor(this._uiState.map(s => s?.editorType ?? InlineCompletionEditorType.TextEditor).read(reader)); return [ n.div({ class: 'originalSeparatorDeletion', style: { ...separatorRect.read(reader).toStyles(), borderRadius: `${BORDER_RADIUS}px`, - border: `${BORDER_WIDTH + separatorWidth}px solid ${asCssVariable(editorBackground)}`, + border: `${BORDER_WIDTH + separatorWidth}px solid ${editorBackground}`, boxSizing: 'border-box', } }), @@ -188,7 +186,7 @@ export class InlineEditsDeletionView extends Disposable implements IInlineEditsV class: 'originalOverlayHiderDeletion', style: { ...overlayhider.read(reader).toStyles(), - backgroundColor: asCssVariable(editorBackground), + backgroundColor: editorBackground, } }) ]; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts index ac9f70f7668..17d310d950c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsInsertionView.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { $, n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -22,20 +20,21 @@ import { ILanguageService } from '../../../../../../common/languages/language.js import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel/inlineDecorations.js'; import { GhostText, GhostTextPart } from '../../../model/ghostText.js'; -import { GhostTextView } from '../../ghostText/ghostTextView.js'; -import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getModifiedBorderColor, modifiedBackgroundColor } from '../theme.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; +import { GhostTextView, IGhostTextWidgetData } from '../../ghostText/ghostTextView.js'; +import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBackgroundColor, getModifiedBorderColor, INLINE_EDITS_BORDER_RADIUS, modifiedBackgroundColor } from '../theme.js'; import { getPrefixTrim, mapOutFalsy } from '../utils/utils.js'; const BORDER_WIDTH = 1; const WIDGET_SEPARATOR_WIDTH = 1; const WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH = 3; -const BORDER_RADIUS = 4; +const BORDER_RADIUS = INLINE_EDITS_BORDER_RADIUS; export class InlineEditsInsertionView extends Disposable implements IInlineEditsView { private readonly _editorObs: ObservableCodeEditor; - private readonly _onDidClick = this._register(new Emitter()); + private readonly _onDidClick = this._register(new Emitter()); readonly onDidClick = this._onDidClick.event; private readonly _state = derived(this, reader => { @@ -127,7 +126,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits lineNumber: number; startColumn: number; text: string; - inDiffEditor: boolean; + editorType: InlineCompletionEditorType; } | undefined>, private readonly _tabAction: IObservable, @IInstantiationService instantiationService: IInstantiationService, @@ -137,26 +136,33 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits this._editorObs = observableCodeEditor(this._editor); - this._ghostTextView = this._register(instantiationService.createInstance(GhostTextView, + this._ghostTextView = this._register(instantiationService.createInstance( + GhostTextView, this._editor, + derived(reader => { + const ghostText = this._ghostText.read(reader); + if (!ghostText) { + return undefined; + } + return { + ghostText: ghostText, + handleInlineCompletionShown: (data) => { + // This is a no-op for the insertion view, as it is handled by the InlineEditsView. + }, + warning: undefined, + } satisfies IGhostTextWidgetData; + }), { - ghostText: this._ghostText, - minReservedLineCount: constObservable(0), - targetTextModel: this._editorObs.model.map(model => model ?? undefined), - warning: constObservable(undefined), - handleInlineCompletionShown: constObservable(() => { - // This is a no-op for the insertion view, as it is handled by the InlineEditsView. - }), - }, - observableValue(this, { syntaxHighlightingEnabled: true, extraClasses: ['inline-edit'] }), - true, - true + extraClasses: ['inline-edit'], + isClickable: true, + shouldKeepCursorStable: true, + } )); this.isHovered = this._ghostTextView.isHovered; this._register(this._ghostTextView.onDidClick((e) => { - this._onDidClick.fire(e); + this._onDidClick.fire(new InlineEditClickEvent(e)); })); this._register(this._editorObs.createOverlayWidget({ @@ -266,17 +272,18 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits layoutInfo.overlay.bottom )).read(reader); - const separatorWidth = this._input.map(i => i?.inDiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH).read(reader); + const separatorWidth = this._input.map(i => i?.editorType === InlineCompletionEditorType.DiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH).read(reader); const overlayRect = overlayLayoutObs.map(l => l.overlay.withMargin(0, BORDER_WIDTH, 0, l.startsAtContentLeft ? 0 : BORDER_WIDTH).intersectHorizontal(new OffsetRange(overlayHider.left, Number.MAX_SAFE_INTEGER))); const underlayRect = overlayRect.map(rect => rect.withMargin(separatorWidth, separatorWidth)); + const editorBackground = getEditorBackgroundColor(this._input.read(undefined)?.editorType ?? InlineCompletionEditorType.TextEditor); return [ n.div({ class: 'originalUnderlayInsertion', style: { ...underlayRect.read(reader).toStyles(), borderRadius: BORDER_RADIUS, - border: `${BORDER_WIDTH + separatorWidth}px solid ${asCssVariable(editorBackground)}`, + border: `${BORDER_WIDTH + separatorWidth}px solid ${editorBackground}`, boxSizing: 'border-box', } }), @@ -294,7 +301,7 @@ export class InlineEditsInsertionView extends Disposable implements IInlineEdits class: 'originalOverlayHiderInsertion', style: { ...overlayHider.toStyles(), - backgroundColor: asCssVariable(editorBackground), + backgroundColor: editorBackground, } }) ]; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts index 9f5e223814c..71e52b529e9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsLineReplacementView.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { $, getWindow, n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; +import { $, n } from '../../../../../../../base/browser/dom.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable, toDisposable } from '../../../../../../../base/common/lifecycle.js'; import { autorunDelta, constObservable, derived, IObservable } from '../../../../../../../base/common/observable.js'; -import { editorBackground, scrollbarShadow } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { scrollbarShadow } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { IEditorMouseEvent, IViewZoneChangeAccessor } from '../../../../../../browser/editorBrowser.js'; @@ -24,14 +23,15 @@ import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; import { InlineDecoration, InlineDecorationType } from '../../../../../../common/viewModel/inlineDecorations.js'; -import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, modifiedChangedLineBackgroundColor, originalBackgroundColor } from '../theme.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; +import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBackgroundColor, getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, INLINE_EDITS_BORDER_RADIUS, modifiedChangedLineBackgroundColor, originalBackgroundColor } from '../theme.js'; import { getEditorValidOverlayRect, getPrefixTrim, mapOutFalsy, rectToProps } from '../utils/utils.js'; export class InlineEditsLineReplacementView extends Disposable implements IInlineEditsView { - private readonly _onDidClick; - readonly onDidClick; + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; private readonly _maxPrefixTrim; @@ -56,14 +56,12 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin modifiedLines: string[]; replacements: Replacement[]; } | undefined>, - private readonly _isInDiffEditor: IObservable, + private readonly _editorType: IObservable, private readonly _tabAction: IObservable, @ILanguageService private readonly _languageService: ILanguageService, @IThemeService private readonly _themeService: IThemeService, ) { super(); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; this._maxPrefixTrim = this._edit.map((e, reader) => e ? getPrefixTrim(e.replacements.flatMap(r => [r.originalRange, r.modifiedRange]), e.originalRange, e.modifiedLines, this._editor.editor, reader) : undefined); this._modifiedLineElements = derived(this, reader => { const lines = []; @@ -210,7 +208,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin const layoutProps = layout.read(reader); const contentLeft = this._editor.layoutInfoContentLeft.read(reader); - const separatorWidth = this._isInDiffEditor.read(reader) ? 3 : 1; + const separatorWidth = this._editorType.read(reader) === InlineCompletionEditorType.DiffEditor ? 3 : 1; modifiedLineElements.lines.forEach((l, i) => { l.style.width = `${layoutProps.lowerText.width}px`; @@ -220,6 +218,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin const modifiedBorderColor = getModifiedBorderColor(this._tabAction).read(reader); const originalBorderColor = getOriginalBorderColor(this._tabAction).read(reader); + const editorBackground = getEditorBackgroundColor(this._editorType.read(reader)); return [ n.div({ @@ -235,9 +234,9 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).background.translateX(-contentLeft).withMargin(separatorWidth)), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, - border: `${separatorWidth + 1}px solid ${asCssVariable(editorBackground)}`, + border: `${separatorWidth + 1}px solid ${editorBackground}`, boxSizing: 'border-box', pointerEvents: 'none', } @@ -247,7 +246,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).background.translateX(-contentLeft)), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, border: getEditorBlendedColor(originalBorderColor, this._themeService).map(c => `1px solid ${c.toString()}`), pointerEvents: 'none', @@ -260,8 +259,8 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).lowerBackground.translateX(-contentLeft)), - borderRadius: '0 0 4px 4px', - background: asCssVariable(editorBackground), + borderRadius: `0 0 ${INLINE_EDITS_BORDER_RADIUS}px ${INLINE_EDITS_BORDER_RADIUS}px`, + background: editorBackground, boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, border: `1px solid ${asCssVariable(modifiedBorderColor)}`, boxSizing: 'border-box', @@ -272,7 +271,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin onmousedown: e => { e.preventDefault(); // This prevents that the editor loses focus }, - onclick: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), + onclick: (e) => this._onDidClick.fire(InlineEditClickEvent.create(e)), }, [ n.div({ style: { @@ -292,7 +291,7 @@ export class InlineEditsLineReplacementView extends Disposable implements IInlin fontWeight: this._editor.getOption(EditorOption.fontWeight), pointerEvents: 'none', whiteSpace: 'nowrap', - borderRadius: '0 0 4px 4px', + borderRadius: `0 0 ${INLINE_EDITS_BORDER_RADIUS}px ${INLINE_EDITS_BORDER_RADIUS}px`, overflow: 'hidden', } }, [...modifiedLineElements.lines]), diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts index 9e1c72711dd..c6e90caab9a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsSideBySideView.ts @@ -3,14 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { $, getWindow, n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Color } from '../../../../../../../base/common/color.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { IObservable, IReader, autorun, constObservable, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; -import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; -import { asCssVariable, asCssVariableWithDefault } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { ICodeEditor } from '../../../../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; @@ -22,10 +20,11 @@ import { Range } from '../../../../../../common/core/range.js'; import { ITextModel } from '../../../../../../common/model.js'; import { StickyScrollController } from '../../../../../stickyScroll/browser/stickyScrollController.js'; import { InlineCompletionContextKeys } from '../../../controller/inlineCompletionContextKeys.js'; -import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; import { InlineEditWithChanges } from '../inlineEditWithChanges.js'; -import { getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, modifiedBackgroundColor, originalBackgroundColor } from '../theme.js'; -import { PathBuilder, getContentRenderWidth, getOffsetForPos, mapOutFalsy, maxContentWidthInRange } from '../utils/utils.js'; +import { getEditorBackgroundColor, getEditorBlendedColor, getModifiedBorderColor, getOriginalBorderColor, INLINE_EDITS_BORDER_RADIUS, modifiedBackgroundColor, originalBackgroundColor } from '../theme.js'; +import { PathBuilder, getContentRenderWidth, getOffsetForPos, mapOutFalsy, maxContentWidthInRange, observeEditorBoundingClientRect } from '../utils/utils.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; const HORIZONTAL_PADDING = 0; const VERTICAL_PADDING = 0; @@ -34,7 +33,7 @@ const ENABLE_OVERFLOW = false; const BORDER_WIDTH = 1; const WIDGET_SEPARATOR_WIDTH = 1; const WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH = 3; -const BORDER_RADIUS = 4; +const BORDER_RADIUS = INLINE_EDITS_BORDER_RADIUS; const ORIGINAL_END_PADDING = 20; const MODIFIED_END_PADDING = 12; @@ -58,8 +57,8 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _editorObs; - private readonly _onDidClick; - readonly onDidClick; + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; constructor( private readonly _editor: ICodeEditor, @@ -67,7 +66,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _previewTextModel: ITextModel, private readonly _uiState: IObservable<{ newTextLineCount: number; - isInDiffEditor: boolean; + editorType: InlineCompletionEditorType; } | undefined>, private readonly _tabAction: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -75,11 +74,9 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit ) { super(); this._editorObs = observableCodeEditor(this._editor); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; this._display = derived(this, reader => !!this._uiState.read(reader) ? 'block' : 'none'); this.previewRef = n.ref(); - const separatorWidthObs = this._uiState.map(s => s?.isInDiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH); + const separatorWidthObs = this._uiState.map(s => s?.editorType === InlineCompletionEditorType.DiffEditor ? WIDGET_SEPARATOR_DIFF_EDITOR_WIDTH : WIDGET_SEPARATOR_WIDTH); this._editorContainer = n.div({ class: ['editorContainer'], style: { position: 'absolute', overflow: 'hidden', cursor: 'pointer' }, @@ -87,7 +84,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit e.preventDefault(); // This prevents that the editor loses focus }, onclick: (e) => { - this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)); + this._onDidClick.fire(InlineEditClickEvent.create(e)); } }, [ n.div({ class: 'preview', style: { pointerEvents: 'none' }, ref: this.previewRef }), @@ -106,6 +103,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit bracketPairsHorizontal: false, highlightActiveIndentation: false, }, + editContext: false, // is a bit faster rulers: [], padding: { top: 0, bottom: 0 }, folding: false, @@ -226,6 +224,9 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit return Math.max(maxWidth, lastValue ?? 0); }); }).map((v, r) => v.read(r)); + + const editorDomContentRect = observeEditorBoundingClientRect(this._editor, this._store); + this._previewEditorLayoutInfo = derived(this, (reader) => { const inlineEdit = this._edit.read(reader); if (!inlineEdit) { @@ -244,7 +245,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit const editorLayout = this._editorObs.layoutInfo.read(reader); const previewContentWidth = this._previewEditorWidth.read(reader); const editorContentAreaWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; - const editorBoundingClientRect = this._editor.getContainerDomNode().getBoundingClientRect(); + const editorBoundingClientRect = editorDomContentRect.read(reader); const clientContentAreaRight = editorLayout.contentLeft + editorLayout.contentWidth + editorBoundingClientRect.left; const remainingWidthRightOfContent = getWindow(this._editor.getContainerDomNode()).innerWidth - clientContentAreaRight; const remainingWidthRightOfEditor = getWindow(this._editor.getContainerDomNode()).innerWidth - editorBoundingClientRect.right; @@ -347,6 +348,9 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit this._originalBackgroundColor = observableFromEvent(this, this._themeService.onDidColorThemeChange, () => { return this._themeService.getColorTheme().getColor(originalBackgroundColor) ?? Color.transparent; }); + this._editorBackgroundColor = this._uiState.map(s => { + return getEditorBackgroundColor(s?.editorType ?? InlineCompletionEditorType.TextEditor); + }); this._backgroundSvg = n.svg({ transform: 'translate(-0.5 -0.5)', style: { overflow: 'visible', pointerEvents: 'none', position: 'absolute' }, @@ -371,7 +375,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit .build(); }), style: { - fill: asCssVariableWithDefault(editorBackground, 'transparent'), + fill: this._editorBackgroundColor, } }), ]).keepUpdated(this._store); @@ -381,9 +385,11 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit const layoutInfoObs = mapOutFalsy(this._previewEditorLayoutInfo).read(reader); if (!layoutInfoObs) { return undefined; } + const editorBackground = this._editorBackgroundColor.read(reader); + const separatorWidth = separatorWidthObs.read(reader); const borderStyling = getOriginalBorderColor(this._tabAction).map(bc => `${BORDER_WIDTH}px solid ${asCssVariable(bc)}`); - const borderStylingSeparator = `${BORDER_WIDTH + separatorWidth}px solid ${asCssVariable(editorBackground)}`; + const borderStylingSeparator = `${BORDER_WIDTH + separatorWidth}px solid ${editorBackground}`; const hasBorderLeft = layoutInfoObs.read(reader).codeScrollLeft !== 0; const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); @@ -453,7 +459,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit borderTop: borderStyling, borderRight: borderStyling, borderRadius: `0 100% 0 0`, - backgroundColor: asCssVariable(editorBackground) + backgroundColor: editorBackground } }) ]), @@ -461,7 +467,7 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit class: 'originalOverlaySideBySideHider', style: { ...overlayHider.toStyles(), - backgroundColor: asCssVariable(editorBackground), + backgroundColor: editorBackground, } }), ]; @@ -473,11 +479,12 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit if (!layoutInfoObs) { return undefined; } const isModifiedLower = layoutInfoObs.map(layoutInfo => layoutInfo.codeRect.bottom < layoutInfo.editRect.bottom); + const editorBackground = this._editorBackgroundColor.read(reader); const separatorWidth = separatorWidthObs.read(reader); const borderRadius = isModifiedLower.map(isLower => `0 ${BORDER_RADIUS}px ${BORDER_RADIUS}px ${isLower ? BORDER_RADIUS : 0}px`); const borderStyling = getEditorBlendedColor(getModifiedBorderColor(this._tabAction), this._themeService).map(c => `1px solid ${c.toString()}`); - const borderStylingSeparator = `${BORDER_WIDTH + separatorWidth}px solid ${asCssVariable(editorBackground)}`; + const borderStylingSeparator = `${BORDER_WIDTH + separatorWidth}px solid ${editorBackground}`; const overlayRect = layoutInfoObs.map(layoutInfo => layoutInfo.editRect.withMargin(0, BORDER_WIDTH)); const separatorRect = overlayRect.map(overlayRect => overlayRect.withMargin(separatorWidth, separatorWidth, separatorWidth, 0)); @@ -613,6 +620,8 @@ export class InlineEditsSideBySideView extends Disposable implements IInlineEdit private readonly _originalBackgroundColor; + private readonly _editorBackgroundColor; + private readonly _backgroundSvg; private readonly _originalOverlay; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts index 2ecf6652ed9..cb9e24dbf3a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordInsertView.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { n } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; -import { Emitter } from '../../../../../../../base/common/event.js'; +import { Event } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { constObservable, derived, IObservable } from '../../../../../../../base/common/observable.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; @@ -16,13 +15,12 @@ import { EditorOption } from '../../../../../../common/config/editorOptions.js'; import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { TextReplacement } from '../../../../../../common/core/edits/textEdit.js'; import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getModifiedBorderColor } from '../theme.js'; +import { getModifiedBorderColor, INLINE_EDITS_BORDER_RADIUS } from '../theme.js'; import { mapOutFalsy, rectToProps } from '../utils/utils.js'; export class InlineEditsWordInsertView extends Disposable implements IInlineEditsView { - private readonly _onDidClick; - readonly onDidClick; + readonly onDidClick = Event.None; private readonly _start; @@ -39,8 +37,6 @@ export class InlineEditsWordInsertView extends Disposable implements IInlineEdit private readonly _tabAction: IObservable ) { super(); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; this._start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); this._layout = derived(this, reader => { const start = this._start.read(reader); @@ -81,7 +77,7 @@ export class InlineEditsWordInsertView extends Disposable implements IInlineEdit style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).lowerBackground), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, background: 'var(--vscode-editor-background)' } }, []), @@ -89,7 +85,7 @@ export class InlineEditsWordInsertView extends Disposable implements IInlineEdit style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).modified), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, padding: '0px', textAlign: 'center', background: 'var(--vscode-inlineEdit-modifiedChangedTextBackground)', @@ -104,7 +100,7 @@ export class InlineEditsWordInsertView extends Disposable implements IInlineEdit style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).background), - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, border: `1px solid ${modifiedBorderColor}`, //background: 'rgba(122, 122, 122, 0.12)', looks better background: 'var(--vscode-inlineEdit-wordReplacementView-background)', diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts index aa176618e16..cf4052733d9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/inlineEditsWordReplacementView.ts @@ -3,13 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow, n, ObserverNodeWithElement } from '../../../../../../../base/browser/dom.js'; -import { IMouseEvent, StandardMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; +import { $, ModifierKeyEmitter, n, ObserverNodeWithElement } from '../../../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { IEquatable } from '../../../../../../../base/common/equals.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; -import { constObservable, derived, IObservable, observableValue } from '../../../../../../../base/common/observable.js'; -import { editorBackground, editorHoverForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { constObservable, derived, IObservable, observableFromEvent, observableFromPromise, observableValue } from '../../../../../../../base/common/observable.js'; +import { OS } from '../../../../../../../base/common/platform.js'; +import { localize } from '../../../../../../../nls.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { editorHoverForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { contrastBorder } from '../../../../../../../platform/theme/common/colors/baseColors.js'; import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; import { LineSource, renderLines, RenderOptions } from '../../../../../../browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; import { EditorOption } from '../../../../../../common/config/editorOptions.js'; @@ -20,25 +28,45 @@ import { TextReplacement } from '../../../../../../common/core/edits/textEdit.js import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; import { ILanguageService } from '../../../../../../common/languages/language.js'; import { LineTokens, TokenArray } from '../../../../../../common/tokens/lineTokens.js'; -import { IInlineEditsView, InlineEditTabAction } from '../inlineEditsViewInterface.js'; -import { getModifiedBorderColor, getOriginalBorderColor, modifiedChangedTextOverlayColor, originalChangedTextOverlayColor } from '../theme.js'; +import { inlineSuggestCommitAlternativeActionId } from '../../../controller/commandIds.js'; +import { InlineSuggestAlternativeAction } from '../../../model/InlineSuggestAlternativeAction.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; +import { IInlineEditsView, InlineEditClickEvent, InlineEditTabAction } from '../inlineEditsViewInterface.js'; +import { getEditorBackgroundColor, getModifiedBorderColor, getOriginalBorderColor, INLINE_EDITS_BORDER_RADIUS, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground, modifiedChangedTextOverlayColor, observeColor, originalChangedTextOverlayColor } from '../theme.js'; import { getEditorValidOverlayRect, mapOutFalsy, rectToProps } from '../utils/utils.js'; +export class WordReplacementsViewData implements IEquatable { + constructor( + public readonly edit: TextReplacement, + public readonly editorType: InlineCompletionEditorType, + public readonly alternativeAction: InlineSuggestAlternativeAction | undefined, + ) { } + + equals(other: WordReplacementsViewData): boolean { + return this.edit.equals(other.edit) && this.alternativeAction === other.alternativeAction; + } +} + const BORDER_WIDTH = 1; +const DOM_ID_OVERLAY = 'word-replacement-view-overlay'; +const DOM_ID_WIDGET = 'word-replacement-view-widget'; +const DOM_ID_REPLACEMENT = 'word-replacement-view-replacement'; +const DOM_ID_RENAME = 'word-replacement-view-rename'; export class InlineEditsWordReplacementView extends Disposable implements IInlineEditsView { public static MAX_LENGTH = 100; - private readonly _onDidClick; - readonly onDidClick; + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; private readonly _start; private readonly _end; private readonly _line; - private readonly _hoverableElement; + private readonly _primaryElement; + private readonly _secondaryElement; readonly isHovered; @@ -46,36 +74,39 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin constructor( private readonly _editor: ObservableCodeEditor, - /** Must be single-line in both sides */ - private readonly _edit: TextReplacement, + private readonly _viewData: WordReplacementsViewData, protected readonly _tabAction: IObservable, @ILanguageService private readonly _languageService: ILanguageService, + @IThemeService private readonly _themeService: IThemeService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IHoverService private readonly _hoverService: IHoverService, ) { super(); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; - this._start = this._editor.observePosition(constObservable(this._edit.range.getStartPosition()), this._store); - this._end = this._editor.observePosition(constObservable(this._edit.range.getEndPosition()), this._store); + this._start = this._editor.observePosition(constObservable(this._viewData.edit.range.getStartPosition()), this._store); + this._end = this._editor.observePosition(constObservable(this._viewData.edit.range.getEndPosition()), this._store); this._line = document.createElement('div'); - this._hoverableElement = observableValue(this, null); - this.isHovered = this._hoverableElement.map((e, reader) => e?.didMouseMoveDuringHover.read(reader) ?? false); + this._primaryElement = observableValue(this, null); + this._secondaryElement = observableValue(this, null); + this.isHovered = this._primaryElement.map((e, reader) => e?.didMouseMoveDuringHover.read(reader) ?? false); this._renderTextEffect = derived(this, _reader => { const tm = this._editor.model.get()!; - const origLine = tm.getLineContent(this._edit.range.startLineNumber); + const origLine = tm.getLineContent(this._viewData.edit.range.startLineNumber); - const edit = StringReplacement.replace(new OffsetRange(this._edit.range.startColumn - 1, this._edit.range.endColumn - 1), this._edit.text); + const edit = StringReplacement.replace(new OffsetRange(this._viewData.edit.range.startColumn - 1, this._viewData.edit.range.endColumn - 1), this._viewData.edit.text); const lineToTokenize = edit.replace(origLine); - const t = tm.tokenization.tokenizeLinesAt(this._edit.range.startLineNumber, [lineToTokenize])?.[0]; + const t = tm.tokenization.tokenizeLinesAt(this._viewData.edit.range.startLineNumber, [lineToTokenize])?.[0]; let tokens: LineTokens; if (t) { - tokens = TokenArray.fromLineTokens(t).slice(edit.getRangeAfterReplace()).toLineTokens(this._edit.text, this._languageService.languageIdCodec); + tokens = TokenArray.fromLineTokens(t).slice(edit.getRangeAfterReplace()).toLineTokens(this._viewData.edit.text, this._languageService.languageIdCodec); } else { - tokens = LineTokens.createEmpty(this._edit.text, this._languageService.languageIdCodec); + tokens = LineTokens.createEmpty(this._viewData.edit.text, this._languageService.languageIdCodec); } const res = renderLines(new LineSource([tokens]), RenderOptions.fromEditor(this._editor.editor).withSetWidth(false).withScrollBeyondLastColumn(0), [], this._line, true); this._line.style.width = `${res.minWidthInPx}px`; }); - const modifiedLineHeight = this._editor.observeLineHeightForPosition(this._edit.range.getStartPosition()); + const modifiedLineHeight = this._editor.observeLineHeightForPosition(this._viewData.edit.range.getStartPosition()); + const altCount = observableFromPromise(this._viewData.alternativeAction?.count ?? new Promise(resolve => resolve(undefined))).map(c => c.value); + const altModifierActive = observableFromEvent(this, ModifierKeyEmitter.getInstance().event, () => ModifierKeyEmitter.getInstance().keyStatus.shiftKey); this._layout = derived(this, reader => { this._renderTextEffect.read(reader); const widgetStart = this._start.read(reader); @@ -94,15 +125,37 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin const modifiedTopOffset = 4; const modifiedOffset = new Point(modifiedLeftOffset, modifiedTopOffset); - const originalLine = Rect.fromPoints(widgetStart, widgetEnd).withHeight(lineHeight).translateX(-scrollLeft); - const modifiedLine = Rect.fromPointSize(originalLine.getLeftBottom().add(modifiedOffset), new Point(this._edit.text.length * w, originalLine.height)); + let alternativeAction = undefined; + if (this._viewData.alternativeAction) { + const label = this._viewData.alternativeAction.label; + const count = altCount.read(reader); + const active = altModifierActive.read(reader); + const occurrencesLabel = count !== undefined ? count === 1 ? + localize('labelOccurence', "{0} 1 occurrence", label) : + localize('labelOccurences', "{0} {1} occurrences", label, count) + : label; + const keybindingTooltip = localize('shiftToSeeOccurences', "{0} show occurrences", '[shift]'); + alternativeAction = { + label: count !== undefined ? (active ? occurrencesLabel : label) : label, + tooltip: occurrencesLabel ? `${occurrencesLabel}\n${keybindingTooltip}` : undefined, + icon: undefined, //this._viewData.alternativeAction.icon, Do not render icon fo the moment + count, + keybinding: this._keybindingService.lookupKeybinding(inlineSuggestCommitAlternativeActionId), + active: altModifierActive, + }; + } + const originalLine = Rect.fromPoints(widgetStart, widgetEnd).withHeight(lineHeight).translateX(-scrollLeft); + const codeLine = Rect.fromPointSize(originalLine.getLeftBottom().add(modifiedOffset), new Point(this._viewData.edit.text.length * w, originalLine.height)); + const modifiedLine = codeLine.withWidth(codeLine.width + (alternativeAction ? alternativeAction.label.length * w + 8 + 4 + 12 : 0)); const lowerBackground = modifiedLine.withLeft(originalLine.left); // debugView(debugLogRects({ lowerBackground }, this._editor.editor.getContainerDomNode()), reader); return { + alternativeAction, originalLine, + codeLine, modifiedLine, lowerBackground, lineHeight, @@ -126,9 +179,46 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin const originalBorderColor = getOriginalBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); const modifiedBorderColor = getModifiedBorderColor(this._tabAction).map(c => asCssVariable(c)).read(reader); + this._line.style.lineHeight = `${layout.read(reader).modifiedLine.height + 2 * BORDER_WIDTH}px`; + const secondaryElementHovered = constObservable(false);//this._secondaryElement.map((e, r) => e?.isHovered.read(r) ?? false); + const alternativeAction = layout.map(l => l.alternativeAction); + const alternativeActionActive = derived(reader => (alternativeAction.read(reader)?.active.read(reader) ?? false) || secondaryElementHovered.read(reader)); + + const isHighContrast = observableFromEvent(this._themeService.onDidColorThemeChange, () => { + const theme = this._themeService.getColorTheme(); + return theme.type === 'hcDark' || theme.type === 'hcLight'; + }).read(reader); + const hcBorderColor = isHighContrast ? observeColor(contrastBorder, this._themeService).read(reader) : null; + + const primaryActiveStyles = { + borderColor: hcBorderColor ? hcBorderColor.toString() : modifiedBorderColor, + backgroundColor: asCssVariable(modifiedChangedTextOverlayColor), + color: '', + opacity: '1', + }; + + const secondaryActiveStyles = { + borderColor: hcBorderColor ? hcBorderColor.toString() : asCssVariable(inlineEditIndicatorPrimaryBorder), + backgroundColor: asCssVariable(inlineEditIndicatorPrimaryBackground), + color: asCssVariable(inlineEditIndicatorPrimaryForeground), + opacity: '1', + }; + + const passiveStyles = { + borderColor: hcBorderColor ? hcBorderColor.toString() : observeColor(editorHoverForeground, this._themeService).map(c => c.transparent(0.2).toString()).read(reader), + backgroundColor: getEditorBackgroundColor(this._viewData.editorType), + color: '', + opacity: '0.7', + }; + + const editorBackground = getEditorBackgroundColor(this._viewData.editorType); + const primaryActionStyles = derived(this, r => alternativeActionActive.read(r) ? primaryActiveStyles : primaryActiveStyles); + const secondaryActionStyles = derived(this, r => alternativeActionActive.read(r) ? secondaryActiveStyles : passiveStyles); + // TODO@benibenj clicking the arrow does not accept suggestion anymore return [ n.div({ + id: DOM_ID_OVERLAY, style: { position: 'absolute', ...rectToProps((r) => getEditorValidOverlayRect(this._editor).read(r)), @@ -140,46 +230,106 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).lowerBackground.withMargin(BORDER_WIDTH, 2 * BORDER_WIDTH, BORDER_WIDTH, 0)), - background: asCssVariable(editorBackground), - //boxShadow: `${asCssVariable(scrollbarShadow)} 0 6px 6px -6px`, + background: editorBackground, cursor: 'pointer', pointerEvents: 'auto', }, - onmousedown: e => { - e.preventDefault(); // This prevents that the editor loses focus - }, - onmouseup: (e) => this._onDidClick.fire(new StandardMouseEvent(getWindow(e), e)), - obsRef: (elem) => { - this._hoverableElement.set(elem, undefined); - } + onmousedown: (e) => this._mouseDown(e), }), n.div({ + id: DOM_ID_WIDGET, style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).modifiedLine.withMargin(BORDER_WIDTH, 2 * BORDER_WIDTH)), - fontFamily: this._editor.getOption(EditorOption.fontFamily), - fontSize: this._editor.getOption(EditorOption.fontSize), - fontWeight: this._editor.getOption(EditorOption.fontWeight), - - pointerEvents: 'none', + width: undefined, + pointerEvents: 'auto', boxSizing: 'border-box', - borderRadius: '4px', - border: `${BORDER_WIDTH}px solid ${modifiedBorderColor}`, + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, - background: asCssVariable(modifiedChangedTextOverlayColor), + background: editorBackground, display: 'flex', - justifyContent: 'center', - alignItems: 'center', + justifyContent: 'left', - outline: `2px solid ${asCssVariable(editorBackground)}`, - } - }, [this._line]), + outline: `2px solid ${editorBackground}`, + }, + onmousedown: (e) => this._mouseDown(e), + }, [ + n.div({ + id: DOM_ID_REPLACEMENT, + style: { + fontFamily: this._editor.getOption(EditorOption.fontFamily), + fontSize: this._editor.getOption(EditorOption.fontSize), + fontWeight: this._editor.getOption(EditorOption.fontWeight), + width: rectToProps(reader => layout.read(reader).codeLine.withMargin(BORDER_WIDTH, 2 * BORDER_WIDTH)).width, + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, + border: primaryActionStyles.map(s => `${BORDER_WIDTH}px solid ${s.borderColor}`), + boxSizing: 'border-box', + padding: `${BORDER_WIDTH}px`, + opacity: primaryActionStyles.map(s => s.opacity), + background: primaryActionStyles.map(s => s.backgroundColor), + display: 'flex', + justifyContent: 'left', + alignItems: 'center', + pointerEvents: 'auto', + cursor: 'pointer', + }, + obsRef: (elem) => { + this._primaryElement.set(elem, undefined); + } + }, [this._line]), + derived(this, reader => { + const altAction = alternativeAction.read(reader); + if (!altAction) { + return undefined; + } + const keybinding = document.createElement('div'); + const keybindingLabel = reader.store.add(new KeybindingLabel(keybinding, OS, { ...unthemedKeybindingLabelOptions, disableTitle: true })); + keybindingLabel.set(altAction.keybinding); + + return n.div({ + id: DOM_ID_RENAME, + style: { + position: 'relative', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, + borderTop: `${BORDER_WIDTH}px solid`, + borderRight: `${BORDER_WIDTH}px solid`, + borderBottom: `${BORDER_WIDTH}px solid`, + borderLeft: `${BORDER_WIDTH}px solid`, + borderColor: secondaryActionStyles.map(s => s.borderColor), + opacity: secondaryActionStyles.map(s => s.opacity), + color: secondaryActionStyles.map(s => s.color), + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + padding: '0 4px 0 1px', + marginLeft: '4px', + background: secondaryActionStyles.map(s => s.backgroundColor), + cursor: 'pointer', + textWrap: 'nowrap', + }, + class: 'inline-edit-alternative-action-label', + obsRef: (elem) => { + this._secondaryElement.set(elem, undefined); + }, + ref: (elem) => { + if (altAction.tooltip) { + reader.store.add(this._hoverService.setupDelayedHoverAtMouse(elem, { content: altAction.tooltip, appearance: { compact: true } })); + } + } + }, [ + keybinding, + $('div.inline-edit-alternative-action-label-separator'), + altAction.icon ? renderIcon(altAction.icon) : undefined, + altAction.label, + ]); + }) + ]), n.div({ style: { position: 'absolute', ...rectToProps(reader => layout.read(reader).originalLine.withMargin(BORDER_WIDTH)), boxSizing: 'border-box', - borderRadius: '4px', + borderRadius: `${INLINE_EDITS_BORDER_RADIUS}px`, border: `${BORDER_WIDTH}px solid ${originalBorderColor}`, background: asCssVariable(originalChangedTextOverlayColor), pointerEvents: 'none', @@ -195,7 +345,9 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin position: 'absolute', left: layout.map(l => l.modifiedLine.left - 16), top: layout.map(l => l.modifiedLine.top + Math.round((l.lineHeight - 14 - 5) / 2)), - } + pointerEvents: 'none', + }, + onmousedown: (e) => this._mouseDown(e), }, [ n.svgElem('path', { d: 'M1 0C1 2.98966 1 5.92087 1 8.49952C1 9.60409 1.89543 10.5 3 10.5H10.5', @@ -225,4 +377,24 @@ export class InlineEditsWordReplacementView extends Disposable implements IInlin private readonly _layout; private readonly _root; + + private _mouseDown(e: MouseEvent): void { + const target_id = traverseParentsUntilId(e.target as HTMLElement, new Set([DOM_ID_WIDGET, DOM_ID_REPLACEMENT, DOM_ID_RENAME, DOM_ID_OVERLAY])); + if (!target_id) { + return; + } + e.preventDefault(); // This prevents that the editor loses focus + this._onDidClick.fire(InlineEditClickEvent.create(e, target_id === DOM_ID_RENAME)); + } +} + +function traverseParentsUntilId(element: HTMLElement, ids: Set): string | null { + let current: HTMLElement | null = element; + while (current) { + if (ids.has(current.id)) { + return current.id; + } + current = current.parentElement; + } + return null; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/jumpToView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/jumpToView.ts new file mode 100644 index 00000000000..874f1efc5f1 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/jumpToView.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { n } from '../../../../../../../base/browser/dom.js'; +import { KeybindingLabel } from '../../../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; +import { RunOnceScheduler } from '../../../../../../../base/common/async.js'; +import { ResolvedKeybinding } from '../../../../../../../base/common/keybindings.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { autorun, constObservable, DebugLocation, derived, IObservable, observableFromEvent } from '../../../../../../../base/common/observable.js'; +import { OS } from '../../../../../../../base/common/platform.js'; +import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { defaultKeybindingLabelStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; +import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; +import { ObservableCodeEditor } from '../../../../../../browser/observableCodeEditor.js'; +import { Rect } from '../../../../../../common/core/2d/rect.js'; +import { Position } from '../../../../../../common/core/position.js'; +import { Range } from '../../../../../../common/core/range.js'; +import { IModelDeltaDecoration } from '../../../../../../common/model.js'; +import { inlineSuggestCommitId } from '../../../controller/commandIds.js'; +import { getEditorBlendedColor, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorPrimaryBorder, inlineEditIndicatorPrimaryForeground } from '../theme.js'; +import { rectToProps } from '../utils/utils.js'; + +export class JumpToView extends Disposable { + private readonly _style: 'label' | 'cursor'; + + constructor( + private readonly _editor: ObservableCodeEditor, + options: { style: 'label' | 'cursor' }, + private readonly _data: IObservable<{ jumpToPosition: Position } | undefined>, + @IThemeService private readonly _themeService: IThemeService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + + this._style = options.style; + this._keybinding = this._getKeybinding(inlineSuggestCommitId); + + const widget = this._widget.keepUpdated(this._store); + + this._register(this._editor.createOverlayWidget({ + domNode: widget.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + + this._register(this._editor.setDecorations(derived(reader => { + const data = this._data.read(reader); + if (!data) { + return []; + } + // use injected text at position + return [{ + range: Range.fromPositions(data.jumpToPosition, data.jumpToPosition), + options: { + description: 'inline-edit-jump-to-decoration', + inlineClassNameAffectsLetterSpacing: true, + showIfCollapsed: true, + after: { + content: this._style === 'label' ? ' ' : ' ', + } + }, + } satisfies IModelDeltaDecoration]; + }))); + } + + private readonly _styles = derived(this, reader => ({ + background: getEditorBlendedColor(inlineEditIndicatorPrimaryBackground, this._themeService).read(reader).toString(), + foreground: getEditorBlendedColor(inlineEditIndicatorPrimaryForeground, this._themeService).read(reader).toString(), + border: getEditorBlendedColor(inlineEditIndicatorPrimaryBorder, this._themeService).read(reader).toString(), + })); + + private readonly _pos = derived(this, reader => { + return this._editor.observePosition(derived(reader => + this._data.read(reader)?.jumpToPosition || null + ), reader.store); + }).flatten(); + + private _getKeybinding(commandId: string | undefined, debugLocation = DebugLocation.ofCaller()) { + if (!commandId) { + return constObservable(undefined); + } + return observableFromEvent(this, this._contextKeyService.onDidChangeContext, () => this._keybindingService.lookupKeybinding(commandId), debugLocation); + // TODO: use contextkeyservice to use different renderings + } + + private readonly _keybinding; + + private readonly _layout = derived(this, reader => { + const data = this._data.read(reader); + if (!data) { + return undefined; + } + + const position = data.jumpToPosition; + const lineHeight = this._editor.observeLineHeightForLine(constObservable(position.lineNumber)).read(reader); + const scrollLeft = this._editor.scrollLeft.read(reader); + + const point = this._pos.read(reader); + + if (!point) { + return undefined; + } + + const layout = this._editor.layoutInfo.read(reader); + + const widgetRect = Rect.fromLeftTopWidthHeight( + point.x + layout.contentLeft + 2 - scrollLeft, + point.y, + 100, + lineHeight + ); + + return { + widgetRect, + }; + }); + + private readonly _blink = animateFixedValues([ + { value: true, durationMs: 600 }, + { value: false, durationMs: 600 }, + ]); + + private readonly _widget = n.div({ + class: 'inline-edit-jump-to-widget', + style: { + position: 'absolute', + display: this._layout.map(l => l ? 'flex' : 'none'), + + alignItems: 'center', + cursor: 'pointer', + userSelect: 'none', + ...rectToProps(reader => this._layout.read(reader)?.widgetRect), + } + }, + derived(reader => { + if (this._data.read(reader) === undefined) { + return []; + } + + // Main content container with rounded border + return n.div({ + style: { + display: 'flex', + alignItems: 'center', + gap: '4px', + padding: '0 4px', + height: '100%', + backgroundColor: this._styles.map(s => s.background), + ['--vscodeIconForeground' as string]: this._styles.map(s => s.foreground), + border: this._styles.map(s => `1px solid ${s.border}`), + borderRadius: '3px', + boxSizing: 'border-box', + fontSize: '11px', + color: this._styles.map(s => s.foreground), + } + }, [ + this._style === 'cursor' ? + n.elem('div', { + style: { + borderLeft: '2px solid', + height: 14, + opacity: this._blink.map(b => b ? '0' : '1'), + } + }) : + + [ + derived(() => n.elem('div', {}, keybindingLabel(this._keybinding))), + n.elem('div', { style: { lineHeight: this._layout.map(l => l?.widgetRect.height), marginTop: '-2px' } }, + ['to jump',] + ) + ], + ]); + + }) + ); +} + +function animateFixedValues(values: { value: T; durationMs: number }[], debugLocation = DebugLocation.ofCaller()): IObservable { + let idx = 0; + return observableFromEvent(undefined, (l) => { + idx = 0; + const timer = new RunOnceScheduler(() => { + idx = (idx + 1) % values.length; + l(null); + timer.schedule(values[idx].durationMs); + }, 0); + timer.schedule(0); + + return timer; + }, () => { + return values[idx].value; + }, debugLocation); +} + +function keybindingLabel(keybinding: IObservable) { + return derived(_reader => n.div({ + style: {}, + ref: elem => { + const keybindingLabel = _reader.store.add(new KeybindingLabel(elem, OS, { + disableTitle: true, + ...defaultKeybindingLabelStyles, + keybindingLabelShadow: undefined, + keybindingLabelForeground: asCssVariable(inlineEditIndicatorPrimaryForeground), + keybindingLabelBackground: 'transparent', + keybindingLabelBorder: asCssVariable(inlineEditIndicatorPrimaryForeground), + keybindingLabelBottomBorder: undefined, + })); + _reader.store.add(autorun(reader => { + keybindingLabel.set(keybinding.read(reader)); + })); + } + })); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/inlineEditsLongDistanceHint.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/inlineEditsLongDistanceHint.ts new file mode 100644 index 00000000000..fa35b11b59e --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/inlineEditsLongDistanceHint.ts @@ -0,0 +1,584 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ChildNode, n, ObserverNode, ObserverNodeWithElement } from '../../../../../../../../base/browser/dom.js'; +import { Event } from '../../../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { IObservable, IReader, autorun, constObservable, debouncedObservable2, derived, derivedDisposable, observableFromEvent } from '../../../../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../../../../browser/observableCodeEditor.js'; +import { Rect } from '../../../../../../../common/core/2d/rect.js'; +import { Position } from '../../../../../../../common/core/position.js'; +import { ITextModel } from '../../../../../../../common/model.js'; +import { IInlineEditsView, InlineEditTabAction } from '../../inlineEditsViewInterface.js'; +import { InlineEditWithChanges } from '../../inlineEditWithChanges.js'; +import { getContentSizeOfLines, rectToProps } from '../../utils/utils.js'; +import { DetailedLineRangeMapping } from '../../../../../../../common/diff/rangeMapping.js'; +import { OffsetRange } from '../../../../../../../common/core/ranges/offsetRange.js'; +import { LineRange } from '../../../../../../../common/core/ranges/lineRange.js'; +import { HideUnchangedRegionsFeature } from '../../../../../../../browser/widget/diffEditor/features/hideUnchangedRegionsFeature.js'; +import { Codicon } from '../../../../../../../../base/common/codicons.js'; +import { renderIcon } from '../../../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { SymbolKinds } from '../../../../../../../common/languages.js'; +import { debugLogHorizontalOffsetRanges, debugLogRects, debugView } from '../debugVisualization.js'; +import { distributeFlexBoxLayout } from '../../utils/flexBoxLayout.js'; +import { Point } from '../../../../../../../common/core/2d/point.js'; +import { Size2D } from '../../../../../../../common/core/2d/size.js'; +import { IThemeService } from '../../../../../../../../platform/theme/common/themeService.js'; +import { IKeybindingService } from '../../../../../../../../platform/keybinding/common/keybinding.js'; +import { getEditorBackgroundColor, getEditorBlendedColor, inlineEditIndicatorPrimaryBackground, inlineEditIndicatorSecondaryBackground, inlineEditIndicatorSuccessfulBackground, observeColor } from '../../theme.js'; +import { asCssVariable, descriptionForeground, editorWidgetBackground } from '../../../../../../../../platform/theme/common/colorRegistry.js'; +import { editorWidgetBorder } from '../../../../../../../../platform/theme/common/colors/editorColors.js'; +import { ILongDistancePreviewProps, LongDistancePreviewEditor } from './longDistancePreviewEditor.js'; +import { InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../components/gutterIndicatorView.js'; +import { jumpToNextInlineEditId } from '../../../../controller/commandIds.js'; +import { splitIntoContinuousLineRanges, WidgetLayoutConstants, WidgetOutline, WidgetPlacementContext } from './longDistnaceWidgetPlacement.js'; +import { InlineCompletionEditorType } from '../../../../model/provideInlineCompletions.js'; +import { basename } from '../../../../../../../../base/common/resources.js'; +import { IModelService } from '../../../../../../../common/services/model.js'; +import { ILanguageService } from '../../../../../../../common/languages/language.js'; +import { getIconClasses } from '../../../../../../../common/services/getIconClasses.js'; +import { FileKind } from '../../../../../../../../platform/files/common/files.js'; +import { TextModelValueReference } from '../../../../model/textModelValueReference.js'; + +const BORDER_RADIUS = 6; +const MAX_WIDGET_WIDTH = { EMPTY_SPACE: 425, OVERLAY: 375 }; +const MIN_WIDGET_WIDTH = 250; + +const DEFAULT_WIDGET_LAYOUT_CONSTANTS: WidgetLayoutConstants = { + previewEditorMargin: 2, + widgetPadding: 2, + widgetBorder: 1, + lowerBarHeight: 20, + minWidgetWidth: MIN_WIDGET_WIDTH, +}; + +export class InlineEditsLongDistanceHint extends Disposable implements IInlineEditsView { + + private readonly _editorObs; + readonly onDidClick = Event.None; + private _viewWithElement: ObserverNodeWithElement | undefined = undefined; + + private readonly _previewEditor; + + constructor( + private readonly _editor: ICodeEditor, + private readonly _viewState: IObservable, + private readonly _previewTextModel: ITextModel, + private readonly _tabAction: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IThemeService private readonly _themeService: IThemeService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IModelService private readonly _modelService: IModelService, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this._styles = derived(reader => { + const v = this._tabAction.read(reader); + + // Check theme type by observing a color - this ensures we react to theme changes + const widgetBorderColor = observeColor(editorWidgetBorder, this._themeService).read(reader); + const isHighContrast = observableFromEvent(this._themeService.onDidColorThemeChange, () => { + const theme = this._themeService.getColorTheme(); + return theme.type === 'hcDark' || theme.type === 'hcLight'; + }).read(reader); + + let borderColor; + if (isHighContrast) { + // Use editorWidgetBorder in high contrast mode for better visibility + borderColor = widgetBorderColor; + } else { + let border; + switch (v) { + case InlineEditTabAction.Inactive: border = inlineEditIndicatorSecondaryBackground; break; + case InlineEditTabAction.Jump: border = inlineEditIndicatorPrimaryBackground; break; + case InlineEditTabAction.Accept: border = inlineEditIndicatorSuccessfulBackground; break; + } + borderColor = getEditorBlendedColor(border, this._themeService).read(reader); + } + + return { + border: borderColor.toString(), + background: getEditorBackgroundColor(this._viewState.map(s => s?.editorType ?? InlineCompletionEditorType.TextEditor).read(reader)), + }; + }); + + this._editorObs = observableCodeEditor(this._editor); + + this._previewEditor = this._register( + this._instantiationService.createInstance( + LongDistancePreviewEditor, + this._previewTextModel, + derived(reader => { + const viewState = this._viewState.read(reader); + if (!viewState) { + return undefined; + } + return { + diff: viewState.diff, + model: viewState.model, + inlineSuggestInfo: viewState.inlineSuggestInfo, + nextCursorPosition: viewState.nextCursorPosition, + target: viewState.target, + } satisfies ILongDistancePreviewProps; + }), + this._editor, + this._tabAction, + ) + ); + + this._viewWithElement = this._view.keepUpdated(this._store); + this._register(this._editorObs.createOverlayWidget({ + domNode: this._viewWithElement.element, + position: constObservable(null), + allowEditorOverflow: false, + minContentWidthInPx: constObservable(0), + })); + + this._widgetContent.get().keepUpdated(this._store); + + this._register(autorun(reader => { + const layoutInfo = this._previewEditorLayoutInfo.read(reader); + if (!layoutInfo) { + return; + } + this._previewEditor.layout(layoutInfo.codeEditorSize.toDimension(), layoutInfo.desiredPreviewEditorScrollLeft); + })); + + this._isVisibleDelayed.recomputeInitiallyAndOnChange(this._store); + } + + private readonly _styles; + + public get isHovered() { return this._widgetContent.get().didMouseMoveDuringHover; } + + private readonly _hintTextPosition = derived(this, (reader) => { + const viewState = this._viewState.read(reader); + return viewState ? new Position(viewState.hint.lineNumber, Number.MAX_SAFE_INTEGER) : null; + }); + + private readonly _lineSizesAroundHintPosition = derived(this, (reader) => { + const viewState = this._viewState.read(reader); + const p = this._hintTextPosition.read(reader); + if (!viewState || !p) { + return []; + } + + const model = this._editorObs.model.read(reader); + if (!model) { + return []; + } + const range = LineRange.ofLength(p.lineNumber, 1).addMargin(5, 5).intersect(LineRange.ofLength(1, model.getLineCount())); + + if (!range) { + return []; + } + + const sizes = getContentSizeOfLines(this._editorObs, range, reader); + const top = this._editorObs.observeTopForLineNumber(range.startLineNumber).read(reader); + + return splitIntoContinuousLineRanges(range, sizes, top, this._editorObs, reader); + }); + + private readonly _isVisibleDelayed = debouncedObservable2( + derived(this, reader => this._viewState.read(reader)?.hint.isVisible), + (lastValue, newValue) => lastValue === true && newValue === false ? 200 : 0, + ); + + private readonly _previewEditorLayoutInfo = derived(this, (reader) => { + const viewState = this._viewState.read(reader); + + if (!viewState || !this._isVisibleDelayed.read(reader)) { + return undefined; + } + + const continousLineRanges = this._lineSizesAroundHintPosition.read(reader); + if (continousLineRanges.length === 0) { + return undefined; + } + + const editorScrollTop = this._editorObs.scrollTop.read(reader); + const editorScrollLeft = this._editorObs.scrollLeft.read(reader); + const editorLayout = this._editorObs.layoutInfo.read(reader); + + const previewContentHeight = this._previewEditor.contentHeight.read(reader); + const previewEditorContentLayout = this._previewEditor.horizontalContentRangeInPreviewEditorToShow.read(reader); + + if (!previewContentHeight || !previewEditorContentLayout) { + return undefined; + } + + // const debugRects = stackSizesDown(new Point(editorLayout.contentLeft, lineSizes.top - scrollTop), lineSizes.sizes); + + const editorTrueContentWidth = editorLayout.contentWidth - editorLayout.verticalScrollbarWidth; + const editorTrueContentRight = editorLayout.contentLeft + editorTrueContentWidth; + + // drawEditorWidths(this._editor, reader); + + const c = this._editorObs.cursorLineNumber.read(reader); + if (!c) { + return undefined; + } + + const layoutConstants = DEFAULT_WIDGET_LAYOUT_CONSTANTS; + const extraGutterMarginToAvoidScrollBar = 2; + const previewEditorHeight = previewContentHeight + extraGutterMarginToAvoidScrollBar; + + // Try to find widget placement in available empty space + let possibleWidgetOutline: WidgetOutline | undefined; + let lastPlacementContext: WidgetPlacementContext | undefined; + + const endOfLinePadding = (lineNumber: number) => lineNumber === viewState.hint.lineNumber ? 40 : 20; + + for (const continousLineRange of continousLineRanges) { + const placementContext = new WidgetPlacementContext( + continousLineRange, + editorTrueContentWidth, + endOfLinePadding + ); + lastPlacementContext = placementContext; + + const showRects = false; + if (showRects) { + const rects2 = stackSizesDown( + new Point(editorTrueContentRight, continousLineRange.top - editorScrollTop), + placementContext.availableSpaceSizes as Size2D[], + 'right' + ); + debugView(debugLogRects({ ...rects2 }, this._editor.getDomNode()!), reader); + } + + possibleWidgetOutline = placementContext.tryFindWidgetOutline( + viewState.hint.lineNumber, + previewEditorHeight, + editorTrueContentRight, + layoutConstants + ); + + if (possibleWidgetOutline) { + break; + } + } + + // Fallback to overlay position if no empty space was found + let position: 'overlay' | 'empty-space' = 'empty-space'; + if (!possibleWidgetOutline) { + position = 'overlay'; + const maxAvailableWidth = Math.min(editorLayout.width - editorLayout.contentLeft, MAX_WIDGET_WIDTH.OVERLAY); + + // Create a fallback placement context for computing overlay vertical position + const fallbackPlacementContext = lastPlacementContext ?? new WidgetPlacementContext( + continousLineRanges[0], + editorTrueContentWidth, + endOfLinePadding, + ); + + possibleWidgetOutline = { + horizontalWidgetRange: OffsetRange.ofStartAndLength(editorTrueContentRight - maxAvailableWidth, maxAvailableWidth), + verticalWidgetRange: fallbackPlacementContext.getWidgetVerticalOutline( + viewState.hint.lineNumber + 2, + previewEditorHeight, + layoutConstants + ).delta(10), + }; + } + + if (!possibleWidgetOutline) { + return undefined; + } + + const rectAvailableSpace = Rect.fromRanges( + possibleWidgetOutline.horizontalWidgetRange, + possibleWidgetOutline.verticalWidgetRange + ).translateX(-editorScrollLeft).translateY(-editorScrollTop); + + const showAvailableSpace = false; + if (showAvailableSpace) { + debugView(debugLogRects({ rectAvailableSpace }, this._editor.getDomNode()!), reader); + } + + const { previewEditorMargin, widgetPadding, widgetBorder, lowerBarHeight } = layoutConstants; + const maxWidgetWidth = Math.min(position === 'overlay' ? MAX_WIDGET_WIDTH.OVERLAY : MAX_WIDGET_WIDTH.EMPTY_SPACE, previewEditorContentLayout.maxEditorWidth + previewEditorMargin + widgetPadding); + + const layout = distributeFlexBoxLayout(rectAvailableSpace.width, { + spaceBefore: { min: 0, max: 10, priority: 1 }, + content: { min: 50, rules: [{ max: 150, priority: 2 }, { max: maxWidgetWidth, priority: 1 }] }, + spaceAfter: { min: 10 }, + }); + + if (!layout) { + return null; + } + + const ranges = lengthsToOffsetRanges([layout.spaceBefore, layout.content, layout.spaceAfter], rectAvailableSpace.left); + const spaceBeforeRect = rectAvailableSpace.withHorizontalRange(ranges[0]); + const widgetRect = rectAvailableSpace.withHorizontalRange(ranges[1]); + const spaceAfterRect = rectAvailableSpace.withHorizontalRange(ranges[2]); + + const showRects2 = false; + if (showRects2) { + debugView(debugLogRects({ spaceBeforeRect, widgetRect, spaceAfterRect }, this._editor.getDomNode()!), reader); + } + + const previewEditorRect = widgetRect.withMargin(-widgetPadding - widgetBorder - previewEditorMargin).withMargin(0, 0, -lowerBarHeight, 0); + + const showEditorRect = false; + if (showEditorRect) { + debugView(debugLogRects({ previewEditorRect }, this._editor.getDomNode()!), reader); + } + + const previewEditorContentWidth = previewEditorRect.width - previewEditorContentLayout.nonContentWidth; + const maxPrefferedRangeLength = previewEditorContentWidth * 0.8; + const preferredRangeToReveal = previewEditorContentLayout.preferredRangeToReveal.intersect(OffsetRange.ofStartAndLength( + previewEditorContentLayout.preferredRangeToReveal.start, + maxPrefferedRangeLength + )) ?? previewEditorContentLayout.preferredRangeToReveal; + const desiredPreviewEditorScrollLeft = scrollToReveal(previewEditorContentLayout.indentationEnd, previewEditorContentWidth, preferredRangeToReveal); + + return { + codeEditorSize: previewEditorRect.getSize(), + codeScrollLeft: editorScrollLeft, + contentLeft: editorLayout.contentLeft, + + widgetRect, + + previewEditorMargin, + widgetPadding, + widgetBorder, + + lowerBarHeight, + + desiredPreviewEditorScrollLeft: desiredPreviewEditorScrollLeft.newScrollPosition, + }; + }); + + private readonly _view = n.div({ + class: 'inline-edits-view', + style: { + position: 'absolute', + overflow: 'visible', + top: '0px', + left: '0px', + display: derived(this, reader => !!this._previewEditorLayoutInfo.read(reader) ? 'block' : 'none'), + }, + }, [ + derived(this, _reader => [this._widgetContent]), + ]); + + private readonly _widgetContent = derived(this, reader => // TODO@hediet: remove when n.div lazily creates previewEditor.element node + n.div({ + class: ['inline-edits-long-distance-hint-widget', 'show-file-icons'], + style: { + position: 'absolute', + overflow: 'hidden', + cursor: 'pointer', + background: asCssVariable(editorWidgetBackground), + padding: this._previewEditorLayoutInfo.map(i => i?.widgetPadding), + boxSizing: 'border-box', + borderRadius: BORDER_RADIUS, + border: derived(reader => `${this._previewEditorLayoutInfo.read(reader)?.widgetBorder}px solid ${this._styles.read(reader).border}`), + display: 'flex', + flexDirection: 'column', + opacity: derived(reader => this._viewState.read(reader)?.hint.isVisible ? '1' : '0'), + transition: 'opacity 200ms ease-in-out', + ...rectToProps(reader => this._previewEditorLayoutInfo.read(reader)?.widgetRect) + }, + onmousedown: e => { + e.preventDefault(); // This prevents that the editor loses focus + }, + onclick: () => { + this._viewState.read(undefined)?.model.jump(); + } + }, [ + n.div({ + class: ['editorContainer'], + style: { + overflow: 'hidden', + padding: this._previewEditorLayoutInfo.map(i => i?.previewEditorMargin), + background: this._styles.map(s => s.background), + pointerEvents: 'none', + }, + }, [ + derived(this, r => this._previewEditor.element), // -- + ]), + n.div({ class: 'bar', style: { color: asCssVariable(descriptionForeground), pointerEvents: 'none', margin: '0 4px', height: this._previewEditorLayoutInfo.map(i => i?.lowerBarHeight), display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, [ + derived(this, reader => { + const children: (HTMLElement | ObserverNode)[] = []; + const viewState = this._viewState.read(reader); + if (!viewState) { + return children; + } + + // Check if this is a cross-file edit + const currentUri = this._editorObs.model.read(reader)?.uri; + const targetUri = viewState.target.uri; + const isCrossFileEdit = targetUri && (!currentUri || targetUri.toString() !== currentUri.toString()); + + if (isCrossFileEdit) { + // For cross-file edits, show target filename instead of outline + const fileName = basename(targetUri); + const iconClasses = getIconClasses(this._modelService, this._languageService, targetUri, FileKind.FILE); + children.push(n.div({ + class: 'target-file', + style: { display: 'flex', alignItems: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, + }, [ + n.elem('span', { class: iconClasses, style: { flexShrink: '0', marginRight: '4px' } }), + fileName, + ])); + } else { + // Outline Element + const source = this._originalOutlineSource.read(reader); + const originalTargetLineNumber = this._originalTargetLineNumber.read(reader); + const outlineItems = source?.getAt(originalTargetLineNumber, reader).slice(0, 1) ?? []; + const outlineElements: ChildNode[] = []; + if (outlineItems.length > 0) { + for (let i = 0; i < outlineItems.length; i++) { + const item = outlineItems[i]; + const icon = SymbolKinds.toIcon(item.kind); + outlineElements.push(n.div({ + class: 'breadcrumb-item', + style: { display: 'flex', alignItems: 'center', flex: '1 1 auto', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, + }, [ + renderIcon(icon), + '\u00a0', + item.name, + ...(i === outlineItems.length - 1 + ? [] + : [renderIcon(Codicon.chevronRight)] + ) + ])); + } + } + children.push(n.div({ class: 'outline-elements', style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, outlineElements)); + } + + // Show Edit Direction + const originalTargetLineNumber = this._originalTargetLineNumber.read(reader); + const arrowIcon = isCrossFileEdit ? Codicon.arrowRight : (viewState.hint.lineNumber < originalTargetLineNumber ? Codicon.arrowDown : Codicon.arrowUp); + const keybinding = this._keybindingService.lookupKeybinding(jumpToNextInlineEditId); + let label = isCrossFileEdit ? 'Go to file' : 'Go to suggestion'; + if (keybinding && keybinding.getLabel() === 'Tab') { + label = isCrossFileEdit ? 'Tab to open' : 'Tab to jump'; + } + children.push(n.div({ + class: 'go-to-label', + style: { position: 'relative', display: 'flex', alignItems: 'center', flex: '0 0 auto', paddingLeft: '6px' }, + }, [ + label, + '\u00a0', + renderIcon(arrowIcon), + ])); + + return children; + }) + ]), + ]) + ); + + // Drives breadcrumbs and symbol icon + private readonly _originalTargetLineNumber = derived(this, (reader) => { + const viewState = this._viewState.read(reader); + if (!viewState) { + return -1; + } + + if (viewState.edit.action?.kind === 'jumpTo') { + return viewState.edit.action.position.lineNumber; + } + + return viewState.diff[0]?.original.startLineNumber ?? -1; + }); + + private readonly _originalOutlineSource = derivedDisposable(this, (reader) => { + const m = this._editorObs.model.read(reader); + const factory = HideUnchangedRegionsFeature._breadcrumbsSourceFactory.read(reader); + return (!m || !factory) ? undefined : factory(m, this._instantiationService); + }); +} + +export interface ILongDistanceHint { + lineNumber: number; + isVisible: boolean; +} + +export interface ILongDistanceViewState { + hint: ILongDistanceHint; + newTextLineCount: number; + edit: InlineEditWithChanges; + diff: DetailedLineRangeMapping[]; + nextCursorPosition: Position | null; + editorType: InlineCompletionEditorType; + target: TextModelValueReference; + + model: SimpleInlineSuggestModel; + inlineSuggestInfo: InlineSuggestionGutterMenuData; +} + +function lengthsToOffsetRanges(lengths: number[], initialOffset = 0): OffsetRange[] { + const result: OffsetRange[] = []; + let offset = initialOffset; + for (const length of lengths) { + result.push(new OffsetRange(offset, offset + length)); + offset += length; + } + return result; +} + +function stackSizesDown(at: Point, sizes: Size2D[], alignment: 'left' | 'right' = 'left'): Rect[] { + const rects: Rect[] = []; + let offset = 0; + for (const s of sizes) { + rects.push( + Rect.fromLeftTopWidthHeight( + at.x + (alignment === 'left' ? 0 : -s.width), + at.y + offset, + s.width, + s.height + ) + ); + offset += s.height; + } + return rects; +} + + + +export function drawEditorWidths(e: ICodeEditor, reader: IReader) { + const layoutInfo = e.getLayoutInfo(); + const contentLeft = new OffsetRange(0, layoutInfo.contentLeft); + const trueContent = OffsetRange.ofStartAndLength(layoutInfo.contentLeft, layoutInfo.contentWidth - layoutInfo.verticalScrollbarWidth); + const minimap = OffsetRange.ofStartAndLength(trueContent.endExclusive, layoutInfo.minimap.minimapWidth); + const verticalScrollbar = OffsetRange.ofStartAndLength(minimap.endExclusive, layoutInfo.verticalScrollbarWidth); + + const r = new OffsetRange(0, 200); + debugView(debugLogHorizontalOffsetRanges({ + contentLeft: Rect.fromRanges(contentLeft, r), + trueContent: Rect.fromRanges(trueContent, r), + minimap: Rect.fromRanges(minimap, r), + verticalScrollbar: Rect.fromRanges(verticalScrollbar, r), + }, e.getDomNode()!), reader); +} + + +/** + * Changes the scroll position as little as possible just to reveal the given range in the window. +*/ +export function scrollToReveal(currentScrollPosition: number, windowWidth: number, contentRangeToReveal: OffsetRange): { newScrollPosition: number } { + const visibleRange = new OffsetRange(currentScrollPosition, currentScrollPosition + windowWidth); + if (visibleRange.containsRange(contentRangeToReveal)) { + return { newScrollPosition: currentScrollPosition }; + } + if (contentRangeToReveal.length > windowWidth) { + return { newScrollPosition: contentRangeToReveal.start }; + } + if (contentRangeToReveal.endExclusive > visibleRange.endExclusive) { + return { newScrollPosition: contentRangeToReveal.endExclusive - windowWidth }; + } + if (contentRangeToReveal.start < visibleRange.start) { + return { newScrollPosition: contentRangeToReveal.start }; + } + return { newScrollPosition: currentScrollPosition }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistancePreviewEditor.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistancePreviewEditor.ts new file mode 100644 index 00000000000..b4df474593b --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistancePreviewEditor.ts @@ -0,0 +1,407 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { n } from '../../../../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { clamp } from '../../../../../../../../base/common/numbers.js'; +import { IObservable, derived, constObservable, IReader, autorun, observableValue } from '../../../../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../../../../browser/editorBrowser.js'; +import { ObservableCodeEditor, observableCodeEditor } from '../../../../../../../browser/observableCodeEditor.js'; +import { EmbeddedCodeEditorWidget } from '../../../../../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; +import { IDimension } from '../../../../../../../common/core/2d/dimension.js'; +import { Position } from '../../../../../../../common/core/position.js'; +import { Range } from '../../../../../../../common/core/range.js'; +import { LineRange } from '../../../../../../../common/core/ranges/lineRange.js'; +import { OffsetRange } from '../../../../../../../common/core/ranges/offsetRange.js'; +import { DetailedLineRangeMapping } from '../../../../../../../common/diff/rangeMapping.js'; +import { IModelDeltaDecoration, ITextModel } from '../../../../../../../common/model.js'; +import { ModelDecorationOptions } from '../../../../../../../common/model/textModel.js'; +import { InlineCompletionContextKeys } from '../../../../controller/inlineCompletionContextKeys.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../components/gutterIndicatorView.js'; +import { InlineEditTabAction } from '../../inlineEditsViewInterface.js'; +import { classNames, maxContentWidthInRange } from '../../utils/utils.js'; +import { JumpToView } from '../jumpToView.js'; +import { TextModelValueReference } from '../../../../model/textModelValueReference.js'; + +export interface ILongDistancePreviewProps { + nextCursorPosition: Position | null; // assert: nextCursorPosition !== null xor diff.length > 0 + diff: DetailedLineRangeMapping[]; + model: SimpleInlineSuggestModel; + inlineSuggestInfo: InlineSuggestionGutterMenuData; + /** + * The URI of the file the edit targets. + * When undefined (or same as the editor's model URI), the edit targets the current file. + */ + target: TextModelValueReference; +} + +export class LongDistancePreviewEditor extends Disposable { + public readonly previewEditor; + private readonly _previewEditorObs; + + private readonly _previewRef = n.ref(); + public readonly element = n.div({ class: 'preview', style: { /*pointerEvents: 'none'*/ }, ref: this._previewRef }); + + private _parentEditorObs: ObservableCodeEditor; + + constructor( + private readonly _previewTextModel: ITextModel, + private readonly _properties: IObservable, + private readonly _parentEditor: ICodeEditor, + private readonly _tabAction: IObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + + this.previewEditor = this._register(this._createPreviewEditor()); + this._parentEditorObs = observableCodeEditor(this._parentEditor); + + this._register(autorun(reader => { + const tm = this._state.read(reader)?.textModel || null; + + if (tm) { + // Avoid transitions from tm -> null -> tm, where tm -> tm would be a no-op. + this.previewEditor.setModel(tm.dangerouslyGetUnderlyingModel()); + } + })); + + this._previewEditorObs = observableCodeEditor(this.previewEditor); + this._register(this._previewEditorObs.setDecorations(derived(reader => { + const state = this._state.read(reader); + const decorations = this._editorDecorations.read(reader); + return (state?.mode === 'original' ? decorations?.originalDecorations : decorations?.modifiedDecorations) ?? []; + }))); + + const showJumpToDecoration = false; + + if (showJumpToDecoration) { + this._register(this._instantiationService.createInstance(JumpToView, this._previewEditorObs, { style: 'cursor' }, derived(reader => { + const p = this._properties.read(reader); + if (!p || !p.nextCursorPosition) { + return undefined; + } + return { + jumpToPosition: p.nextCursorPosition, + + }; + }))); + } + + // Mirror the cursor position. Allows the gutter arrow to point in the correct direction. + this._register(autorun((reader) => { + if (!this._properties.read(reader)) { + return; + } + const cursorPosition = this._parentEditorObs.cursorPosition.read(reader); + if (cursorPosition) { + this.previewEditor.setPosition(this._previewTextModel.validatePosition(cursorPosition), 'longDistanceHintPreview'); + } + })); + + this._register(autorun(reader => { + const state = this._state.read(reader); + if (!state) { + return; + } + // Ensure there is enough space to the left of the line number for the gutter indicator to fits. + const lineNumberDigets = state.visibleLineRange.startLineNumber.toString().length; + this.previewEditor.updateOptions({ lineNumbersMinChars: lineNumberDigets + 1 }); + })); + + this._register(this._instantiationService.createInstance( + InlineEditsGutterIndicator, + this._previewEditorObs, + derived(reader => { + const state = this._state.read(reader); + if (!state) { return undefined; } + const props = this._properties.read(reader); + if (!props) { return undefined; } + return new InlineEditsGutterIndicatorData( + props.inlineSuggestInfo, + LineRange.ofLength(state.visibleLineRange.startLineNumber, 1), + props.model, + undefined, + ); + }), + this._tabAction, + constObservable(0), + constObservable(false), + observableValue(this, false), + )); + + this.updatePreviewEditorEffect.recomputeInitiallyAndOnChange(this._store); + } + + private readonly _state = derived<{ + mode: 'original' | 'modified'; + visibleLineRange: LineRange; + textModel: TextModelValueReference | undefined; + diff: DetailedLineRangeMapping[]; + } | undefined>(this, reader => { + const props = this._properties.read(reader); + if (!props) { + return undefined; + } + + let mode: 'original' | 'modified'; + let visibleRange: LineRange; + + if (props.nextCursorPosition !== null) { + mode = 'original'; + visibleRange = LineRange.ofLength(props.nextCursorPosition.lineNumber, 1); + } else { + if (props.diff[0].innerChanges?.every(c => c.modifiedRange.isEmpty())) { + mode = 'original'; + visibleRange = LineRange.ofLength(props.diff[0].original.startLineNumber, 1); + } else { + mode = 'modified'; + visibleRange = LineRange.ofLength(props.diff[0].modified.startLineNumber, 1); + } + } + + const textModel = mode === 'modified' + ? TextModelValueReference.snapshot(this._previewTextModel) + : props.target; + + return { + mode, + visibleLineRange: visibleRange, + textModel, + diff: props.diff, + }; + }); + + private _createPreviewEditor() { + return this._instantiationService.createInstance( + EmbeddedCodeEditorWidget, + this._previewRef.element, + { + glyphMargin: false, + lineNumbers: 'on', + minimap: { enabled: false }, + guides: { + indentation: false, + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveIndentation: false, + }, + editContext: false, // is a bit faster + rulers: [], + padding: { top: 0, bottom: 0 }, + //folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + columnSelection: false, + overviewRulerBorder: false, + overviewRulerLanes: 0, + //lineDecorationsWidth: 0, + //lineNumbersMinChars: 0, + revealHorizontalRightPadding: 0, + bracketPairColorization: { enabled: true, independentColorPoolPerBracketType: false }, + scrollBeyondLastLine: false, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + handleMouseWheel: false, + }, + readOnly: true, + wordWrap: 'off', + wordWrapOverride1: 'off', + wordWrapOverride2: 'off', + }, + { + contextKeyValues: { + [InlineCompletionContextKeys.inInlineEditsPreviewEditor.key]: true, + }, + contributions: [], + }, + this._parentEditor + ); + } + + public readonly updatePreviewEditorEffect = derived(this, reader => { + // this._widgetContent.readEffect(reader); + this._previewEditorObs.model.read(reader); // update when the model is set + + const range = this._state.read(reader)?.visibleLineRange; + if (!range) { + return; + } + const hiddenAreas: Range[] = []; + if (range.startLineNumber > 1) { + hiddenAreas.push(new Range(1, 1, range.startLineNumber - 1, 1)); + } + if (range.endLineNumberExclusive < this._previewTextModel.getLineCount() + 1) { + hiddenAreas.push(new Range(range.endLineNumberExclusive, 1, this._previewTextModel.getLineCount() + 1, 1)); + } + this.previewEditor.setHiddenAreas(hiddenAreas, undefined, true); + }); + + public readonly horizontalContentRangeInPreviewEditorToShow = derived(this, reader => { + return this._getHorizontalContentRangeInPreviewEditorToShow(this.previewEditor, reader); + }); + + public readonly contentHeight = derived(this, (reader) => { + const viewState = this._state.read(reader); + if (!viewState) { + return constObservable(null); + } + + const previewEditorHeight = this._previewEditorObs.observeLineHeightForLine(viewState.visibleLineRange.startLineNumber); + return previewEditorHeight; + }).flatten(); + + private _getHorizontalContentRangeInPreviewEditorToShow(editor: ICodeEditor, reader: IReader) { + const state = this._state.read(reader); + if (!state) { return undefined; } + + const diff = state.diff; + const jumpToPos = this._properties.read(reader)?.nextCursorPosition; + + const visibleRange = state.visibleLineRange; + const l = this._previewEditorObs.layoutInfo.read(reader); + const trueContentWidth = maxContentWidthInRange(this._previewEditorObs, visibleRange, reader); + + let firstCharacterChange: Range; + if (jumpToPos) { + firstCharacterChange = Range.fromPositions(jumpToPos); + } else if (diff[0].innerChanges) { + firstCharacterChange = state.mode === 'modified' ? diff[0].innerChanges[0].modifiedRange : diff[0].innerChanges[0].originalRange; + } else { + return undefined; + } + + + // find the horizontal range we want to show. + const preferredRange = growUntilVariableBoundaries(editor.getModel()!, firstCharacterChange, 5); + const leftOffset = this._previewEditorObs.getLeftOfPosition(preferredRange.getStartPosition(), reader); + const rightOffset = this._previewEditorObs.getLeftOfPosition(preferredRange.getEndPosition(), reader); + + const left = clamp(leftOffset, 0, trueContentWidth); + const right = clamp(rightOffset, left, trueContentWidth); + + const indentCol = editor.getModel()!.getLineFirstNonWhitespaceColumn(preferredRange.startLineNumber); + const indentationEnd = this._previewEditorObs.getLeftOfPosition(new Position(preferredRange.startLineNumber, indentCol), reader); + + const preferredRangeToReveal = new OffsetRange(left, right); + + return { + indentationEnd, + preferredRangeToReveal, + maxEditorWidth: trueContentWidth + l.contentLeft, + contentWidth: trueContentWidth, + nonContentWidth: l.contentLeft, // Width of area that is not content + }; + } + + public layout(dimension: IDimension, desiredPreviewEditorScrollLeft: number): void { + this.previewEditor.layout(dimension); + this._previewEditorObs.editor.setScrollLeft(desiredPreviewEditorScrollLeft); + } + + private readonly _editorDecorations = derived(this, reader => { + const state = this._state.read(reader); + if (!state) { return undefined; } + + const diff = { + mode: 'insertionInline' as const, + diff: state.diff, + }; + const originalDecorations: IModelDeltaDecoration[] = []; + const modifiedDecorations: IModelDeltaDecoration[] = []; + + const diffWholeLineDeleteDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-delete', + description: 'char-delete', + isWholeLine: false, + zIndex: 1, // be on top of diff background decoration + }); + + const diffWholeLineAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + isWholeLine: true, + }); + + const diffAddDecoration = ModelDecorationOptions.register({ + className: 'inlineCompletions-char-insert', + description: 'char-insert', + shouldFillLineOnLineBreak: true, + }); + + const hideEmptyInnerDecorations = true; // diff.mode === 'lineReplacement'; + for (const m of diff.diff) { + if (m.modified.isEmpty || m.original.isEmpty) { + if (!m.original.isEmpty) { + originalDecorations.push({ range: m.original.toInclusiveRange()!, options: diffWholeLineDeleteDecoration }); + } + if (!m.modified.isEmpty) { + modifiedDecorations.push({ range: m.modified.toInclusiveRange()!, options: diffWholeLineAddDecoration }); + } + } else { + for (const i of m.innerChanges || []) { + // Don't show empty markers outside the line range + if (m.original.contains(i.originalRange.startLineNumber) && !(hideEmptyInnerDecorations && i.originalRange.isEmpty())) { + originalDecorations.push({ + range: i.originalRange, + options: { + description: 'char-delete', + shouldFillLineOnLineBreak: false, + className: classNames( + 'inlineCompletions-char-delete', + // i.originalRange.isSingleLine() && diff.mode === 'insertionInline' && 'single-line-inline', + i.originalRange.isEmpty() && 'empty', + ), + zIndex: 1 + } + }); + } + if (m.modified.contains(i.modifiedRange.startLineNumber)) { + modifiedDecorations.push({ + range: i.modifiedRange, + options: diffAddDecoration + }); + } + } + } + } + + return { originalDecorations, modifiedDecorations }; + }); +} + +/* + * Grows the range on each ends until it includes a none-variable-name character + * or the next character would be a whitespace character + * or the maxGrow limit is reached + */ +function growUntilVariableBoundaries(textModel: ITextModel, range: Range, maxGrow: number): Range { + const startPosition = range.getStartPosition(); + const endPosition = range.getEndPosition(); + const line = textModel.getLineContent(startPosition.lineNumber); + + function isVariableNameCharacter(col: number): boolean { + const char = line.charAt(col - 1); + return (/[a-zA-Z0-9_]/).test(char); + } + + function isWhitespace(col: number): boolean { + const char = line.charAt(col - 1); + return char === ' ' || char === '\t'; + } + + let startColumn = startPosition.column; + while (startColumn > 1 && isVariableNameCharacter(startColumn) && !isWhitespace(startColumn - 1) && startPosition.column - startColumn < maxGrow) { + startColumn--; + } + + let endColumn = endPosition.column - 1; + while (endColumn <= line.length && isVariableNameCharacter(endColumn) && !isWhitespace(endColumn + 1) && endColumn - endPosition.column < maxGrow) { + endColumn++; + } + + return new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endColumn + 1); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistnaceWidgetPlacement.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistnaceWidgetPlacement.ts new file mode 100644 index 00000000000..79f910cc91f --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistnaceWidgetPlacement.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { derived, IReader } from '../../../../../../../../base/common/observable.js'; +import { ObservableCodeEditor } from '../../../../../../../browser/observableCodeEditor.js'; +import { Size2D } from '../../../../../../../common/core/2d/size.js'; +import { LineRange } from '../../../../../../../common/core/ranges/lineRange.js'; +import { OffsetRange } from '../../../../../../../common/core/ranges/offsetRange.js'; +import { getMaxTowerHeightInAvailableArea } from '../../utils/towersLayout.js'; + +/** + * Layout constants used for the long-distance hint widget. + */ +export interface WidgetLayoutConstants { + readonly previewEditorMargin: number; + readonly widgetPadding: number; + readonly widgetBorder: number; + readonly lowerBarHeight: number; + readonly minWidgetWidth: number; +} +/** + * Represents a widget placement outline with horizontal and vertical ranges. + */ +export interface WidgetOutline { + readonly horizontalWidgetRange: OffsetRange; + readonly verticalWidgetRange: OffsetRange; +} +/** + * Represents a continuous range of lines with their sizes and positioning. + * Used to compute available space for widget placement. + */ +export interface ContinuousLineSizes { + readonly lineRange: LineRange; + readonly top: number; + readonly sizes: Size2D[]; +} +/** + * Context for computing widget placement within a continuous line range. + */ +export class WidgetPlacementContext { + public readonly availableSpaceSizes: Size2D[]; + public readonly availableSpaceHeightPrefixSums: number[]; + public readonly availableSpaceSizesTransposed: Size2D[]; + + constructor( + private readonly _lineRangeInfo: ContinuousLineSizes, + editorTrueContentWidth: number, + endOfLinePadding: (lineNumber: number) => number, + ) { + this.availableSpaceSizes = _lineRangeInfo.sizes.map((s, idx) => { + const lineNumber = _lineRangeInfo.lineRange.startLineNumber + idx; + const linePaddingLeft = endOfLinePadding(lineNumber); + return new Size2D(Math.max(0, editorTrueContentWidth - s.width - linePaddingLeft), s.height); + }); + + this.availableSpaceHeightPrefixSums = getSums(this.availableSpaceSizes, s => s.height); + this.availableSpaceSizesTransposed = this.availableSpaceSizes.map(s => s.transpose()); + } + + /** + * Computes the vertical outline for a widget placed at the given line number. + */ + public getWidgetVerticalOutline( + lineNumber: number, + previewEditorHeight: number, + layoutConstants: WidgetLayoutConstants + ): OffsetRange { + const sizeIdx = lineNumber - this._lineRangeInfo.lineRange.startLineNumber; + const top = this._lineRangeInfo.top + this.availableSpaceHeightPrefixSums[sizeIdx]; + const editorRange = OffsetRange.ofStartAndLength(top, previewEditorHeight); + const { previewEditorMargin, widgetPadding, widgetBorder, lowerBarHeight } = layoutConstants; + const verticalWidgetRange = editorRange.withMargin(previewEditorMargin + widgetPadding + widgetBorder).withMargin(0, lowerBarHeight); + return verticalWidgetRange; + } + + /** + * Tries to find a valid widget outline within this line range context. + */ + public tryFindWidgetOutline( + targetLineNumber: number, + previewEditorHeight: number, + editorTrueContentRight: number, + layoutConstants: WidgetLayoutConstants + ): WidgetOutline | undefined { + if (this._lineRangeInfo.lineRange.length < 3) { + return undefined; + } + return findFirstMinimzeDistance( + this._lineRangeInfo.lineRange.addMargin(-1, -1), + targetLineNumber, + lineNumber => { + const verticalWidgetRange = this.getWidgetVerticalOutline(lineNumber, previewEditorHeight, layoutConstants); + const maxWidth = getMaxTowerHeightInAvailableArea( + verticalWidgetRange.delta(-this._lineRangeInfo.top), + this.availableSpaceSizesTransposed + ); + if (maxWidth < layoutConstants.minWidgetWidth) { + return undefined; + } + const horizontalWidgetRange = OffsetRange.ofStartAndLength(editorTrueContentRight - maxWidth, maxWidth); + return { horizontalWidgetRange, verticalWidgetRange }; + } + ); + } +} +/** + * Splits line size information into continuous ranges, breaking at positions where + * the expected vertical position differs from the actual position (e.g., due to folded regions). + */ +export function splitIntoContinuousLineRanges( + lineRange: LineRange, + sizes: Size2D[], + top: number, + editorObs: ObservableCodeEditor, + reader: IReader, +): ContinuousLineSizes[] { + const result: ContinuousLineSizes[] = []; + let currentRangeStart = lineRange.startLineNumber; + let currentRangeTop = top; + let currentSizes: Size2D[] = []; + + for (let i = 0; i < sizes.length; i++) { + const lineNumber = lineRange.startLineNumber + i; + const expectedTop = currentRangeTop + currentSizes.reduce((p, c) => p + c.height, 0); + const actualTop = editorObs.editor.getTopForLineNumber(lineNumber); + + if (i > 0 && actualTop !== expectedTop) { + // Discontinuity detected - push the current range and start a new one + result.push({ + lineRange: LineRange.ofLength(currentRangeStart, lineNumber - currentRangeStart), + top: currentRangeTop, + sizes: currentSizes, + }); + currentRangeStart = lineNumber; + currentRangeTop = actualTop; + currentSizes = []; + } + currentSizes.push(sizes[i]); + } + + // Push the final range + result.push({ + lineRange: LineRange.ofLength(currentRangeStart, lineRange.endLineNumberExclusive - currentRangeStart), + top: currentRangeTop, + sizes: currentSizes, + }); + + // Don't observe each line individually for performance reasons + derived({ owner: 'splitIntoContinuousLineRanges' }, r => { + return editorObs.observeTopForLineNumber(lineRange.endLineNumberExclusive - 1).read(r); + }).read(reader); + + return result; +} + +function findFirstMinimzeDistance(range: LineRange, targetLine: number, predicate: (lineNumber: number) => T | undefined): T | undefined { + for (let offset = 0; ; offset++) { + const down = targetLine + offset; + if (down <= range.endLineNumberExclusive) { + const result = predicate(down); + if (result !== undefined) { + return result; + } + } + const up = targetLine - offset; + if (up >= range.startLineNumber) { + const result = predicate(up); + if (result !== undefined) { + return result; + } + } + if (up < range.startLineNumber && down > range.endLineNumberExclusive) { + return undefined; + } + } +} + +function getSums(array: T[], fn: (item: T) => number): number[] { + const result: number[] = [0]; + let sum = 0; + for (const item of array) { + sum += fn(item); + result.push(sum); + } + return result; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts index 81c55e7d5ea..c86dc3a9ae5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViews/originalEditorInlineDiffView.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMouseEvent } from '../../../../../../../base/browser/mouseEvent.js'; import { Emitter } from '../../../../../../../base/common/event.js'; import { Disposable } from '../../../../../../../base/common/lifecycle.js'; import { autorunWithStore, derived, IObservable, observableFromEvent } from '../../../../../../../base/common/observable.js'; @@ -16,14 +15,15 @@ import { AbstractText } from '../../../../../../common/core/text/abstractText.js import { DetailedLineRangeMapping } from '../../../../../../common/diff/rangeMapping.js'; import { EndOfLinePreference, IModelDeltaDecoration, InjectedTextCursorStops, ITextModel } from '../../../../../../common/model.js'; import { ModelDecorationOptions } from '../../../../../../common/model/textModel.js'; -import { IInlineEditsView } from '../inlineEditsViewInterface.js'; +import { IInlineEditsView, InlineEditClickEvent } from '../inlineEditsViewInterface.js'; import { classNames } from '../utils/utils.js'; +import { InlineCompletionEditorType } from '../../../model/provideInlineCompletions.js'; export interface IOriginalEditorInlineDiffViewState { diff: DetailedLineRangeMapping[]; modifiedText: AbstractText; mode: 'insertionInline' | 'sideBySide' | 'deletion' | 'lineReplacement'; - isInDiffEditor: boolean; + editorType: InlineCompletionEditorType; modifiedCodeEditor: ICodeEditor; } @@ -33,8 +33,8 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE return allowsTrueInlineDiffRendering(mapping); } - private readonly _onDidClick; - readonly onDidClick; + private readonly _onDidClick = this._register(new Emitter()); + readonly onDidClick = this._onDidClick.event; readonly isHovered; @@ -46,8 +46,6 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE private readonly _modifiedTextModel: ITextModel, ) { super(); - this._onDidClick = this._register(new Emitter()); - this.onDidClick = this._onDidClick.event; this.isHovered = observableCodeEditor(this._originalEditor).isTargetHovered( p => p.target.type === MouseTargetType.CONTENT_TEXT && p.target.detail.injectedText?.options.attachedData instanceof InlineEditAttachedData && @@ -212,7 +210,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE } } - if (diff.isInDiffEditor) { + if (diff.editorType === InlineCompletionEditorType.DiffEditor) { for (const m of diff.diff) { if (!m.original.isEmpty) { originalDecorations.push({ @@ -242,7 +240,7 @@ export class OriginalEditorInlineDiffView extends Disposable implements IInlineE } const a = e.target.detail.injectedText?.options.attachedData; if (a instanceof InlineEditAttachedData && a.owner === this) { - this._onDidClick.fire(e.event); + this._onDidClick.fire(new InlineEditClickEvent(e.event)); } })); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts index d398a4a56b8..af04a4122d9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/theme.ts @@ -3,13 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { assertNever } from '../../../../../../base/common/assert.js'; import { Color } from '../../../../../../base/common/color.js'; import { BugIndicatingError } from '../../../../../../base/common/errors.js'; import { IObservable, observableFromEventOpts } from '../../../../../../base/common/observable.js'; import { localize } from '../../../../../../nls.js'; -import { buttonBackground, buttonForeground, buttonSecondaryBackground, buttonSecondaryForeground, diffInserted, diffInsertedLine, diffRemoved, editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; -import { ColorIdentifier, darken, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; +import { buttonBackground, buttonForeground, diffInserted, diffInsertedLine, diffRemoved, editorBackground, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { asCssVariable, ColorIdentifier, darken, registerColor, transparent } from '../../../../../../platform/theme/common/colorUtils.js'; import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; +import { InlineCompletionEditorType } from '../../model/provideInlineCompletions.js'; import { InlineEditTabAction } from './inlineEditsViewInterface.js'; export const originalBackgroundColor = registerColor( @@ -83,33 +85,33 @@ export const inlineEditIndicatorPrimaryBackground = registerColor( export const inlineEditIndicatorSecondaryForeground = registerColor( 'inlineEdit.gutterIndicator.secondaryForeground', - buttonSecondaryForeground, + editorHoverForeground, localize('inlineEdit.gutterIndicator.secondaryForeground', 'Foreground color for the secondary inline edit gutter indicator.') ); export const inlineEditIndicatorSecondaryBorder = registerColor( 'inlineEdit.gutterIndicator.secondaryBorder', - buttonSecondaryBackground, + editorHoverBorder, localize('inlineEdit.gutterIndicator.secondaryBorder', 'Border color for the secondary inline edit gutter indicator.') ); export const inlineEditIndicatorSecondaryBackground = registerColor( 'inlineEdit.gutterIndicator.secondaryBackground', - inlineEditIndicatorSecondaryBorder, + editorHoverBackground, localize('inlineEdit.gutterIndicator.secondaryBackground', 'Background color for the secondary inline edit gutter indicator.') ); -export const inlineEditIndicatorsuccessfulForeground = registerColor( +export const inlineEditIndicatorSuccessfulForeground = registerColor( 'inlineEdit.gutterIndicator.successfulForeground', buttonForeground, localize('inlineEdit.gutterIndicator.successfulForeground', 'Foreground color for the successful inline edit gutter indicator.') ); -export const inlineEditIndicatorsuccessfulBorder = registerColor( +export const inlineEditIndicatorSuccessfulBorder = registerColor( 'inlineEdit.gutterIndicator.successfulBorder', buttonBackground, localize('inlineEdit.gutterIndicator.successfulBorder', 'Border color for the successful inline edit gutter indicator.') ); -export const inlineEditIndicatorsuccessfulBackground = registerColor( +export const inlineEditIndicatorSuccessfulBackground = registerColor( 'inlineEdit.gutterIndicator.successfulBackground', - inlineEditIndicatorsuccessfulBorder, + inlineEditIndicatorSuccessfulBorder, localize('inlineEdit.gutterIndicator.successfulBackground', 'Background color for the successful inline edit gutter indicator.') ); @@ -191,6 +193,22 @@ export function getEditorBlendedColor(colorIdentifier: ColorIdentifier | IObserv return color.map((c, reader) => /** @description makeOpaque */ c.makeOpaque(backgroundColor.read(reader))); } +export function getEditorBackgroundColor(editorType: InlineCompletionEditorType): string { + let color; + switch (editorType) { + case InlineCompletionEditorType.TextEditor: + color = editorBackground; break; + case InlineCompletionEditorType.DiffEditor: + color = editorBackground; break; + case InlineCompletionEditorType.Notebook: + color = 'notebook.cellEditorBackground'; break; + default: + assertNever(editorType, 'Not supported editor type yet'); + } + return asCssVariable(color); +} + + export function observeColor(colorIdentifier: ColorIdentifier, themeService: IThemeService): IObservable { return observableFromEventOpts( { @@ -208,3 +226,6 @@ export function observeColor(colorIdentifier: ColorIdentifier, themeService: ITh } ); } + +// Styles +export const INLINE_EDITS_BORDER_RADIUS = 3; // also used in CSS file diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/flexBoxLayout.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/flexBoxLayout.ts new file mode 100644 index 00000000000..18de06150ca --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/flexBoxLayout.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IFlexBoxPartGrowthRule extends IFlexBoxPartExtensionRule { + min?: number; + rules?: IFlexBoxPartExtensionRule[]; +} + +export interface IFlexBoxPartExtensionRule { + max?: number; + priority?: number; + share?: number; +} + + +/** + * Distributes a total size into parts that each have a list of growth rules. + * Returns `null` if the layout is not possible. + * The sum of all returned sizes will be equal to `totalSize`. + * + * First, each part gets its minimum size. + * Then, remaining space is distributed to the rules with the highest priority, as long as the max constraint allows it (considering share). + * This continues with next lower priority rules until no space is left. +*/ +export function distributeFlexBoxLayout>( + totalSize: number, + parts: T & Record +): Record | null { + // Normalize parts to always have array of rules + const normalizedParts: Record = {}; + for (const [key, part] of Object.entries(parts)) { + if (Array.isArray(part)) { + normalizedParts[key] = { min: 0, rules: part }; + } else { + normalizedParts[key] = { + min: part.min ?? 0, + rules: part.rules ?? [{ max: part.max, priority: part.priority, share: part.share }] + }; + } + } + + // Initialize result with minimum sizes + const result: Record = {}; + let usedSize = 0; + for (const [key, part] of Object.entries(normalizedParts)) { + result[key] = part.min; + usedSize += part.min; + } + + // Check if we can satisfy minimum constraints + if (usedSize > totalSize) { + return null; + } + + let remainingSize = totalSize - usedSize; + + // Distribute remaining space by priority levels + while (remainingSize > 0) { + // Find all rules at current highest priority that can still grow + const candidateRules: Array<{ + partKey: string; + ruleIndex: number; + rule: IFlexBoxPartExtensionRule; + priority: number; + share: number; + }> = []; + + for (const [key, part] of Object.entries(normalizedParts)) { + for (let i = 0; i < part.rules.length; i++) { + const rule = part.rules[i]; + const currentUsage = result[key]; + const maxSize = rule.max ?? Infinity; + + if (currentUsage < maxSize) { + candidateRules.push({ + partKey: key, + ruleIndex: i, + rule, + priority: rule.priority ?? 0, + share: rule.share ?? 1 + }); + } + } + } + + if (candidateRules.length === 0) { + // No rules can grow anymore, but we have remaining space + break; + } + + // Find the highest priority among candidates + const maxPriority = Math.max(...candidateRules.map(c => c.priority)); + const highestPriorityCandidates = candidateRules.filter(c => c.priority === maxPriority); + + // Calculate total share + const totalShare = highestPriorityCandidates.reduce((sum, c) => sum + c.share, 0); + + // Distribute space proportionally by share + let distributedThisRound = 0; + const distributions: Array<{ partKey: string; ruleIndex: number; amount: number }> = []; + + for (const candidate of highestPriorityCandidates) { + const rule = candidate.rule; + const currentUsage = result[candidate.partKey]; + const maxSize = rule.max ?? Infinity; + const availableForThisRule = maxSize - currentUsage; + + // Calculate ideal share + const idealShare = (remainingSize * candidate.share) / totalShare; + const actualAmount = Math.min(idealShare, availableForThisRule); + + distributions.push({ + partKey: candidate.partKey, + ruleIndex: candidate.ruleIndex, + amount: actualAmount + }); + + distributedThisRound += actualAmount; + } + + if (distributedThisRound === 0) { + // No progress can be made + break; + } + + // Apply distributions + for (const dist of distributions) { + result[dist.partKey] += dist.amount; + } + + remainingSize -= distributedThisRound; + + // Break if remaining is negligible (floating point precision) + if (remainingSize < 0.0001) { + break; + } + } + + return result as Record; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/towersLayout.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/towersLayout.ts new file mode 100644 index 00000000000..314568adbd0 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/towersLayout.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Size2D } from '../../../../../../common/core/2d/size.js'; +import { OffsetRange } from '../../../../../../common/core/ranges/offsetRange.js'; + +/** + * The tower areas are arranged from left to right, touch and are aligned at the bottom. + * How high can a tower be placed at the requested horizontal range, so that its size fits into the union of the stacked availableTowerAreas? + */ +export function getMaxTowerHeightInAvailableArea(towerHorizontalRange: OffsetRange, availableTowerAreas: Size2D[]): number { + const towerLeftOffset = towerHorizontalRange.start; + const towerRightOffset = towerHorizontalRange.endExclusive; + + let minHeight = Number.MAX_VALUE; + + // Calculate the accumulated width to find which tower areas the requested tower overlaps + let currentLeftOffset = 0; + for (const availableArea of availableTowerAreas) { + const currentRightOffset = currentLeftOffset + availableArea.width; + + // Check if the requested tower overlaps with this available area + const overlapLeft = Math.max(towerLeftOffset, currentLeftOffset); + const overlapRight = Math.min(towerRightOffset, currentRightOffset); + + if (overlapLeft < overlapRight) { + // There is an overlap - track the minimum height + minHeight = Math.min(minHeight, availableArea.height); + } + + currentLeftOffset = currentRightOffset; + } + + if (towerRightOffset > currentLeftOffset) { + return 0; + } + + // If no overlap was found, return 0 + return minHeight === Number.MAX_VALUE ? 0 : minHeight; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts index 9e8cd3197a2..580d401fa7a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/utils/utils.ts @@ -8,7 +8,7 @@ import { KeybindingLabel, unthemedKeybindingLabelOptions } from '../../../../../ import { numberComparator } from '../../../../../../../base/common/arrays.js'; import { findFirstMin } from '../../../../../../../base/common/arraysFind.js'; import { DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { DebugLocation, derived, derivedObservableWithCache, derivedOpts, IObservable, IReader, observableValue, transaction } from '../../../../../../../base/common/observable.js'; +import { DebugLocation, derived, derivedObservableWithCache, derivedOpts, IObservable, IReader, observableSignalFromEvent, observableValue, transaction } from '../../../../../../../base/common/observable.js'; import { OS } from '../../../../../../../base/common/platform.js'; import { splitLines } from '../../../../../../../base/common/strings.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -28,25 +28,18 @@ import { ITextModel } from '../../../../../../common/model.js'; import { indentOfLine } from '../../../../../../common/model/textModel.js'; import { CharCode } from '../../../../../../../base/common/charCode.js'; import { BugIndicatingError } from '../../../../../../../base/common/errors.js'; +import { Size2D } from '../../../../../../common/core/2d/size.js'; +/** + * Warning: might return 0. +*/ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): number { - editor.layoutInfo.read(reader); - editor.value.read(reader); - const model = editor.model.read(reader); if (!model) { return 0; } let maxContentWidth = 0; - editor.scrollTop.read(reader); for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { - const column = model.getLineMaxColumn(i); - let lineContentWidth = editor.editor.getOffsetForColumn(i, column); - if (lineContentWidth === -1) { - // approximation - const typicalHalfwidthCharacterWidth = editor.editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; - const approximation = column * typicalHalfwidthCharacterWidth; - lineContentWidth = approximation; - } + const lineContentWidth = editor.getWidthOfLine(i, reader); maxContentWidth = Math.max(maxContentWidth, lineContentWidth); } const lines = range.mapToLineArray(l => model.getLineContent(l)); @@ -57,6 +50,31 @@ export function maxContentWidthInRange(editor: ObservableCodeEditor, range: Line return maxContentWidth; } +export function getContentSizeOfLines(editor: ObservableCodeEditor, range: LineRange, reader: IReader | undefined): Size2D[] { + observableSignalFromEvent(editor, editor.editor.onDidChangeLineHeight).read(reader); + + const model = editor.model.read(reader); + if (!model) { throw new BugIndicatingError('Model is required'); } + + const sizes: Size2D[] = []; + + for (let i = range.startLineNumber; i < range.endLineNumberExclusive; i++) { + let lineContentWidth = editor.getWidthOfLine(i, reader); + if (lineContentWidth === -1) { + // approximation + const column = model.getLineMaxColumn(i); + const typicalHalfwidthCharacterWidth = editor.editor.getOption(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const approximation = column * typicalHalfwidthCharacterWidth; + lineContentWidth = approximation; + } + + const height = editor.editor.getLineHeightForPosition(new Position(i, 1)); + sizes.push(new Size2D(lineContentWidth, height)); + } + + return sizes; +} + export function getOffsetForPos(editor: ObservableCodeEditor, pos: Position, reader: IReader): number { editor.layoutInfo.read(reader); editor.value.read(reader); @@ -394,13 +412,37 @@ export function observeElementPosition(element: HTMLElement, store: DisposableSt }; } -export function rectToProps(fn: (reader: IReader) => Rect, debugLocation: DebugLocation = DebugLocation.ofCaller()) { +export function rectToProps(fn: (reader: IReader) => Rect | undefined, debugLocation: DebugLocation = DebugLocation.ofCaller()) { return { - left: derived({ name: 'editor.validOverlay.left' }, reader => /** @description left */ fn(reader).left, debugLocation), - top: derived({ name: 'editor.validOverlay.top' }, reader => /** @description top */ fn(reader).top, debugLocation), - width: derived({ name: 'editor.validOverlay.width' }, reader => /** @description width */ fn(reader).right - fn(reader).left, debugLocation), - height: derived({ name: 'editor.validOverlay.height' }, reader => /** @description height */ fn(reader).bottom - fn(reader).top, debugLocation), + left: derived({ name: 'editor.validOverlay.left' }, reader => /** @description left */ fn(reader)?.left, debugLocation), + top: derived({ name: 'editor.validOverlay.top' }, reader => /** @description top */ fn(reader)?.top, debugLocation), + width: derived({ name: 'editor.validOverlay.width' }, reader => { + /** @description width */ + const val = fn(reader); + if (!val) { + return undefined; + } + return val.width; + }, debugLocation), + height: derived({ name: 'editor.validOverlay.height' }, reader => { + /** @description height */ + const val = fn(reader); + if (!val) { + return undefined; + } + return val.height; + }, debugLocation), }; } export type FirstFnArg = T extends (arg: infer U) => any ? U : never; + + +export function observeEditorBoundingClientRect(editor: ICodeEditor, store: DisposableStore): IObservable { + const dom = editor.getContainerDomNode()!; + const initialDomRect = observableValue('domRect', dom.getBoundingClientRect()); + store.add(editor.onDidLayoutChange(e => { + initialDomRect.set(dom.getBoundingClientRect(), undefined); + })); + return initialDomRect; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css index f93e4862603..cbc244fc3ed 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineEdits/view.css @@ -8,6 +8,8 @@ */ .monaco-editor { + --inline-edit-border-radius: 3px; + .inline-edits-view-indicator { display: flex; @@ -17,7 +19,7 @@ color: var(--vscode-inlineEdit-gutterIndicator-primaryForeground); background-color: var(--vscode-inlineEdit-gutterIndicator-background); border: 1px solid var(--vscode-inlineEdit-gutterIndicator-primaryBorder); - border-radius: 3px; + border-radius: var(--inline-edit-border-radius); align-items: center; padding: 2px; @@ -132,13 +134,13 @@ border-bottom: 1px solid var(--vscode-inlineEdit-modifiedBorder); /* TODO: Do not set border inline but create overlaywidget (like deletion view) */ } .inlineCompletions-char-insert.single-line-inline.start { - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; + border-top-left-radius: var(--inline-edit-border-radius); + border-bottom-left-radius: var(--inline-edit-border-radius); border-left: 1px solid var(--vscode-inlineEdit-modifiedBorder); /* TODO: Do not set border inline but create overlaywidget (like deletion view) */ } .inlineCompletions-char-insert.single-line-inline.end { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; + border-top-right-radius: var(--inline-edit-border-radius); + border-bottom-right-radius: var(--inline-edit-border-radius); border-right: 1px solid var(--vscode-inlineEdit-modifiedBorder); /* TODO: Do not set border inline but create overlaywidget (like deletion view) */ } @@ -188,6 +190,17 @@ .inlineCompletions-original-lines { background: var(--vscode-editor-background); } + + .inline-edit-jump-to-widget { + + + .monaco-keybinding { + .monaco-keybinding-key { + font-size: 11px; + padding: 1px 2px 2px 2px; + } + } + } } .monaco-menu-option { @@ -217,3 +230,44 @@ } } } + +.inline-edits-long-distance-hint-widget .go-to-label::before { + content: ''; + position: absolute; + left: -12px; + top: 0; + width: 12px; + height: 100%; + background: linear-gradient(to left, var(--vscode-editorWidget-background) 0, transparent 12px); +} + +.hc-black .inline-edits-long-distance-hint-widget .go-to-label::before, +.hc-light .inline-edits-long-distance-hint-widget .go-to-label::before { + /* Remove gradient in high contrast mode for clearer separation */ + background: var(--vscode-editorWidget-background); +} + +.inline-edit-alternative-action-label .codicon { + font-size: 12px !important; + padding-right: 4px; +} + +.inline-edit-alternative-action-label .monaco-keybinding-key { + padding: 2px 3px; +} + +.inline-edit-alternative-action-label .inline-edit-alternative-action-label-separator { + width: 4px; +} + +/* File icon in long distance hint widget */ +.inline-edits-long-distance-hint-widget.show-file-icons .target-file .file-icon::before { + display: inline-block; + width: 16px; + height: 16px; + vertical-align: text-bottom; + background-size: 16px; + background-position: center; + background-repeat: no-repeat; +} + diff --git a/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts new file mode 100644 index 00000000000..21a2c598aa0 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/view/inlineSuggestionsView.ts @@ -0,0 +1,230 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createStyleSheetFromObservable } from '../../../../../base/browser/domStylesheets.js'; +import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { derived, mapObservableArrayCached, derivedDisposable, derivedObservableWithCache, IObservable, ISettableObservable, constObservable, observableValue } from '../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js'; +import { EditorOption } from '../../../../common/config/editorOptions.js'; +import { LineRange } from '../../../../common/core/ranges/lineRange.js'; +import { InlineCompletionsHintsWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js'; +import { GhostTextOrReplacement } from '../model/ghostText.js'; +import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; +import { InlineCompletionItem } from '../model/inlineSuggestionItem.js'; +import { convertItemsToStableObservables } from '../utils.js'; +import { GhostTextView, GhostTextWidgetWarning, IGhostTextWidgetData } from './ghostText/ghostTextView.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from './inlineEdits/components/gutterIndicatorView.js'; +import { InlineEditsOnboardingExperience } from './inlineEdits/inlineEditsNewUsers.js'; +import { InlineCompletionViewKind, InlineEditTabAction } from './inlineEdits/inlineEditsViewInterface.js'; +import { InlineEditsViewAndDiffProducer } from './inlineEdits/inlineEditsViewProducer.js'; + +export class InlineSuggestionsView extends Disposable { + public static hot = createHotClass(this); + + private readonly _ghostTexts = derived(this, (reader) => { + const model = this._model.read(reader); + return model?.ghostTexts.read(reader) ?? []; + }); + + private readonly _stablizedGhostTexts; + private readonly _editorObs; + private readonly _ghostTextWidgets; + + private readonly _inlineEdit = derived(this, reader => this._model.read(reader)?.inlineEditState.read(reader)?.inlineSuggestion); + private readonly _everHadInlineEdit = derivedObservableWithCache(this, + (reader, last) => last || !!this._inlineEdit.read(reader) + || !!this._model.read(reader)?.inlineCompletionState.read(reader)?.inlineSuggestion?.showInlineEditMenu + ); + + // To break a cyclic dependency + private readonly _indicatorIsHoverVisible = observableValue | undefined>(this, undefined); + + private readonly _showInlineEditCollapsed = derived(this, reader => { + const s = this._model.read(reader)?.showCollapsed.read(reader) ?? false; + return s && !this._indicatorIsHoverVisible.read(reader)?.read(reader); + }); + + private readonly _inlineEditWidget = derivedDisposable(reader => { + if (!this._everHadInlineEdit.read(reader)) { + return undefined; + } + return this._instantiationService.createInstance(InlineEditsViewAndDiffProducer, this._editor, this._model, this._showInlineEditCollapsed); + }); + + private readonly _fontFamily; + + constructor( + private readonly _editor: ICodeEditor, + private readonly _model: IObservable, + private readonly _focusIsInMenu: ISettableObservable, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + + this._stablizedGhostTexts = convertItemsToStableObservables(this._ghostTexts, this._store); + this._editorObs = observableCodeEditor(this._editor); + + this._ghostTextWidgets = mapObservableArrayCached( + this, + this._stablizedGhostTexts, + (ghostText, store) => store.add(this._createGhostText(ghostText)) + ).recomputeInitiallyAndOnChange(this._store); + + this._inlineEditWidget.recomputeInitiallyAndOnChange(this._store); + + this._fontFamily = this._editorObs.getOption(EditorOption.inlineSuggest).map(val => val.fontFamily); + + this._register(createStyleSheetFromObservable(derived(reader => { + const fontFamily = this._fontFamily.read(reader); + return ` +.monaco-editor .ghost-text-decoration, +.monaco-editor .ghost-text-decoration-preview, +.monaco-editor .ghost-text { + font-family: ${fontFamily}; +}`; + }))); + + this._register(new InlineCompletionsHintsWidget(this._editor, this._model, this._instantiationService)); + + this._indicator = this._register(this._instantiationService.createInstance( + InlineEditsGutterIndicator, + this._editorObs, + derived(reader => { + const s = this._gutterIndicatorState.read(reader); + if (!s) { return undefined; } + return new InlineEditsGutterIndicatorData( + InlineSuggestionGutterMenuData.fromInlineSuggestion(s.inlineSuggestion), + s.displayRange, + SimpleInlineSuggestModel.fromInlineCompletionModel(s.model), + s.inlineSuggestion.action?.kind === 'edit' ? s.inlineSuggestion.action.alternativeAction : undefined, + ); + }), + this._gutterIndicatorState.map((s, reader) => s?.tabAction.read(reader) ?? InlineEditTabAction.Inactive), + this._gutterIndicatorState.map((s, reader) => s?.gutterIndicatorOffset.read(reader) ?? 0), + this._inlineEditWidget.map((w, reader) => w?.view.inlineEditsIsHovered.read(reader) ?? false), + this._focusIsInMenu, + )); + this._indicatorIsHoverVisible.set(this._indicator.isHoverVisible, undefined); + + derived(reader => { + const w = this._inlineEditWidget.read(reader); + if (!w) { return undefined; } + return reader.store.add(this._instantiationService.createInstance( + InlineEditsOnboardingExperience, + w._inlineEditModel, + constObservable(this._indicator), + w.view._inlineCollapsedView, + )); + }).recomputeInitiallyAndOnChange(this._store); + } + + private _createGhostText(ghostText: IObservable): GhostTextView { + return this._instantiationService.createInstance( + GhostTextView, + this._editor, + derived(reader => { + const model = this._model.read(reader); + const inlineCompletion = model?.inlineCompletionState.read(reader)?.inlineSuggestion; + if (!model || !inlineCompletion) { + // editor.suggest.preview: true causes situations where we have ghost text, but no suggest preview. + return { + ghostText: ghostText.read(reader), + handleInlineCompletionShown: () => { /* no-op */ }, + warning: undefined, + }; + } + return { + ghostText: ghostText.read(reader), + handleInlineCompletionShown: (viewData) => model.handleInlineSuggestionShown(inlineCompletion, InlineCompletionViewKind.GhostText, viewData, Date.now()), + warning: GhostTextWidgetWarning.from(model?.warning.read(reader)), + } satisfies IGhostTextWidgetData; + }), + { + useSyntaxHighlighting: this._editorObs.getOption(EditorOption.inlineSuggest).map(v => v.syntaxHighlightingEnabled), + highlightShortSuggestions: true, + }, + ); + } + + public shouldShowHoverAtViewZone(viewZoneId: string): boolean { + return this._ghostTextWidgets.get()[0]?.ownsViewZone(viewZoneId) ?? false; + } + + private readonly _gutterIndicatorState = derived(reader => { + const model = this._model.read(reader); + if (!model) { + return undefined; + } + + const state = model.state.read(reader); + + if (state?.kind === 'ghostText' && state.inlineSuggestion?.showInlineEditMenu) { + return { + displayRange: LineRange.ofLength(state.primaryGhostText.lineNumber, 1), + tabAction: derived(this, + reader => this._editorObs.isFocused.read(reader) ? InlineEditTabAction.Accept : InlineEditTabAction.Inactive + ), + gutterIndicatorOffset: constObservable(getGhostTextTopOffset(state.inlineSuggestion, this._editor)), + inlineSuggestion: state.inlineSuggestion, + model, + }; + } else if (state?.kind === 'inlineEdit') { + const inlineEditWidget = this._inlineEditWidget.read(reader)?.view; + if (!inlineEditWidget) { return undefined; } + + const displayRange = inlineEditWidget.displayRange.read(reader); + if (!displayRange) { return undefined; } + return { + displayRange, + tabAction: derived(reader => { + if (this._editorObs.isFocused.read(reader)) { + if (model.tabShouldJumpToInlineEdit.read(reader)) { return InlineEditTabAction.Jump; } + if (model.tabShouldAcceptInlineEdit.read(reader)) { return InlineEditTabAction.Accept; } + } + return InlineEditTabAction.Inactive; + }), + gutterIndicatorOffset: inlineEditWidget.gutterIndicatorOffset, + inlineSuggestion: state.inlineSuggestion, + model, + }; + } else { + return undefined; + } + }); + + protected readonly _indicator; +} + +function getGhostTextTopOffset(inlineCompletion: InlineCompletionItem, editor: ICodeEditor): number { + const replacement = inlineCompletion.getSingleTextEdit(); + const textModel = editor.getModel(); + if (!textModel) { + return 0; + } + + const EOL = textModel.getEOL(); + if (replacement.range.isEmpty() && replacement.text.startsWith(EOL)) { + const lineHeight = editor.getLineHeightForPosition(replacement.range.getStartPosition()); + return countPrefixRepeats(replacement.text, EOL) * lineHeight; + } + + return 0; +} + +function countPrefixRepeats(str: string, prefix: string): number { + if (!prefix.length) { + return 0; + } + let count = 0; + let i = 0; + while (str.startsWith(prefix, i)) { + count++; + i += prefix.length; + } + return count; +} diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/editKind.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/editKind.test.ts new file mode 100644 index 00000000000..dc983a4a50d --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/editKind.test.ts @@ -0,0 +1,484 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Position } from '../../../../common/core/position.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; +import { StringEdit } from '../../../../common/core/edits/stringEdit.js'; +import { createTextModel } from '../../../../test/common/testTextModel.js'; +import { computeEditKind, InsertProperties, DeleteProperties, ReplaceProperties } from '../../browser/model/editKind.js'; + +suite('computeEditKind', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('Insert operations', () => { + test('single character insert - syntactical', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, ';'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + assert.strictEqual(result.edits[0].charactersInserted, 1); + assert.strictEqual(result.edits[0].charactersDeleted, 0); + assert.strictEqual(result.edits[0].linesInserted, 0); + assert.strictEqual(result.edits[0].linesDeleted, 0); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.textShape.kind, 'singleLine'); + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isSingleCharacter, true); + assert.strictEqual(props.textShape.singleCharacterKind, 'syntactical'); + } + model.dispose(); + }); + + test('single character insert - identifier', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, 'a'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isSingleCharacter, true); + assert.strictEqual(props.textShape.singleCharacterKind, 'identifier'); + } + model.dispose(); + }); + + test('single character insert - whitespace', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, ' '); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isSingleCharacter, true); + assert.strictEqual(props.textShape.singleCharacterKind, 'whitespace'); + } + model.dispose(); + }); + + test('word insert', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, 'foo'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isWord, true); + assert.strictEqual(props.textShape.isMultipleWords, false); + } + model.dispose(); + }); + + test('multiple words insert', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, 'foo bar baz'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isMultipleWords, true); + } + model.dispose(); + }); + + test('multi-line insert', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, 'line1\nline2\nline3'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + assert.strictEqual(result.edits[0].charactersInserted, 17); + assert.strictEqual(result.edits[0].charactersDeleted, 0); + assert.strictEqual(result.edits[0].linesInserted, 2); + assert.strictEqual(result.edits[0].linesDeleted, 0); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.textShape.kind, 'multiLine'); + if (props.textShape.kind === 'multiLine') { + assert.strictEqual(props.textShape.lineCount, 3); + } + model.dispose(); + }); + + test('insert at end of line', () => { + const model = createTextModel('hello'); + const edit = StringEdit.insert(5, ' world'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.locationShape, 'endOfLine'); + model.dispose(); + }); + + test('insert on empty line', () => { + const model = createTextModel('hello\n\nworld'); + const edit = StringEdit.insert(6, 'text'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.locationShape, 'emptyLine'); + model.dispose(); + }); + + test('insert at start of line', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(0, 'prefix'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.locationShape, 'startOfLine'); + model.dispose(); + }); + + test('insert in middle of line', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, '_'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.strictEqual(props.locationShape, 'middleOfLine'); + model.dispose(); + }); + + test('insert relative to cursor - at cursor', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(5, 'text'); + const cursor = new Position(1, 6); // column is 1-based + const result = computeEditKind(edit, model, cursor); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.ok(props.relativeToCursor); + assert.strictEqual(props.relativeToCursor.atCursor, true); + model.dispose(); + }); + + test('insert relative to cursor - before cursor on same line', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(2, 'text'); + const cursor = new Position(1, 8); + const result = computeEditKind(edit, model, cursor); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.ok(props.relativeToCursor); + assert.strictEqual(props.relativeToCursor.beforeCursorOnSameLine, true); + model.dispose(); + }); + + test('insert relative to cursor - after cursor on same line', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.insert(8, 'text'); + const cursor = new Position(1, 4); + const result = computeEditKind(edit, model, cursor); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.ok(props.relativeToCursor); + assert.strictEqual(props.relativeToCursor.afterCursorOnSameLine, true); + model.dispose(); + }); + + test('insert relative to cursor - lines above', () => { + const model = createTextModel('line1\nline2\nline3'); + const edit = StringEdit.insert(0, 'text'); + const cursor = new Position(3, 1); + const result = computeEditKind(edit, model, cursor); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.ok(props.relativeToCursor); + assert.strictEqual(props.relativeToCursor.linesAbove, 2); + model.dispose(); + }); + + test('insert relative to cursor - lines below', () => { + const model = createTextModel('line1\nline2\nline3'); + const edit = StringEdit.insert(12, 'text'); // after 'line2\n' + const cursor = new Position(1, 1); + const result = computeEditKind(edit, model, cursor); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + assert.ok(props.relativeToCursor); + assert.strictEqual(props.relativeToCursor.linesBelow, 2); + model.dispose(); + }); + + test('duplicated whitespace insert', () => { + const model = createTextModel('hello'); + const edit = StringEdit.insert(5, ' '); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'insert'); + const props = result.edits[0].properties as InsertProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.hasDuplicatedWhitespace, true); + } + model.dispose(); + }); + }); + + suite('Delete operations', () => { + test('single character delete - identifier', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.delete(new OffsetRange(4, 5)); // delete 'o' + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'delete'); + const props = result.edits[0].properties as DeleteProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isSingleCharacter, true); + assert.strictEqual(props.textShape.singleCharacterKind, 'identifier'); + } + model.dispose(); + }); + + test('single character delete - syntactical', () => { + const model = createTextModel('hello;world'); + const edit = StringEdit.delete(new OffsetRange(5, 6)); // delete ';' + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'delete'); + const props = result.edits[0].properties as DeleteProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isSingleCharacter, true); + assert.strictEqual(props.textShape.singleCharacterKind, 'syntactical'); + } + model.dispose(); + }); + + test('word delete', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.delete(new OffsetRange(0, 5)); // delete 'hello' + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'delete'); + assert.strictEqual(result.edits[0].charactersInserted, 0); + assert.strictEqual(result.edits[0].charactersDeleted, 5); + assert.strictEqual(result.edits[0].linesInserted, 0); + assert.strictEqual(result.edits[0].linesDeleted, 0); + const props = result.edits[0].properties as DeleteProperties; + if (props.textShape.kind === 'singleLine') { + assert.strictEqual(props.textShape.isWord, true); + } + model.dispose(); + }); + + test('multi-line delete', () => { + const model = createTextModel('line1\nline2\nline3'); + const edit = StringEdit.delete(new OffsetRange(0, 12)); // delete 'line1\nline2\n' + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'delete'); + assert.strictEqual(result.edits[0].charactersInserted, 0); + assert.strictEqual(result.edits[0].charactersDeleted, 12); + assert.strictEqual(result.edits[0].linesInserted, 0); + assert.strictEqual(result.edits[0].linesDeleted, 2); + const props = result.edits[0].properties as DeleteProperties; + assert.strictEqual(props.textShape.kind, 'multiLine'); + model.dispose(); + }); + + test('delete entire line content', () => { + const model = createTextModel('hello'); + const edit = StringEdit.delete(new OffsetRange(0, 5)); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'delete'); + const props = result.edits[0].properties as DeleteProperties; + assert.strictEqual(props.deletesEntireLineContent, true); + model.dispose(); + }); + }); + + suite('Replace operations', () => { + test('word to word replacement', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.replace(new OffsetRange(0, 5), 'goodbye'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + assert.strictEqual(result.edits[0].charactersInserted, 7); + assert.strictEqual(result.edits[0].charactersDeleted, 5); + assert.strictEqual(result.edits[0].linesInserted, 0); + assert.strictEqual(result.edits[0].linesDeleted, 0); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isWordToWordReplacement, true); + model.dispose(); + }); + + test('additive replacement', () => { + const model = createTextModel('hi world'); + const edit = StringEdit.replace(new OffsetRange(0, 2), 'hello'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + assert.strictEqual(result.edits[0].charactersInserted, 5); + assert.strictEqual(result.edits[0].charactersDeleted, 2); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isAdditive, true); + assert.strictEqual(props.isSubtractive, false); + model.dispose(); + }); + + test('subtractive replacement', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.replace(new OffsetRange(0, 5), 'hi'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + assert.strictEqual(result.edits[0].charactersInserted, 2); + assert.strictEqual(result.edits[0].charactersDeleted, 5); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isSubtractive, true); + assert.strictEqual(props.isAdditive, false); + model.dispose(); + }); + + test('single line to multi-line replacement', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.replace(new OffsetRange(0, 5), 'line1\nline2'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + assert.strictEqual(result.edits[0].linesInserted, 1); + assert.strictEqual(result.edits[0].linesDeleted, 0); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isSingleLineToMultiLine, true); + model.dispose(); + }); + + test('multi-line to single line replacement', () => { + const model = createTextModel('line1\nline2\nline3'); + const edit = StringEdit.replace(new OffsetRange(0, 12), 'hello'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + assert.strictEqual(result.edits[0].linesInserted, 0); + assert.strictEqual(result.edits[0].linesDeleted, 2); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isMultiLineToSingleLine, true); + model.dispose(); + }); + + test('single line to single line replacement', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.replace(new OffsetRange(0, 5), 'goodbye'); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 1); + assert.strictEqual(result.edits[0].operation, 'replace'); + const props = result.edits[0].properties as ReplaceProperties; + assert.strictEqual(props.isSingleLineToSingleLine, true); + model.dispose(); + }); + }); + + suite('Empty edit', () => { + test('empty edit returns undefined', () => { + const model = createTextModel('hello world'); + const edit = StringEdit.empty; + const result = computeEditKind(edit, model); + + assert.strictEqual(result, undefined); + model.dispose(); + }); + }); + + suite('Multiple replacements', () => { + test('multiple inserts', () => { + const model = createTextModel('hello world'); + const edit = new StringEdit([ + StringEdit.insert(0, 'A').replacements[0], + StringEdit.insert(5, 'B').replacements[0], + ]); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 2); + assert.strictEqual(result.edits[0].operation, 'insert'); + assert.strictEqual(result.edits[1].operation, 'insert'); + model.dispose(); + }); + + test('mixed operations', () => { + const model = createTextModel('hello world'); + const edit = new StringEdit([ + StringEdit.insert(0, 'prefix').replacements[0], + StringEdit.delete(new OffsetRange(5, 6)).replacements[0], + ]); + const result = computeEditKind(edit, model); + + assert.ok(result); + assert.strictEqual(result.edits.length, 2); + assert.strictEqual(result.edits[0].operation, 'insert'); + assert.strictEqual(result.edits[1].operation, 'delete'); + model.dispose(); + }); + }); +}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts index 0f26af1bbb8..c9909d45346 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletions.test.ts @@ -761,4 +761,80 @@ suite('Multi Cursor Support', () => { } ); }); + + test('Change hint is passed from onDidChange to provideInlineCompletions', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('foo'); + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); + model.triggerExplicitly(); + await timeout(1000); + + const firstCallHistory = provider.getAndClearCallHistory(); + assert.strictEqual(firstCallHistory.length, 1); + assert.strictEqual((firstCallHistory[0] as { changeHint?: unknown }).changeHint, undefined); + + // Change cursor position to avoid cache hit + editor.setPosition({ lineNumber: 1, column: 3 }); + + + const changeHintData = { reason: 'modelUpdated', version: 42 }; + provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 4) }); + provider.fireOnDidChange({ data: changeHintData }); + await timeout(1000); + + const secondCallHistory = provider.getAndClearCallHistory(); + + assert.deepStrictEqual( + secondCallHistory, + [{ + changeHint: { + data: { + reason: 'modelUpdated', + version: 42, + } + }, + position: '(1,3)', + text: 'foo', + triggerKind: 0 + }] + ); + } + ); + }); + + test('Change hint is undefined when onDidChange fires without hint', async function () { + const provider = new MockInlineCompletionsProvider(); + await withAsyncTestCodeEditorAndInlineCompletionsModel('', + { fakeClock: true, provider, inlineSuggest: { enabled: true } }, + async ({ editor, editorViewModel, model, context }) => { + context.keyboardType('foo'); + provider.setReturnValue({ insertText: 'foobar', range: new Range(1, 1, 1, 4) }); + model.triggerExplicitly(); + await timeout(1000); + + provider.getAndClearCallHistory(); + + // Change cursor position to avoid cache hit + editor.setPosition({ lineNumber: 1, column: 3 }); + + provider.setReturnValue({ insertText: 'foobaz', range: new Range(1, 1, 1, 4) }); + provider.fireOnDidChange(); + await timeout(1000); + + const callHistory = provider.getAndClearCallHistory(); + + assert.deepStrictEqual( + callHistory, + [{ + position: '(1,3)', + text: 'foo', + triggerKind: 0 + }] + ); + } + ); + }); }); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts index ba30b1c11db..c22e15507dd 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineEdits.test.ts @@ -18,6 +18,10 @@ class Point { getLength2D(): number { return↓ Math.sqrt(this.x * this.x + this.y * this.y↓); } + + getJson(): string { + return ↓Ü; + } } `); @@ -57,6 +61,10 @@ class Point { getLength3D(): number { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } + + getJson(): string { + return Ü; + } } `); }); @@ -90,6 +98,24 @@ class Point { }); }); + test('Inline Edit Is Correctly Shifted When Typing', async function () { + await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => { + provider.add('Ü', '{x: this.x, y: this.y}'); + await model.trigger(); + await timeout(10000); + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + undefined, + '...\n\t\treturn ❰Ü↦{x: t...is.y}❱;\n' + ])); + editor.setPosition(val.getMarkerPosition(2)); + editorViewModel.type('{'); + + assert.deepStrictEqual(view.getAndClearViewStates(), ([ + '...\t\treturn {❰Ü↦x: th...is.y}❱;\n' + ])); + }); + }); + test('Inline Edit Stays On Unrelated Edit', async function () { await runTest(async ({ context, model, editor, editorViewModel }, provider, view) => { provider.add(`getLength2D(): number { diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/layout.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/layout.test.ts new file mode 100644 index 00000000000..d24f3823309 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/layout.test.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Size2D } from '../../../../common/core/2d/size.js'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; +import { getMaxTowerHeightInAvailableArea } from '../../browser/view/inlineEdits/utils/towersLayout.js'; + +suite('Layout - getMaxTowerHeightInAvailableArea', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('tower fits within single available area', () => { + const towerHorizontalRange = new OffsetRange(5, 15); // width of 10 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return the available height (30) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 30); + }); + + test('max height available in area', () => { + const towerHorizontalRange = new OffsetRange(5, 15); // width of 10 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return the available height (30), even if original tower was 40 + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 30); + }); + + test('tower extends beyond available width', () => { + const towerHorizontalRange = new OffsetRange(0, 60); // width of 60 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return 0 because tower extends beyond available areas + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 0); + }); + + test('tower fits across multiple available areas', () => { + const towerHorizontalRange = new OffsetRange(10, 40); // width of 30 + const availableTowerAreas = [ + new Size2D(20, 30), + new Size2D(20, 25), + new Size2D(20, 30) + ]; + + // Should return the minimum height across overlapping areas (25) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 25); + }); + + test('min height across overlapping areas', () => { + const towerHorizontalRange = new OffsetRange(10, 40); // width of 30 + const availableTowerAreas = [ + new Size2D(20, 30), + new Size2D(20, 15), // Shortest area + new Size2D(20, 30) + ]; + + // Should return the minimum height (15) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 15); + }); + + test('tower at left edge of available areas', () => { + const towerHorizontalRange = new OffsetRange(0, 10); // width of 10 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return the available height (30) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 30); + }); + + test('tower at right edge of available areas', () => { + const towerHorizontalRange = new OffsetRange(40, 50); // width of 10 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return the available height (30) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 30); + }); + + test('tower exactly matches available area', () => { + const towerHorizontalRange = new OffsetRange(0, 50); // width of 50 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return the available height (30) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 30); + }); + + test('empty available areas', () => { + const towerHorizontalRange = new OffsetRange(0, 10); // width of 10 + const availableTowerAreas: Size2D[] = []; + + // Should return 0 for empty areas + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 0); + }); + + test('tower spans exactly two available areas', () => { + const towerHorizontalRange = new OffsetRange(10, 50); // width of 40 + const availableTowerAreas = [ + new Size2D(30, 25), + new Size2D(30, 25) + ]; + + // Should return the minimum height across both areas (25) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 25); + }); + + test('tower starts at boundary between two areas', () => { + const towerHorizontalRange = new OffsetRange(30, 50); // width of 20 + const availableTowerAreas = [ + new Size2D(30, 25), + new Size2D(30, 25) + ]; + + // Should return the height of the second area (25) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 25); + }); + + test('tower with varying height available areas', () => { + const towerHorizontalRange = new OffsetRange(0, 50); // width of 50 + const availableTowerAreas = [ + new Size2D(10, 30), + new Size2D(10, 15), // Shortest area + new Size2D(10, 25), + new Size2D(10, 30), + new Size2D(10, 40) + ]; + + // Should return the minimum height (15) + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 15); + }); + + test('tower beyond all available areas to the right', () => { + const towerHorizontalRange = new OffsetRange(100, 110); // width of 10 + const availableTowerAreas = [new Size2D(50, 30)]; + + // Should return 0 because tower is beyond available areas + assert.strictEqual(getMaxTowerHeightInAvailableArea(towerHorizontalRange, availableTowerAreas), 0); + }); +}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/longDistanceWidgetPlacement.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/longDistanceWidgetPlacement.test.ts new file mode 100644 index 00000000000..72a8501ebc6 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/longDistanceWidgetPlacement.test.ts @@ -0,0 +1,297 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Size2D } from '../../../../common/core/2d/size.js'; +import { LineRange } from '../../../../common/core/ranges/lineRange.js'; +import { WidgetLayoutConstants, WidgetPlacementContext, ContinuousLineSizes } from '../../browser/view/inlineEdits/inlineEditsViews/longDistanceHint/longDistnaceWidgetPlacement.js'; + +suite('WidgetPlacementContext', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function createLineRangeInfo(startLine: number, sizes: Size2D[], top: number = 0): ContinuousLineSizes { + return { + lineRange: LineRange.ofLength(startLine, sizes.length), + top, + sizes, + }; + } + + const defaultLayoutConstants: WidgetLayoutConstants = { + previewEditorMargin: 5, + widgetPadding: 2, + widgetBorder: 1, + lowerBarHeight: 10, + minWidgetWidth: 50, + }; + + suite('constructor - availableSpaceSizes computation', () => { + test('computes available space sizes correctly with no padding', () => { + const sizes = [new Size2D(100, 20), new Size2D(150, 20), new Size2D(80, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + const editorTrueContentWidth = 500; + const endOfLinePadding = () => 0; + + const context = new WidgetPlacementContext(lineRangeInfo, editorTrueContentWidth, endOfLinePadding); + + assert.strictEqual(context.availableSpaceSizes.length, 3); + assert.strictEqual(context.availableSpaceSizes[0].width, 400); // 500 - 100 + assert.strictEqual(context.availableSpaceSizes[1].width, 350); // 500 - 150 + assert.strictEqual(context.availableSpaceSizes[2].width, 420); // 500 - 80 + }); + + test('computes available space sizes with end of line padding', () => { + const sizes = [new Size2D(100, 20), new Size2D(150, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + const editorTrueContentWidth = 500; + const endOfLinePadding = (lineNumber: number) => lineNumber * 10; + + const context = new WidgetPlacementContext(lineRangeInfo, editorTrueContentWidth, endOfLinePadding); + + assert.strictEqual(context.availableSpaceSizes[0].width, 390); // 500 - 100 - 10 + assert.strictEqual(context.availableSpaceSizes[1].width, 330); // 500 - 150 - 20 + }); + + test('available space width is never negative', () => { + const sizes = [new Size2D(600, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + const editorTrueContentWidth = 500; + const endOfLinePadding = () => 0; + + const context = new WidgetPlacementContext(lineRangeInfo, editorTrueContentWidth, endOfLinePadding); + + assert.strictEqual(context.availableSpaceSizes[0].width, 0); + }); + + test('preserves heights in available space sizes', () => { + const sizes = [new Size2D(100, 25), new Size2D(100, 30), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + const editorTrueContentWidth = 500; + const endOfLinePadding = () => 0; + + const context = new WidgetPlacementContext(lineRangeInfo, editorTrueContentWidth, endOfLinePadding); + + assert.strictEqual(context.availableSpaceSizes[0].height, 25); + assert.strictEqual(context.availableSpaceSizes[1].height, 30); + assert.strictEqual(context.availableSpaceSizes[2].height, 20); + }); + }); + + suite('constructor - prefix sums computation', () => { + test('computes height prefix sums correctly', () => { + const sizes = [new Size2D(100, 20), new Size2D(100, 30), new Size2D(100, 25)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + assert.deepStrictEqual(context.availableSpaceHeightPrefixSums, [0, 20, 50, 75]); + }); + + test('prefix sums start with 0 and have length = sizes.length + 1', () => { + const sizes = [new Size2D(100, 10), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + assert.strictEqual(context.availableSpaceHeightPrefixSums[0], 0); + assert.strictEqual(context.availableSpaceHeightPrefixSums.length, 3); + }); + }); + + suite('constructor - transposed sizes', () => { + test('transposes width and height correctly', () => { + const sizes = [new Size2D(100, 20), new Size2D(150, 30)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + // Transposed: width becomes height and vice versa + // Available widths are 400 and 350, heights are 20 and 30 + assert.strictEqual(context.availableSpaceSizesTransposed[0].width, 20); + assert.strictEqual(context.availableSpaceSizesTransposed[0].height, 400); + assert.strictEqual(context.availableSpaceSizesTransposed[1].width, 30); + assert.strictEqual(context.availableSpaceSizesTransposed[1].height, 350); + }); + }); + + suite('getWidgetVerticalOutline', () => { + test('computes vertical outline for first line', () => { + const sizes = [new Size2D(100, 20), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 100); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const outline = context.getWidgetVerticalOutline(1, 50, defaultLayoutConstants); + + // previewEditorMargin + widgetPadding + widgetBorder = 5 + 2 + 1 = 8 + // editorRange = [100, 150) + // verticalWidgetRange = [100 - 8, 150 + 8 + 10) = [92, 168) + assert.strictEqual(outline.start, 92); + assert.strictEqual(outline.endExclusive, 168); + }); + + test('computes vertical outline for second line', () => { + const sizes = [new Size2D(100, 20), new Size2D(100, 25)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 100); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const outline = context.getWidgetVerticalOutline(2, 50, defaultLayoutConstants); + + // Line 2 is at index 1, prefixSum[1] = 20 + // top = 100 + 20 = 120 + // editorRange = [120, 170) + // margin = 8, lowerBarHeight = 10 + // verticalWidgetRange = [120 - 8, 170 + 8 + 10) = [112, 188) + assert.strictEqual(outline.start, 112); + assert.strictEqual(outline.endExclusive, 188); + }); + + test('works with zero margins', () => { + const sizes = [new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 0); + const zeroConstants: WidgetLayoutConstants = { + previewEditorMargin: 0, + widgetPadding: 0, + widgetBorder: 0, + lowerBarHeight: 0, + minWidgetWidth: 50, + }; + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const outline = context.getWidgetVerticalOutline(1, 50, zeroConstants); + + assert.strictEqual(outline.start, 0); + assert.strictEqual(outline.endExclusive, 50); + }); + }); + + suite('tryFindWidgetOutline', () => { + test('returns undefined when no line has enough width', () => { + // All lines have content that leaves less than minWidgetWidth + const sizes = [new Size2D(460, 20), new Size2D(470, 20), new Size2D(480, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const result = context.tryFindWidgetOutline(2, 15, 500, defaultLayoutConstants); + + assert.strictEqual(result, undefined); + }); + + test('finds widget outline on target line when it has enough space', () => { + const sizes = [new Size2D(100, 20), new Size2D(100, 20), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 0); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const result = context.tryFindWidgetOutline(2, 15, 500, defaultLayoutConstants); + + assert.ok(result !== undefined); + assert.ok(result.horizontalWidgetRange.length >= defaultLayoutConstants.minWidgetWidth); + }); + + test('searches outward from target line', () => { + // First and last lines are excluded from placement + // Lines 2, 3 have no space, line 4 has space + const sizes = [ + new Size2D(100, 20), // line 1 - excluded (first) + new Size2D(460, 20), // line 2 - no space + new Size2D(460, 20), // line 3 - no space (target) + new Size2D(100, 20), // line 4 - has space + new Size2D(100, 20), // line 5 - has space + new Size2D(100, 20), // line 6 - has space + new Size2D(100, 20), // line 7 - excluded (last) + ]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 0); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + // Target is line 3, but it should find line 4 (searching outward) + const result = context.tryFindWidgetOutline(3, 15, 500, defaultLayoutConstants); + + assert.ok(result !== undefined); + }); + + test('prefers closer lines to target', () => { + const sizes = [ + new Size2D(100, 20), // line 0 - excluded (first) + new Size2D(100, 20), // line 1 - has space + new Size2D(100, 20), // line 2 - has space + new Size2D(100, 20), // line 3 - has space + new Size2D(500, 9999),// line 4 - no space (target) + new Size2D(100, 20), // line 5 - has space + new Size2D(100, 20), // line 6 - has space + new Size2D(100, 20), // line 7 - has space + new Size2D(100, 20), // line 8 - excluded (last) + ]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 0); + + for (let targetLine = 0; targetLine <= 4; targetLine++) { + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const result = context.tryFindWidgetOutline(targetLine, 15, 500, defaultLayoutConstants); + assert.ok(result !== undefined); + assert.ok(result.verticalWidgetRange.endExclusive < 9999); + } + + for (let targetLine = 5; targetLine <= 10 /* test outside line range */; targetLine++) { + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const result = context.tryFindWidgetOutline(targetLine, 15, 500, defaultLayoutConstants); + assert.ok(result !== undefined); + assert.ok(result.verticalWidgetRange.start > 9999); + } + }); + + test('horizontal widget range ends at editor content right', () => { + const sizes = [new Size2D(100, 20), new Size2D(100, 20), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 0); + const editorTrueContentRight = 500; + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + const result = context.tryFindWidgetOutline(2, 15, editorTrueContentRight, defaultLayoutConstants); + + assert.ok(result !== undefined); + assert.strictEqual(result.horizontalWidgetRange.endExclusive, editorTrueContentRight); + }); + }); + + suite('edge cases', () => { + test('handles single line range', () => { + const sizes = [new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(5, sizes, 50); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + assert.strictEqual(context.availableSpaceSizes.length, 1); + assert.deepStrictEqual(context.availableSpaceHeightPrefixSums, [0, 20]); + }); + + test('handles empty content lines (width 0)', () => { + const sizes = [new Size2D(0, 20), new Size2D(0, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + assert.strictEqual(context.availableSpaceSizes[0].width, 500); + assert.strictEqual(context.availableSpaceSizes[1].width, 500); + }); + + test('handles varying line heights', () => { + const sizes = [new Size2D(100, 10), new Size2D(100, 30), new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(1, sizes, 100); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + // Verify prefix sums account for varying heights + assert.deepStrictEqual(context.availableSpaceHeightPrefixSums, [0, 10, 40, 60]); + }); + + test('handles very large line numbers', () => { + const sizes = [new Size2D(100, 20)]; + const lineRangeInfo = createLineRangeInfo(10000, sizes, 0); + + const context = new WidgetPlacementContext(lineRangeInfo, 500, () => 0); + + const outline = context.getWidgetVerticalOutline(10000, 50, defaultLayoutConstants); + assert.ok(outline !== undefined); + }); + }); +}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/renameSymbolProcessor.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/renameSymbolProcessor.test.ts new file mode 100644 index 00000000000..d19ba204189 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/renameSymbolProcessor.test.ts @@ -0,0 +1,271 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { Range } from '../../../../common/core/range.js'; +import { RenameInferenceEngine } from '../../browser/model/renameSymbolProcessor.js'; +import { createTextModel } from '../../../../test/common/testTextModel.js'; +import type { Position } from '../../../../common/core/position.js'; +import { StandardTokenType } from '../../../../common/encodedTokenAttributes.js'; +import type { ITextModel } from '../../../../common/model.js'; + +class TestRenameInferenceEngine extends RenameInferenceEngine { + + constructor(private readonly identifiers: { type: StandardTokenType; range: Range }[]) { + super(); + } + + protected override getTokenAtPosition(textModel: ITextModel, position: Position): { type: StandardTokenType; range: Range } { + for (const id of this.identifiers) { + if (id.range.containsPosition(position)) { + return { type: id.type, range: id.range }; + } + } + throw new Error('No token found at position'); + } +} + +function assertDefined(value: T | undefined | null): asserts value is T { + assert.ok(value !== undefined && value !== null); +} + +suite('renameSymbolProcessor', () => { + + // This got copied from the TypeScript language configuration. + const wordPattern = /(-?\d*\.\d\w*)|([^\`\@\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]+)/; + + let disposables: DisposableStore; + + setup(() => { + disposables = new DisposableStore(); + }); + + teardown(() => { + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('Full identifier rename', () => { + const model = createTextModel([ + 'const foo = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 10) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 7, 1, 10), 'bar', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'foo'); + assert.strictEqual(result.renames.newName, 'bar'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 10); + assert.strictEqual(edit.text, 'bar'); + }); + + test('Prefix rename - replacement', () => { + const model = createTextModel([ + 'const fooABC = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 13) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 7, 1, 10), 'bazz', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'fooABC'); + assert.strictEqual(result.renames.newName, 'bazzABC'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 13); + assert.strictEqual(edit.text, 'bazzABC'); + }); + + test('Prefix rename - full line', () => { + const model = createTextModel([ + 'const fooABC = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 13) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 18), 'const bazzABC = 1;', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'fooABC'); + assert.strictEqual(result.renames.newName, 'bazzABC'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 13); + assert.strictEqual(edit.text, 'bazzABC'); + }); + + test('Insertion - with whitespace', () => { + const model = createTextModel([ + 'foo', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 1, 1, 4) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 4, 1, 4), '.map(x => x);', wordPattern); + assert.ok(result === undefined); + }); + + test('Insertion - with whitespace - full line', () => { + const model = createTextModel([ + 'foo', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 1, 1, 4) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 4), 'foo.map(x => x);', wordPattern); + assert.ok(result === undefined); + }); + + test('Insertion - no word', () => { + const model = createTextModel([ + 'foo', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 1, 1, 4) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 4, 1, 4), '.map(x=>x);', wordPattern); + assert.ok(result === undefined); + }); + + test('Insertion - no word - full line', () => { + const model = createTextModel([ + 'foo', + ].join('\n'), 'typescript', {}); + disposables.add(model); + + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 1, 1, 4) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 4), '.map(x=>x);', wordPattern); + assert.ok(result === undefined); + }); + + test('Suffix rename - replacement', () => { + const model = createTextModel([ + 'const ABCfoo = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 13) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 10, 1, 13), 'bazz', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'ABCfoo'); + assert.strictEqual(result.renames.newName, 'ABCbazz'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 13); + assert.strictEqual(edit.text, 'ABCbazz'); + }); + + test('Suffix rename - full line', () => { + const model = createTextModel([ + 'const ABCfoo = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 13) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 18), 'const ABCbazz = 1;', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.oldName, 'ABCfoo'); + assert.strictEqual(result.renames.newName, 'ABCbazz'); + assert.strictEqual(result.renames.edits.length, 1); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 13); + assert.strictEqual(edit.text, 'ABCbazz'); + }); + + test('Prefix and suffix rename - full line', () => { + const model = createTextModel([ + 'const abcfooxyz = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 16) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 21), 'const ABCfooXYZ = 1;', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'abcfooxyz'); + assert.strictEqual(result.renames.newName, 'ABCfooXYZ'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 16); + assert.strictEqual(edit.text, 'ABCfooXYZ'); + }); + + test('Prefix and suffix rename - replacement', () => { + const model = createTextModel([ + 'const abcfooxyz = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 16) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 7, 1, 16), 'ABCfooXYZ', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'abcfooxyz'); + assert.strictEqual(result.renames.newName, 'ABCfooXYZ'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 16); + assert.strictEqual(edit.text, 'ABCfooXYZ'); + }); + + test('No rename - different identifiers - replacement', () => { + const model = createTextModel([ + 'const foo bar = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 15) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 7, 1, 15), 'faz baz', wordPattern); + assert.ok(result === undefined); + }); + + test('No rename - different identifiers - full line', () => { + const model = createTextModel([ + 'const foo bar = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 15) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 1, 1, 18), 'const faz baz = 1;', wordPattern); + assert.ok(result === undefined); + }); + + test('Suffix insertion', () => { + const model = createTextModel([ + 'const w = 1;', + ].join('\n'), 'typescript', {}); + disposables.add(model); + const renameInferenceEngine = new TestRenameInferenceEngine([{ type: StandardTokenType.Other, range: new Range(1, 7, 1, 8) }, { type: StandardTokenType.Other, range: new Range(1, 8, 1, 9) }]); + const result = renameInferenceEngine.inferRename(model, new Range(1, 8, 1, 8), 'idth', wordPattern); + assertDefined(result); + assert.strictEqual(result.renames.edits.length, 1); + assert.strictEqual(result.renames.oldName, 'w'); + assert.strictEqual(result.renames.newName, 'width'); + const edit = result.renames.edits[0]; + assert.strictEqual(edit.range.startLineNumber, 1); + assert.strictEqual(edit.range.startColumn, 7); + assert.strictEqual(edit.range.endLineNumber, 1); + assert.strictEqual(edit.range.endColumn, 8); + assert.strictEqual(edit.text, 'width'); + }); +}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/scrollToReveal.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/scrollToReveal.test.ts new file mode 100644 index 00000000000..57248887489 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/scrollToReveal.test.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { scrollToReveal } from '../../browser/view/inlineEdits/inlineEditsViews/longDistanceHint/inlineEditsLongDistanceHint.js'; + +suite('scrollToReveal', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should not scroll when content is already visible', () => { + // Content range [20, 30) is fully contained in window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(20, 30)); + assert.strictEqual(result.newScrollPosition, 10); + }); + + test('should not scroll when content exactly fits the visible window', () => { + // Content range [10, 50) exactly matches visible window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(10, 50)); + assert.strictEqual(result.newScrollPosition, 10); + }); + + test('should scroll left when content starts before visible window', () => { + // Content range [5, 15) starts before visible window [20, 60) + const result = scrollToReveal(20, 40, new OffsetRange(5, 15)); + assert.strictEqual(result.newScrollPosition, 5); + }); + + test('should scroll right when content ends after visible window', () => { + // Content range [50, 80) ends after visible window [10, 50) + // New scroll position should be 80 - 40 = 40 so window becomes [40, 80) + const result = scrollToReveal(10, 40, new OffsetRange(50, 80)); + assert.strictEqual(result.newScrollPosition, 40); + }); + + test('should show start of content when content is larger than window', () => { + // Content range [20, 100) is larger than window width 40 + // Should position at start of content + const result = scrollToReveal(10, 40, new OffsetRange(20, 100)); + assert.strictEqual(result.newScrollPosition, 20); + }); + + test('should handle edge case with zero-width content', () => { + // Empty content range [25, 25) in window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(25, 25)); + assert.strictEqual(result.newScrollPosition, 10); + }); + + test('should handle edge case with zero window width', () => { + // Any non-empty content with zero window width should position at content start + const result = scrollToReveal(10, 0, new OffsetRange(20, 30)); + assert.strictEqual(result.newScrollPosition, 20); + }); + + test('should handle content at exact window boundaries - left edge', () => { + // Content range [10, 20) starts exactly at visible window start [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(10, 20)); + assert.strictEqual(result.newScrollPosition, 10); + }); + + test('should handle content at exact window boundaries - right edge', () => { + // Content range [40, 50) ends exactly at visible window end [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(40, 50)); + assert.strictEqual(result.newScrollPosition, 10); + }); + + test('should scroll right when content extends beyond right boundary', () => { + // Content range [40, 60) extends beyond visible window [10, 50) + // New scroll position should be 60 - 40 = 20 so window becomes [20, 60) + const result = scrollToReveal(10, 40, new OffsetRange(40, 60)); + assert.strictEqual(result.newScrollPosition, 20); + }); + + test('should scroll left when content extends beyond left boundary', () => { + // Content range [5, 25) starts before visible window [20, 60) + // Should position at start of content + const result = scrollToReveal(20, 40, new OffsetRange(5, 25)); + assert.strictEqual(result.newScrollPosition, 5); + }); + + test('should handle content overlapping both boundaries', () => { + // Content range [5, 70) overlaps both sides of visible window [20, 60) + // Since content is larger than window, should position at start of content + const result = scrollToReveal(20, 40, new OffsetRange(5, 70)); + assert.strictEqual(result.newScrollPosition, 5); + }); + + test('should handle negative scroll positions', () => { + // Current scroll at -10, window width 40, so visible range [-10, 30) + // Content [35, 45) is beyond the visible window + const result = scrollToReveal(-10, 40, new OffsetRange(35, 45)); + assert.strictEqual(result.newScrollPosition, 5); // 45 - 40 = 5 + }); + + test('should handle large numbers', () => { + // Test with large numbers to ensure no overflow issues + const result = scrollToReveal(1000000, 500, new OffsetRange(1000600, 1000700)); + assert.strictEqual(result.newScrollPosition, 1000200); // 1000700 - 500 = 1000200 + }); + + test('should prioritize left scroll when content spans window but starts before', () => { + // Content [5, 55) spans wider than window width 40, starting before visible [20, 60) + // Should position at start of content + const result = scrollToReveal(20, 40, new OffsetRange(5, 55)); + assert.strictEqual(result.newScrollPosition, 5); + }); + + test('should handle single character content requiring scroll', () => { + // Single character at position [100, 101) with visible window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(100, 101)); + assert.strictEqual(result.newScrollPosition, 61); // 101 - 40 = 61 + }); + + test('should handle content just barely outside visible area - left', () => { + // Content [9, 19) with one unit outside visible window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(9, 19)); + assert.strictEqual(result.newScrollPosition, 9); + }); + + test('should handle content just barely outside visible area - right', () => { + // Content [45, 51) with one unit outside visible window [10, 50) + const result = scrollToReveal(10, 40, new OffsetRange(45, 51)); + assert.strictEqual(result.newScrollPosition, 11); // 51 - 40 = 11 + }); + + test('should handle fractional-like scenarios with minimum window', () => { + // Minimum window width 1, content needs to be revealed + const result = scrollToReveal(50, 1, new OffsetRange(100, 105)); + assert.strictEqual(result.newScrollPosition, 100); // Content larger than window, show start + }); + + test('should preserve scroll when content partially visible on left', () => { + // Content [5, 25) partially visible in window [20, 60), overlaps [20, 25) + // Since content starts before window, scroll to show start + const result = scrollToReveal(20, 40, new OffsetRange(5, 25)); + assert.strictEqual(result.newScrollPosition, 5); + }); + + test('should preserve scroll when content partially visible on right', () => { + // Content [45, 65) partially visible in window [20, 60), overlaps [45, 60) + // Since content extends beyond window, scroll to show end + const result = scrollToReveal(20, 40, new OffsetRange(45, 65)); + assert.strictEqual(result.newScrollPosition, 25); // 65 - 40 = 25 + }); +}); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 8d111bda207..6743ca65a9c 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -36,6 +36,9 @@ import { autorun } from '../../../../../base/common/observable.js'; import { setUnexpectedErrorHandler } from '../../../../../base/common/errors.js'; import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { ModifierKeyEmitter } from '../../../../../base/browser/dom.js'; +import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js'; suite('Suggest Widget Model', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -164,7 +167,12 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( [IAccessibilitySignalService, { playSignal: async () => { }, isSoundEnabled(signal: unknown) { return false; }, - } as any] + } as any], + [IDefaultAccountService, new class extends mock() { + override onDidChangeDefaultAccount = Event.None; + override getDefaultAccount = async () => null; + override setDefaultAccountProvider = () => { }; + }], ); if (options.provider) { @@ -174,6 +182,9 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( } await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => { + instantiationService.stubInstance(InlineSuggestionsView, { + dispose: () => { } + }); editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); editor.registerAndInstantiateContribution(InlineCompletionsController.ID, InlineCompletionsController); @@ -185,6 +196,7 @@ async function withAsyncTestCodeEditorAndInlineCompletionsModel( }); } finally { disposableStore.dispose(); + ModifierKeyEmitter.disposeInstance(); } }); } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index ee9d50d372b..6892e304e96 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -5,11 +5,11 @@ import { timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IReference } from '../../../../../base/common/lifecycle.js'; import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js'; import { Position } from '../../../../common/core/position.js'; import { ITextModel } from '../../../../common/model.js'; -import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js'; +import { IInlineCompletionChangeHint, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider } from '../../../../common/languages.js'; import { ITestCodeEditor, TestCodeEditorInstantiationOptions, withAsyncTestCodeEditor } from '../../../../test/browser/testCodeEditor.js'; import { InlineCompletionsModel } from '../../browser/model/inlineCompletionsModel.js'; import { autorun, derived } from '../../../../../base/common/observable.js'; @@ -24,6 +24,15 @@ import { Range } from '../../../../common/core/range.js'; import { TextEdit } from '../../../../common/core/edits/textEdit.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js'; +import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js'; +import { IBulkEditService } from '../../../../browser/services/bulkEditService.js'; +import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IRenameSymbolTrackerService, NullRenameSymbolTrackerService } from '../../../../browser/services/renameSymbolTrackerService.js'; +import { ITextModelService, IResolvedTextEditorModel } from '../../../../common/services/resolverService.js'; +import { IModelService } from '../../../../common/services/model.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; @@ -32,6 +41,9 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider private callHistory = new Array(); private calledTwiceIn50Ms = false; + private readonly _onDidChangeEmitter = new Emitter(); + public readonly onDidChangeInlineCompletions: Event = this._onDidChangeEmitter.event; + constructor( public readonly enableForwardStability = false, ) { } @@ -58,6 +70,13 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider } } + /** + * Fire an onDidChange event with an optional change hint. + */ + public fireOnDidChange(changeHint?: IInlineCompletionChangeHint): void { + this._onDidChangeEmitter.fire(changeHint); + } + private lastTimeMs: number | undefined = undefined; async provideInlineCompletions(model: ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): Promise { @@ -70,7 +89,8 @@ export class MockInlineCompletionsProvider implements InlineCompletionsProvider this.callHistory.push({ position: position.toString(), triggerKind: context.triggerKind, - text: model.getValue() + text: model.getValue(), + ...(context.changeHint !== undefined ? { changeHint: context.changeHint } : {}), }); const result = new Array(); for (const v of this.returnValue) { @@ -242,14 +262,37 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel( playSignal: async () => { }, isSoundEnabled(signal: unknown) { return false; }, } as any); + options.serviceCollection.set(IBulkEditService, { + apply: async () => { throw new Error('IBulkEditService.apply not implemented'); }, + hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); }, + setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); }, + _serviceBrand: undefined, + }); + options.serviceCollection.set(ITextModelService, new SyncDescriptor(MockTextModelService)); + options.serviceCollection.set(IDefaultAccountService, { + _serviceBrand: undefined, + onDidChangeDefaultAccount: Event.None, + onDidChangePolicyData: Event.None, + policyData: null, + getDefaultAccount: async () => null, + setDefaultAccountProvider: () => { }, + getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; }, + refresh: async () => { return null; }, + signIn: async () => { return null; }, + }); + options.serviceCollection.set(IRenameSymbolTrackerService, new NullRenameSymbolTrackerService()); + const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider); disposableStore.add(d); } let result: T; await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { + instantiationService.stubInstance(InlineSuggestionsView, { + shouldShowHoverAtViewZone: () => false, + dispose: () => { }, + }); const controller = instantiationService.createInstance(InlineCompletionsController, editor); - controller.testOnlyDisableUi(); const model = controller.model.get()!; const context = new GhostTextContext(model, editor); try { @@ -327,3 +370,40 @@ export class AnnotatedText extends AnnotatedString { return this._transformer.getPosition(this.getMarkerOffset(markerIdx)); } } + +class MockTextModelService implements ITextModelService { + readonly _serviceBrand: undefined; + + constructor( + @IModelService private readonly _modelService: IModelService, + ) { } + + async createModelReference(resource: URI): Promise> { + const model = this._modelService.getModel(resource); + if (!model) { + throw new Error(`MockTextModelService: Model not found for ${resource.toString()}`); + } + return { + object: { + textEditorModel: model, + getLanguageId: () => model.getLanguageId(), + isReadonly: () => false, + isDisposed: () => model.isDisposed(), + isResolved: () => true, + onWillDispose: model.onWillDispose, + resolve: async () => { }, + createSnapshot: () => model.createSnapshot(), + dispose: () => { }, + }, + dispose: () => { }, + }; + } + + registerTextModelContentProvider(): never { + throw new Error('MockTextModelService.registerTextModelContentProvider not implemented'); + } + + canHandleResource(): boolean { + return false; + } +} diff --git a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts index e67488579e1..8e69736c87b 100644 --- a/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/browser/linesOperations.ts @@ -251,7 +251,7 @@ export abstract class AbstractSortLinesAction extends EditorAction { const model = editor.getModel(); let selections = editor.getSelections(); - if (selections.length === 1 && selections[0].isEmpty()) { + if (selections.length === 1 && selections[0].isSingleLine()) { // Apply to whole document. selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))]; } @@ -322,7 +322,7 @@ export class DeleteDuplicateLinesAction extends EditorAction { let updateSelection = true; let selections = editor.getSelections(); - if (selections.length === 1 && selections[0].isEmpty()) { + if (selections.length === 1 && selections[0].isSingleLine()) { // Apply to whole document. selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))]; updateSelection = false; @@ -356,7 +356,7 @@ export class DeleteDuplicateLinesAction extends EditorAction { adjustedSelectionStart, 1, adjustedSelectionStart + lines.length - 1, - lines[lines.length - 1].length + lines[lines.length - 1].length + 1 ); edits.push(EditOperation.replace(selectionToReplace, lines.join('\n'))); @@ -389,7 +389,7 @@ export class ReverseLinesAction extends EditorAction { const model: ITextModel = editor.getModel(); const originalSelections = editor.getSelections(); let selections = originalSelections; - if (selections.length === 1 && selections[0].isEmpty()) { + if (selections.length === 1 && selections[0].isSingleLine()) { // Apply to whole document. selections = [new Selection(1, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount()))]; } @@ -559,6 +559,7 @@ export class DeleteLinesAction extends EditorAction { editor.pushUndoStop(); editor.executeEdits(this.id, edits, cursorState); + editor.revealAllCursors(true); editor.pushUndoStop(); } diff --git a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts index 40fdd0378e1..bcaa9b698dd 100644 --- a/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts +++ b/src/vs/editor/contrib/linesOperations/browser/moveLinesCommand.ts @@ -40,14 +40,28 @@ export class MoveLinesCommand implements ICommand { this._moveEndLineSelectionShrink = false; } - public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { - - const getLanguageId = () => { - return model.getLanguageId(); - }; - const getLanguageIdAtPosition = (lineNumber: number, column: number) => { - return model.getLanguageIdAtPosition(lineNumber, column); + private createVirtualModel( + model: ITextModel, + lineNumberMapper: (lineNumber: number) => number, + contentOverride?: (lineNumber: number) => string | undefined + ): IVirtualModel { + return { + tokenization: { + getLineTokens: (lineNumber) => model.tokenization.getLineTokens(lineNumberMapper(lineNumber)), + getLanguageId: () => model.getLanguageId(), + getLanguageIdAtPosition: (lineNumber, column) => model.getLanguageIdAtPosition(lineNumber, column) + }, + getLineContent: (lineNumber) => { + const customContent = contentOverride?.(lineNumber); + if (customContent !== undefined) { + return customContent; + } + return model.getLineContent(lineNumberMapper(lineNumber)); + } }; + } + + public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { const modelLineCount = model.getLineCount(); @@ -113,26 +127,10 @@ export class MoveLinesCommand implements ICommand { insertingText = newIndentation + this.trimStart(movingLineText); } else { // no enter rule matches, let's check indentatin rules then. - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return model.tokenization.getLineTokens(movingLineNumber); - } else { - return model.tokenization.getLineTokens(lineNumber); - } - }, - getLanguageId, - getLanguageIdAtPosition, - }, - getLineContent: (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - return model.getLineContent(movingLineNumber); - } else { - return model.getLineContent(lineNumber); - } - }, - }; + const virtualModel = this.createVirtualModel( + model, + (lineNumber) => lineNumber === s.startLineNumber ? movingLineNumber : lineNumber + ); const indentOfMovingLine = getGoodIndentForLine( this._autoIndent, virtualModel, @@ -165,31 +163,20 @@ export class MoveLinesCommand implements ICommand { } } else { // it doesn't match onEnter rules, let's check indentation rules then. - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - if (lineNumber === s.startLineNumber) { - // TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this. - return model.tokenization.getLineTokens(movingLineNumber); - } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { - return model.tokenization.getLineTokens(lineNumber - 1); - } else { - return model.tokenization.getLineTokens(lineNumber); - } - }, - getLanguageId, - getLanguageIdAtPosition, - }, - getLineContent: (lineNumber: number) => { + const virtualModel = this.createVirtualModel( + model, + (lineNumber) => { if (lineNumber === s.startLineNumber) { - return insertingText; + // TODO@aiday-mar: the tokens here don't correspond exactly to the corresponding content (after indentation adjustment), have to fix this. + return movingLineNumber; } else if (lineNumber >= s.startLineNumber + 1 && lineNumber <= s.endLineNumber + 1) { - return model.getLineContent(lineNumber - 1); + return lineNumber - 1; } else { - return model.getLineContent(lineNumber); + return lineNumber; } }, - }; + (lineNumber) => lineNumber === s.startLineNumber ? insertingText : undefined + ); const newIndentatOfMovingBlock = getGoodIndentForLine( this._autoIndent, @@ -226,26 +213,10 @@ export class MoveLinesCommand implements ICommand { builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + movingLineText); if (this.shouldAutoIndent(model, s)) { - const virtualModel: IVirtualModel = { - tokenization: { - getLineTokens: (lineNumber: number) => { - if (lineNumber === movingLineNumber) { - return model.tokenization.getLineTokens(s.startLineNumber); - } else { - return model.tokenization.getLineTokens(lineNumber); - } - }, - getLanguageId, - getLanguageIdAtPosition, - }, - getLineContent: (lineNumber: number) => { - if (lineNumber === movingLineNumber) { - return model.getLineContent(s.startLineNumber); - } else { - return model.getLineContent(lineNumber); - } - }, - }; + const virtualModel = this.createVirtualModel( + model, + (lineNumber) => lineNumber === movingLineNumber ? s.startLineNumber : lineNumber + ); const ret = this.matchEnterRule(model, indentConverter, tabSize, s.startLineNumber, s.startLineNumber - 2); // check if s.startLineNumber - 2 matches onEnter rules, if so adjust the moving block by onEnter rules. diff --git a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts index 5560f78f70d..b599882be99 100644 --- a/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/browser/linesOperations.test.ts @@ -106,6 +106,26 @@ suite('Editor Contrib - Line Operations', () => { }); }); }); + + test('applies to whole document when selection is single line', function () { + withTestCodeEditor( + [ + 'omicron', + 'beta', + 'alpha' + ], {}, (editor) => { + const model = editor.getModel()!; + const sortLinesAscendingAction = new SortLinesAscendingAction(); + + editor.setSelection(new Selection(2, 1, 2, 4)); + executeAction(sortLinesAscendingAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron' + ]); + }); + }); }); suite('SortLinesDescendingAction', () => { @@ -187,7 +207,7 @@ suite('Editor Contrib - Line Operations', () => { 'beta', 'omicron', ]); - assertSelection(editor, new Selection(1, 1, 3, 7)); + assertSelection(editor, new Selection(1, 1, 3, 8)); }); }); @@ -240,14 +260,31 @@ suite('Editor Contrib - Line Operations', () => { 'beta' ]); const expectedSelections = [ - new Selection(1, 1, 3, 7), - new Selection(5, 1, 6, 4) + new Selection(1, 1, 3, 8), + new Selection(5, 1, 6, 5) ]; editor.getSelections()!.forEach((actualSelection, index) => { assert.deepStrictEqual(actualSelection.toString(), expectedSelections[index].toString()); }); }); }); + + test('applies to whole document when selection is single line', function () { + withTestCodeEditor( + [ + 'alpha', + 'beta', + 'alpha', + 'omicron' + ], {}, (editor) => { + const model = editor.getModel()!; + const deleteDuplicateLinesAction = new DeleteDuplicateLinesAction(); + + editor.setSelection(new Selection(2, 1, 2, 2)); + executeAction(deleteDuplicateLinesAction, editor); + assert.deepStrictEqual(model.getLinesContent(), ['alpha', 'beta', 'omicron']); + }); + }); }); @@ -729,7 +766,7 @@ suite('Editor Contrib - Line Operations', () => { }); }); - test('handles single line selection', function () { + test('applies to whole document when selection is single line', function () { withTestCodeEditor( [ 'line1', @@ -742,8 +779,7 @@ suite('Editor Contrib - Line Operations', () => { // Select only line 2 editor.setSelection(new Selection(2, 1, 2, 6)); executeAction(reverseLinesAction, editor); - // Single line should remain unchanged - assert.deepStrictEqual(model.getLinesContent(), ['line1', 'line2', 'line3']); + assert.deepStrictEqual(model.getLinesContent(), ['line3', 'line2', 'line1']); }); }); @@ -765,6 +801,26 @@ suite('Editor Contrib - Line Operations', () => { assert.deepStrictEqual(model.getLinesContent(), ['line1', 'line3', 'line2', 'line4', 'line5']); }); }); + + test('applies to whole document when selection is single line', function () { + withTestCodeEditor( + [ + 'omicron', + 'beta', + 'alpha' + ], {}, (editor) => { + const model = editor.getModel()!; + const reverseLinesAction = new ReverseLinesAction(); + + editor.setSelection(new Selection(2, 1, 2, 4)); + executeAction(reverseLinesAction, editor); + assert.deepStrictEqual(model.getLinesContent(), [ + 'alpha', + 'beta', + 'omicron' + ]); + }); + }); }); test('transpose', () => { diff --git a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts index 749259c2711..9ec8fbfc480 100644 --- a/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess.ts @@ -147,8 +147,9 @@ export abstract class AbstractEditorNavigationQuickAccessProvider implements IQu if (!options.preserveFocus) { editor.focus(); } - const model = editor.getModel(); - if (model && 'getLineContent' in model) { + + const model = this.getModel(editor); + if (model) { status(`${model.getLineContent(options.range.startLineNumber)}`); } } diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts index 12be47788be..f12321212b3 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; +import { IQuickInputButton, IQuickPick, IQuickPickItem, QuickInputButtonLocation } from '../../../../platform/quickinput/common/quickInput.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from '../../../../platform/theme/common/colors/inputColors.js'; -import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; import { getCodeEditor } from '../../../browser/editorBrowser.js'; import { EditorOption, RenderLineNumbersType } from '../../../common/config/editorOptions.js'; +import { CursorColumns } from '../../../common/core/cursorColumns.js'; import { IPosition } from '../../../common/core/position.js'; import { IRange } from '../../../common/core/range.js'; import { IEditor, ScrollType } from '../../../common/editorCommon.js'; @@ -23,7 +22,8 @@ interface IGotoLineQuickPickItem extends IQuickPickItem, Partial { } export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider { - static readonly PREFIX = ':'; + static readonly GO_TO_LINE_PREFIX = ':'; + static readonly GO_TO_OFFSET_PREFIX = '::'; private static readonly ZERO_BASED_OFFSET_STORAGE_KEY = 'gotoLine.useZeroBasedOffset'; constructor() { @@ -48,7 +48,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor } protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line."); + const label = localize('gotoLine.noEditor', "Open a text editor first to go to a line or an offset."); picker.items = [{ label }]; picker.ariaLabel = label; @@ -76,13 +76,21 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor } })); + // Add a toggle to switch between 1- and 0-based offsets. + const offsetButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.indexZero), + tooltip: localize('gotoLineToggleButton', "Toggle Zero-Based Offset"), + location: QuickInputButtonLocation.Input, + toggle: { checked: this.useZeroBasedOffset } + }; + // React to picker changes const updatePickerAndEditor = () => { - const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.PREFIX.length); + const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX.length); const { inOffsetMode, lineNumber, column, label } = this.parsePosition(editor, inputText); // Show toggle only when input text starts with '::'. - toggle.visible = !!inOffsetMode; + picker.buttons = inOffsetMode ? [offsetButton] : []; // Picker picker.items = [{ @@ -91,9 +99,6 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor label, }]; - // ARIA Label - picker.ariaLabel = label; - // Clear decorations for invalid range if (!lineNumber) { this.clearDecorations(editor); @@ -108,23 +113,12 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor this.addDecorations(editor, range); }; - // Add a toggle to switch between 1- and 0-based offsets. - const toggle = new Toggle({ - title: localize('gotoLineToggle', "Use Zero-Based Offset"), - icon: Codicon.indexZero, - isChecked: this.useZeroBasedOffset, - inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), - inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), - inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground) - }); - - disposables.add( - toggle.onChange(() => { - this.useZeroBasedOffset = !this.useZeroBasedOffset; + disposables.add(picker.onDidTriggerButton(button => { + if (button === offsetButton) { + this.useZeroBasedOffset = button.toggle?.checked ?? !this.useZeroBasedOffset; updatePickerAndEditor(); - })); - - picker.toggles = [toggle]; + } + })); updatePickerAndEditor(); disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor())); @@ -157,7 +151,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor const model = this.getModel(editor); if (!model) { return { - label: localize('gotoLine.noEditor', "Open a text editor first to go to a line.") + label: localize('gotoLine.noEditor', "Open a text editor first to go to a line or an offset.") }; } @@ -179,15 +173,22 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor // Convert 1-based offset to model's 0-based. offset -= Math.sign(offset); } + if (reverse) { // Offset from the end of the buffer offset += maxOffset; } + const pos = model.getPositionAt(offset); + const visibleColumn = CursorColumns.visibleColumnFromColumn( + model.getLineContent(pos.lineNumber), + pos.column, + model.getOptions().tabSize) + 1; + return { ...pos, inOffsetMode: true, - label: localize('gotoLine.goToPosition', "Press 'Enter' to go to line {0} at column {1}.", pos.lineNumber, pos.column) + label: localize('gotoLine.goToPosition', "Press 'Enter' to go to line {0} at column {1}.", pos.lineNumber, visibleColumn) }; } } else { @@ -206,14 +207,18 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor lineNumber = lineNumber >= 0 ? lineNumber : (maxLine + 1) + lineNumber; lineNumber = Math.min(Math.max(1, lineNumber), maxLine); - const maxColumn = model.getLineMaxColumn(lineNumber); + // Treat column number as visible column + const tabSize = model.getOptions().tabSize; + const lineContent = model.getLineContent(lineNumber); + const maxColumn = CursorColumns.visibleColumnFromColumn(lineContent, model.getLineMaxColumn(lineNumber), tabSize) + 1; + let column = parseInt(parts[1]?.trim(), 10); if (parts.length < 2 || isNaN(column)) { return { lineNumber, column: 1, label: parts.length < 2 ? - localize('gotoLine.lineColumnPrompt', "Press 'Enter' to go to line {0} or enter : to add a column number.", lineNumber) : + localize('gotoLine.lineColumnPrompt', "Press 'Enter' to go to line {0} or enter colon : to add a column number.", lineNumber) : localize('gotoLine.columnPrompt', "Press 'Enter' to go to line {0} or enter a column number (from 1 to {1}).", lineNumber, maxColumn) }; } @@ -222,9 +227,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor column = column >= 0 ? column : maxColumn + column; column = Math.min(Math.max(1, column), maxColumn); + const realColumn = CursorColumns.columnFromVisibleColumn(lineContent, column - 1, tabSize); return { lineNumber, - column, + column: realColumn, label: localize('gotoLine.goToPosition', "Press 'Enter' to go to line {0} at column {1}.", lineNumber, column) }; } diff --git a/src/vs/editor/contrib/quickAccess/test/browser/gotoLineQuickAccess.test.ts b/src/vs/editor/contrib/quickAccess/test/browser/gotoLineQuickAccess.test.ts index 0737f73e1e0..866c21427e4 100644 --- a/src/vs/editor/contrib/quickAccess/test/browser/gotoLineQuickAccess.test.ts +++ b/src/vs/editor/contrib/quickAccess/test/browser/gotoLineQuickAccess.test.ts @@ -95,4 +95,45 @@ suite('AbstractGotoLineQuickAccessProvider', () => { runTest('2,4', 2, 4); runTest(' 2 : 3 ', 2, 3); }); + + function runTabTest(input: string, expectedLine: number, expectedColumn?: number, zeroBased = false) { + const provider = new TestGotoLineQuickAccessProvider(zeroBased); + withTestCodeEditor([ + '\tline 1', + '\t\tline 2', + '\tline 3' + ], {}, (editor, _) => { + const { lineNumber, column } = provider.parsePositionTest(editor, input); + assert.strictEqual(lineNumber, expectedLine); + assert.strictEqual(column, expectedColumn ?? 1); + }); + } + + test('parsePosition works with tabs', () => { + // :line,column + runTabTest('1:1', 1, 1); + runTabTest('1:2', 1, 1); + runTabTest('1:3', 1, 1); + runTabTest('1:4', 1, 2); + runTabTest('1:5', 1, 2); + runTabTest('1:6', 1, 3); + runTabTest('1:11', 1, 8); + runTabTest('1:12', 1, 8); + runTabTest('1:-5', 1, 3); + runTabTest('1:-6', 1, 2); + runTabTest('1:-7', 1, 2); + runTabTest('1:-8', 1, 1); + runTabTest('1:-9', 1, 1); + runTabTest('1:-10', 1, 1); + runTabTest('1:-11', 1, 1); + runTabTest('2:1', 2, 1); + runTabTest('2:2', 2, 1); + runTabTest('2:3', 2, 1); + runTabTest('2:4', 2, 2); + runTabTest('2:5', 2, 2); + runTabTest('2:8', 2, 3); + runTabTest('2:9', 2, 3); + runTabTest('2:11', 2, 5); + }); }); + diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index 27a7eb5f518..cf675f54cf4 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -119,6 +119,21 @@ class RenameSkeleton { } } +export function hasProvider(registry: LanguageFeatureRegistry, model: ITextModel): boolean { + const providers = registry.ordered(model); + return providers.length > 0; +} + +export async function prepareRename(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, cancellationToken?: CancellationToken): Promise { + const skeleton = new RenameSkeleton(model, position, registry); + return skeleton.resolveRenameLocation(cancellationToken ?? CancellationToken.None); +} + +export async function rawRename(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, newName: string, cancellationToken?: CancellationToken): Promise { + const skeleton = new RenameSkeleton(model, position, registry); + return skeleton.provideRenameEdits(newName, cancellationToken ?? CancellationToken.None); +} + export async function rename(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, newName: string): Promise { const skeleton = new RenameSkeleton(model, position, registry); const loc = await skeleton.resolveRenameLocation(CancellationToken.None); @@ -295,7 +310,7 @@ class RenameController implements IEditorContribution { code: 'undoredo.rename', quotableLabel: nls.localize('quotableLabel', "Renaming {0} to {1}", loc?.text, inputFieldResult.newName), respectAutoSaveConfig: true, - reason: EditSources.rename(), + reason: EditSources.rename(loc?.text, inputFieldResult.newName), }).then(result => { trace('edits applied'); if (result.ariaSummary) { diff --git a/src/vs/editor/contrib/rename/browser/renameWidget.css b/src/vs/editor/contrib/rename/browser/renameWidget.css index 66f241efd1c..acd375f2afb 100644 --- a/src/vs/editor/contrib/rename/browser/renameWidget.css +++ b/src/vs/editor/contrib/rename/browser/renameWidget.css @@ -15,7 +15,7 @@ .monaco-editor .rename-box .rename-input-with-button { padding: 3px; - border-radius: 2px; + border-radius: 4px; width: calc(100% - 8px); /* 4px padding on each side */ } diff --git a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts index 8fda7ce5b66..e88fa56ddd5 100644 --- a/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts +++ b/src/vs/editor/contrib/sectionHeaders/browser/sectionHeaders.ts @@ -156,8 +156,10 @@ export class SectionHeaderDetector extends Disposable implements IEditorContribu const tokens = model.tokenization.getLineTokens(validRange.startLineNumber); const idx = tokens.findTokenIndexAtOffset(validRange.startColumn - 1); const tokenType = tokens.getStandardTokenType(idx); - const languageId = tokens.getLanguageId(idx); - return (languageId === model.getLanguageId() && tokenType === StandardTokenType.Comment); + + const languageIdAtPosition = model.getLanguageIdAtPosition(validRange.startLineNumber, validRange.startColumn); + const tokenLanguageId = tokens.getLanguageId(idx); + return (tokenLanguageId === languageIdAtPosition && tokenType === StandardTokenType.Comment); }); } diff --git a/src/vs/editor/contrib/snippet/browser/snippet.md b/src/vs/editor/contrib/snippet/browser/snippet.md index a5836e270e9..4068bf80675 100644 --- a/src/vs/editor/contrib/snippet/browser/snippet.md +++ b/src/vs/editor/contrib/snippet/browser/snippet.md @@ -118,7 +118,7 @@ variable ::= '$' var | '${' var }' | '${' var transform '}' transform ::= '/' regex '/' (format | text)+ '/' options format ::= '$' int | '${' int '}' - | '${' int ':' '/upcase' | '/downcase' | '/capitalize' | '/camelcase' | '/pascalcase' '}' + | '${' int ':' '/upcase' | '/downcase' | '/capitalize' | '/camelcase' | '/pascalcase' | '/kebabcase' | '/snakecase' '}' | '${' int ':+' if '}' | '${' int ':?' if ':' else '}' | '${' int ':-' else '}' | '${' int ':' else '}' diff --git a/src/vs/editor/contrib/snippet/browser/snippetParser.ts b/src/vs/editor/contrib/snippet/browser/snippetParser.ts index f045cf0cef9..8fc2c6ae149 100644 --- a/src/vs/editor/contrib/snippet/browser/snippetParser.ts +++ b/src/vs/editor/contrib/snippet/browser/snippetParser.ts @@ -387,6 +387,10 @@ export class FormatString extends Marker { return !value ? '' : this._toPascalCase(value); } else if (this.shorthandName === 'camelcase') { return !value ? '' : this._toCamelCase(value); + } else if (this.shorthandName === 'kebabcase') { + return !value ? '' : this._toKebabCase(value); + } else if (this.shorthandName === 'snakecase') { + return !value ? '' : this._toSnakeCase(value); } else if (Boolean(value) && typeof this.ifValue === 'string') { return this.ifValue; } else if (!Boolean(value) && typeof this.elseValue === 'string') { @@ -396,8 +400,41 @@ export class FormatString extends Marker { } } + // Note: word-based case transforms rely on uppercase/lowercase distinctions. + // For scripts without case, transforms are effectively no-ops. + private _toKebabCase(value: string): string { + const match = value.match(/[\p{L}0-9]+/gu); + if (!match) { + return value; + } + + if (!value.match(/[\p{L}0-9]/u)) { + return value + .trim() + .toLowerCase() + .replace(/^_+|_+$/g, '') + .replace(/[\s_]+/g, '-'); + } + + const cleaned = value.trim().replace(/^_+|_+$/g, ''); + + const match2 = cleaned.match(/\p{Lu}{2,}(?=\p{Lu}\p{Ll}+[0-9]*|[\s_-]|$)|\p{Lu}?\p{Ll}+[0-9]*|\p{Lu}(?=\p{Lu}\p{Ll})|\p{Lu}(?=[\s_-]|$)|[0-9]+/gu); + + if (!match2) { + return cleaned + .split(/[\s_-]+/) + .filter(word => word.length > 0) + .map(word => word.toLowerCase()) + .join('-'); + } + + return match2 + .map(x => x.toLowerCase()) + .join('-'); + } + private _toPascalCase(value: string): string { - const match = value.match(/[a-z0-9]+/gi); + const match = value.match(/[\p{L}0-9]+/gu); if (!match) { return value; } @@ -408,7 +445,7 @@ export class FormatString extends Marker { } private _toCamelCase(value: string): string { - const match = value.match(/[a-z0-9]+/gi); + const match = value.match(/[\p{L}0-9]+/gu); if (!match) { return value; } @@ -421,6 +458,12 @@ export class FormatString extends Marker { .join(''); } + private _toSnakeCase(value: string): string { + return value.replace(/(\p{Ll})(\p{Lu})/gu, '$1_$2') + .replace(/[\s\-]+/g, '_') + .toLowerCase(); + } + toTextmateString(): string { let value = '${'; value += this.index; diff --git a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts index 31fda916089..efb7decee2c 100644 --- a/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts +++ b/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from '../../browser/snippetParser.js'; +import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable, VariableResolver } from '../../browser/snippetParser.js'; suite('SnippetParser', () => { @@ -668,6 +668,20 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, 'camelcase').resolve('snake_AndCamelCase'), 'snakeAndCamelCase'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('kebab-AndCamelCase'), 'kebabAndCamelCase'); assert.strictEqual(new FormatString(1, 'camelcase').resolve('_JustCamelCase'), 'justCamelCase'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('barFoo'), 'bar-foo'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('BarFoo'), 'bar-foo'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('ABarFoo'), 'a-bar-foo'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('bar42Foo'), 'bar42-foo'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('snake_AndPascalCase'), 'snake-and-pascal-case'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('kebab-AndCamelCase'), 'kebab-and-camel-case'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('_justPascalCase'), 'just-pascal-case'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('__UPCASE__'), 'upcase'); + assert.strictEqual(new FormatString(1, 'kebabcase').resolve('__BAR_FOO__'), 'bar-foo'); + assert.strictEqual(new FormatString(1, 'snakecase').resolve('bar-foo'), 'bar_foo'); + assert.strictEqual(new FormatString(1, 'snakecase').resolve('bar-42-foo'), 'bar_42_foo'); + assert.strictEqual(new FormatString(1, 'snakecase').resolve('snake_AndPascalCase'), 'snake_and_pascal_case'); + assert.strictEqual(new FormatString(1, 'snakecase').resolve('kebab-AndPascalCase'), 'kebab_and_pascal_case'); + assert.strictEqual(new FormatString(1, 'snakecase').resolve('_justPascalCase'), '_just_pascal_case'); assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input'); // if @@ -686,6 +700,44 @@ suite('SnippetParser', () => { assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar'); }); + test('Unicode Variable Transformations', () => { + const resolver = new class implements VariableResolver { + resolve(variable: Variable): string | undefined { + const values: { [key: string]: string } = { + 'RUSSIAN': 'одинДва', + 'GREEK': 'έναςΔύο', + 'TURKISH': 'istanbulLı', + 'JAPANESE': 'こんにちは' + }; + return values[variable.name]; + } + }; + + function assertTransform(transformName: string, varName: string, expected: string) { + const p = new SnippetParser(); + const snippet = p.parse(`\${${varName}/(.*)/\${1:/${transformName}}/}`); + const variable = snippet.children[0] as Variable; + variable.resolve(resolver); + const resolved = variable.toString(); + assert.strictEqual(resolved, expected, `${transformName} failed for ${varName}`); + } + + assertTransform('kebabcase', 'RUSSIAN', 'один-два'); + assertTransform('kebabcase', 'GREEK', 'ένας-δύο'); + assertTransform('snakecase', 'RUSSIAN', 'один_два'); + assertTransform('snakecase', 'GREEK', 'ένας_δύο'); + assertTransform('camelcase', 'RUSSIAN', 'одинДва'); + assertTransform('camelcase', 'GREEK', 'έναςΔύο'); + assertTransform('pascalcase', 'RUSSIAN', 'ОдинДва'); + assertTransform('pascalcase', 'GREEK', 'ΈναςΔύο'); + assertTransform('upcase', 'RUSSIAN', 'ОДИНДВА'); + assertTransform('downcase', 'RUSSIAN', 'одиндва'); + assertTransform('kebabcase', 'TURKISH', 'istanbul-lı'); + assertTransform('pascalcase', 'TURKISH', 'IstanbulLı'); + assertTransform('upcase', 'JAPANESE', 'こんにちは'); + assertTransform('kebabcase', 'JAPANESE', 'こんにちは'); + }); + test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function () { const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"'); assert.strictEqual(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"'); diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts index 4589c4cabe9..2c8b1c55fca 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollController.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { IDisposable, Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ICodeEditor, MouseTargetType } from '../../../browser/editorBrowser.js'; import { IEditorContribution, ScrollType } from '../../../common/editorCommon.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; @@ -75,6 +75,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib private _showEndForLine: number | undefined; private _minRebuildFromLine: number | undefined; private _mouseTarget: EventTarget | null = null; + private _cursorPositionListener: IDisposable | undefined; private readonly _onDidChangeStickyScrollHeight = this._register(new Emitter<{ height: number }>()); public readonly onDidChangeStickyScrollHeight = this._onDidChangeStickyScrollHeight.event; @@ -410,6 +411,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib this._contextMenuService.showContextMenu({ menuId: MenuId.StickyScrollContext, getAnchor: () => event, + menuActionOptions: { renderShortTitle: true }, }); } @@ -475,10 +477,17 @@ export class StickyScrollController extends Disposable implements IEditorContrib const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { - this._sessionStore.add(this._editor.onDidChangeCursorPosition(() => { - this._showEndForLine = undefined; - this._renderStickyScroll(0); - })); + if (!this._cursorPositionListener) { + this._cursorPositionListener = this._editor.onDidChangeCursorPosition(() => { + this._showEndForLine = undefined; + this._renderStickyScroll(0); + }); + this._sessionStore.add(this._cursorPositionListener); + } + } else if (this._cursorPositionListener) { + this._sessionStore.delete(this._cursorPositionListener); + this._cursorPositionListener.dispose(); + this._cursorPositionListener = undefined; } } diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 74235c230cc..38947cfee8d 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -60,7 +60,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private readonly _editor: ICodeEditor; private _state: StickyScrollWidgetState | undefined; - private _lineHeight: number; private _renderedStickyLines: RenderedStickyLine[] = []; private _lineNumbers: number[] = []; private _lastLineRelativePosition: number = 0; @@ -79,7 +78,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { super(); this._editor = editor; - this._lineHeight = editor.getOption(EditorOption.lineHeight); this._lineNumbersDomNode.className = 'sticky-widget-line-numbers'; this._lineNumbersDomNode.setAttribute('role', 'none'); @@ -102,9 +100,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { if (e.hasChanged(EditorOption.stickyScroll)) { updateScrollLeftPosition(); } - if (e.hasChanged(EditorOption.lineHeight)) { - this._lineHeight = this._editor.getOption(EditorOption.lineHeight); - } })); this._register(this._editor.onDidScrollChange((e) => { if (e.scrollLeftChanged) { @@ -278,6 +273,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } private _setFoldingHoverListeners(): void { + this._foldingIconStore.clear(); const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); if (showFoldingControls !== 'mouseover') { return; @@ -295,92 +291,15 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } private _renderChildNode(viewModel: IViewModel, index: number, line: number, top: number, isLastLine: boolean, foldingModel: FoldingModel | undefined, layoutInfo: EditorLayoutInfo): RenderedStickyLine { - const viewLineNumber = viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(line, 1)).lineNumber; - const lineRenderingData = viewModel.getViewLineRenderingData(viewLineNumber); - const lineNumberOption = this._editor.getOption(EditorOption.lineNumbers); - const verticalScrollbarSize = this._editor.getOption(EditorOption.scrollbar).verticalScrollbarSize; - - let actualInlineDecorations: LineDecoration[]; - try { - actualInlineDecorations = LineDecoration.filter(lineRenderingData.inlineDecorations, viewLineNumber, lineRenderingData.minColumn, lineRenderingData.maxColumn); - } catch (err) { - actualInlineDecorations = []; - } - - const lineHeight = this._editor.getLineHeightForPosition(new Position(line, 1)); - const textDirection = viewModel.getTextDirection(line); - const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, - lineRenderingData.continuesWithWrappedLine, - lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, - lineRenderingData.tokens, actualInlineDecorations, - lineRenderingData.tabSize, lineRenderingData.startVisibleColumn, - 1, 1, 1, 500, 'none', true, true, null, - textDirection, verticalScrollbarSize - ); - - const sb = new StringBuilder(2000); - const renderOutput = renderViewLine(renderLineInput, sb); - - let newLine; - if (_ttPolicy) { - newLine = _ttPolicy.createHTML(sb.build()); - } else { - newLine = sb.build(); - } - - const lineHTMLNode = document.createElement('span'); - lineHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); - lineHTMLNode.setAttribute(STICKY_IS_LINE_ATTR, ''); - lineHTMLNode.setAttribute('role', 'listitem'); - lineHTMLNode.tabIndex = 0; - lineHTMLNode.className = 'sticky-line-content'; - lineHTMLNode.classList.add(`stickyLine${line}`); - lineHTMLNode.style.lineHeight = `${lineHeight}px`; - lineHTMLNode.innerHTML = newLine as string; - - const lineNumberHTMLNode = document.createElement('span'); - lineNumberHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); - lineNumberHTMLNode.setAttribute(STICKY_IS_LINE_NUMBER_ATTR, ''); - lineNumberHTMLNode.className = 'sticky-line-number'; - lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`; - const lineNumbersWidth = layoutInfo.contentLeft; - lineNumberHTMLNode.style.width = `${lineNumbersWidth}px`; - - const innerLineNumberHTML = document.createElement('span'); - if (lineNumberOption.renderType === RenderLineNumbersType.On || lineNumberOption.renderType === RenderLineNumbersType.Interval && line % 10 === 0) { - innerLineNumberHTML.innerText = line.toString(); - } else if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { - innerLineNumberHTML.innerText = Math.abs(line - this._editor.getPosition()!.lineNumber).toString(); - } - innerLineNumberHTML.className = 'sticky-line-number-inner'; - innerLineNumberHTML.style.width = `${layoutInfo.lineNumbersWidth}px`; - innerLineNumberHTML.style.paddingLeft = `${layoutInfo.lineNumbersLeft}px`; - - lineNumberHTMLNode.appendChild(innerLineNumberHTML); - const foldingIcon = this._renderFoldingIconForLine(foldingModel, line); - if (foldingIcon) { - lineNumberHTMLNode.appendChild(foldingIcon.domNode); - foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`; - foldingIcon.domNode.style.lineHeight = `${lineHeight}px`; - } - - this._editor.applyFontInfo(lineHTMLNode); - this._editor.applyFontInfo(lineNumberHTMLNode); - - lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`; - lineHTMLNode.style.lineHeight = `${lineHeight}px`; - lineNumberHTMLNode.style.height = `${lineHeight}px`; - lineHTMLNode.style.height = `${lineHeight}px`; const renderedLine = new RenderedStickyLine( + this._editor, + viewModel, + layoutInfo, + foldingModel, + this._isOnGlyphMargin, index, - line, - lineHTMLNode, - lineNumberHTMLNode, - foldingIcon, - renderOutput.characterMapping, - lineHTMLNode.scrollWidth, - lineHeight + line ); return this._updatePosition(renderedLine, top, isLastLine); } @@ -405,25 +324,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { return stickyLine; } - private _renderFoldingIconForLine(foldingModel: FoldingModel | undefined, line: number): StickyFoldingIcon | undefined { - const showFoldingControls: 'mouseover' | 'always' | 'never' = this._editor.getOption(EditorOption.showFoldingControls); - if (!foldingModel || showFoldingControls === 'never') { - return; - } - const foldingRegions = foldingModel.regions; - const indexOfFoldingRegion = foldingRegions.findRange(line); - const startLineNumber = foldingRegions.getStartLineNumber(indexOfFoldingRegion); - const isFoldingScope = line === startLineNumber; - if (!isFoldingScope) { - return; - } - const isCollapsed = foldingRegions.isCollapsed(indexOfFoldingRegion); - const foldingIcon = new StickyFoldingIcon(isCollapsed, startLineNumber, foldingRegions.getEndLineNumber(indexOfFoldingRegion), this._lineHeight); - foldingIcon.setVisible(this._isOnGlyphMargin ? true : (isCollapsed || showFoldingControls === 'always')); - foldingIcon.domNode.setAttribute(STICKY_IS_FOLDING_ICON_ATTR, ''); - return foldingIcon; - } - getId(): string { return 'editor.contrib.stickyScrollWidget'; } @@ -435,7 +335,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { getPosition(): IOverlayWidgetPosition | null { return { preference: OverlayWidgetPositionPreference.TOP_CENTER, - stackOridinal: 10, + stackOrdinal: 10, }; } @@ -521,16 +421,127 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } class RenderedStickyLine { + + public readonly lineDomNode: HTMLElement; + public readonly lineNumberDomNode: HTMLElement; + + public readonly foldingIcon: StickyFoldingIcon | undefined; + public readonly characterMapping: CharacterMapping; + + public readonly scrollWidth: number; + public readonly height: number; + constructor( + editor: ICodeEditor, + viewModel: IViewModel, + layoutInfo: EditorLayoutInfo, + foldingModel: FoldingModel | undefined, + isOnGlyphMargin: boolean, public readonly index: number, public readonly lineNumber: number, - public readonly lineDomNode: HTMLElement, - public readonly lineNumberDomNode: HTMLElement, - public readonly foldingIcon: StickyFoldingIcon | undefined, - public readonly characterMapping: CharacterMapping, - public readonly scrollWidth: number, - public readonly height: number - ) { } + ) { + const viewLineNumber = viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(lineNumber, 1)).lineNumber; + const lineRenderingData = viewModel.getViewLineRenderingData(viewLineNumber); + const lineNumberOption = editor.getOption(EditorOption.lineNumbers); + const verticalScrollbarSize = editor.getOption(EditorOption.scrollbar).verticalScrollbarSize; + + let actualInlineDecorations: LineDecoration[]; + try { + actualInlineDecorations = LineDecoration.filter(lineRenderingData.inlineDecorations, viewLineNumber, lineRenderingData.minColumn, lineRenderingData.maxColumn); + } catch (err) { + actualInlineDecorations = []; + } + + const lineHeight = editor.getLineHeightForPosition(new Position(lineNumber, 1)); + const textDirection = viewModel.getTextDirection(lineNumber); + const renderLineInput: RenderLineInput = new RenderLineInput(true, true, lineRenderingData.content, + lineRenderingData.continuesWithWrappedLine, + lineRenderingData.isBasicASCII, lineRenderingData.containsRTL, 0, + lineRenderingData.tokens, actualInlineDecorations, + lineRenderingData.tabSize, lineRenderingData.startVisibleColumn, + 1, 1, 1, 500, 'none', true, true, null, + textDirection, verticalScrollbarSize + ); + + const sb = new StringBuilder(2000); + const renderOutput = renderViewLine(renderLineInput, sb); + this.characterMapping = renderOutput.characterMapping; + + let newLine; + if (_ttPolicy) { + newLine = _ttPolicy.createHTML(sb.build()); + } else { + newLine = sb.build(); + } + + const lineHTMLNode = document.createElement('span'); + lineHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); + lineHTMLNode.setAttribute(STICKY_IS_LINE_ATTR, ''); + lineHTMLNode.setAttribute('role', 'listitem'); + lineHTMLNode.tabIndex = 0; + lineHTMLNode.className = 'sticky-line-content'; + lineHTMLNode.classList.add(`stickyLine${lineNumber}`); + lineHTMLNode.style.lineHeight = `${lineHeight}px`; + lineHTMLNode.innerHTML = newLine as string; + + const lineNumberHTMLNode = document.createElement('span'); + lineNumberHTMLNode.setAttribute(STICKY_INDEX_ATTR, String(index)); + lineNumberHTMLNode.setAttribute(STICKY_IS_LINE_NUMBER_ATTR, ''); + lineNumberHTMLNode.className = 'sticky-line-number'; + lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`; + const lineNumbersWidth = layoutInfo.contentLeft; + lineNumberHTMLNode.style.width = `${lineNumbersWidth}px`; + + const innerLineNumberHTML = document.createElement('span'); + if (lineNumberOption.renderType === RenderLineNumbersType.On || lineNumberOption.renderType === RenderLineNumbersType.Interval && lineNumber % 10 === 0) { + innerLineNumberHTML.innerText = lineNumber.toString(); + } else if (lineNumberOption.renderType === RenderLineNumbersType.Relative) { + innerLineNumberHTML.innerText = Math.abs(lineNumber - editor.getPosition()!.lineNumber).toString(); + } + innerLineNumberHTML.className = 'sticky-line-number-inner'; + innerLineNumberHTML.style.width = `${layoutInfo.lineNumbersWidth}px`; + innerLineNumberHTML.style.paddingLeft = `${layoutInfo.lineNumbersLeft}px`; + + lineNumberHTMLNode.appendChild(innerLineNumberHTML); + const foldingIcon = this._renderFoldingIconForLine(editor, foldingModel, lineNumber, lineHeight, isOnGlyphMargin); + if (foldingIcon) { + lineNumberHTMLNode.appendChild(foldingIcon.domNode); + foldingIcon.domNode.style.left = `${layoutInfo.lineNumbersWidth + layoutInfo.lineNumbersLeft}px`; + foldingIcon.domNode.style.lineHeight = `${lineHeight}px`; + } + + editor.applyFontInfo(lineHTMLNode); + editor.applyFontInfo(lineNumberHTMLNode); + + lineNumberHTMLNode.style.lineHeight = `${lineHeight}px`; + lineHTMLNode.style.lineHeight = `${lineHeight}px`; + lineNumberHTMLNode.style.height = `${lineHeight}px`; + lineHTMLNode.style.height = `${lineHeight}px`; + + this.scrollWidth = lineHTMLNode.scrollWidth; + this.lineDomNode = lineHTMLNode; + this.lineNumberDomNode = lineNumberHTMLNode; + this.height = lineHeight; + } + + private _renderFoldingIconForLine(editor: ICodeEditor, foldingModel: FoldingModel | undefined, line: number, lineHeight: number, isOnGlyphMargin: boolean): StickyFoldingIcon | undefined { + const showFoldingControls: 'mouseover' | 'always' | 'never' = editor.getOption(EditorOption.showFoldingControls); + if (!foldingModel || showFoldingControls === 'never') { + return; + } + const foldingRegions = foldingModel.regions; + const indexOfFoldingRegion = foldingRegions.findRange(line); + const startLineNumber = foldingRegions.getStartLineNumber(indexOfFoldingRegion); + const isFoldingScope = line === startLineNumber; + if (!isFoldingScope) { + return; + } + const isCollapsed = foldingRegions.isCollapsed(indexOfFoldingRegion); + const foldingIcon = new StickyFoldingIcon(isCollapsed, startLineNumber, foldingRegions.getEndLineNumber(indexOfFoldingRegion), lineHeight); + foldingIcon.setVisible(isOnGlyphMargin ? true : (isCollapsed || showFoldingControls === 'always')); + foldingIcon.domNode.setAttribute(STICKY_IS_FOLDING_ICON_ATTR, ''); + return foldingIcon; + } } class StickyFoldingIcon { diff --git a/src/vs/editor/contrib/suggest/browser/suggest.ts b/src/vs/editor/contrib/suggest/browser/suggest.ts index 06e412ef142..468a6645a33 100644 --- a/src/vs/editor/contrib/suggest/browser/suggest.ts +++ b/src/vs/editor/contrib/suggest/browser/suggest.ts @@ -33,6 +33,7 @@ export const Context = { Visible: historyNavigationVisible, HasFocusedSuggestion: new RawContextKey('suggestWidgetHasFocusedSuggestion', false, localize('suggestWidgetHasSelection', "Whether any suggestion is focused")), DetailsVisible: new RawContextKey('suggestWidgetDetailsVisible', false, localize('suggestWidgetDetailsVisible', "Whether suggestion details are visible")), + DetailsFocused: new RawContextKey('suggestWidgetDetailsFocused', false, localize('suggestWidgetDetailsFocused', "Whether the details pane of the suggest widget has focus")), MultipleSuggestions: new RawContextKey('suggestWidgetMultipleSuggestions', false, localize('suggestWidgetMultipleSuggestions', "Whether there are multiple suggestions to pick from")), MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true, localize('suggestionMakesTextEdit', "Whether inserting the current suggestion yields in a change or has everything already been typed")), AcceptSuggestionsOnEnter: new RawContextKey('acceptSuggestionOnEnter', true, localize('acceptSuggestionOnEnter', "Whether suggestions are inserted when pressing Enter")), diff --git a/src/vs/editor/contrib/suggest/browser/suggestController.ts b/src/vs/editor/contrib/suggest/browser/suggestController.ts index 95639548d0c..9a6895a8242 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestController.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestController.ts @@ -9,9 +9,7 @@ import { CancellationTokenSource } from '../../../../base/common/cancellation.js import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { KeyCodeChord } from '../../../../base/common/keybindings.js'; import { DisposableStore, dispose, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import * as platform from '../../../../base/common/platform.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType, isObject } from '../../../../base/common/types.js'; import { StableEditorScrollState } from '../../../browser/stableEditorScroll.js'; @@ -214,21 +212,6 @@ export class SuggestController implements IEditorContribution { ctxCanResolve.set(Boolean(item.provider.resolveCompletionItem) || Boolean(item.completion.documentation) || item.completion.detail !== item.completion.label); })); - this._toDispose.add(widget.onDetailsKeyDown(e => { - // cmd + c on macOS, ctrl + c on Win / Linux - if ( - e.toKeyCodeChord().equals(new KeyCodeChord(true, false, false, false, KeyCode.KeyC)) || - (platform.isMacintosh && e.toKeyCodeChord().equals(new KeyCodeChord(false, false, false, true, KeyCode.KeyC))) - ) { - e.stopPropagation(); - return; - } - - if (!e.toKeyCodeChord().isModifierKey()) { - this.editor.focus(); - } - })); - if (this._wantsForceRenderingAbove) { widget.forceRenderingAbove(); } @@ -878,19 +861,19 @@ registerEditorCommand(new SuggestCommand({ title: nls.localize('accept.insert', "Insert"), group: 'left', order: 1, - when: SuggestContext.HasInsertAndReplaceRange.toNegated() + when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange.toNegated()) }, { menuId: suggestWidgetStatusbarMenu, title: nls.localize('accept.insert', "Insert"), group: 'left', order: 1, - when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')) + when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')) }, { menuId: suggestWidgetStatusbarMenu, title: nls.localize('accept.replace', "Replace"), group: 'left', order: 1, - when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')) + when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')) }] })); @@ -910,13 +893,13 @@ registerEditorCommand(new SuggestCommand({ menuId: suggestWidgetStatusbarMenu, group: 'left', order: 2, - when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')), + when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')), title: nls.localize('accept.replace', "Replace") }, { menuId: suggestWidgetStatusbarMenu, group: 'left', order: 2, - when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')), + when: ContextKeyExpr.and(SuggestContext.HasFocusedSuggestion, SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')), title: nls.localize('accept.insert', "Insert") }] })); @@ -947,6 +930,13 @@ registerEditorCommand(new SuggestCommand({ primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } + }, + menuOpts: { + menuId: suggestWidgetStatusbarMenu, + group: 'left', + order: 0, + when: SuggestContext.HasFocusedSuggestion.toNegated(), + title: nls.localize('focus.suggestion', "Select") } })); @@ -1126,6 +1116,24 @@ registerEditorCommand(new SuggestCommand({ })); +registerEditorCommand(new class extends EditorCommand { + constructor() { + super({ + id: 'suggestWidgetCopy', + precondition: SuggestContext.DetailsFocused, + kbOpts: { + weight: weight + 10, + kbExpr: SuggestContext.DetailsFocused, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, + win: { primary: KeyMod.CtrlCmd | KeyCode.KeyC, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] } + } + }); + } + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { + getWindow(editor.getDomNode()).document.execCommand('copy'); + } +}()); + registerEditorAction(class extends EditorAction { constructor() { diff --git a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts index ae310dc3616..3ef96524560 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestInlineCompletions.ts @@ -26,6 +26,7 @@ import { WordDistance } from './wordDistance.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; class SuggestInlineCompletion implements InlineCompletion { + readonly doNotLog = true; constructor( readonly range: IRange, diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts index 012df469c9d..8e94fe1ef61 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidget.ts @@ -130,6 +130,7 @@ export class SuggestWidget implements IDisposable { private readonly _ctxSuggestWidgetDetailsVisible: IContextKey; private readonly _ctxSuggestWidgetMultipleSuggestions: IContextKey; private readonly _ctxSuggestWidgetHasFocusedSuggestion: IContextKey; + private readonly _ctxSuggestWidgetDetailsFocused: IContextKey; private readonly _showTimeout = new TimeoutTimer(); private readonly _disposables = new DisposableStore(); @@ -269,7 +270,7 @@ export class SuggestWidget implements IDisposable { listInactiveFocusOutline: activeContrastBorder })); - this._status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, suggestWidgetStatusbarMenu); + this._status = instantiationService.createInstance(SuggestWidgetStatus, this.element.domNode, suggestWidgetStatusbarMenu, undefined); const applyStatusBarStyle = () => this.element.domNode.classList.toggle('with-status-bar', this.editor.getOption(EditorOption.suggest).showStatusBar); applyStatusBarStyle(); @@ -292,6 +293,12 @@ export class SuggestWidget implements IDisposable { this._ctxSuggestWidgetDetailsVisible = SuggestContext.DetailsVisible.bindTo(_contextKeyService); this._ctxSuggestWidgetMultipleSuggestions = SuggestContext.MultipleSuggestions.bindTo(_contextKeyService); this._ctxSuggestWidgetHasFocusedSuggestion = SuggestContext.HasFocusedSuggestion.bindTo(_contextKeyService); + this._ctxSuggestWidgetDetailsFocused = SuggestContext.DetailsFocused.bindTo(_contextKeyService); + + const detailsFocusTracker = dom.trackFocus(this._details.widget.domNode); + this._disposables.add(detailsFocusTracker); + this._disposables.add(detailsFocusTracker.onDidFocus(() => this._ctxSuggestWidgetDetailsFocused.set(true))); + this._disposables.add(detailsFocusTracker.onDidBlur(() => this._ctxSuggestWidgetDetailsFocused.set(false))); this._disposables.add(dom.addStandardDisposableListener(this._details.widget.domNode, 'keydown', e => { this._onDetailsKeydown.fire(e); diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts index 72fcb2b85fc..c13978ec43f 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetDetails.ts @@ -406,15 +406,25 @@ export class SuggestDetailsOverlay implements IOverlayWidget { })(); // SOUTH - const southPacement: Placement = (function () { + const southPlacement: Placement = (function () { const left = anchorBox.left; const top = -info.borderWidth + anchorBox.top + anchorBox.height; const maxSizeBottom = new dom.Dimension(anchorBox.width - info.borderHeight, bodyBox.height - anchorBox.top - anchorBox.height - info.verticalPadding); return { top, left, fit: maxSizeBottom.height - size.height, maxSizeBottom, maxSizeTop: maxSizeBottom, minSize: defaultMinSize.with(maxSizeBottom.width) }; })(); + // NORTH + const northPlacement: Placement = (function () { + const left = anchorBox.left; + const maxSizeTop = new dom.Dimension(anchorBox.width - info.borderHeight, anchorBox.top - info.verticalPadding); + const top = Math.max(info.verticalPadding, anchorBox.top - size.height); + return { top, left, fit: maxSizeTop.height - size.height, maxSizeTop, maxSizeBottom: maxSizeTop, minSize: defaultMinSize.with(maxSizeTop.width) }; + })(); + // take first placement that fits or the first with "least bad" fit - const placements = [eastPlacement, westPlacement, southPacement]; + // when the suggest widget is rendering above the cursor (preferAlignAtTop=false), prefer NORTH over SOUTH + const verticalPlacement = preferAlignAtTop ? southPlacement : northPlacement; + const placements = [eastPlacement, westPlacement, verticalPlacement]; const placement = placements.find(p => p.fit >= 0) ?? placements.sort((a, b) => b.fit - a.fit)[0]; // top/bottom placement @@ -445,7 +455,10 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } let { top, left } = placement; - if (!alignAtTop && height > anchorBox.height) { + if (placement === northPlacement) { + // For NORTH placement, position the details above the anchor + top = anchorBox.top - height + info.borderWidth; + } else if (!alignAtTop && height > anchorBox.height) { top = bottom - height; } const editorDomNode = this._editor.getDomNode(); @@ -457,7 +470,15 @@ export class SuggestDetailsOverlay implements IOverlayWidget { } this._applyTopLeft({ left, top }); - this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); + // enableSashes(north, east, south, west) + // For NORTH placement: enable north sash (resize upward from top), disable south (can't resize into the anchor) + // Also enable west sash for horizontal resizing, consistent with SOUTH placement + // For SOUTH placement and EAST/WEST placements: use existing logic based on alignAtTop + if (placement === northPlacement) { + this._resizable.enableSashes(true, false, false, true); + } else { + this._resizable.enableSashes(!alignAtTop, placement === eastPlacement, alignAtTop, placement !== eastPlacement); + } this._resizable.minSize = placement.minSize; this._resizable.maxSize = maxSize; diff --git a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts index 4104925c0be..4949077a9e7 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestWidgetStatus.ts @@ -7,11 +7,19 @@ import * as dom from '../../../../base/browser/dom.js'; import { ActionBar, IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IAction } from '../../../../base/common/actions.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { TextOnlyMenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { MenuEntryActionViewItem, TextOnlyMenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +export interface ISuggestWidgetStatusOptions { + /** + * Whether to show icons instead of text where possible and avoid + * keybindings all together. + */ + readonly showIconsNoKeybindings?: boolean; +} + export class SuggestWidgetStatus { readonly element: HTMLElement; @@ -23,6 +31,7 @@ export class SuggestWidgetStatus { constructor( container: HTMLElement, private readonly _menuId: MenuId, + options: ISuggestWidgetStatusOptions | undefined, @IInstantiationService instantiationService: IInstantiationService, @IMenuService private _menuService: IMenuService, @IContextKeyService private _contextKeyService: IContextKeyService, @@ -30,7 +39,11 @@ export class SuggestWidgetStatus { this.element = dom.append(container, dom.$('.suggest-status-bar')); const actionViewItemProvider = (action => { - return action instanceof MenuItemAction ? instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { useComma: false }) : undefined; + if (options?.showIconsNoKeybindings) { + return action instanceof MenuItemAction ? instantiationService.createInstance(MenuEntryActionViewItem, action, undefined) : undefined; + } else { + return action instanceof MenuItemAction ? instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { useComma: false }) : undefined; + } }); this._leftActions = new ActionBar(this.element, { actionViewItemProvider }); this._rightActions = new ActionBar(this.element, { actionViewItemProvider }); diff --git a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts index e957e5c6a74..dccb55ef441 100644 --- a/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/suggestModel.test.ts @@ -110,7 +110,7 @@ suite('SuggestModel - Context', function () { for (let i = 0; i < tokens.length; i++) { tokens[i] = tokensArr[i]; } - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } })); } diff --git a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts index 5c50c04166b..76c51955837 100644 --- a/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts +++ b/src/vs/editor/contrib/suggest/test/browser/wordDistance.test.ts @@ -65,7 +65,7 @@ suite('suggest, word distance', function () { private _worker = new EditorWorker(); constructor() { - super(null!, modelService, new class extends mock() { }, new NullLogService(), new TestLanguageConfigurationService(), new LanguageFeaturesService()); + super(modelService, new class extends mock() { }, new NullLogService(), new TestLanguageConfigurationService(), new LanguageFeaturesService(), null!); this._worker.$acceptNewModel({ url: model.uri.toString(), lines: model.getLinesContent(), diff --git a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css index 006bcb2f730..5f6dd96f262 100644 --- a/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css +++ b/src/vs/editor/contrib/symbolIcons/browser/symbolIcons.css @@ -6,70 +6,70 @@ /* stylelint-disable layer-checker */ .monaco-editor .codicon.codicon-symbol-array, -.monaco-workbench .codicon.codicon-symbol-array { color: var(--vscode-symbolIcon-arrayForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-array { color: var(--vscode-symbolIcon-arrayForeground); } .monaco-editor .codicon.codicon-symbol-boolean, -.monaco-workbench .codicon.codicon-symbol-boolean { color: var(--vscode-symbolIcon-booleanForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-boolean { color: var(--vscode-symbolIcon-booleanForeground); } .monaco-editor .codicon.codicon-symbol-class, -.monaco-workbench .codicon.codicon-symbol-class { color: var(--vscode-symbolIcon-classForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-class { color: var(--vscode-symbolIcon-classForeground); } .monaco-editor .codicon.codicon-symbol-method, -.monaco-workbench .codicon.codicon-symbol-method { color: var(--vscode-symbolIcon-methodForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-method { color: var(--vscode-symbolIcon-methodForeground); } .monaco-editor .codicon.codicon-symbol-color, -.monaco-workbench .codicon.codicon-symbol-color { color: var(--vscode-symbolIcon-colorForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-color { color: var(--vscode-symbolIcon-colorForeground); } .monaco-editor .codicon.codicon-symbol-constant, -.monaco-workbench .codicon.codicon-symbol-constant { color: var(--vscode-symbolIcon-constantForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-constant { color: var(--vscode-symbolIcon-constantForeground); } .monaco-editor .codicon.codicon-symbol-constructor, -.monaco-workbench .codicon.codicon-symbol-constructor { color: var(--vscode-symbolIcon-constructorForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-constructor { color: var(--vscode-symbolIcon-constructorForeground); } .monaco-editor .codicon.codicon-symbol-value, -.monaco-workbench .codicon.codicon-symbol-value, +.monaco-workbench .codicon-colored.codicon.codicon-symbol-value, .monaco-editor .codicon.codicon-symbol-enum, -.monaco-workbench .codicon.codicon-symbol-enum { color: var(--vscode-symbolIcon-enumeratorForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-enum { color: var(--vscode-symbolIcon-enumeratorForeground); } .monaco-editor .codicon.codicon-symbol-enum-member, -.monaco-workbench .codicon.codicon-symbol-enum-member { color: var(--vscode-symbolIcon-enumeratorMemberForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-enum-member { color: var(--vscode-symbolIcon-enumeratorMemberForeground); } .monaco-editor .codicon.codicon-symbol-event, -.monaco-workbench .codicon.codicon-symbol-event { color: var(--vscode-symbolIcon-eventForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-event { color: var(--vscode-symbolIcon-eventForeground); } .monaco-editor .codicon.codicon-symbol-field, -.monaco-workbench .codicon.codicon-symbol-field { color: var(--vscode-symbolIcon-fieldForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-field { color: var(--vscode-symbolIcon-fieldForeground); } .monaco-editor .codicon.codicon-symbol-file, -.monaco-workbench .codicon.codicon-symbol-file { color: var(--vscode-symbolIcon-fileForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-file { color: var(--vscode-symbolIcon-fileForeground); } .monaco-editor .codicon.codicon-symbol-folder, -.monaco-workbench .codicon.codicon-symbol-folder { color: var(--vscode-symbolIcon-folderForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-folder { color: var(--vscode-symbolIcon-folderForeground); } .monaco-editor .codicon.codicon-symbol-function, -.monaco-workbench .codicon.codicon-symbol-function { color: var(--vscode-symbolIcon-functionForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-function { color: var(--vscode-symbolIcon-functionForeground); } .monaco-editor .codicon.codicon-symbol-interface, -.monaco-workbench .codicon.codicon-symbol-interface { color: var(--vscode-symbolIcon-interfaceForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-interface { color: var(--vscode-symbolIcon-interfaceForeground); } .monaco-editor .codicon.codicon-symbol-key, -.monaco-workbench .codicon.codicon-symbol-key { color: var(--vscode-symbolIcon-keyForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-key { color: var(--vscode-symbolIcon-keyForeground); } .monaco-editor .codicon.codicon-symbol-keyword, -.monaco-workbench .codicon.codicon-symbol-keyword { color: var(--vscode-symbolIcon-keywordForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-keyword { color: var(--vscode-symbolIcon-keywordForeground); } .monaco-editor .codicon.codicon-symbol-module, -.monaco-workbench .codicon.codicon-symbol-module { color: var(--vscode-symbolIcon-moduleForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-module { color: var(--vscode-symbolIcon-moduleForeground); } .monaco-editor .codicon.codicon-symbol-namespace, -.monaco-workbench .codicon.codicon-symbol-namespace { color: var(--vscode-symbolIcon-namespaceForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-namespace { color: var(--vscode-symbolIcon-namespaceForeground); } .monaco-editor .codicon.codicon-symbol-null, -.monaco-workbench .codicon.codicon-symbol-null { color: var(--vscode-symbolIcon-nullForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-null { color: var(--vscode-symbolIcon-nullForeground); } .monaco-editor .codicon.codicon-symbol-number, -.monaco-workbench .codicon.codicon-symbol-number { color: var(--vscode-symbolIcon-numberForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-number { color: var(--vscode-symbolIcon-numberForeground); } .monaco-editor .codicon.codicon-symbol-object, -.monaco-workbench .codicon.codicon-symbol-object { color: var(--vscode-symbolIcon-objectForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-object { color: var(--vscode-symbolIcon-objectForeground); } .monaco-editor .codicon.codicon-symbol-operator, -.monaco-workbench .codicon.codicon-symbol-operator { color: var(--vscode-symbolIcon-operatorForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-operator { color: var(--vscode-symbolIcon-operatorForeground); } .monaco-editor .codicon.codicon-symbol-package, -.monaco-workbench .codicon.codicon-symbol-package { color: var(--vscode-symbolIcon-packageForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-package { color: var(--vscode-symbolIcon-packageForeground); } .monaco-editor .codicon.codicon-symbol-property, -.monaco-workbench .codicon.codicon-symbol-property { color: var(--vscode-symbolIcon-propertyForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-property { color: var(--vscode-symbolIcon-propertyForeground); } .monaco-editor .codicon.codicon-symbol-reference, -.monaco-workbench .codicon.codicon-symbol-reference { color: var(--vscode-symbolIcon-referenceForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-reference { color: var(--vscode-symbolIcon-referenceForeground); } .monaco-editor .codicon.codicon-symbol-snippet, -.monaco-workbench .codicon.codicon-symbol-snippet { color: var(--vscode-symbolIcon-snippetForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-snippet { color: var(--vscode-symbolIcon-snippetForeground); } .monaco-editor .codicon.codicon-symbol-string, -.monaco-workbench .codicon.codicon-symbol-string { color: var(--vscode-symbolIcon-stringForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-string { color: var(--vscode-symbolIcon-stringForeground); } .monaco-editor .codicon.codicon-symbol-struct, -.monaco-workbench .codicon.codicon-symbol-struct { color: var(--vscode-symbolIcon-structForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-struct { color: var(--vscode-symbolIcon-structForeground); } .monaco-editor .codicon.codicon-symbol-text, -.monaco-workbench .codicon.codicon-symbol-text { color: var(--vscode-symbolIcon-textForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-text { color: var(--vscode-symbolIcon-textForeground); } .monaco-editor .codicon.codicon-symbol-type-parameter, -.monaco-workbench .codicon.codicon-symbol-type-parameter { color: var(--vscode-symbolIcon-typeParameterForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-type-parameter { color: var(--vscode-symbolIcon-typeParameterForeground); } .monaco-editor .codicon.codicon-symbol-unit, -.monaco-workbench .codicon.codicon-symbol-unit { color: var(--vscode-symbolIcon-unitForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-unit { color: var(--vscode-symbolIcon-unitForeground); } .monaco-editor .codicon.codicon-symbol-variable, -.monaco-workbench .codicon.codicon-symbol-variable { color: var(--vscode-symbolIcon-variableForeground); } +.monaco-workbench .codicon-colored.codicon.codicon-symbol-variable { color: var(--vscode-symbolIcon-variableForeground); } diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 3f8ecb82ecb..d8728fd5fcf 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -586,7 +586,7 @@ export class DisableHighlightingInCommentsAction extends EditorAction implements }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const configurationService = accessor.get(IConfigurationService); if (configurationService) { this.runAction(configurationService); @@ -609,7 +609,7 @@ export class DisableHighlightingInStringsAction extends EditorAction implements }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const configurationService = accessor.get(IConfigurationService); if (configurationService) { this.runAction(configurationService); @@ -633,7 +633,7 @@ export class DisableHighlightingOfAmbiguousCharactersAction extends Action2 impl }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const configurationService = accessor.get(IConfigurationService); if (configurationService) { this.runAction(configurationService); @@ -657,7 +657,7 @@ export class DisableHighlightingOfInvisibleCharactersAction extends Action2 impl }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const configurationService = accessor.get(IConfigurationService); if (configurationService) { this.runAction(configurationService); @@ -681,7 +681,7 @@ export class DisableHighlightingOfNonBasicAsciiCharactersAction extends Action2 }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { const configurationService = accessor.get(IConfigurationService); if (configurationService) { this.runAction(configurationService); diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index a0d0dac11e6..71165784d77 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -272,7 +272,7 @@ class WordHighlighter { return; } - this.runDelayer.trigger(() => { this._onPositionChanged(e); }); + this.runDelayer.trigger(() => { this._onPositionChanged(e); }).catch(onUnexpectedError); })); this.toUnhook.add(editor.onDidFocusEditorText((e) => { if (this.occurrencesHighlightEnablement === 'off') { @@ -281,7 +281,7 @@ class WordHighlighter { } if (!this.workerRequest) { - this.runDelayer.trigger(() => { this._run(); }); + this.runDelayer.trigger(() => { this._run(); }).catch(onUnexpectedError); } })); this.toUnhook.add(editor.onDidChangeModelContent((e) => { @@ -364,7 +364,7 @@ class WordHighlighter { } this.runDelayer.cancel(); - this.runDelayer.trigger(() => { this._run(false, delay); }); + this.runDelayer.trigger(() => { this._run(false, delay); }).catch(onUnexpectedError); } public trigger() { @@ -969,7 +969,7 @@ class TriggerWordHighlightAction extends EditorAction { }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { const controller = WordHighlighterContribution.get(editor); if (!controller) { return; diff --git a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts index 61ce84b25df..8e944c23c3f 100644 --- a/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/browser/wordOperations.ts @@ -474,6 +474,22 @@ export class DeleteInsideWord extends EditorAction { id: 'deleteInsideWord', precondition: EditorContextKeys.writable, label: nls.localize2('deleteInsideWord', "Delete Word"), + metadata: { + description: nls.localize2('deleteInsideWord.description', "Delete the word at the cursor"), + args: [{ + name: 'args', + schema: { + type: 'object', + properties: { + 'onlyWord': { + type: 'boolean', + default: false, + description: nls.localize('deleteInsideWord.args.onlyWord', "Delete only the word and leave surrounding whitespace") + } + } + } + }] + } }); } @@ -481,12 +497,15 @@ export class DeleteInsideWord extends EditorAction { if (!editor.hasModel()) { return; } + + type DeleteInsideWordArgs = { readonly onlyWord?: boolean }; + const onlyWord = !!(args && typeof args === 'object' && (args as DeleteInsideWordArgs).onlyWord); const wordSeparators = getMapForWordSeparators(editor.getOption(EditorOption.wordSeparators), editor.getOption(EditorOption.wordSegmenterLocales)); const model = editor.getModel(); const selections = editor.getSelections(); const commands = selections.map((sel) => { - const deleteRange = WordOperations.deleteInsideWord(wordSeparators, model, sel); + const deleteRange = WordOperations.deleteInsideWord(wordSeparators, model, sel, onlyWord); return new ReplaceCommand(deleteRange, ''); }); diff --git a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts index 24090c00e6d..c162a282476 100644 --- a/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/browser/wordOperations.test.ts @@ -112,8 +112,8 @@ suite('WordOperations', () => { function deleteWordEndRight(editor: ICodeEditor): void { runEditorCommand(editor, _deleteWordEndRight); } - function deleteInsideWord(editor: ICodeEditor): void { - _deleteInsideWord.run(null!, editor, null); + function deleteInsideWord(editor: ICodeEditor, args?: unknown): void { + _deleteInsideWord.run(null!, editor, args); } test('cursorWordLeft - simple', () => { @@ -1003,4 +1003,26 @@ suite('WordOperations', () => { assert.strictEqual(model.getValue(), ''); }); }); + + test('deleteInsideWord - onlyWord: does not delete whitespace before last word', () => { + withTestCodeEditor([ + 'hello world' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 9)); + deleteInsideWord(editor, { onlyWord: true }); + assert.strictEqual(model.getValue(), 'hello '); + }); + }); + + test('deleteInsideWord - onlyWord: deletes just the word (leaves double spaces)', () => { + withTestCodeEditor([ + 'This is interesting' + ], {}, (editor, _) => { + const model = editor.getModel()!; + editor.setPosition(new Position(1, 7)); + deleteInsideWord(editor, { onlyWord: true }); + assert.strictEqual(model.getValue(), 'This interesting'); + }); + }); }); diff --git a/src/vs/editor/contrib/wordPartOperations/test/browser/utils.ts b/src/vs/editor/contrib/wordPartOperations/test/browser/utils.ts index 9a7366828cf..efffe2981a6 100644 --- a/src/vs/editor/contrib/wordPartOperations/test/browser/utils.ts +++ b/src/vs/editor/contrib/wordPartOperations/test/browser/utils.ts @@ -20,9 +20,4 @@ export class StaticServiceAccessor implements ServicesAccessor { } return value as T; } - - getIfExists(id: ServiceIdentifier): T | undefined { - const value = this.services.get(id); - return value as T | undefined; - } } diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts index 839ab7bb60b..5237e2f091c 100644 --- a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -52,7 +52,7 @@ export class GotoLineAction extends EditorAction { } run(accessor: ServicesAccessor): void { - accessor.get(IQuickInputService).quickAccess.show(StandaloneGotoLineQuickAccessProvider.PREFIX); + accessor.get(IQuickInputService).quickAccess.show(StandaloneGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX); } } @@ -60,6 +60,33 @@ registerEditorAction(GotoLineAction); Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ ctor: StandaloneGotoLineQuickAccessProvider, - prefix: StandaloneGotoLineQuickAccessProvider.PREFIX, + prefix: StandaloneGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX, helpEntries: [{ description: GoToLineNLS.gotoLineActionLabel, commandId: GotoLineAction.ID }] }); + +class GotoOffsetAction extends EditorAction { + + static readonly ID = 'editor.action.gotoOffset'; + + constructor() { + super({ + id: GotoOffsetAction.ID, + label: GoToLineNLS.gotoOffsetActionLabel, + alias: 'Go to Offset...', + precondition: undefined, + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IQuickInputService).quickAccess.show(StandaloneGotoLineQuickAccessProvider.GO_TO_OFFSET_PREFIX); + } +} + +registerEditorAction(GotoOffsetAction); + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: StandaloneGotoLineQuickAccessProvider, + prefix: StandaloneGotoLineQuickAccessProvider.GO_TO_OFFSET_PREFIX, + helpEntries: [{ description: GoToLineNLS.gotoOffsetActionLabel, commandId: GotoOffsetAction.ID }] +}); + diff --git a/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts new file mode 100644 index 00000000000..1ff3fb838d9 --- /dev/null +++ b/src/vs/editor/standalone/browser/services/standaloneWebWorkerService.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getMonacoEnvironment } from '../../../../base/browser/browser.js'; +import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js'; +import { WebWorkerService } from '../../../../platform/webWorker/browser/webWorkerServiceImpl.js'; + +export class StandaloneWebWorkerService extends WebWorkerService { + protected override _createWorker(descriptor: WebWorkerDescriptor): Promise { + const monacoEnvironment = getMonacoEnvironment(); + if (monacoEnvironment) { + if (typeof monacoEnvironment.getWorker === 'function') { + const worker = monacoEnvironment.getWorker('workerMain.js', descriptor.label); + if (worker !== undefined) { + return Promise.resolve(worker); + } + } + } + + return super._createWorker(descriptor); + } + + protected override _getWorkerLoadingFailedErrorMessage(descriptor: WebWorkerDescriptor): string | undefined { + const examplePath = '\'...?esm\''; // Broken up to avoid detection by bundler plugin + return `Failed to load worker script for label: ${descriptor.label}. +Ensure your bundler properly bundles modules referenced by "new URL(${examplePath}, import.meta.url)".`; + } + + override getWorkerUrl(descriptor: WebWorkerDescriptor): string { + const monacoEnvironment = getMonacoEnvironment(); + if (monacoEnvironment) { + if (typeof monacoEnvironment.getWorkerUrl === 'function') { + const workerUrl = monacoEnvironment.getWorkerUrl('workerMain.js', descriptor.label); + if (workerUrl !== undefined) { + const absoluteUrl = new URL(workerUrl, document.baseURI).toString(); + return absoluteUrl; + } + } + } + + if (!descriptor.esmModuleLocationBundler) { + throw new Error(`You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker for the worker label: ${descriptor.label}`); + } + + const url = typeof descriptor.esmModuleLocationBundler === 'function' ? descriptor.esmModuleLocationBundler() : descriptor.esmModuleLocationBundler; + const urlStr = url.toString(); + return urlStr; + } +} diff --git a/src/vs/editor/standalone/browser/standaloneEditor.ts b/src/vs/editor/standalone/browser/standaloneEditor.ts index f635ace5f1b..cdf1f4081f0 100644 --- a/src/vs/editor/standalone/browser/standaloneEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneEditor.ts @@ -39,6 +39,7 @@ import { IKeybindingService } from '../../../platform/keybinding/common/keybindi import { IMarker, IMarkerData, IMarkerService } from '../../../platform/markers/common/markers.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { MultiDiffEditorWidget } from '../../browser/widget/multiDiffEditor/multiDiffEditorWidget.js'; +import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; /** * Create a new editor under `domElement`. @@ -332,7 +333,7 @@ export function onDidChangeModelLanguage(listener: (e: { readonly model: ITextMo * Specify an AMD module to load that will `create` an object that will be proxied. */ export function createWebWorker(opts: IInternalWebWorkerOptions): MonacoWebWorker { - return actualCreateWebWorker(StandaloneServices.get(IModelService), opts); + return actualCreateWebWorker(StandaloneServices.get(IModelService), StandaloneServices.get(IWebWorkerService), opts); } /** diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 31b294e1d03..c06acad60f1 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -131,7 +131,7 @@ export class EncodedTokenizationSupportAdapter implements languages.ITokenizatio public tokenizeEncoded(line: string, hasEOL: boolean, state: languages.IState): languages.EncodedTokenizationResult { const result = this._actual.tokenizeEncoded(line, state); - return new languages.EncodedTokenizationResult(result.tokens, result.endState); + return new languages.EncodedTokenizationResult(result.tokens, [], result.endState); } } @@ -249,7 +249,7 @@ export class TokenizationSupportAdapter implements languages.ITokenizationSuppor endState = actualResult.endState; } - return new languages.EncodedTokenizationResult(tokens, endState); + return new languages.EncodedTokenizationResult(tokens, [], endState); } } diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 6dea954b5a2..61aa37410e2 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -21,6 +21,7 @@ import { IDisposable, IReference, ImmortalReference, toDisposable, DisposableSto import { OS, isLinux, isMacintosh } from '../../../base/common/platform.js'; import Severity from '../../../base/common/severity.js'; import { URI } from '../../../base/common/uri.js'; +import { IRenameSymbolTrackerService, NullRenameSymbolTrackerService } from '../../browser/services/renameSymbolTrackerService.js'; import { IBulkEditOptions, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceTextEdit } from '../../browser/services/bulkEditService.js'; import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from '../../common/config/editorConfigurationSchema.js'; import { EditOperation, ISingleEditOperation } from '../../common/core/editOperation.js'; @@ -61,8 +62,6 @@ import { LanguageService } from '../../common/services/languageService.js'; import { ContextMenuService } from '../../../platform/contextview/browser/contextMenuService.js'; import { getSingletonServiceDescriptors, InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js'; import { OpenerService } from '../../browser/services/openerService.js'; -import { IEditorWorkerService } from '../../common/services/editorWorker.js'; -import { EditorWorkerService } from '../../browser/services/editorWorkerService.js'; import { ILanguageService } from '../../common/languages/language.js'; import { MarkerDecorationsService } from '../../common/services/markerDecorationsService.js'; import { IMarkerDecorationsService } from '../../common/services/markerDecorations.js'; @@ -89,18 +88,19 @@ import { IStorageService, InMemoryStorageService } from '../../../platform/stora import { DefaultConfiguration } from '../../../platform/configuration/common/configurations.js'; import { WorkspaceEdit } from '../../common/languages.js'; import { AccessibilitySignal, AccessibilityModality, IAccessibilitySignalService, Sound } from '../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { ILanguageFeaturesService } from '../../common/services/languageFeatures.js'; -import { ILanguageConfigurationService } from '../../common/languages/languageConfigurationRegistry.js'; import { LogService } from '../../../platform/log/common/logService.js'; import { getEditorFeatures } from '../../common/editorFeatures.js'; import { onUnexpectedError } from '../../../base/common/errors.js'; import { ExtensionKind, IEnvironmentService, IExtensionHostDebugParams } from '../../../platform/environment/common/environment.js'; import { mainWindow } from '../../../base/browser/window.js'; import { ResourceMap } from '../../../base/common/map.js'; -import { IWebWorkerDescriptor } from '../../../base/browser/webWorkerFactory.js'; import { ITreeSitterLibraryService } from '../../common/services/treeSitter/treeSitterLibraryService.js'; import { StandaloneTreeSitterLibraryService } from './standaloneTreeSitterLibraryService.js'; import { IDataChannelService, NullDataChannelService } from '../../../platform/dataChannel/common/dataChannel.js'; +import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; +import { StandaloneWebWorkerService } from './services/standaloneWebWorkerService.js'; +import { IDefaultAccountService } from '../../../platform/defaultAccount/common/defaultAccount.js'; +import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IPolicyData } from '../../../base/common/defaultAccount.js'; class SimpleModel implements IResolvedTextEditorModel { @@ -220,6 +220,7 @@ class StandaloneEnvironmentService implements IEnvironmentService { readonly keyboardLayoutResource: URI = URI.from({ scheme: 'monaco', authority: 'keyboardLayoutResource' }); readonly argvResource: URI = URI.from({ scheme: 'monaco', authority: 'argvResource' }); readonly untitledWorkspacesHome: URI = URI.from({ scheme: 'monaco', authority: 'untitledWorkspacesHome' }); + readonly builtinWorkbenchModesHome: URI = URI.from({ scheme: 'monaco', authority: 'builtinWorkbenchModesHome' }); readonly workspaceStorageHome: URI = URI.from({ scheme: 'monaco', authority: 'workspaceStorageHome' }); readonly localHistoryHome: URI = URI.from({ scheme: 'monaco', authority: 'localHistoryHome' }); readonly cacheHome: URI = URI.from({ scheme: 'monaco', authority: 'cacheHome' }); @@ -1075,23 +1076,6 @@ class StandaloneContextMenuService extends ContextMenuService { } } -const standaloneEditorWorkerDescriptor: IWebWorkerDescriptor = { - esmModuleLocation: undefined, - label: 'editorWorkerService' -}; - -class StandaloneEditorWorkerService extends EditorWorkerService { - constructor( - @IModelService modelService: IModelService, - @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, - @ILogService logService: ILogService, - @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - super(standaloneEditorWorkerDescriptor, modelService, configurationService, logService, languageConfigurationService, languageFeaturesService); - } -} - class StandaloneAccessbilitySignalService implements IAccessibilitySignalService { _serviceBrand: undefined; async playSignal(cue: AccessibilitySignal, options: {}): Promise { @@ -1127,11 +1111,40 @@ class StandaloneAccessbilitySignalService implements IAccessibilitySignalService } } +class StandaloneDefaultAccountService implements IDefaultAccountService { + declare readonly _serviceBrand: undefined; + + readonly onDidChangeDefaultAccount: Event = Event.None; + readonly onDidChangePolicyData: Event = Event.None; + readonly policyData: IPolicyData | null = null; + + async getDefaultAccount(): Promise { + return null; + } + + setDefaultAccountProvider(): void { + // no-op + } + + async refresh(): Promise { + return null; + } + + getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { + return { id: 'default', name: 'Default', enterprise: false }; + } + + async signIn(): Promise { + return null; + } +} + export interface IEditorOverrideServices { [index: string]: unknown; } +registerSingleton(IWebWorkerService, StandaloneWebWorkerService, InstantiationType.Eager); registerSingleton(ILogService, StandaloneLogService, InstantiationType.Eager); registerSingleton(IConfigurationService, StandaloneConfigurationService, InstantiationType.Eager); registerSingleton(ITextResourceConfigurationService, StandaloneResourceConfigurationService, InstantiationType.Eager); @@ -1151,7 +1164,6 @@ registerSingleton(IContextKeyService, ContextKeyService, InstantiationType.Eager registerSingleton(IProgressService, StandaloneProgressService, InstantiationType.Eager); registerSingleton(IEditorProgressService, StandaloneEditorProgressService, InstantiationType.Eager); registerSingleton(IStorageService, InMemoryStorageService, InstantiationType.Eager); -registerSingleton(IEditorWorkerService, StandaloneEditorWorkerService, InstantiationType.Eager); registerSingleton(IBulkEditService, StandaloneBulkEditService, InstantiationType.Eager); registerSingleton(IWorkspaceTrustManagementService, StandaloneWorkspaceTrustManagementService, InstantiationType.Eager); registerSingleton(ITextModelService, StandaloneTextModelService, InstantiationType.Eager); @@ -1169,6 +1181,8 @@ registerSingleton(IAccessibilitySignalService, StandaloneAccessbilitySignalServi registerSingleton(ITreeSitterLibraryService, StandaloneTreeSitterLibraryService, InstantiationType.Eager); registerSingleton(ILoggerService, NullLoggerService, InstantiationType.Eager); registerSingleton(IDataChannelService, NullDataChannelService, InstantiationType.Eager); +registerSingleton(IDefaultAccountService, StandaloneDefaultAccountService, InstantiationType.Eager); +registerSingleton(IRenameSymbolTrackerService, NullRenameSymbolTrackerService, InstantiationType.Eager); /** * We don't want to eagerly instantiate services because embedders get a one time chance diff --git a/src/vs/editor/standalone/browser/standaloneThemeService.ts b/src/vs/editor/standalone/browser/standaloneThemeService.ts index 0ef9470da84..67fe9f8420d 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeService.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeService.ts @@ -16,7 +16,7 @@ import { hc_black, hc_light, vs, vs_dark } from '../common/themes.js'; import { IEnvironmentService } from '../../../platform/environment/common/environment.js'; import { Registry } from '../../../platform/registry/common/platform.js'; import { asCssVariableName, ColorIdentifier, Extensions, IColorRegistry } from '../../../platform/theme/common/colorRegistry.js'; -import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IProductIconTheme, IThemingRegistry, ITokenStyle } from '../../../platform/theme/common/themeService.js'; +import { Extensions as ThemingExtensions, ICssStyleCollector, IFileIconTheme, IProductIconTheme, IThemingRegistry, ITokenStyle, IFontTokenOptions } from '../../../platform/theme/common/themeService.js'; import { IDisposable, Disposable } from '../../../base/common/lifecycle.js'; import { ColorScheme, isDark, isHighContrast } from '../../../platform/theme/common/theme.js'; import { getIconsStyleSheet, UnthemedProductIconTheme } from '../../../platform/theme/browser/iconsStyleSheet.js'; @@ -179,6 +179,10 @@ class StandaloneTheme implements IStandaloneTheme { return []; } + public get tokenFontMap(): IFontTokenOptions[] { + return []; + } + public readonly semanticHighlighting = false; } diff --git a/src/vs/editor/standalone/browser/standaloneWebWorker.ts b/src/vs/editor/standalone/browser/standaloneWebWorker.ts index a34425aa444..cf1f15d4255 100644 --- a/src/vs/editor/standalone/browser/standaloneWebWorker.ts +++ b/src/vs/editor/standalone/browser/standaloneWebWorker.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../base/common/uri.js'; +import { IWebWorkerService } from '../../../platform/webWorker/browser/webWorkerService.js'; import { EditorWorkerClient } from '../../browser/services/editorWorkerService.js'; import { IModelService } from '../../common/services/model.js'; @@ -11,8 +12,8 @@ import { IModelService } from '../../common/services/model.js'; * Create a new web worker that has model syncing capabilities built in. * Specify an AMD module to load that will `create` an object that will be proxied. */ -export function createWebWorker(modelService: IModelService, opts: IInternalWebWorkerOptions): MonacoWebWorker { - return new MonacoWebWorkerImpl(modelService, opts); +export function createWebWorker(modelService: IModelService, webWorkerService: IWebWorkerService, opts: IInternalWebWorkerOptions): MonacoWebWorker { + return new MonacoWebWorkerImpl(modelService, webWorkerService, opts); } /** @@ -55,8 +56,8 @@ class MonacoWebWorkerImpl extends EditorWorkerClient implement private readonly _foreignModuleHost: { [method: string]: Function } | null; private _foreignProxy: Promise; - constructor(modelService: IModelService, opts: IInternalWebWorkerOptions) { - super(opts.worker, opts.keepIdleModels || false, modelService); + constructor(modelService: IModelService, webWorkerService: IWebWorkerService, opts: IInternalWebWorkerOptions) { + super(opts.worker, opts.keepIdleModels || false, modelService, webWorkerService); this._foreignModuleHost = opts.host || null; this._foreignProxy = this._getProxy().then(proxy => { return new Proxy({}, { diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index 9e82b00116b..bb68a158bdf 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -380,6 +380,7 @@ class MonarchModernTokensCollector implements IMonarchTokensCollector { public finalize(endState: MonarchLineState): languages.EncodedTokenizationResult { return new languages.EncodedTokenizationResult( MonarchModernTokensCollector._merge(this._prependTokens, this._tokens, null), + [], endState ); } diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 0fa82cf782b..92943556aaa 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -74,7 +74,9 @@ suite('TokenizationSupport2Adapter', () => { semanticHighlighting: false, - tokenColorMap: [] + tokenColorMap: [], + + tokenFontMap: [] }; } setColorMapOverride(colorMapOverride: Color[] | null): void { diff --git a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts index 503dc55f875..3de2f356e81 100644 --- a/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts +++ b/src/vs/editor/test/browser/commands/trimTrailingWhitespaceCommand.test.ts @@ -146,20 +146,20 @@ suite('Editor Commands - Trim Trailing Whitespace Command', () => { 0, otherMetadata, 10, stringMetadata, ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case ' a string ': { const tokens = new Uint32Array([ 0, stringMetadata, ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case '`; ': { const tokens = new Uint32Array([ 0, stringMetadata, 1, otherMetadata ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } } throw new Error(`Unexpected`); diff --git a/src/vs/editor/test/browser/config/editorConfiguration.test.ts b/src/vs/editor/test/browser/config/editorConfiguration.test.ts index ad945e54f39..c5df6580e86 100644 --- a/src/vs/editor/test/browser/config/editorConfiguration.test.ts +++ b/src/vs/editor/test/browser/config/editorConfiguration.test.ts @@ -204,13 +204,13 @@ suite('Common Editor Config', () => { const hoverOptions: IEditorHoverOptions = {}; Object.defineProperty(hoverOptions, 'enabled', { writable: false, - value: true + value: 'on' }); const config = new TestConfiguration({ hover: hoverOptions }); - assert.strictEqual(config.options.get(EditorOption.hover).enabled, true); - config.updateOptions({ hover: { enabled: false } }); - assert.strictEqual(config.options.get(EditorOption.hover).enabled, false); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, 'on'); + config.updateOptions({ hover: { enabled: 'off' } }); + assert.strictEqual(config.options.get(EditorOption.hover).enabled, 'off'); config.dispose(); }); @@ -380,8 +380,8 @@ suite('migrateOptions', () => { assert.deepStrictEqual(migrate({ quickSuggestions: { comments: 'on', strings: 'off' } }), { quickSuggestions: { comments: 'on', strings: 'off' } }); }); test('hover', () => { - assert.deepStrictEqual(migrate({ hover: true }), { hover: { enabled: true } }); - assert.deepStrictEqual(migrate({ hover: false }), { hover: { enabled: false } }); + assert.deepStrictEqual(migrate({ hover: true }), { hover: { enabled: 'on' } }); + assert.deepStrictEqual(migrate({ hover: false }), { hover: { enabled: 'off' } }); }); test('parameterHints', () => { assert.deepStrictEqual(migrate({ parameterHints: true }), { parameterHints: { enabled: true } }); diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 1322aae8fba..61ddc81e06d 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -1335,7 +1335,7 @@ suite('Editor Controller - Cursor', () => { getInitialState: () => NullState, tokenize: undefined!, tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { - return new EncodedTokenizationResult(new Uint32Array(0), state); + return new EncodedTokenizationResult(new Uint32Array(0), [], state); } }; @@ -1533,7 +1533,7 @@ suite('Editor Controller', () => { ); startIndex += tokens[i].length; } - return new EncodedTokenizationResult(result, state); + return new EncodedTokenizationResult(result, [], state); function advance(): void { if (state instanceof BaseState) { @@ -2239,6 +2239,37 @@ suite('Editor Controller', () => { }); }); + test('issue #256039: paste from multiple cursors with empty selections and multiCursorPaste full', () => { + usingCursor({ + text: [ + 'line1', + 'line2', + 'line3' + ], + editorOpts: { + multiCursorPaste: 'full' + } + }, (editor, model, viewModel) => { + // 2 cursors on lines 1 and 2 + viewModel.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); + + viewModel.paste( + 'line1\nline2\n', + true, + ['line1\n', 'line2\n'] + ); + + // Each cursor gets its respective line + assert.strictEqual(model.getValue(), [ + 'line1', + 'line1', + 'line2', + 'line2', + 'line3' + ].join('\n')); + }); + }); + test('issue #3071: Investigate why undo stack gets corrupted', () => { const model = createTextModel( [ @@ -2794,7 +2825,7 @@ suite('Editor Controller', () => { getInitialState: () => NullState, tokenize: undefined!, tokenizeEncoded: (line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult => { - return new EncodedTokenizationResult(new Uint32Array(0), state); + return new EncodedTokenizationResult(new Uint32Array(0), [], state); } }; diff --git a/src/vs/editor/test/browser/controller/imeRecorder.html b/src/vs/editor/test/browser/controller/imeRecorder.html deleted file mode 100644 index ebc4e977a9d..00000000000 --- a/src/vs/editor/test/browser/controller/imeRecorder.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/vs/editor/test/browser/controller/imeRecorder.ts b/src/vs/editor/test/browser/controller/imeRecorder.ts deleted file mode 100644 index 95bd1c5fd7c..00000000000 --- a/src/vs/editor/test/browser/controller/imeRecorder.ts +++ /dev/null @@ -1,180 +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 { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IRecorded, IRecordedCompositionEvent, IRecordedEvent, IRecordedInputEvent, IRecordedKeyboardEvent, IRecordedTextareaState } from './imeRecordedTypes.js'; -import * as browser from '../../../../base/browser/browser.js'; -import * as platform from '../../../../base/common/platform.js'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { TextAreaWrapper } from '../../../browser/controller/editContext/textArea/textAreaEditContextInput.js'; - -(() => { - - // eslint-disable-next-line no-restricted-syntax - const startButton = mainWindow.document.getElementById('startRecording')!; - // eslint-disable-next-line no-restricted-syntax - const endButton = mainWindow.document.getElementById('endRecording')!; - - let inputarea: HTMLTextAreaElement; - const disposables = new DisposableStore(); - let originTimeStamp = 0; - let recorded: IRecorded = { - env: null!, - initial: null!, - events: [], - final: null! - }; - - const readTextareaState = (): IRecordedTextareaState => { - return { - selectionDirection: inputarea.selectionDirection, - selectionEnd: inputarea.selectionEnd, - selectionStart: inputarea.selectionStart, - value: inputarea.value, - }; - }; - - startButton.onclick = () => { - disposables.clear(); - startTest(); - originTimeStamp = 0; - recorded = { - env: { - OS: platform.OS, - browser: { - isAndroid: browser.isAndroid, - isFirefox: browser.isFirefox, - isChrome: browser.isChrome, - isSafari: browser.isSafari - } - }, - initial: readTextareaState(), - events: [], - final: null! - }; - }; - endButton.onclick = () => { - recorded.final = readTextareaState(); - console.log(printRecordedData()); - }; - - function printRecordedData() { - const lines = []; - lines.push(`const recorded: IRecorded = {`); - lines.push(`\tenv: ${JSON.stringify(recorded.env)}, `); - lines.push(`\tinitial: ${printState(recorded.initial)}, `); - lines.push(`\tevents: [\n\t\t${recorded.events.map(ev => printEvent(ev)).join(',\n\t\t')}\n\t],`); - lines.push(`\tfinal: ${printState(recorded.final)},`); - lines.push(`}`); - - return lines.join('\n'); - - function printString(str: string) { - return str.replace(/\\/g, '\\\\').replace(/'/g, '\\\''); - } - function printState(state: IRecordedTextareaState) { - return `{ value: '${printString(state.value)}', selectionStart: ${state.selectionStart}, selectionEnd: ${state.selectionEnd}, selectionDirection: '${state.selectionDirection}' }`; - } - function printEvent(ev: IRecordedEvent) { - if (ev.type === 'keydown' || ev.type === 'keypress' || ev.type === 'keyup') { - return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', altKey: ${ev.altKey}, charCode: ${ev.charCode}, code: '${ev.code}', ctrlKey: ${ev.ctrlKey}, isComposing: ${ev.isComposing}, key: '${ev.key}', keyCode: ${ev.keyCode}, location: ${ev.location}, metaKey: ${ev.metaKey}, repeat: ${ev.repeat}, shiftKey: ${ev.shiftKey} }`; - } - if (ev.type === 'compositionstart' || ev.type === 'compositionupdate' || ev.type === 'compositionend') { - return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: '${printString(ev.data)}' }`; - } - if (ev.type === 'beforeinput' || ev.type === 'input') { - return `{ timeStamp: ${ev.timeStamp.toFixed(2)}, state: ${printState(ev.state)}, type: '${ev.type}', data: ${ev.data === null ? 'null' : `'${printString(ev.data)}'`}, inputType: '${ev.inputType}', isComposing: ${ev.isComposing} }`; - } - return JSON.stringify(ev); - } - } - - function startTest() { - inputarea = document.createElement('textarea'); - mainWindow.document.body.appendChild(inputarea); - inputarea.focus(); - disposables.add(toDisposable(() => { - inputarea.remove(); - })); - const wrapper = disposables.add(new TextAreaWrapper(inputarea)); - - wrapper.setValue('', `aaaa`); - wrapper.setSelectionRange('', 2, 2); - - const recordEvent = (e: IRecordedEvent) => { - recorded.events.push(e); - }; - - const recordKeyboardEvent = (e: KeyboardEvent): void => { - if (e.type !== 'keydown' && e.type !== 'keypress' && e.type !== 'keyup') { - throw new Error(`Not supported!`); - } - if (originTimeStamp === 0) { - originTimeStamp = e.timeStamp; - } - const ev: IRecordedKeyboardEvent = { - timeStamp: e.timeStamp - originTimeStamp, - state: readTextareaState(), - type: e.type, - altKey: e.altKey, - charCode: e.charCode, - code: e.code, - ctrlKey: e.ctrlKey, - isComposing: e.isComposing, - key: e.key, - keyCode: e.keyCode, - location: e.location, - metaKey: e.metaKey, - repeat: e.repeat, - shiftKey: e.shiftKey - }; - recordEvent(ev); - }; - - const recordCompositionEvent = (e: CompositionEvent): void => { - if (e.type !== 'compositionstart' && e.type !== 'compositionupdate' && e.type !== 'compositionend') { - throw new Error(`Not supported!`); - } - if (originTimeStamp === 0) { - originTimeStamp = e.timeStamp; - } - const ev: IRecordedCompositionEvent = { - timeStamp: e.timeStamp - originTimeStamp, - state: readTextareaState(), - type: e.type, - data: e.data, - }; - recordEvent(ev); - }; - - const recordInputEvent = (e: InputEvent): void => { - if (e.type !== 'beforeinput' && e.type !== 'input') { - throw new Error(`Not supported!`); - } - if (originTimeStamp === 0) { - originTimeStamp = e.timeStamp; - } - const ev: IRecordedInputEvent = { - timeStamp: e.timeStamp - originTimeStamp, - state: readTextareaState(), - type: e.type, - data: e.data, - inputType: e.inputType, - isComposing: e.isComposing, - }; - recordEvent(ev); - }; - - wrapper.onKeyDown(recordKeyboardEvent); - wrapper.onKeyPress(recordKeyboardEvent); - wrapper.onKeyUp(recordKeyboardEvent); - wrapper.onCompositionStart(recordCompositionEvent); - wrapper.onCompositionUpdate(recordCompositionEvent); - wrapper.onCompositionEnd(recordCompositionEvent); - wrapper.onBeforeInput(recordInputEvent); - wrapper.onInput(recordInputEvent); - } - -})(); diff --git a/src/vs/editor/test/browser/controller/imeTester.html b/src/vs/editor/test/browser/controller/imeTester.html deleted file mode 100644 index 42adc4f56a5..00000000000 --- a/src/vs/editor/test/browser/controller/imeTester.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - -

Detailed setup steps at https://github.com/microsoft/vscode/wiki/IME-Test

- - - - diff --git a/src/vs/editor/test/browser/controller/imeTester.ts b/src/vs/editor/test/browser/controller/imeTester.ts deleted file mode 100644 index 1f6a67c9ccd..00000000000 --- a/src/vs/editor/test/browser/controller/imeTester.ts +++ /dev/null @@ -1,213 +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 '../../../common/core/position.js'; -import { IRange, Range } from '../../../common/core/range.js'; -import { EndOfLinePreference } from '../../../common/model.js'; -import * as dom from '../../../../base/browser/dom.js'; -import * as browser from '../../../../base/browser/browser.js'; -import * as platform from '../../../../base/common/platform.js'; -import { mainWindow } from '../../../../base/browser/window.js'; -import { TestAccessibilityService } from '../../../../platform/accessibility/test/common/testAccessibilityService.js'; -import { NullLogService } from '../../../../platform/log/common/log.js'; -import { SimplePagedScreenReaderStrategy } from '../../../browser/controller/editContext/screenReaderUtils.js'; -import { ISimpleModel } from '../../../common/viewModel/screenReaderSimpleModel.js'; -import { TextAreaState } from '../../../browser/controller/editContext/textArea/textAreaEditContextState.js'; -import { ITextAreaInputHost, TextAreaInput, TextAreaWrapper } from '../../../browser/controller/editContext/textArea/textAreaEditContextInput.js'; -import { Selection } from '../../../common/core/selection.js'; - -// To run this test, open imeTester.html - -class SingleLineTestModel implements ISimpleModel { - - private _line: string; - - constructor(line: string) { - this._line = line; - } - - _setText(text: string) { - this._line = text; - } - - getLineContent(lineNumber: number): string { - return this._line; - } - - getLineMaxColumn(lineNumber: number): number { - return this._line.length + 1; - } - - getValueInRange(range: IRange, eol: EndOfLinePreference): string { - return this._line.substring(range.startColumn - 1, range.endColumn - 1); - } - - getValueLengthInRange(range: Range, eol: EndOfLinePreference): number { - return this.getValueInRange(range, eol).length; - } - - modifyPosition(position: Position, offset: number): Position { - const column = Math.min(this.getLineMaxColumn(position.lineNumber), Math.max(1, position.column + offset)); - return new Position(position.lineNumber, column); - } - - getModelLineContent(lineNumber: number): string { - return this._line; - } - - getLineCount(): number { - return 1; - } -} - -class TestView { - - private readonly _model: SingleLineTestModel; - - constructor(model: SingleLineTestModel) { - this._model = model; - } - - public paint(output: HTMLElement) { - dom.clearNode(output); - for (let i = 1; i <= this._model.getLineCount(); i++) { - const textNode = document.createTextNode(this._model.getModelLineContent(i)); - output.appendChild(textNode); - const br = document.createElement('br'); - output.appendChild(br); - } - } -} - -function doCreateTest(description: string, inputStr: string, expectedStr: string): HTMLElement { - let cursorOffset: number = 0; - let cursorLength: number = 0; - - const container = document.createElement('div'); - container.className = 'container'; - - const title = document.createElement('div'); - title.className = 'title'; - - const inputStrStrong = document.createElement('strong'); - inputStrStrong.innerText = inputStr; - - title.innerText = description + '. Type '; - title.appendChild(inputStrStrong); - - container.appendChild(title); - - const startBtn = document.createElement('button'); - startBtn.innerText = 'Start'; - container.appendChild(startBtn); - - - const input = document.createElement('textarea'); - input.setAttribute('rows', '10'); - input.setAttribute('cols', '40'); - container.appendChild(input); - - const model = new SingleLineTestModel('some text'); - const screenReaderStrategy = new SimplePagedScreenReaderStrategy(); - const textAreaInputHost: ITextAreaInputHost = { - getDataToCopy: () => { - return { - isFromEmptySelection: false, - multicursorText: null, - text: '', - html: undefined, - mode: null - }; - }, - getScreenReaderContent: (): TextAreaState => { - const selection = new Selection(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength); - - const screenReaderContentState = screenReaderStrategy.fromEditorSelection(model, selection, 10, true); - return TextAreaState.fromScreenReaderContentState(screenReaderContentState); - }, - deduceModelPosition: (viewAnchorPosition: Position, deltaOffset: number, lineFeedCnt: number): Position => { - return null!; - } - }; - - const handler = new TextAreaInput(textAreaInputHost, new TextAreaWrapper(input), platform.OS, { - isAndroid: browser.isAndroid, - isFirefox: browser.isFirefox, - isChrome: browser.isChrome, - isSafari: browser.isSafari, - }, new TestAccessibilityService(), new NullLogService()); - - const output = document.createElement('pre'); - output.className = 'output'; - container.appendChild(output); - - const check = document.createElement('pre'); - check.className = 'check'; - container.appendChild(check); - - const br = document.createElement('br'); - br.style.clear = 'both'; - container.appendChild(br); - - const view = new TestView(model); - - const updatePosition = (off: number, len: number) => { - cursorOffset = off; - cursorLength = len; - handler.writeNativeTextAreaContent('selection changed'); - handler.focusTextArea(); - }; - - const updateModelAndPosition = (text: string, off: number, len: number) => { - model._setText(text); - updatePosition(off, len); - view.paint(output); - - const expected = 'some ' + expectedStr + ' text'; - if (text === expected) { - check.innerText = '[GOOD]'; - check.className = 'check good'; - } else { - check.innerText = '[BAD]'; - check.className = 'check bad'; - } - check.appendChild(document.createTextNode(expected)); - }; - - handler.onType((e) => { - console.log('type text: ' + e.text + ', replaceCharCnt: ' + e.replacePrevCharCnt); - const text = model.getModelLineContent(1); - const preText = text.substring(0, cursorOffset - e.replacePrevCharCnt); - const postText = text.substring(cursorOffset + cursorLength); - const midText = e.text; - - updateModelAndPosition(preText + midText + postText, (preText + midText).length, 0); - }); - - view.paint(output); - - startBtn.onclick = function () { - updateModelAndPosition('some text', 5, 0); - input.focus(); - }; - - return container; -} - -const TESTS = [ - { description: 'Japanese IME 1', in: 'sennsei [Enter]', out: 'せんせい' }, - { description: 'Japanese IME 2', in: 'konnichiha [Enter]', out: 'こんいちは' }, - { description: 'Japanese IME 3', in: 'mikann [Enter]', out: 'みかん' }, - { description: 'Korean IME 1', in: 'gksrmf [Space]', out: '한글 ' }, - { description: 'Chinese IME 1', in: '.,', out: '。,' }, - { description: 'Chinese IME 2', in: 'ni [Space] hao [Space]', out: '你好' }, - { description: 'Chinese IME 3', in: 'hazni [Space]', out: '哈祝你' }, - { description: 'Mac dead key 1', in: '`.', out: '`.' }, - { description: 'Mac hold key 1', in: 'e long press and 1', out: 'é' } -]; - -TESTS.forEach((t) => { - mainWindow.document.body.appendChild(doCreateTest(t.description, t.in, t.out)); -}); diff --git a/src/vs/editor/test/browser/controller/textAreaInput.test.ts b/src/vs/editor/test/browser/controller/textAreaInput.test.ts index 2ef8dcb1ce6..ab25ca262c4 100644 --- a/src/vs/editor/test/browser/controller/textAreaInput.test.ts +++ b/src/vs/editor/test/browser/controller/textAreaInput.test.ts @@ -13,7 +13,6 @@ import { IRecorded, IRecordedEvent, IRecordedTextareaState } from './imeRecorded import { TestAccessibilityService } from '../../../../platform/accessibility/test/common/testAccessibilityService.js'; import { NullLogService } from '../../../../platform/log/common/log.js'; import { IBrowser, ICompleteTextAreaWrapper, ITextAreaInputHost, TextAreaInput } from '../../../browser/controller/editContext/textArea/textAreaEditContextInput.js'; -import { ClipboardDataToCopy } from '../../../browser/controller/editContext/clipboardUtils.js'; import { TextAreaState } from '../../../browser/controller/editContext/textArea/textAreaEditContextState.js'; suite('TextAreaInput', () => { @@ -49,9 +48,7 @@ suite('TextAreaInput', () => { async function simulateInteraction(recorded: IRecorded): Promise { const disposables = new DisposableStore(); const host: ITextAreaInputHost = { - getDataToCopy: function (): ClipboardDataToCopy { - throw new Error('Function not implemented.'); - }, + context: null!, getScreenReaderContent: function (): TextAreaState { return new TextAreaState('', 0, 0, null, undefined); }, diff --git a/src/vs/editor/test/browser/editorTestServices.ts b/src/vs/editor/test/browser/editorTestServices.ts index 4567ca51837..38594483bac 100644 --- a/src/vs/editor/test/browser/editorTestServices.ts +++ b/src/vs/editor/test/browser/editorTestServices.ts @@ -19,7 +19,8 @@ export class TestCodeEditorService extends AbstractCodeEditorService { } getActiveCodeEditor(): ICodeEditor | null { - return null; + const editors = this.listCodeEditors(); + return editors.length > 0 ? editors[editors.length - 1] : null; } public lastInput?: IResourceEditorInput; override openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { diff --git a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts index c36d8ddadc1..29927436943 100644 --- a/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts +++ b/src/vs/editor/test/browser/gpu/decorationCssRulerExtractor.test.ts @@ -91,4 +91,26 @@ suite('DecorationCssRulerExtractor', () => { `.${testClassName} { color: red; }`, ]); }); + + test('should pick up styles with pseudo-class selectors', () => { + addStyleElement(`.${testClassName} { background-color: green; }`); + addStyleElement(`.${testClassName}:not(.other) { color: blue; }`); + const rules = extractor.getStyleRules(container, testClassName); + deepStrictEqual(rules.length, 2); + deepStrictEqual(rules[0].style.backgroundColor, 'green'); + deepStrictEqual(rules[1].style.color, 'blue'); + }); + + test('should pick up styles when className has multiple space-separated classes', () => { + const secondClassName = randomClass(); + addStyleElement([ + `.${testClassName} { color: red; }`, + `.${secondClassName} { opacity: 0.5; }`, + `.${testClassName}.${secondClassName} { font-weight: bold; }`, + ].join('\n')); + // Pass space-separated classes like 'class1 class2' + const rules = extractor.getStyleRules(container, `${testClassName} ${secondClassName}`); + // Should find rules for both classes and the chained selector + deepStrictEqual(rules.length, 3); + }); }); diff --git a/src/vs/editor/test/browser/view/viewController.test.ts b/src/vs/editor/test/browser/view/viewController.test.ts new file mode 100644 index 00000000000..a3b26791ce8 --- /dev/null +++ b/src/vs/editor/test/browser/view/viewController.test.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TestThemeService } from '../../../../platform/theme/test/common/testThemeService.js'; +import { NavigationCommandRevealType } from '../../../browser/coreCommands.js'; +import { ViewController } from '../../../browser/view/viewController.js'; +import { ViewUserInputEvents } from '../../../browser/view/viewUserInputEvents.js'; +import { Position } from '../../../common/core/position.js'; +import { ILanguageService } from '../../../common/languages/language.js'; +import { ILanguageConfigurationService } from '../../../common/languages/languageConfigurationRegistry.js'; +import { MonospaceLineBreaksComputerFactory } from '../../../common/viewModel/monospaceLineBreaksComputer.js'; +import { ViewModel } from '../../../common/viewModel/viewModelImpl.js'; +import { instantiateTextModel } from '../../../test/common/testTextModel.js'; +import { TestLanguageConfigurationService } from '../../common/modes/testLanguageConfigurationService.js'; +import { TestConfiguration } from '../config/testConfiguration.js'; +import { createCodeEditorServices } from '../testCodeEditor.js'; + +suite('ViewController - Bracket content selection', () => { + let disposables: DisposableStore; + let instantiationService: TestInstantiationService; + let languageConfigurationService: ILanguageConfigurationService; + let languageService: ILanguageService; + let viewModel: ViewModel | undefined; + + setup(() => { + disposables = new DisposableStore(); + instantiationService = createCodeEditorServices(disposables); + languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + languageService = instantiationService.get(ILanguageService); + viewModel = undefined; + }); + + teardown(() => { + viewModel?.dispose(); + viewModel = undefined; + disposables.dispose(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createViewControllerWithText(text: string): ViewController { + const languageId = 'testMode'; + disposables.add(languageService.registerLanguage({ id: languageId })); + disposables.add(languageConfigurationService.register(languageId, { + brackets: [ + ['{', '}'], + ['[', ']'], + ['(', ')'], + ] + })); + + const configuration = disposables.add(new TestConfiguration({})); + const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); + + viewModel = new ViewModel( + 1, // editorId + configuration, + disposables.add(instantiateTextModel(instantiationService, text, languageId)), + monospaceLineBreaksComputerFactory, + monospaceLineBreaksComputerFactory, + null!, + disposables.add(new TestLanguageConfigurationService()), + new TestThemeService(), + { setVisibleLines() { } }, + { batchChanges: (cb: any) => cb() } + ); + + return new ViewController( + configuration, + viewModel, + new ViewUserInputEvents(viewModel.coordinatesConverter), + { + paste: () => { }, + type: () => { }, + compositionType: () => { }, + startComposition: () => { }, + endComposition: () => { }, + cut: () => { } + } + ); + } + + function testBracketSelection(text: string, position: Position, expectedText: string | undefined) { + const controller = createViewControllerWithText(text); + controller.dispatchMouse({ + position, + mouseColumn: position.column, + startedOnLineNumbers: false, + revealType: NavigationCommandRevealType.Minimal, + mouseDownCount: 2, + inSelectionMode: false, + altKey: false, + ctrlKey: false, + metaKey: false, + shiftKey: false, + leftButton: true, + middleButton: false, + onInjectedText: false + }); + + const selections = viewModel!.getSelections(); + const selectedText = viewModel!.model.getValueInRange(selections[0]); + if (expectedText === undefined) { + assert.notStrictEqual(selectedText, expectedText); + } else { + assert.strictEqual(selectedText, expectedText); + } + } + + test('Select content after opening curly brace', () => { + testBracketSelection('var x = { hello };', new Position(1, 10), ' hello '); + }); + + test('Select content before closing curly brace', () => { + testBracketSelection('var x = { hello };', new Position(1, 17), ' hello '); + }); + + test('Select content after opening parenthesis', () => { + testBracketSelection('function foo(arg1, arg2) {}', new Position(1, 14), 'arg1, arg2'); + }); + + test('Select content before closing parenthesis', () => { + testBracketSelection('function foo(arg1, arg2) {}', new Position(1, 24), 'arg1, arg2'); + }); + + test('Select content after opening square bracket', () => { + testBracketSelection('const arr = [ 1, 2, 3 ];', new Position(1, 14), ' 1, 2, 3 '); + }); + + test('Select content before closing square bracket', () => { + testBracketSelection('const arr = [ 1, 2, 3 ];', new Position(1, 23), ' 1, 2, 3 '); + }); + + test('Select innermost bracket content with nested brackets', () => { + testBracketSelection('var x = { a: { b: 123 }};', new Position(1, 15), ' b: 123 '); + }); + + test('Empty brackets create empty selection', () => { + testBracketSelection('var x = {};', new Position(1, 10), ''); + }); +}); diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index fbf899499eb..ddbb919151a 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -104,14 +104,7 @@ suite('Editor ViewModel - SplitLinesCollection', () => { const wrapOnEscapedLineFeeds = config.options.get(EditorOption.wrapOnEscapedLineFeeds); const lineBreaksComputerFactory = new MonospaceLineBreaksComputerFactory(wordWrapBreakBeforeCharacters, wordWrapBreakAfterCharacters); - const model = createTextModel([ - 'int main() {', - '\tprintf("Hello world!");', - '}', - 'int main() {', - '\tprintf("Hello world!");', - '}', - ].join('\n')); + const model = createTextModel(text); const linesCollection = new ViewModelLinesFromProjectedModel( 1, @@ -353,7 +346,7 @@ suite('SplitLinesCollection', () => { tokens[i].value << MetadataConsts.FOREGROUND_OFFSET ); } - return new languages.EncodedTokenizationResult(result, state); + return new languages.EncodedTokenizationResult(result, [], state); } }; const LANGUAGE_ID = 'modelModeTest1'; diff --git a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts index 8afa07a8b56..4767de7021d 100644 --- a/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/browser/viewModel/viewModelImpl.test.ts @@ -122,7 +122,7 @@ suite('ViewModel', () => { function assertGetPlainTextToCopy(text: string[], ranges: Range[], emptySelectionClipboard: boolean, expected: string | string[]): void { testViewModel(text, {}, (viewModel, model) => { const actual = viewModel.getPlainTextToCopy(ranges, emptySelectionClipboard, false); - assert.deepStrictEqual(actual, expected); + assert.deepStrictEqual(actual.sourceText, expected); }); } @@ -198,7 +198,27 @@ suite('ViewModel', () => { new Range(3, 2, 3, 2), ], true, - 'line2\nline3\n' + [ + 'line2\n', + 'line3\n' + ] + ); + }); + + test('issue #256039: getPlainTextToCopy with multiple cursors and empty selections should return array', () => { + // Bug: When copying with multiple cursors (empty selections) with emptySelectionClipboard enabled, + // the result should be an array so that pasting with "editor.multiCursorPaste": "full" + // correctly distributes each line to the corresponding cursor. + // Without the fix, this returns 'line2\nline3\n' (a single string). + // With the fix, this returns ['line2\n', 'line3\n'] (an array). + assertGetPlainTextToCopy( + USUAL_TEXT, + [ + new Range(2, 1, 2, 1), + new Range(3, 1, 3, 1), + ], + true, + ['line2\n', 'line3\n'] ); }); @@ -222,7 +242,7 @@ suite('ViewModel', () => { new Range(3, 2, 3, 2), ], true, - ['ine2', 'line3'] + ['ine2', 'line3\n'] ); }); @@ -259,7 +279,10 @@ suite('ViewModel', () => { new Range(3, 2, 3, 2), ], true, - 'line2\nline3\n' + [ + 'line2\n', + 'line3\n' + ] ); }); @@ -267,7 +290,7 @@ suite('ViewModel', () => { testViewModel(USUAL_TEXT, {}, (viewModel, model) => { model.setEOL(EndOfLineSequence.LF); const actual = viewModel.getPlainTextToCopy([new Range(2, 1, 5, 1)], true, true); - assert.deepStrictEqual(actual, 'line2\r\nline3\r\nline4\r\n'); + assert.deepStrictEqual(actual.sourceText, 'line2\r\nline3\r\nline4\r\n'); }); }); diff --git a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts index fead379bd24..821496ef86e 100644 --- a/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts +++ b/src/vs/editor/test/browser/widget/codeEditorWidget.test.ts @@ -222,4 +222,24 @@ suite('CodeEditorWidget', () => { }); }); + test('getBottomForLineNumber should handle invalid line numbers gracefully', () => { + withTestCodeEditor('line1\nline2\nline3', {}, (editor, viewModel) => { + // Test with lineNumber greater than line count + const result1 = editor.getBottomForLineNumber(100); + assert.ok(result1 >= 0, 'Should return a valid position for out-of-bounds line number'); + + // Test with lineNumber less than 1 + const result2 = editor.getBottomForLineNumber(0); + assert.ok(result2 >= 0, 'Should return a valid position for line number 0'); + + // Test with negative lineNumber + const result3 = editor.getBottomForLineNumber(-5); + assert.ok(result3 >= 0, 'Should return a valid position for negative line number'); + + // Test with valid lineNumber should still work + const result4 = editor.getBottomForLineNumber(2); + assert.ok(result4 > 0, 'Should return a valid position for valid line number'); + }); + }); + }); diff --git a/src/vs/editor/test/common/core/edit.test.ts b/src/vs/editor/test/common/core/edit.test.ts deleted file mode 100644 index 521b13dcbed..00000000000 --- a/src/vs/editor/test/common/core/edit.test.ts +++ /dev/null @@ -1,192 +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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { Random } from './random.js'; -import { StringEdit, StringReplacement } from '../../../common/core/edits/stringEdit.js'; -import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; -import { ArrayEdit, ArrayReplacement } from '../../../common/core/edits/arrayEdit.js'; - -suite('Edit', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('StringEdit', () => { - test('basic', () => { - const arr = '0123456789'; - const edit = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); - const result = edit.apply(arr); - assert.deepStrictEqual(result, '0123xyz6789'); - }); - - test('inverse', () => { - for (let i = 0; i < 1000; i++) { - test('case' + i, () => { - runTest(i); - }); - } - - test.skip('fuzz', () => { - for (let i = 0; i < 1_000_000; i++) { - runTest(i); - } - }); - - function runTest(seed: number) { - const rng = Random.create(seed); - - const s0 = 'abcde\nfghij\nklmno\npqrst\n'; - - const e = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); - const eInv = e.inverse(s0); - - assert.strictEqual(eInv.apply(e.apply(s0)), s0); - } - }); - - suite('compose', () => { - for (let i = 0; i < 1000; i++) { - test('case' + i, () => { - runTest(i); - }); - } - - test.skip('fuzz', () => { - for (let i = 0; i < 1_000_000; i++) { - runTest(i); - } - }); - - function runTest(seed: number) { - const rng = Random.create(seed); - - const s0 = 'abcde\nfghij\nklmno\npqrst\n'; - - const edits1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); - const s1 = edits1.apply(s0); - - const edits2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng); - const s2 = edits2.apply(s1); - - const combinedEdits = edits1.compose(edits2); - const s2C = combinedEdits.apply(s0); - - assert.strictEqual(s2C, s2); - } - }); - - test('equals', () => { - const edit1 = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); - const edit2 = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); - const edit3 = StringEdit.replace(new OffsetRange(5, 6), 'xyz'); - const edit4 = StringEdit.replace(new OffsetRange(4, 6), 'xy'); - - assert.ok(edit1.equals(edit1)); - assert.ok(edit1.equals(edit2)); - assert.ok(edit2.equals(edit1)); - - assert.ok(!edit1.equals(edit3)); - assert.ok(!edit1.equals(edit4)); - }); - - test('getNewRanges', () => { - const edit = StringEdit.create([ - new StringReplacement(new OffsetRange(4, 6), 'abcde'), - new StringReplacement(new OffsetRange(7, 9), 'a'), - ]); - const ranges = edit.getNewRanges(); - assert.deepStrictEqual(ranges, [ - new OffsetRange(4, 9), - new OffsetRange(10, 11), - ]); - }); - - test('getJoinedReplaceRange', () => { - const edit = StringEdit.create([ - new StringReplacement(new OffsetRange(4, 6), 'abcde'), - new StringReplacement(new OffsetRange(7, 9), 'a'), - ]); - const range = edit.getJoinedReplaceRange(); - assert.deepStrictEqual(range, new OffsetRange(4, 9)); - }); - - test('getLengthDelta', () => { - const edit = StringEdit.create([ - new StringReplacement(new OffsetRange(4, 6), 'abcde'), - new StringReplacement(new OffsetRange(7, 9), 'a'), - ]); - const delta = edit.getLengthDelta(); - assert.strictEqual(delta, 2); - assert.strictEqual(edit.replacements[0].getLengthDelta(), 3); - assert.strictEqual(edit.replacements[1].getLengthDelta(), -1); - }); - }); - - suite('ArrayEdit', () => { - test('basic', () => { - const arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; - const edit = ArrayEdit.replace(new OffsetRange(4, 6), ['x', 'y', 'z']); - const result = edit.apply(arr); - assert.deepStrictEqual(result, ['0', '1', '2', '3', 'x', 'y', 'z', '6', '7', '8', '9']); - }); - - suite('compose', () => { - for (let i = 0; i < 100; i++) { - test('case' + i, () => { - runTest(i); - }); - } - - function runTest(seed: number) { - const rng = Random.create(seed); - - const s0 = 'abcde\nfghij\nklmno\npqrst\n'; - - const e1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); - const s1 = e1.apply(s0); - - const e2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng); - - const ae1 = ArrayEdit.create(e1.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); - const ae2 = ArrayEdit.create(e2.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); - const as0 = [...s0]; - const as1 = ae1.apply(as0); - const as2 = ae2.apply(as1); - const aCombinedEdits = ae1.compose(ae2); - - const as2C = aCombinedEdits.apply(as0); - assert.deepStrictEqual(as2, as2C); - } - }); - }); - - - function getRandomEdit(str: string, count: number, rng: Random): StringEdit { - const edits: StringReplacement[] = []; - let i = 0; - for (let j = 0; j < count; j++) { - if (i >= str.length) { - break; - } - edits.push(getRandomSingleEdit(str, i, rng)); - i = edits[j].replaceRange.endExclusive + 1; - } - return StringEdit.create(edits); - } - - function getRandomSingleEdit(str: string, rangeOffsetStart: number, rng: Random): StringReplacement { - const offsetStart = rng.nextIntRange(rangeOffsetStart, str.length); - const offsetEnd = rng.nextIntRange(offsetStart, str.length); - - const textStart = rng.nextIntRange(0, str.length); - const textLen = rng.nextIntRange(0, Math.min(7, str.length - textStart)); - - return new StringReplacement( - new OffsetRange(offsetStart, offsetEnd), - str.substring(textStart, textStart + textLen) - ); - } -}); diff --git a/src/vs/editor/test/common/core/stringEdit.test.ts b/src/vs/editor/test/common/core/stringEdit.test.ts new file mode 100644 index 00000000000..e5d9484997b --- /dev/null +++ b/src/vs/editor/test/common/core/stringEdit.test.ts @@ -0,0 +1,344 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ArrayEdit, ArrayReplacement } from '../../../common/core/edits/arrayEdit.js'; +import { StringEdit, StringReplacement } from '../../../common/core/edits/stringEdit.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { Random } from './random.js'; + +suite('Edit', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('StringEdit', () => { + test('basic', () => { + const arr = '0123456789'; + const edit = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); + const result = edit.apply(arr); + assert.deepStrictEqual(result, '0123xyz6789'); + }); + + test('inverse', () => { + for (let i = 0; i < 1000; i++) { + test('case' + i, () => { + runTest(i); + }); + } + + test.skip('fuzz', () => { + for (let i = 0; i < 1_000_000; i++) { + runTest(i); + } + }); + + function runTest(seed: number) { + const rng = Random.create(seed); + + const s0 = 'abcde\nfghij\nklmno\npqrst\n'; + + const e = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + const eInv = e.inverse(s0); + + assert.strictEqual(eInv.apply(e.apply(s0)), s0); + } + }); + + suite('compose', () => { + for (let i = 0; i < 1000; i++) { + test('case' + i, () => { + runTest(i); + }); + } + + test.skip('fuzz', () => { + for (let i = 0; i < 1_000_000; i++) { + runTest(i); + } + }); + + function runTest(seed: number) { + const rng = Random.create(seed); + + const s0 = 'abcde\nfghij\nklmno\npqrst\n'; + + const edits1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + const s1 = edits1.apply(s0); + + const edits2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng); + const s2 = edits2.apply(s1); + + const combinedEdits = edits1.compose(edits2); + const s2C = combinedEdits.apply(s0); + + assert.strictEqual(s2C, s2); + } + }); + + test('equals', () => { + const edit1 = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); + const edit2 = StringEdit.replace(new OffsetRange(4, 6), 'xyz'); + const edit3 = StringEdit.replace(new OffsetRange(5, 6), 'xyz'); + const edit4 = StringEdit.replace(new OffsetRange(4, 6), 'xy'); + + assert.ok(edit1.equals(edit1)); + assert.ok(edit1.equals(edit2)); + assert.ok(edit2.equals(edit1)); + + assert.ok(!edit1.equals(edit3)); + assert.ok(!edit1.equals(edit4)); + }); + + test('getNewRanges', () => { + const edit = StringEdit.create([ + new StringReplacement(new OffsetRange(4, 6), 'abcde'), + new StringReplacement(new OffsetRange(7, 9), 'a'), + ]); + const ranges = edit.getNewRanges(); + assert.deepStrictEqual(ranges, [ + new OffsetRange(4, 9), + new OffsetRange(10, 11), + ]); + }); + + test('getJoinedReplaceRange', () => { + const edit = StringEdit.create([ + new StringReplacement(new OffsetRange(4, 6), 'abcde'), + new StringReplacement(new OffsetRange(7, 9), 'a'), + ]); + const range = edit.getJoinedReplaceRange(); + assert.deepStrictEqual(range, new OffsetRange(4, 9)); + }); + + test('getLengthDelta', () => { + const edit = StringEdit.create([ + new StringReplacement(new OffsetRange(4, 6), 'abcde'), + new StringReplacement(new OffsetRange(7, 9), 'a'), + ]); + const delta = edit.getLengthDelta(); + assert.strictEqual(delta, 2); + assert.strictEqual(edit.replacements[0].getLengthDelta(), 3); + assert.strictEqual(edit.replacements[1].getLengthDelta(), -1); + }); + + test('adjacent edit and insert should rebase successfully', () => { + // A replacement ending at X followed by an insert at X should not conflict + const firstEdit = StringEdit.create([ + StringReplacement.replace(new OffsetRange(1826, 1838), 'function fib(n: number): number {'), + ]); + const followupEdit = StringEdit.create([ + StringReplacement.replace(new OffsetRange(1838, 1838), '\n\tif (n <= 1) {\n\t\treturn n;\n\t}\n\treturn fib(n - 1) + fib(n - 2);\n}'), + ]); + const rebasedEdit = followupEdit.tryRebase(firstEdit); + + // Since firstEdit replaces [1826, 1838) with text of length 33, + // the insert at 1838 should be rebased to 1826 + 33 = 1859 + assert.ok(rebasedEdit); + assert.strictEqual(rebasedEdit?.replacements[0].replaceRange.start, 1859); + assert.strictEqual(rebasedEdit?.replacements[0].replaceRange.endExclusive, 1859); + }); + + test('concurrent inserts at same position should conflict', () => { + // Two inserts at the exact same position conflict because order matters + const firstEdit = StringEdit.create([ + StringReplacement.replace(new OffsetRange(1838, 1838), '1'), + ]); + const followupEdit = StringEdit.create([ + StringReplacement.replace(new OffsetRange(1838, 1838), '2'), + ]); + const rebasedEdit = followupEdit.tryRebase(firstEdit); + + // This should return undefined because both are inserts at the same position + assert.strictEqual(rebasedEdit, undefined); + }); + + test('tryRebase should return undefined when rebasing would produce non-disjoint edits (negative offset case)', () => { + // ourEdit1: [100, 110) -> "A" + // ourEdit2: [120, 120) -> "B" + // baseEdit: [110, 125) -> "" (delete 15 chars, offset = -15) + // After transformation, ourEdit2 at [105, 105) < ourEdit1 end (110) + + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(OffsetRange.emptyAt(120), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(110, 125), ''), + ]); + + const result = ourEdit.tryRebase(baseEdit); + assert.strictEqual(result, undefined); + }); + + test('tryRebase should succeed when edits remain disjoint after rebasing', () => { + // ourEdit1: [100, 110) -> "A" + // ourEdit2: [200, 210) -> "B" + // baseEdit: [50, 60) -> "" (delete 10 chars, offset = -10) + // After: ourEdit1 at [90, 100), ourEdit2 at [190, 200) - still disjoint + + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(new OffsetRange(200, 210), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(50, 60), ''), + ]); + + const result = ourEdit.tryRebase(baseEdit); + assert.ok(result); + assert.strictEqual(result?.replacements[0].replaceRange.start, 90); + assert.strictEqual(result?.replacements[1].replaceRange.start, 190); + }); + + test('rebaseSkipConflicting should skip edits that would produce non-disjoint results', () => { + const ourEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(100, 110), 'A'), + new StringReplacement(OffsetRange.emptyAt(120), 'B'), + ]); + + const baseEdit = StringEdit.create([ + new StringReplacement(new OffsetRange(110, 125), ''), + ]); + + // Should not throw, and should skip the conflicting edit + const result = ourEdit.rebaseSkipConflicting(baseEdit); + assert.strictEqual(result.replacements.length, 1); + assert.strictEqual(result.replacements[0].replaceRange.start, 100); + }); + }); + + suite('ArrayEdit', () => { + test('basic', () => { + const arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; + const edit = ArrayEdit.replace(new OffsetRange(4, 6), ['x', 'y', 'z']); + const result = edit.apply(arr); + assert.deepStrictEqual(result, ['0', '1', '2', '3', 'x', 'y', 'z', '6', '7', '8', '9']); + }); + + suite('compose', () => { + for (let i = 0; i < 100; i++) { + test('case' + i, () => { + runTest(i); + }); + } + + function runTest(seed: number) { + const rng = Random.create(seed); + + const s0 = 'abcde\nfghij\nklmno\npqrst\n'; + + const e1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + const s1 = e1.apply(s0); + + const e2 = getRandomEdit(s1, rng.nextIntRange(1, 4), rng); + + const ae1 = ArrayEdit.create(e1.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); + const ae2 = ArrayEdit.create(e2.replacements.map(r => new ArrayReplacement(r.replaceRange, [...r.newText]))); + const as0 = [...s0]; + const as1 = ae1.apply(as0); + const as2 = ae2.apply(as1); + const aCombinedEdits = ae1.compose(ae2); + + const as2C = aCombinedEdits.apply(as0); + assert.deepStrictEqual(as2, as2C); + } + }); + }); + + + function getRandomEdit(str: string, count: number, rng: Random): StringEdit { + const edits: StringReplacement[] = []; + let i = 0; + for (let j = 0; j < count; j++) { + if (i >= str.length) { + break; + } + edits.push(getRandomSingleEdit(str, i, rng)); + i = edits[j].replaceRange.endExclusive + 1; + } + return StringEdit.create(edits); + } + + function getRandomSingleEdit(str: string, rangeOffsetStart: number, rng: Random): StringReplacement { + const offsetStart = rng.nextIntRange(rangeOffsetStart, str.length); + const offsetEnd = rng.nextIntRange(offsetStart, str.length); + + const textStart = rng.nextIntRange(0, str.length); + const textLen = rng.nextIntRange(0, Math.min(7, str.length - textStart)); + + return new StringReplacement( + new OffsetRange(offsetStart, offsetEnd), + str.substring(textStart, textStart + textLen) + ); + } + + suite('tryRebase invariants', () => { + for (let i = 0; i < 1000; i++) { + test('case' + i, () => { + runTest(i); + }); + } + + function runTest(seed: number) { + const rng = Random.create(seed); + const s0 = 'abcde\nfghij\nklmno\npqrst\n'; + + const e1 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + const e2 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + + const e1RebasedOnE2 = e1.tryRebase(e2); + const e2RebasedOnE1 = e2.tryRebase(e1); + + // Invariant 1: e1.rebase(e2) != undefined <=> e2.rebase(e1) != undefined + assert.strictEqual( + e1RebasedOnE2 !== undefined, + e2RebasedOnE1 !== undefined, + `Symmetry violated: e1.rebase(e2)=${e1RebasedOnE2 !== undefined}, e2.rebase(e1)=${e2RebasedOnE1 !== undefined}` + ); + + // Invariant 2: e1.rebase(e2) != undefined => e1.compose(e2.rebase(e1)) = e2.compose(e1.rebase(e2)) + if (e1RebasedOnE2 !== undefined && e2RebasedOnE1 !== undefined) { + const path1 = e1.compose(e2RebasedOnE1); + const path2 = e2.compose(e1RebasedOnE2); + + // Both paths should produce the same result when applied to s0 + const result1 = path1.apply(s0); + const result2 = path2.apply(s0); + assert.strictEqual(result1, result2, `Diamond property violated`); + } + + // Invariant 3: empty.rebase(e) = empty + const emptyRebasedOnE1 = StringEdit.empty.tryRebase(e1); + assert.ok(emptyRebasedOnE1 !== undefined); + assert.ok(emptyRebasedOnE1.isEmpty); + + // Invariant 4: e.rebase(empty) = e + const e1RebasedOnEmpty = e1.tryRebase(StringEdit.empty); + assert.ok(e1RebasedOnEmpty !== undefined); + assert.ok(e1.equals(e1RebasedOnEmpty), `e.rebase(empty) should equal e`); + + // Invariant 5 (TP2): T(T(e3, e1), T(e2, e1)) = T(T(e3, e2), T(e1, e2)) + // For 3+ concurrent operations, transformation order shouldn't matter + const e3 = getRandomEdit(s0, rng.nextIntRange(1, 4), rng); + + const e2OnE1 = e2.tryRebase(e1); + const e1OnE2 = e1.tryRebase(e2); + const e3OnE1 = e3.tryRebase(e1); + const e3OnE2 = e3.tryRebase(e2); + + if (e2OnE1 && e1OnE2 && e3OnE1 && e3OnE2) { + const path1 = e3OnE1.tryRebase(e2OnE1); // T(T(e3, e1), T(e2, e1)) + const path2 = e3OnE2.tryRebase(e1OnE2); // T(T(e3, e2), T(e1, e2)) + + if (path1 && path2) { + assert.ok(path1.equals(path2), `TP2 violated: transformation order matters`); + } + } + } + }); +}); diff --git a/src/vs/editor/test/common/core/textEdit.test.ts b/src/vs/editor/test/common/core/textEdit.test.ts index 3528d57fc1e..c7be3228042 100644 --- a/src/vs/editor/test/common/core/textEdit.test.ts +++ b/src/vs/editor/test/common/core/textEdit.test.ts @@ -10,9 +10,9 @@ import { StringText } from '../../../common/core/text/abstractText.js'; import { Random } from './random.js'; suite('TextEdit', () => { - suite('inverse', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + ensureNoDisposablesAreLeakedInTestSuite(); + suite('inverse', () => { function runTest(seed: number): void { const rand = Random.create(seed); const source = new StringText(rand.nextMultiLineString(10, new OffsetRange(0, 10))); @@ -36,4 +36,34 @@ suite('TextEdit', () => { test(`test ${seed}`, () => runTest(seed)); } }); + + suite('compose', () => { + function runTest(seed: number): void { + const rand = Random.create(seed); + + const s0 = new StringText(rand.nextMultiLineString(10, new OffsetRange(0, 10))); + + const edits1 = rand.nextTextEdit(s0, rand.nextIntRange(1, 4)); + const s1 = edits1.applyToString(s0.value); + + const s1Text = new StringText(s1); + const edits2 = rand.nextTextEdit(s1Text, rand.nextIntRange(1, 4)); + const s2 = edits2.applyToString(s1); + + const combinedEdits = edits1.compose(edits2); + const s2C = combinedEdits.applyToString(s0.value); + assert.strictEqual(s2C, s2); + } + + test.skip('fuzz', function () { + this.timeout(0); + for (let i = 0; i < 1_000_000; i++) { + runTest(i); + } + }); + + for (let seed = 0; seed < 100; seed++) { + test(`case ${seed}`, () => runTest(seed)); + } + }); }); diff --git a/src/vs/editor/test/common/languages/defaultDocumentColorsComputer.test.ts b/src/vs/editor/test/common/languages/defaultDocumentColorsComputer.test.ts index 95b42693c25..2ed38e08411 100644 --- a/src/vs/editor/test/common/languages/defaultDocumentColorsComputer.test.ts +++ b/src/vs/editor/test/common/languages/defaultDocumentColorsComputer.test.ts @@ -99,4 +99,93 @@ suite('Default Document Colors Computer', () => { assert.strictEqual(colors[0].color.blue, 0, 'Blue component should be 0'); assert.strictEqual(colors[0].color.alpha, 1, 'Alpha should be 1 (ff/255)'); }); + + test('hsl 100 percent saturation works with decimals', () => { + const model = new TestDocumentModel('const color = hsl(253, 100.00%, 47.10%);'); + const colors = computeDefaultDocumentColors(model); + + assert.strictEqual(colors.length, 1, 'Should detect one hsl color'); + }); + + test('hsl 100 percent saturation works without decimals', () => { + const model = new TestDocumentModel('const color = hsl(253, 100%, 47.10%);'); + const colors = computeDefaultDocumentColors(model); + + assert.strictEqual(colors.length, 1, 'Should detect one hsl color'); + }); + + test('hsl not 100 percent saturation should also work', () => { + const model = new TestDocumentModel('const color = hsl(0, 83.60%, 47.80%);'); + const colors = computeDefaultDocumentColors(model); + + assert.strictEqual(colors.length, 1, 'Should detect one hsl color'); + }); + + test('hsl with decimal hue values should work', () => { + // Test case from issue #180436 comment + const testCases = [ + { content: 'hsl(253.5, 100%, 50%)', name: 'decimal hue' }, + { content: 'hsl(360.0, 50%, 50%)', name: '360.0 hue' }, + { content: 'hsl(100.5, 50.5%, 50.5%)', name: 'all decimals' }, + { content: 'hsl(0.5, 50%, 50%)', name: 'small decimal hue' }, + { content: 'hsl(359.9, 100%, 50%)', name: 'near-max decimal hue' } + ]; + + testCases.forEach(testCase => { + const model = new TestDocumentModel(`const color = ${testCase.content};`); + const colors = computeDefaultDocumentColors(model); + assert.strictEqual(colors.length, 1, `Should detect hsl color with ${testCase.name}: ${testCase.content}`); + }); + }); + + test('hsla with decimal values should work', () => { + const testCases = [ + { content: 'hsla(253.5, 100%, 50%, 0.5)', name: 'decimal hue with alpha' }, + { content: 'hsla(360.0, 50.5%, 50.5%, 1)', name: 'all decimals with alpha 1' }, + { content: 'hsla(0.5, 50%, 50%, 0.25)', name: 'small decimal hue with alpha' } + ]; + + testCases.forEach(testCase => { + const model = new TestDocumentModel(`const color = ${testCase.content};`); + const colors = computeDefaultDocumentColors(model); + assert.strictEqual(colors.length, 1, `Should detect hsla color with ${testCase.name}: ${testCase.content}`); + }); + }); + + test('hsl with space separator (CSS Level 4 syntax) should work', () => { + // CSS Level 4 allows space-separated values instead of comma-separated + const testCases = [ + { content: 'hsl(253 100% 50%)', name: 'space-separated' }, + { content: 'hsl(253.5 100% 50%)', name: 'space-separated with decimal hue' }, + { content: 'hsla(253 100% 50% / 0.5)', name: 'hsla with slash separator for alpha' }, + { content: 'hsla(253.5 100% 50% / 0.5)', name: 'hsla with decimal hue and slash separator' }, + { content: 'hsla(253 100% 50% / 1)', name: 'hsla with slash and alpha 1' } + ]; + + testCases.forEach(testCase => { + const model = new TestDocumentModel(`const color = ${testCase.content};`); + const colors = computeDefaultDocumentColors(model); + assert.strictEqual(colors.length, 1, `Should detect hsl color with ${testCase.name}: ${testCase.content}`); + }); + }); + + test('rgb and rgba with CSS Level 4 space-separated syntax should work', () => { + // CSS Level 4 allows space-separated values for RGB/RGBA + const testCases = [ + { content: 'rgb(255 0 0)', name: 'rgb space-separated' }, + { content: 'rgb(128 128 128)', name: 'rgb space-separated gray' }, + { content: 'rgba(255 0 0 / 0.5)', name: 'rgba with slash separator for alpha' }, + { content: 'rgba(128 128 128 / 0.8)', name: 'rgba gray with slash separator' }, + { content: 'rgba(255 0 0 / 1)', name: 'rgba with slash and alpha 1' }, + // Traditional comma syntax should still work + { content: 'rgb(255, 0, 0)', name: 'rgb comma-separated (traditional)' }, + { content: 'rgba(255, 0, 0, 0.5)', name: 'rgba comma-separated (traditional)' } + ]; + + testCases.forEach(testCase => { + const model = new TestDocumentModel(`const color = ${testCase.content};`); + const colors = computeDefaultDocumentColors(model); + assert.strictEqual(colors.length, 1, `Should detect rgb/rgba color with ${testCase.name}: ${testCase.content}`); + }); + }); }); diff --git a/src/vs/editor/test/common/model/annotations.test.ts b/src/vs/editor/test/common/model/annotations.test.ts new file mode 100644 index 00000000000..133cb256e1a --- /dev/null +++ b/src/vs/editor/test/common/model/annotations.test.ts @@ -0,0 +1,500 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { AnnotatedString, AnnotationsUpdate, IAnnotation, IAnnotationUpdate } from '../../../common/model/tokens/annotations.js'; +import { OffsetRange } from '../../../common/core/ranges/offsetRange.js'; +import { StringEdit } from '../../../common/core/edits/stringEdit.js'; + +// ============================================================================ +// Visual Annotation Test Infrastructure +// ============================================================================ +// This infrastructure allows representing annotations visually using brackets: +// - '[id:text]' marks an annotation with the given id covering 'text' +// - Plain text represents unannotated content +// +// Example: "Lorem [1:ipsum] dolor [2:sit] amet" represents: +// - annotation "1" at offset 6-11 (content "ipsum") +// - annotation "2" at offset 18-21 (content "sit") +// +// For updates: +// - '[id:text]' sets an annotation +// - '' deletes an annotation in that range +// ============================================================================ + +/** + * Parses a visual string representation into annotations. + * The visual string uses '[id:text]' to mark annotation boundaries. + * The id becomes the annotation value, and text is the annotated content. + */ +function parseVisualAnnotations(visual: string): { annotations: IAnnotation[]; baseString: string } { + const annotations: IAnnotation[] = []; + let baseString = ''; + let i = 0; + + while (i < visual.length) { + if (visual[i] === '[') { + // Find the colon and closing bracket + const colonIdx = visual.indexOf(':', i + 1); + const closeIdx = visual.indexOf(']', colonIdx + 1); + if (colonIdx === -1 || closeIdx === -1) { + throw new Error(`Invalid annotation format at position ${i}`); + } + const id = visual.substring(i + 1, colonIdx); + const text = visual.substring(colonIdx + 1, closeIdx); + const startOffset = baseString.length; + baseString += text; + annotations.push({ range: new OffsetRange(startOffset, baseString.length), annotation: id }); + i = closeIdx + 1; + } else { + baseString += visual[i]; + i++; + } + } + + return { annotations, baseString }; +} + +/** + * Converts annotations to a visual string representation. + * Uses '[id:text]' to mark annotation boundaries. + * + * @param annotations - The annotations to visualize + * @param baseString - The base string content + */ +function toVisualString( + annotations: IAnnotation[], + baseString: string +): string { + if (annotations.length === 0) { + return baseString; + } + + // Sort annotations by start position + const sortedAnnotations = [...annotations].sort((a, b) => a.range.start - b.range.start); + + // Build the visual representation + let result = ''; + let pos = 0; + + for (const ann of sortedAnnotations) { + // Add plain text before this annotation + result += baseString.substring(pos, ann.range.start); + // Add annotated content with id + const annotatedText = baseString.substring(ann.range.start, ann.range.endExclusive); + result += `[${ann.annotation}:${annotatedText}]`; + pos = ann.range.endExclusive; + } + + // Add remaining text after last annotation + result += baseString.substring(pos); + + return result; +} + +/** + * Represents an AnnotatedString with its base string for visual testing. + */ +class VisualAnnotatedString { + constructor( + public readonly annotatedString: AnnotatedString, + public baseString: string + ) { } + + setAnnotations(update: AnnotationsUpdate): void { + this.annotatedString.setAnnotations(update); + } + + applyEdit(edit: StringEdit): void { + this.annotatedString.applyEdit(edit); + this.baseString = edit.apply(this.baseString); + } + + getAnnotationsIntersecting(range: OffsetRange): IAnnotation[] { + return this.annotatedString.getAnnotationsIntersecting(range); + } + + getAllAnnotations(): IAnnotation[] { + return this.annotatedString.getAllAnnotations(); + } + + clone(): VisualAnnotatedString { + return new VisualAnnotatedString(this.annotatedString.clone() as AnnotatedString, this.baseString); + } +} + +/** + * Creates a VisualAnnotatedString from a visual representation. + */ +function fromVisual(visual: string): VisualAnnotatedString { + const { annotations, baseString } = parseVisualAnnotations(visual); + return new VisualAnnotatedString(new AnnotatedString(annotations), baseString); +} + +/** + * Converts a VisualAnnotatedString to a visual representation. + */ +function toVisual(vas: VisualAnnotatedString): string { + return toVisualString(vas.getAllAnnotations(), vas.baseString); +} + +/** + * Parses visual update annotations, where: + * - '[id:text]' represents an annotation to set + * - '' represents an annotation to delete (range is tracked but annotation is undefined) + */ +function parseVisualUpdate(visual: string): { updates: IAnnotationUpdate[]; baseString: string } { + const updates: IAnnotationUpdate[] = []; + let baseString = ''; + let i = 0; + + while (i < visual.length) { + if (visual[i] === '[') { + // Set annotation: [id:text] + const colonIdx = visual.indexOf(':', i + 1); + const closeIdx = visual.indexOf(']', colonIdx + 1); + if (colonIdx === -1 || closeIdx === -1) { + throw new Error(`Invalid annotation format at position ${i}`); + } + const id = visual.substring(i + 1, colonIdx); + const text = visual.substring(colonIdx + 1, closeIdx); + const startOffset = baseString.length; + baseString += text; + updates.push({ range: new OffsetRange(startOffset, baseString.length), annotation: id }); + i = closeIdx + 1; + } else if (visual[i] === '<') { + // Delete annotation: + const colonIdx = visual.indexOf(':', i + 1); + const closeIdx = visual.indexOf('>', colonIdx + 1); + if (colonIdx === -1 || closeIdx === -1) { + throw new Error(`Invalid delete format at position ${i}`); + } + const text = visual.substring(colonIdx + 1, closeIdx); + const startOffset = baseString.length; + baseString += text; + updates.push({ range: new OffsetRange(startOffset, baseString.length), annotation: undefined }); + i = closeIdx + 1; + } else { + baseString += visual[i]; + i++; + } + } + + return { updates, baseString }; +} + +/** + * Creates an AnnotationsUpdate from a visual representation. + */ +function updateFromVisual(...visuals: string[]): AnnotationsUpdate { + const updates: IAnnotationUpdate[] = []; + + for (const visual of visuals) { + const { updates: parsedUpdates } = parseVisualUpdate(visual); + updates.push(...parsedUpdates); + } + + return AnnotationsUpdate.create(updates); +} + +/** + * Helper to create a StringEdit from visual notation. + * Uses a pattern matching approach where: + * - 'd' marks positions to delete + * - 'i:text:' inserts 'text' at the marked position + * + * Simpler approach: just use offset-based helpers + */ +function editDelete(start: number, end: number): StringEdit { + return StringEdit.replace(new OffsetRange(start, end), ''); +} + +function editInsert(pos: number, text: string): StringEdit { + return StringEdit.insert(pos, text); +} + +function editReplace(start: number, end: number, text: string): StringEdit { + return StringEdit.replace(new OffsetRange(start, end), text); +} + +/** + * Asserts that a VisualAnnotatedString matches the expected visual representation. + * Only compares annotations, not the base string (since setAnnotations doesn't change the base string). + */ +function assertVisual(vas: VisualAnnotatedString, expectedVisual: string): void { + const actual = toVisual(vas); + const { annotations: expectedAnnotations } = parseVisualAnnotations(expectedVisual); + const actualAnnotations = vas.getAllAnnotations(); + + // Compare annotations for better error messages + if (actualAnnotations.length !== expectedAnnotations.length) { + assert.fail( + `Annotation count mismatch.\n` + + ` Expected: ${expectedVisual}\n` + + ` Actual: ${actual}\n` + + ` Expected ${expectedAnnotations.length} annotations, got ${actualAnnotations.length}` + ); + } + + for (let i = 0; i < actualAnnotations.length; i++) { + const expected = expectedAnnotations[i]; + const actualAnn = actualAnnotations[i]; + if (actualAnn.range.start !== expected.range.start || actualAnn.range.endExclusive !== expected.range.endExclusive) { + assert.fail( + `Annotation ${i} range mismatch.\n` + + ` Expected: (${expected.range.start}, ${expected.range.endExclusive})\n` + + ` Actual: (${actualAnn.range.start}, ${actualAnn.range.endExclusive})\n` + + ` Expected visual: ${expectedVisual}\n` + + ` Actual visual: ${actual}` + ); + } + if (actualAnn.annotation !== expected.annotation) { + assert.fail( + `Annotation ${i} value mismatch.\n` + + ` Expected: "${expected.annotation}"\n` + + ` Actual: "${actualAnn.annotation}"` + ); + } + } +} + +/** + * Helper to visualize the effect of an edit on annotations. + * Returns both before and after states as visual strings. + */ +function visualizeEdit( + beforeAnnotations: string, + edit: StringEdit +): { before: string; after: string } { + const vas = fromVisual(beforeAnnotations); + const before = toVisual(vas); + + vas.applyEdit(edit); + + const after = toVisual(vas); + return { before, after }; +} + +// ============================================================================ +// Visual Annotations Test Suite +// ============================================================================ +// These tests use a visual representation for better readability: +// - '[id:text]' marks annotated regions with id and content +// - Plain text represents unannotated content +// - '' marks regions to delete (in updates) +// +// Example: "Lorem [1:ipsum] dolor [2:sit] amet" represents two annotations: +// "1" at (6,11) covering "ipsum", "2" at (18,21) covering "sit" +// ============================================================================ + +suite('Annotations Suite', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('setAnnotations 1', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + vas.setAnnotations(updateFromVisual('[4:Lorem i]')); + assertVisual(vas, '[4:Lorem i]psum [2:dolor] sit [3:amet]'); + vas.setAnnotations(updateFromVisual('Lorem ip[5:s]')); + assertVisual(vas, '[4:Lorem i]p[5:s]um [2:dolor] sit [3:amet]'); + }); + + test('setAnnotations 2', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + vas.setAnnotations(updateFromVisual( + 'L<_:orem ipsum d>', + '[4:Lorem ]' + )); + assertVisual(vas, '[4:Lorem ]ipsum dolor sit [3:amet]'); + vas.setAnnotations(updateFromVisual( + 'Lorem <_:ipsum dolor sit amet>', + '[5:Lor]' + )); + assertVisual(vas, '[5:Lor]em ipsum dolor sit amet'); + vas.setAnnotations(updateFromVisual('L[6:or]')); + assertVisual(vas, 'L[6:or]em ipsum dolor sit amet'); + }); + + test('setAnnotations 3', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + vas.setAnnotations(updateFromVisual('Lore[4:m ipsum dolor ]')); + assertVisual(vas, 'Lore[4:m ipsum dolor ]sit [3:amet]'); + vas.setAnnotations(updateFromVisual('Lorem ipsum dolor sit [5:a]')); + assertVisual(vas, 'Lore[4:m ipsum dolor ]sit [5:a]met'); + }); + + test('getAnnotationsIntersecting 1', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + const result1 = vas.getAnnotationsIntersecting(new OffsetRange(0, 13)); + assert.strictEqual(result1.length, 2); + assert.deepStrictEqual(result1.map(a => a.annotation), ['1', '2']); + const result2 = vas.getAnnotationsIntersecting(new OffsetRange(0, 22)); + assert.strictEqual(result2.length, 2); + assert.deepStrictEqual(result2.map(a => a.annotation), ['1', '2']); + }); + + test('getAnnotationsIntersecting 2', () => { + const vas = fromVisual('[1:Lorem] [2:i]p[3:s]'); + + const result1 = vas.getAnnotationsIntersecting(new OffsetRange(5, 7)); + assert.strictEqual(result1.length, 1); + assert.deepStrictEqual(result1.map(a => a.annotation), ['2']); + const result2 = vas.getAnnotationsIntersecting(new OffsetRange(5, 9)); + assert.strictEqual(result2.length, 2); + assert.deepStrictEqual(result2.map(a => a.annotation), ['2', '3']); + }); + + test('getAnnotationsIntersecting 3', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor]'); + const result1 = vas.getAnnotationsIntersecting(new OffsetRange(4, 13)); + assert.strictEqual(result1.length, 2); + assert.deepStrictEqual(result1.map(a => a.annotation), ['1', '2']); + vas.setAnnotations(updateFromVisual('[3:Lore]m[4: ipsu]')); + assertVisual(vas, '[3:Lore]m[4: ipsu]m [2:dolor]'); + const result2 = vas.getAnnotationsIntersecting(new OffsetRange(7, 13)); + assert.strictEqual(result2.length, 2); + assert.deepStrictEqual(result2.map(a => a.annotation), ['4', '2']); + }); + + test('getAnnotationsIntersecting 4', () => { + const vas = fromVisual('[1:Lorem ipsum] sit'); + vas.setAnnotations(updateFromVisual('Lorem ipsum [2:sit]')); + const result = vas.getAnnotationsIntersecting(new OffsetRange(2, 8)); + assert.strictEqual(result.length, 1); + assert.deepStrictEqual(result.map(a => a.annotation), ['1']); + }); + + test('getAnnotationsIntersecting 5', () => { + const vas = fromVisual('[1:Lorem ipsum] [2:dol] [3:or]'); + const result = vas.getAnnotationsIntersecting(new OffsetRange(1, 16)); + assert.strictEqual(result.length, 2); + assert.deepStrictEqual(result.map(a => a.annotation), ['1', '2']); + }); + + test('applyEdit 1 - deletion within annotation', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editDelete(0, 3) + ); + assert.strictEqual(result.after, '[1:em] ipsum [2:dolor] sit [3:amet]'); + }); + + test('applyEdit 2 - deletion and insertion within annotation', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editReplace(1, 3, 'XXXXX') + ); + assert.strictEqual(result.after, '[1:LXXXXXem] ipsum [2:dolor] sit [3:amet]'); + }); + + test('applyEdit 3 - deletion across several annotations', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editReplace(4, 22, 'XXXXX') + ); + assert.strictEqual(result.after, '[1:LoreXXXXX][3:amet]'); + }); + + test('applyEdit 4 - deletion between annotations', () => { + const result = visualizeEdit( + '[1:Lorem ip]sum and [2:dolor] sit [3:amet]', + editDelete(10, 12) + ); + assert.strictEqual(result.after, '[1:Lorem ip]suand [2:dolor] sit [3:amet]'); + }); + + test('applyEdit 5 - deletion that covers annotation', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editDelete(0, 5) + ); + assert.strictEqual(result.after, ' ipsum [2:dolor] sit [3:amet]'); + }); + + test('applyEdit 6 - several edits', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + const edit = StringEdit.compose([ + StringEdit.replace(new OffsetRange(0, 6), ''), + StringEdit.replace(new OffsetRange(6, 12), ''), + StringEdit.replace(new OffsetRange(12, 17), '') + ]); + vas.applyEdit(edit); + assertVisual(vas, 'ipsum sit [3:am]'); + }); + + test('applyEdit 7 - several edits', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet]'); + const edit1 = StringEdit.replace(new OffsetRange(0, 3), 'XXXX'); + const edit2 = StringEdit.replace(new OffsetRange(0, 2), ''); + vas.applyEdit(edit1.compose(edit2)); + assertVisual(vas, '[1:XXem] ipsum [2:dolor] sit [3:amet]'); + }); + + test('applyEdit 9 - insertion at end of annotation', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editInsert(17, 'XXX') + ); + assert.strictEqual(result.after, '[1:Lorem] ipsum [2:dolor]XXX sit [3:amet]'); + }); + + test('applyEdit 10 - insertion in middle of annotation', () => { + const result = visualizeEdit( + '[1:Lorem] ipsum [2:dolor] sit [3:amet]', + editInsert(14, 'XXX') + ); + assert.strictEqual(result.after, '[1:Lorem] ipsum [2:doXXXlor] sit [3:amet]'); + }); + + test('applyEdit 11 - replacement consuming annotation', () => { + const result = visualizeEdit( + '[1:L]o[2:rem] [3:i]', + editReplace(1, 6, 'X') + ); + assert.strictEqual(result.after, '[1:L]X[3:i]'); + }); + + test('applyEdit 12 - multiple disjoint edits', () => { + const vas = fromVisual('[1:Lorem] ipsum [2:dolor] sit [3:amet!] [4:done]'); + + const edit = StringEdit.compose([ + StringEdit.insert(0, 'X'), + StringEdit.delete(new OffsetRange(12, 13)), + StringEdit.replace(new OffsetRange(21, 22), 'YY'), + StringEdit.replace(new OffsetRange(28, 32), 'Z') + ]); + vas.applyEdit(edit); + assertVisual(vas, 'X[1:Lorem] ipsum[2:dolor] sitYY[3:amet!]Z[4:e]'); + }); + + test('applyEdit 13 - edit on the left border', () => { + const result = visualizeEdit( + 'lorem ipsum dolor[1: ]', + editInsert(17, 'X') + ); + assert.strictEqual(result.after, 'lorem ipsum dolorX[1: ]'); + }); + + test('rebase', () => { + const a = new VisualAnnotatedString( + new AnnotatedString([{ range: new OffsetRange(2, 5), annotation: '1' }]), + 'sitamet' + ); + const b = a.clone(); + const update: AnnotationsUpdate = AnnotationsUpdate.create([{ range: new OffsetRange(4, 5), annotation: '2' }]); + + b.setAnnotations(update); + const edit: StringEdit = StringEdit.replace(new OffsetRange(1, 6), 'XXX'); + + a.applyEdit(edit); + b.applyEdit(edit); + + update.rebase(edit); + + a.setAnnotations(update); + assert.deepStrictEqual(a.getAllAnnotations(), b.getAllAnnotations()); + }); +}); diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index 3b18e1d835c..2e8099a60a6 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -187,7 +187,7 @@ export class TokenizedDocument { offset += t.text.length; } - return new EncodedTokenizationResult(new Uint32Array(arr), new State(state2.lineNumber + 1)); + return new EncodedTokenizationResult(new Uint32Array(arr), [], new State(state2.lineNumber + 1)); } }; } diff --git a/src/vs/editor/test/common/model/editableTextModel.test.ts b/src/vs/editor/test/common/model/editableTextModel.test.ts index d2b73c7f536..05f5f958074 100644 --- a/src/vs/editor/test/common/model/editableTextModel.test.ts +++ b/src/vs/editor/test/common/model/editableTextModel.test.ts @@ -1119,3 +1119,122 @@ suite('EditorModel - EditableTextModel.applyEdits', () => { model.dispose(); }); }); + +suite('CRLF edit normalization', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('edit ending with \\r followed by \\n in buffer should strip trailing \\r', () => { + // Document: "abc\r\ndef\r\n" + // Edit: Replace range (1,1)-(1,4) "abc" with "xyz\r" + // The \r at end of replacement should be stripped since next char is \n + const model = createTextModel('abc\r\ndef\r\n'); + model.setEOL(EndOfLineSequence.CRLF); + + assert.strictEqual(model.getEOL(), '\r\n'); + assert.strictEqual(model.getLineCount(), 3); + assert.strictEqual(model.getLineContent(1), 'abc'); + assert.strictEqual(model.getLineContent(2), 'def'); + + model.applyEdits([ + { range: new Range(1, 1, 1, 4), text: 'xyz\r' } + ]); + + // The trailing \r should be stripped, so we get "xyz" not "xyz\r" + assert.strictEqual(model.getLineContent(1), 'xyz'); + assert.strictEqual(model.getLineContent(2), 'def'); + assert.strictEqual(model.getLineCount(), 3); + + model.dispose(); + }); + + test('edit ending with \\r\\n should NOT be modified', () => { + // Document: "abc\r\ndef\r\n" + // Edit: Replace range (1,1)-(1,4) "abc" with "xyz\r\n" + // This is a proper CRLF so should not be modified + const model = createTextModel('abc\r\ndef\r\n'); + model.setEOL(EndOfLineSequence.CRLF); + + model.applyEdits([ + { range: new Range(1, 1, 1, 4), text: 'xyz\r\n' } + ]); + + // Should add a new line + assert.strictEqual(model.getLineContent(1), 'xyz'); + assert.strictEqual(model.getLineContent(2), ''); + assert.strictEqual(model.getLineContent(3), 'def'); + assert.strictEqual(model.getLineCount(), 4); + + model.dispose(); + }); + + test('edit ending with \\r NOT followed by \\n should NOT be modified', () => { + // Document: "abcdef" (no newline after) + // Edit: Replace range (1,1)-(1,4) "abc" with "xyz\r" + // Since there's no \n after the range, the \r should stay + const model = createTextModel('abcdef'); + model.setEOL(EndOfLineSequence.CRLF); + + model.applyEdits([ + { range: new Range(1, 1, 1, 4), text: 'xyz\r' } + ]); + + // The \r should cause a new line since buffer normalizes EOL + // Actually since buffer uses CRLF, the lone \r will be normalized to \r\n + assert.strictEqual(model.getLineCount(), 2); + + model.dispose(); + }); + + test('edit in LF buffer should NOT strip trailing \\r', () => { + // Document with LF: "abc\ndef\n" + // Edit: Replace range (1,1)-(1,4) "abc" with "xyz\r" + // Since buffer is LF, no special handling needed + const model = createTextModel('abc\ndef\n'); + model.setEOL(EndOfLineSequence.LF); + + assert.strictEqual(model.getEOL(), '\n'); + assert.strictEqual(model.getLineCount(), 3); + + model.applyEdits([ + { range: new Range(1, 1, 1, 4), text: 'xyz\r' } + ]); + + // The \r will be normalized to \n (buffer's EOL) + assert.strictEqual(model.getLineCount(), 4); + + model.dispose(); + }); + + test('LSP include sorting scenario - edit ending with \\r should be normalized', () => { + // This is the real-world scenario from the issue + // Document: "#include \"a.h\"\r\n#include \"c.h\"\r\n#include \"b.h\"\r\n" + // Edit: Replace lines 1-3 with reordered includes ending with \r + const model = createTextModel('#include "a.h"\r\n#include "c.h"\r\n#include "b.h"\r\n'); + model.setEOL(EndOfLineSequence.CRLF); + + assert.strictEqual(model.getEOL(), '\r\n'); + assert.strictEqual(model.getLineCount(), 4); + assert.strictEqual(model.getLineContent(1), '#include "a.h"'); + assert.strictEqual(model.getLineContent(2), '#include "c.h"'); + assert.strictEqual(model.getLineContent(3), '#include "b.h"'); + + // Edit: replace range (1,1)-(3,16) with text ending in \r + // Range covers: #include "a.h"\r\n#include "c.h"\r\n#include "b.h" + // Note: line 3 col 16 is after the last char "h" but before the \r\n + model.applyEdits([ + { + range: new Range(1, 1, 3, 16), + text: '#include "a.h"\r\n#include "b.h"\r\n#include "c.h"\r' + } + ]); + + // The trailing \r should be stripped because the next char after range is \n + assert.strictEqual(model.getLineCount(), 4); + assert.strictEqual(model.getLineContent(1), '#include "a.h"'); + assert.strictEqual(model.getLineContent(2), '#include "b.h"'); + assert.strictEqual(model.getLineContent(3), '#include "c.h"'); + assert.strictEqual(model.getLineContent(4), ''); + + model.dispose(); + }); +}); diff --git a/src/vs/editor/test/common/model/model.line.test.ts b/src/vs/editor/test/common/model/model.line.test.ts index 446e7acaf42..b28e6ea067e 100644 --- a/src/vs/editor/test/common/model/model.line.test.ts +++ b/src/vs/editor/test/common/model/model.line.test.ts @@ -122,7 +122,7 @@ class ManualTokenizationSupport implements ITokenizationSupport { tokenizeEncoded(line: string, hasEOL: boolean, state: IState): EncodedTokenizationResult { const s = state as LineState; - return new EncodedTokenizationResult(this.tokens.get(s.lineNumber)!, new LineState(s.lineNumber + 1)); + return new EncodedTokenizationResult(this.tokens.get(s.lineNumber)!, [], new LineState(s.lineNumber + 1)); } /** diff --git a/src/vs/editor/test/common/model/model.modes.test.ts b/src/vs/editor/test/common/model/model.modes.test.ts index a7ad097c019..d65eb62519c 100644 --- a/src/vs/editor/test/common/model/model.modes.test.ts +++ b/src/vs/editor/test/common/model/model.modes.test.ts @@ -31,7 +31,7 @@ suite('Editor Model - Model Modes 1', () => { tokenize: undefined!, tokenizeEncoded: (line: string, hasEOL: boolean, state: languages.IState): languages.EncodedTokenizationResult => { calledFor.push(line.charAt(0)); - return new languages.EncodedTokenizationResult(new Uint32Array(0), state); + return new languages.EncodedTokenizationResult(new Uint32Array(0), [], state); } }; @@ -188,7 +188,7 @@ suite('Editor Model - Model Modes 2', () => { tokenizeEncoded: (line: string, hasEOL: boolean, state: languages.IState): languages.EncodedTokenizationResult => { calledFor.push(line); (state).prevLineContent = line; - return new languages.EncodedTokenizationResult(new Uint32Array(0), state); + return new languages.EncodedTokenizationResult(new Uint32Array(0), [], state); } }; diff --git a/src/vs/editor/test/common/model/model.test.ts b/src/vs/editor/test/common/model/model.test.ts index e18b8438525..e6544a65608 100644 --- a/src/vs/editor/test/common/model/model.test.ts +++ b/src/vs/editor/test/common/model/model.test.ts @@ -428,7 +428,7 @@ suite('Editor Model - Words', () => { for (let i = 0; i < tokens.length; i++) { tokens[i] = tokensArr[i]; } - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } })); } diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index c6c281bfe2b..286389ef0e9 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -708,6 +708,74 @@ suite('Editor Model - TextModel', () => { ]); }); + test('issue #65668: YAML file indented with 2 spaces', () => { + // Full YAML file from the issue - should detect as 2 spaces + assertGuess(true, 2, [ + 'version: 2', + '', + 'jobs:', + ' build:', + ' docker:', + ' - circleci/golang:1.11', + '', + ' environment:', + ' TEST_RESULTS: /tmp/test-results', + '', + ' steps:', + ' - checkout', + ' - run: mkdir -p $TEST_RESULTS', + '', + ' - restore_cache:', + ' keys:', + ' - v1-pkg-cache', + '', + ' - run:', + ' name: dep ensure', + ' command: dep ensure -v', + '', + ' - run:', + ' name: Run unit tests', + ' command: |', + ' trap "go-junit-report <${TEST_RESULTS}/go-test.out > ${TEST_RESULTS}/go-test-report.xml" EXIT', + ' go test -v ./... | tee ${TEST_RESULTS}/go-test.out', + '', + ' - run:', + ' name: Build', + ' command: go build -v', + '', + ' - save_cache:', + ' key: v1-pkg-cache', + ' paths:', + ' - "/go/pkg"', + '', + ' - store_artifacts:', + ' path: /tmp/test-results', + ' destination: raw-test-output', + '', + ' - store_test_results:', + ' path: /tmp/test-results', + ]); + }); + + test('issue #249040: 4-space indent should win over 2-space when predominant', () => { + // File with mostly 4-space indents but some 2-space indents should detect as 4 spaces + assertGuess(true, 4, [ + 'function foo() {', + ' let a = 1;', + ' let b = 2;', + ' if (true) {', + ' console.log(a);', + ' console.log(b);', + ' }', + ' const obj = {', + ' x: 1,', // 2-space indent here + ' y: 2', // 2-space indent here + ' };', + ' return obj;', + '}', + ]); + }); + test('validatePosition', () => { const m = createTextModel('line one\nline two'); diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index f5118870ca0..bcf65679059 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -390,7 +390,7 @@ suite('TextModelWithTokens 2', () => { 12, otherMetadata1, 13, otherMetadata1, ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case ' return

{true}

;': { const tokens = new Uint32Array([ @@ -408,13 +408,13 @@ suite('TextModelWithTokens 2', () => { 21, otherMetadata2, 22, otherMetadata2, ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case '}': { const tokens = new Uint32Array([ 0, otherMetadata1 ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } } throw new Error(`Unexpected`); @@ -487,7 +487,7 @@ suite('TextModelWithTokens 2', () => { const tokens = new Uint32Array([ 0, otherMetadata ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case ' console.log(`${100}`);': { const tokens = new Uint32Array([ @@ -497,13 +497,13 @@ suite('TextModelWithTokens 2', () => { 22, stringMetadata, 24, otherMetadata, ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } case '}': { const tokens = new Uint32Array([ 0, otherMetadata ]); - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } } throw new Error(`Unexpected`); @@ -585,7 +585,7 @@ suite('TextModelWithTokens regression tests', () => { tokens[1] = ( myId << MetadataConsts.FOREGROUND_OFFSET ) >>> 0; - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } }; @@ -694,7 +694,7 @@ suite('TextModelWithTokens regression tests', () => { tokens[1] = ( encodedInnerMode << MetadataConsts.LANGUAGEID_OFFSET ) >>> 0; - return new EncodedTokenizationResult(tokens, state); + return new EncodedTokenizationResult(tokens, [], state); } }; diff --git a/src/vs/editor/test/common/modes/supports/indentationRules.ts b/src/vs/editor/test/common/modes/supports/indentationRules.ts index 0967de48bff..80fec490b9a 100644 --- a/src/vs/editor/test/common/modes/supports/indentationRules.ts +++ b/src/vs/editor/test/common/modes/supports/indentationRules.ts @@ -40,3 +40,11 @@ export const luaIndentationRules = { decreaseIndentPattern: /^\s*((\b(elseif|else|end|until)\b)|(\})|(\)))/, increaseIndentPattern: /^((?!(\-\-)).)*((\b(else|function|then|do|repeat)\b((?!\b(end|until)\b).)*)|(\{\s*))$/, }; + +export const vbIndentationRules = { + // Decrease indent when line starts with End , Else, ElseIf, Case, Catch, Finally, Loop, Next, Wend, Until + decreaseIndentPattern: /^\s*((End\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Else|ElseIf|Case|Catch|Finally|Loop|Next|Wend|Until)\b/i, + // Increase indent after lines with block-starting keywords (Sub, Function, Class, Module, If...Then, etc.) + // Both alternatives are anchored to start of line with ^\s* + increaseIndentPattern: /^\s*((If|ElseIf).*Then(?!.*End\s+If)\s*(('|REM).*)?|(Else|While|For|Do|Select\s+Case|Case|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Try|Catch|Finally|SyncLock|Using|Property|Get|Set|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b(?!.*\bEnd\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator)\b).*(('|REM).*)?)$/i, +}; diff --git a/src/vs/editor/test/common/modes/supports/onEnterRules.ts b/src/vs/editor/test/common/modes/supports/onEnterRules.ts index b3cb35e27d5..8b52dd28c01 100644 --- a/src/vs/editor/test/common/modes/supports/onEnterRules.ts +++ b/src/vs/editor/test/common/modes/supports/onEnterRules.ts @@ -132,6 +132,16 @@ export const htmlOnEnterRules = [ } ]; +export const vbOnEnterRules = [ + // Prevent indent after End statements and block terminators (but NOT ElseIf...Then or Else which should indent) + { + beforeText: /^\s*((End\s+(If|Sub|Function|Class|Module|Enum|Structure|Interface|Namespace|With|Select|Try|While|For|Property|Get|Set|SyncLock|Using|AddHandler|RaiseEvent|RemoveHandler|Event|Operator))|Loop|Next|Wend|Until)\b.*$/i, + action: { + indentAction: IndentAction.None + } + } +]; + /* export enum IndentAction { None = 0, diff --git a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts index 7c0ab5d725b..59e562aec8c 100644 --- a/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts +++ b/src/vs/editor/test/common/modes/textToHtmlTokenizer.test.ts @@ -393,7 +393,7 @@ class Mode extends Disposable { for (let i = 0; i < tokens.length; i++) { tokens[i] = tokensArr[i]; } - return new EncodedTokenizationResult(tokens, null!); + return new EncodedTokenizationResult(tokens, [], null!); } })); } diff --git a/src/vs/editor/test/common/services/languagesAssociations.test.ts b/src/vs/editor/test/common/services/languagesAssociations.test.ts index 891260d0fb5..f32a33d4a89 100644 --- a/src/vs/editor/test/common/services/languagesAssociations.test.ts +++ b/src/vs/editor/test/common/services/languagesAssociations.test.ts @@ -129,4 +129,26 @@ suite('LanguagesAssociations', () => { assert.deepStrictEqual(getMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); + + test('Shebang detection for TypeScript runtimes', () => { + registerPlatformLanguageAssociation({ id: 'typescript', mime: 'text/typescript', firstline: /^#!.*\b(deno|bun|ts-node)\b/ }); + + // Deno shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env deno'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S deno -A'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/deno'), ['text/typescript', 'text/plain']); + + // Bun shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env bun'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S bun run'), ['text/typescript', 'text/plain']); + + // ts-node shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env ts-node'), ['text/typescript', 'text/plain']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env -S ts-node --esm'), ['text/typescript', 'text/plain']); + + // Should NOT match other shebangs + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env node'), ['application/unknown']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/usr/bin/env python'), ['application/unknown']); + assert.deepStrictEqual(getMimeTypes(URI.file('script'), '#!/bin/bash'), ['application/unknown']); + }); }); diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__11485__Visible_whitespace_conflicts_with_before_decorator_attachment.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-11485.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__11485__Visible_whitespace_conflicts_with_before_decorator_attachment.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-11485.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__11485__Visible_whitespace_conflicts_with_before_decorator_attachment.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-11485.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__11485__Visible_whitespace_conflicts_with_before_decorator_attachment.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-11485.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__116939__Important_control_characters_aren_t_rendered.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-116939.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__116939__Important_control_characters_aren_t_rendered.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-116939.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__116939__Important_control_characters_aren_t_rendered.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-116939.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__116939__Important_control_characters_aren_t_rendered.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-116939.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__118759__enable_multiple_text_editor_decorations_in_empty_lines.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-118759.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__118759__enable_multiple_text_editor_decorations_in_empty_lines.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-118759.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__118759__enable_multiple_text_editor_decorations_in_empty_lines.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-118759.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__118759__enable_multiple_text_editor_decorations_in_empty_lines.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-118759.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__119416__Delete_Control_Character__U_007F_____127___displayed_as_space.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-119416.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__119416__Delete_Control_Character__U_007F_____127___displayed_as_space.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-119416.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__119416__Delete_Control_Character__U_007F_____127___displayed_as_space.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-119416.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__119416__Delete_Control_Character__U_007F_____127___displayed_as_space.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-119416.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__124038__Multiple_end-of-line_text_decorations_get_merged.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-124038.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__124038__Multiple_end-of-line_text_decorations_get_merged.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-124038.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__124038__Multiple_end-of-line_text_decorations_get_merged.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-124038.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__124038__Multiple_end-of-line_text_decorations_get_merged.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-124038.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__136622__Inline_decorations_are_not_rendering_on_non-ASCII_lines_when_renderControlCharacters_is_on.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-136622.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__136622__Inline_decorations_are_not_rendering_on_non-ASCII_lines_when_renderControlCharacters_is_on.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-136622.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__136622__Inline_decorations_are_not_rendering_on_non-ASCII_lines_when_renderControlCharacters_is_on.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-136622.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__136622__Inline_decorations_are_not_rendering_on_non-ASCII_lines_when_renderControlCharacters_is_on.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-136622.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__18616__Inline_decorations_ending_at_the_text_length_are_no_longer_rendered.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-18616.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__18616__Inline_decorations_ending_at_the_text_length_are_no_longer_rendered.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-18616.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__18616__Inline_decorations_ending_at_the_text_length_are_no_longer_rendered.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-18616.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__18616__Inline_decorations_ending_at_the_text_length_are_no_longer_rendered.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-18616.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__19207__Link_in_Monokai_is_not_rendered_correctly.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-19207.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__19207__Link_in_Monokai_is_not_rendered_correctly.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-19207.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__19207__Link_in_Monokai_is_not_rendered_correctly.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-19207.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__19207__Link_in_Monokai_is_not_rendered_correctly.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-19207.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__COMBINING_ACUTE_ACCENT__U_0301_.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-1.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__COMBINING_ACUTE_ACCENT__U_0301_.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-1.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__COMBINING_ACUTE_ACCENT__U_0301_.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-1.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__COMBINING_ACUTE_ACCENT__U_0301_.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-1.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__Partially_Broken_Complex_Script_Rendering_of_Tamil.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-2.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__Partially_Broken_Complex_Script_Rendering_of_Tamil.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-2.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__Partially_Broken_Complex_Script_Rendering_of_Tamil.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-2.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22352__Partially_Broken_Complex_Script_Rendering_of_Tamil.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22352-2.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-1.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-1.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-1.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-1.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs__render_whitespace_.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-2.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs__render_whitespace_.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-2.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs__render_whitespace_.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-2.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__22832__Consider_fullwidth_characters_when_rendering_tabs__render_whitespace_.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-22832-2.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__30133__Empty_lines_don_t_render_inline_decorations.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-30133.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__30133__Empty_lines_don_t_render_inline_decorations.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-30133.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__30133__Empty_lines_don_t_render_inline_decorations.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-30133.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__30133__Empty_lines_don_t_render_inline_decorations.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-30133.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__32436__Non-monospace_font___visible_whitespace___After_decorator_causes_line_to__jump_.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-32436.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__32436__Non-monospace_font___visible_whitespace___After_decorator_causes_line_to__jump_.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-32436.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__32436__Non-monospace_font___visible_whitespace___After_decorator_causes_line_to__jump_.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-32436.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__32436__Non-monospace_font___visible_whitespace___After_decorator_causes_line_to__jump_.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-32436.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-1.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-1.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-1.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-1.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations_-_not_possible.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-2.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations_-_not_possible.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-2.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations_-_not_possible.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-2.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__33525__Long_line_with_ligatures_takes_a_long_time_to_paint_decorations_-_not_possible.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-33525-2.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37208__Collapsing_bullet_point_containing_emoji_in_Markdown_document_results_in______character.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37208.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37208__Collapsing_bullet_point_containing_emoji_in_Markdown_document_results_in______character.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37208.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37208__Collapsing_bullet_point_containing_emoji_in_Markdown_document_results_in______character.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37208.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37208__Collapsing_bullet_point_containing_emoji_in_Markdown_document_results_in______character.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37208.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37401__40127__Allow_both_before_and_after_decorations_on_empty_line.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37401.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37401__40127__Allow_both_before_and_after_decorations_on_empty_line.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37401.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37401__40127__Allow_both_before_and_after_decorations_on_empty_line.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37401.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__37401__40127__Allow_both_before_and_after_decorations_on_empty_line.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-37401.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38123__editor_renderWhitespace___boundary__renders_whitespace_at_line_wrap_point_when_line_is_wrapped.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38123.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38123__editor_renderWhitespace___boundary__renders_whitespace_at_line_wrap_point_when_line_is_wrapped.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38123.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38123__editor_renderWhitespace___boundary__renders_whitespace_at_line_wrap_point_when_line_is_wrapped.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38123.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38123__editor_renderWhitespace___boundary__renders_whitespace_at_line_wrap_point_when_line_is_wrapped.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38123.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38935__GitLens_end-of-line_blame_no_longer_rendering.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38935.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38935__GitLens_end-of-line_blame_no_longer_rendering.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38935.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38935__GitLens_end-of-line_blame_no_longer_rendering.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38935.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__38935__GitLens_end-of-line_blame_no_longer_rendering.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-38935.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__42700__Hindi_characters_are_not_being_rendered_properly.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-42700.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__42700__Hindi_characters_are_not_being_rendered_properly.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-42700.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__42700__Hindi_characters_are_not_being_rendered_properly.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-42700.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__42700__Hindi_characters_are_not_being_rendered_properly.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-42700.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__91936__Semantic_token_color_highlighting_fails_on_line_with_selected_text.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-91936.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__91936__Semantic_token_color_highlighting_fails_on_line_with_selected_text.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-91936.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__91936__Semantic_token_color_highlighting_fails_on_line_with_selected_text.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-91936.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_issue__91936__Semantic_token_color_highlighting_fails_on_line_with_selected_text.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_issue-91936.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_simple.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_simple.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_simple.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_simple.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple_two_tokens.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_two-tokens.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple_two_tokens.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_two-tokens.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple_two_tokens.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_two-tokens.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_simple_two_tokens.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_two-tokens.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_can_handle_unsorted_inline_decorations.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_unsorted-deco.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_can_handle_unsorted_inline_decorations.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_unsorted-deco.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_can_handle_unsorted_inline_decorations.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_unsorted-deco.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_can_handle_unsorted_inline_decorations.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_unsorted-deco.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_2_leading_tabs.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-2-tabs.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_2_leading_tabs.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-2-tabs.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_2_leading_tabs.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-2-tabs.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_2_leading_tabs.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-2-tabs.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_4_leading_spaces.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-4-leading.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_4_leading_spaces.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-4-leading.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_4_leading_spaces.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-4-leading.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_4_leading_spaces.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-4-leading.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_8_leading_spaces.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-8-leading.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_8_leading_spaces.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-8-leading.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_8_leading_spaces.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-8-leading.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_8_leading_spaces.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-8-leading.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_all_in_middle.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-all-middle.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_all_in_middle.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-all-middle.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_all_in_middle.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-all-middle.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_all_in_middle.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-all-middle.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_skips_faux_indent.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-faux-indent.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_skips_faux_indent.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-faux-indent.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_skips_faux_indent.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-faux-indent.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_skips_faux_indent.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-faux-indent.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_in_middle_but_not_for_one_space.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-middle.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_in_middle_but_not_for_one_space.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-middle.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_in_middle_but_not_for_one_space.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-middle.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_in_middle_but_not_for_one_space.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-middle.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_mixed_leading_spaces_and_tabs.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-mixed.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_mixed_leading_spaces_and_tabs.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-mixed.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_mixed_leading_spaces_and_tabs.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-mixed.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_-_mixed_leading_spaces_and_tabs.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-mixed.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_does_not_emit_width_for_monospace_fonts.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-monospace.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_does_not_emit_width_for_monospace_fonts.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-monospace.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_does_not_emit_width_for_monospace_fonts.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-monospace.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_does_not_emit_width_for_monospace_fonts.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-monospace.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selections_next_to_each_other.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-adjacent.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selections_next_to_each_other.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-adjacent.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selections_next_to_each_other.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-adjacent.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selections_next_to_each_other.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-adjacent.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple__initially_unsorted_selections.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-multiple.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple__initially_unsorted_selections.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-multiple.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple__initially_unsorted_selections.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-multiple.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple__initially_unsorted_selections.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-multiple.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_no_selections.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-none.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_no_selections.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-none.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_no_selections.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-none.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_no_selections.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-none.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selection_spanning_part_of_whitespace.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-partial.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selection_spanning_part_of_whitespace.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-partial.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selection_spanning_part_of_whitespace.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-partial.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_selection_spanning_part_of_whitespace.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-partial.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple_selections.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-unsorted.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple_selections.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-unsorted.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple_selections.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-unsorted.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_multiple_selections.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-unsorted.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_whole_line_selection.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-whole.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_whole_line_selection.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-whole.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_whole_line_selection.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-whole.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_selection_with_whole_line_selection.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-sel-whole.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_8_leading_and_8_trailing_whitespaces.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-8-8.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_8_leading_and_8_trailing_whitespaces.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-8-8.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_8_leading_and_8_trailing_whitespaces.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-8-8.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_8_leading_and_8_trailing_whitespaces.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-8-8.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_without_trailing_whitespace.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-no-trail.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_without_trailing_whitespace.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-no-trail.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_without_trailing_whitespace.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-no-trail.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_without_trailing_whitespace.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-no-trail.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_line_containing_only_whitespaces.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-only.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_line_containing_only_whitespaces.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-only.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_line_containing_only_whitespaces.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-only.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_line_containing_only_whitespaces.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-only.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_trailing_whitespace.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-with-trail.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_trailing_whitespace.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-with-trail.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_trailing_whitespace.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-with-trail.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_2_createLineParts_render_whitespace_for_trailing_with_leading__inner__and_trailing_whitespace.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine2_ws-trail-with-trail.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-137036.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-137036.0.html new file mode 100644 index 00000000000..a62351f3c23 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-137036.0.html @@ -0,0 +1 @@ +<option value="العربية">العربية</option> \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__137036__Issue_in_RTL_languages_in_recent_versions.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-137036.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__137036__Issue_in_RTL_languages_in_recent_versions.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-137036.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__19673__Monokai_Theme_bad-highlighting_in_line_wrap.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-19673.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__19673__Monokai_Theme_bad-highlighting_in_line_wrap.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-19673.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__19673__Monokai_Theme_bad-highlighting_in_line_wrap.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-19673.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__19673__Monokai_Theme_bad-highlighting_in_line_wrap.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-19673.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__20624__Unaligned_surrogate_pairs_are_corrupted_at_multiples_of_50_columns.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-20624.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__20624__Unaligned_surrogate_pairs_are_corrupted_at_multiples_of_50_columns.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-20624.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_1.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-1.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_1.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-1.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_1.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-1.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_1.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-1.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_2.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-2.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_2.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-2.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_2.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-2.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__2255__Weird_line_rendering_part_2.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-2255-2.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-260239.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-260239.0.html new file mode 100644 index 00000000000..644e719a9d7 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-260239.0.html @@ -0,0 +1 @@ +<p class="myclass" title="العربي">نشاط التدويل!</p> \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__260239__HTML_containing_bidirectional_text_is_rendered_incorrectly.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-260239.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__260239__HTML_containing_bidirectional_text_is_rendered_incorrectly.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-260239.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.0.html new file mode 100644 index 00000000000..6167d9bc4a0 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.0.html @@ -0,0 +1 @@ +test.com##a:-abp-contains(إ) \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.1.snap new file mode 100644 index 00000000000..24a33db70f1 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-274604.1.snap @@ -0,0 +1,31 @@ +[ + [ 0, 0, 0 ], + [ 0, 1, 1 ], + [ 0, 2, 2 ], + [ 0, 3, 3 ], + [ 0, 4, 4 ], + [ 0, 5, 5 ], + [ 0, 6, 6 ], + [ 0, 7, 7 ], + [ 0, 8, 8 ], + [ 0, 9, 9 ], + [ 0, 10, 10 ], + [ 0, 11, 11 ], + [ 0, 12, 12 ], + [ 0, 13, 13 ], + [ 0, 14, 14 ], + [ 0, 15, 15 ], + [ 0, 16, 16 ], + [ 0, 17, 17 ], + [ 0, 18, 18 ], + [ 0, 19, 19 ], + [ 0, 20, 20 ], + [ 0, 21, 21 ], + [ 0, 22, 22 ], + [ 0, 23, 23 ], + [ 0, 24, 24 ], + [ 0, 25, 25 ], + [ 0, 26, 26 ], + [ 0, 27, 27 ], + [ 0, 28, 28 ] +] \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.0.html new file mode 100644 index 00000000000..c3b55361233 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.0.html @@ -0,0 +1 @@ +نام کاربر${user.firstName} \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.1.snap new file mode 100644 index 00000000000..447c8398d29 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-277693.1.snap @@ -0,0 +1,31 @@ +[ + [ 0, 0, 0 ], + [ 0, 1, 1 ], + [ 0, 2, 2 ], + [ 0, 3, 3 ], + [ 0, 4, 4 ], + [ 0, 5, 5 ], + [ 0, 6, 6 ], + [ 0, 7, 7 ], + [ 0, 8, 8 ], + [ 1, 0, 9 ], + [ 1, 1, 10 ], + [ 2, 0, 11 ], + [ 2, 1, 12 ], + [ 3, 0, 13 ], + [ 3, 1, 14 ], + [ 3, 2, 15 ], + [ 3, 3, 16 ], + [ 4, 0, 17 ], + [ 5, 0, 18 ], + [ 5, 1, 19 ], + [ 5, 2, 20 ], + [ 5, 3, 21 ], + [ 5, 4, 22 ], + [ 5, 5, 23 ], + [ 5, 6, 24 ], + [ 5, 7, 25 ], + [ 5, 8, 26 ], + [ 6, 0, 27 ], + [ 6, 1, 28 ] +] \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-6885-rtl.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-6885-rtl.0.html new file mode 100644 index 00000000000..81ea223a52c --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-6885-rtl.0.html @@ -0,0 +1 @@ +את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר. \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__91178__after_decoration_type_shown_before_cursor.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-91178.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__91178__after_decoration_type_shown_before_cursor.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-91178.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__91178__after_decoration_type_shown_before_cursor.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-91178.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__91178__after_decoration_type_shown_before_cursor.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-91178.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__95685__Uses_unicode_replacement_character_for_Paragraph_Separator.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-95685.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__95685__Uses_unicode_replacement_character_for_Paragraph_Separator.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-95685.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__95685__Uses_unicode_replacement_character_for_Paragraph_Separator.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-95685.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__95685__Uses_unicode_replacement_character_for_Paragraph_Separator.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-95685.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-99589.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-99589.0.html new file mode 100644 index 00000000000..db880d5d98c --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-99589.0.html @@ -0,0 +1 @@ +·‌·‌·‌·‌["🖨️ چاپ فاکتور","🎨 تنظیمات"] \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__99589__Rendering_whitespace_influences_bidi_layout.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-99589.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__99589__Rendering_whitespace_influences_bidi_layout.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_issue-99589.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_monaco-280.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_monaco-280.0.html new file mode 100644 index 00000000000..e8031a89415 --- /dev/null +++ b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_monaco-280.0.html @@ -0,0 +1 @@ +var קודמות = "מיותר קודמות צ'ט של, אם לשון העברית שינויים ויש, אם"; \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue_microsoft_monaco-editor_280__Improved_source_code_rendering_for_RTL_languages.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_monaco-280.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue_microsoft_monaco-editor_280__Improved_source_code_rendering_for_RTL_languages.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_monaco-280.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_overflow.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_overflow.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_overflow.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_overflow.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_overflow.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_overflow.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_overflow.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_overflow.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_typical_line.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_typical.0.html similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_typical_line.0.html rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_typical.0.html diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_typical_line.1.snap b/src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_typical.1.snap similarity index 100% rename from src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_typical_line.1.snap rename to src/vs/editor/test/common/viewLayout/__snapshots__/renderViewLine_typical.1.snap diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__137036__Issue_in_RTL_languages_in_recent_versions.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__137036__Issue_in_RTL_languages_in_recent_versions.0.html deleted file mode 100644 index 75e9cd7c670..00000000000 --- a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__137036__Issue_in_RTL_languages_in_recent_versions.0.html +++ /dev/null @@ -1 +0,0 @@ -<option value="العربية">العربية</option> \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__260239__HTML_containing_bidirectional_text_is_rendered_incorrectly.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__260239__HTML_containing_bidirectional_text_is_rendered_incorrectly.0.html deleted file mode 100644 index 1f4c55eedc2..00000000000 --- a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__260239__HTML_containing_bidirectional_text_is_rendered_incorrectly.0.html +++ /dev/null @@ -1 +0,0 @@ -<p class="myclass" title="العربي">نشاط التدويل!</p> \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__6885__Does_not_split_large_tokens_in_RTL_text.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__6885__Does_not_split_large_tokens_in_RTL_text.0.html deleted file mode 100644 index eee164124b1..00000000000 --- a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__6885__Does_not_split_large_tokens_in_RTL_text.0.html +++ /dev/null @@ -1 +0,0 @@ -את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר. \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__99589__Rendering_whitespace_influences_bidi_layout.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__99589__Rendering_whitespace_influences_bidi_layout.0.html deleted file mode 100644 index 4258b886a0e..00000000000 --- a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue__99589__Rendering_whitespace_influences_bidi_layout.0.html +++ /dev/null @@ -1 +0,0 @@ -·‌·‌·‌·‌["🖨️ چاپ فاکتور","🎨 تنظیمات"] \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue_microsoft_monaco-editor_280__Improved_source_code_rendering_for_RTL_languages.0.html b/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue_microsoft_monaco-editor_280__Improved_source_code_rendering_for_RTL_languages.0.html deleted file mode 100644 index 08fb00c6a51..00000000000 --- a/src/vs/editor/test/common/viewLayout/__snapshots__/viewLineRenderer_renderLine_issue_microsoft_monaco-editor_280__Improved_source_code_rendering_for_RTL_languages.0.html +++ /dev/null @@ -1 +0,0 @@ -var קודמות = "מיותר קודמות צ'ט של, אם לשון העברית שינויים ויש, אם"; \ No newline at end of file diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index dd786b19db5..475ba0463ff 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -115,7 +115,7 @@ function createRenderLineInput(opts: IRelaxedRenderLineInputOptions): RenderLine ); } -suite('viewLineRenderer.renderLine', () => { +suite('renderViewLine', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -191,6 +191,7 @@ suite('viewLineRenderer.renderLine', () => { assertParts('xyz', 4, [createPart(2, 1), createPart(3, 2)], 'xyz', [[0, [0, 0]], [1, [0, 1]], [2, [1, 0]], [3, [1, 1]]]); }); + // overflow test('overflow', async () => { const _actual = renderViewLine(createRenderLineInput({ lineContent: 'Hello world!', @@ -217,7 +218,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('typical line', async () => { + // typical line + test('typical', async () => { const lineContent = '\t export class Game { // http://test.com '; const lineTokens = createViewLineTokens([ createPart(5, 1), @@ -244,7 +246,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #2255: Weird line rendering part 1', async () => { + // issue #2255: Weird line rendering part 1 + test('issue-2255-1', async () => { const lineContent = '\t\t\tcursorStyle:\t\t\t\t\t\t(prevOpts.cursorStyle !== newOpts.cursorStyle),'; const lineTokens = createViewLineTokens([ createPart(3, 1), // 3 chars @@ -268,7 +271,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #2255: Weird line rendering part 2', async () => { + // issue #2255: Weird line rendering part 2 + test('issue-2255-2', async () => { const lineContent = ' \t\t\tcursorStyle:\t\t\t\t\t\t(prevOpts.cursorStyle !== newOpts.cursorStyle),'; const lineTokens = createViewLineTokens([ @@ -293,7 +297,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #91178: after decoration type shown before cursor', async () => { + // issue #91178: after decoration type shown before cursor + test('issue-91178', async () => { const lineContent = '//just a comment'; const lineTokens = createViewLineTokens([ createPart(16, 1) @@ -314,7 +319,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue microsoft/monaco-editor#280: Improved source code rendering for RTL languages', async () => { + // issue microsoft/monaco-editor#280: Improved source code rendering for RTL languages + test('monaco-280', async () => { const lineContent = 'var קודמות = \"מיותר קודמות צ\'ט של, אם לשון העברית שינויים ויש, אם\";'; const lineTokens = createViewLineTokens([ createPart(3, 6), @@ -334,7 +340,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #137036: Issue in RTL languages in recent versions', async () => { + // issue #137036: Issue in RTL languages in recent versions + test('issue-137036', async () => { const lineContent = ''; const lineTokens = createViewLineTokens([ createPart(1, 2), @@ -361,7 +368,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #99589: Rendering whitespace influences bidi layout', async () => { + // issue #99589: Rendering whitespace influences bidi layout + test('issue-99589', async () => { const lineContent = ' [\"🖨️ چاپ فاکتور\",\"🎨 تنظیمات\"]'; const lineTokens = createViewLineTokens([ createPart(5, 2), @@ -384,7 +392,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #260239: HTML containing bidirectional text is rendered incorrectly', async () => { + // issue #260239: HTML containing bidirectional text is rendered incorrectly + test('issue-260239', async () => { // Simulating HTML like:

نشاط التدويل!

// The line contains both LTR (class="myclass") and RTL (title="العربي") attribute values const lineContent = '

نشاط التدويل!

'; @@ -439,7 +448,50 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #6885: Splits large tokens', async () => { + // issue #274604: Mixed LTR and RTL in a single token + test('issue-274604', async () => { + const lineContent = 'test.com##a:-abp-contains(إ)'; + const lineTokens = createViewLineTokens([ + createPart(lineContent.length, 1) + ]); + const actual = renderViewLine(createRenderLineInput({ + lineContent, + isBasicASCII: false, + containsRTL: true, + lineTokens + })); + + const inflated = inflateRenderLineOutput(actual); + await assertSnapshot(inflated.html.join(''), HTML_EXTENSION); + await assertSnapshot(inflated.mapping); + }); + + // issue #277693: Mixed LTR and RTL in a single token with template literal + test('issue-277693', async () => { + const lineContent = 'نام کاربر: ${user.firstName}'; + const lineTokens = createViewLineTokens([ + createPart(9, 1), // نام کاربر (RTL string content) + createPart(11, 1), // : (space) + createPart(13, 2), // ${ (template expression punctuation) + createPart(17, 3), // user (variable) + createPart(18, 4), // . (punctuation) + createPart(27, 3), // firstName (property) + createPart(28, 2), // } (template expression punctuation) + ]); + const actual = renderViewLine(createRenderLineInput({ + lineContent, + isBasicASCII: false, + containsRTL: true, + lineTokens + })); + + const inflated = inflateRenderLineOutput(actual); + await assertSnapshot(inflated.html.join(''), HTML_EXTENSION); + await assertSnapshot(inflated.mapping); + }); + + // issue #6885: Splits large tokens + test('issue-6885', async () => { // 1 1 1 // 1 2 3 4 5 6 7 8 9 0 1 2 // 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 @@ -526,7 +578,8 @@ suite('viewLineRenderer.renderLine', () => { } }); - test('issue #21476: Does not split large tokens when ligatures are on', async () => { + // issue #21476: Does not split large tokens when ligatures are on + test('issue-21476', async () => { // 1 1 1 // 1 2 3 4 5 6 7 8 9 0 1 2 // 1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234 @@ -556,7 +609,8 @@ suite('viewLineRenderer.renderLine', () => { } }); - test('issue #20624: Unaligned surrogate pairs are corrupted at multiples of 50 columns', async () => { + // issue #20624: Unaligned surrogate pairs are corrupted at multiples of 50 columns + test('issue-20624', async () => { const lineContent = 'a𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷𠮷'; const lineTokens = createViewLineTokens([createPart(lineContent.length, 1)]); const actual = renderViewLine(createRenderLineInput({ @@ -568,7 +622,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflateRenderLineOutput(actual).html.join(''), HTML_EXTENSION); }); - test('issue #6885: Does not split large tokens in RTL text', async () => { + // issue #6885: Does not split large tokens in RTL text + test('issue-6885-rtl', async () => { const lineContent = 'את גרמנית בהתייחסות שמו, שנתי המשפט אל חפש, אם כתב אחרים ולחבר. של התוכן אודות בויקיפדיה כלל, של עזרה כימיה היא. על עמוד יוצרים מיתולוגיה סדר, אם שכל שתפו לעברית שינויים, אם שאלות אנגלית עזה. שמות בקלות מה סדר.'; const lineTokens = createViewLineTokens([createPart(lineContent.length, 1)]); const actual = renderViewLine(createRenderLineInput({ @@ -581,7 +636,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(actual.html, HTML_EXTENSION); }); - test('issue #95685: Uses unicode replacement character for Paragraph Separator', async () => { + // issue #95685: Uses unicode replacement character for Paragraph Separator + test('issue-95685', async () => { const lineContent = 'var ftext = [\u2029"Und", "dann", "eines"];'; const lineTokens = createViewLineTokens([createPart(lineContent.length, 1)]); const actual = renderViewLine(createRenderLineInput({ @@ -594,7 +650,8 @@ suite('viewLineRenderer.renderLine', () => { await assertSnapshot(inflated.mapping); }); - test('issue #19673: Monokai Theme bad-highlighting in line wrap', async () => { + // issue #19673: Monokai Theme bad-highlighting in line wrap + test('issue-19673', async () => { const lineContent = ' MongoCallback): void {'; const lineTokens = createViewLineTokens([ createPart(17, 1), @@ -648,7 +705,7 @@ function assertCharacterMapping3(actual: CharacterMapping, expectedInfo: Charact assert.strictEqual(actual.length, expectedInfo.length, `length mismatch`); } -suite('viewLineRenderer.renderLine 2', () => { +suite('renderViewLine2', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -664,7 +721,8 @@ suite('viewLineRenderer.renderLine 2', () => { return inflateRenderLineOutput(actual); } - test('issue #18616: Inline decorations ending at the text length are no longer rendered', async () => { + // issue #18616: Inline decorations ending at the text length are no longer rendered + test('issue-18616', async () => { const lineContent = 'https://microsoft.com'; const actual = renderViewLine(createRenderLineInput({ lineContent, @@ -677,7 +735,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #19207: Link in Monokai is not rendered correctly', async () => { + // issue #19207: Link in Monokai is not rendered correctly + test('issue-19207', async () => { const lineContent = '\'let url = `http://***/_api/web/lists/GetByTitle(\\\'Teambuildingaanvragen\\\')/items`;\''; const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -699,7 +758,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('createLineParts simple', async () => { + // createLineParts simple + test('simple', async () => { const actual = testCreateLineParts( false, 'Hello world!', @@ -714,7 +774,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts simple two tokens', async () => { + // createLineParts simple two tokens + test('two-tokens', async () => { const actual = testCreateLineParts( false, 'Hello world!', @@ -730,7 +791,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace - 4 leading spaces', async () => { + // createLineParts render whitespace - 4 leading spaces + test('ws-4-leading', async () => { const actual = testCreateLineParts( false, ' Hello world! ', @@ -747,7 +809,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace - 8 leading spaces', async () => { + // createLineParts render whitespace - 8 leading spaces + test('ws-8-leading', async () => { const actual = testCreateLineParts( false, ' Hello world! ', @@ -764,7 +827,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace - 2 leading tabs', async () => { + // createLineParts render whitespace - 2 leading tabs + test('ws-2-tabs', async () => { const actual = testCreateLineParts( false, '\t\tHello world!\t', @@ -781,7 +845,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace - mixed leading spaces and tabs', async () => { + // createLineParts render whitespace - mixed leading spaces and tabs + test('ws-mixed', async () => { const actual = testCreateLineParts( false, ' \t\t Hello world! \t \t \t ', @@ -798,7 +863,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace skips faux indent', async () => { + // createLineParts render whitespace skips faux indent + test('ws-faux-indent', async () => { const actual = testCreateLineParts( false, '\t\t Hello world! \t \t \t ', @@ -815,7 +881,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts does not emit width for monospace fonts', async () => { + // createLineParts does not emit width for monospace fonts + test('ws-monospace', async () => { const actual = testCreateLineParts( true, '\t\t Hello world! \t \t \t ', @@ -832,7 +899,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace in middle but not for one space', async () => { + // createLineParts render whitespace in middle but not for one space + test('ws-middle', async () => { const actual = testCreateLineParts( false, 'it it it it', @@ -849,7 +917,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for all in middle', async () => { + // createLineParts render whitespace for all in middle + test('ws-all-middle', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -866,7 +935,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with no selections', async () => { + // createLineParts render whitespace for selection with no selections + test('ws-sel-none', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -883,7 +953,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with whole line selection', async () => { + // createLineParts render whitespace for selection with whole line selection + test('ws-sel-whole', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -900,7 +971,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with selection spanning part of whitespace', async () => { + // createLineParts render whitespace for selection with selection spanning part of whitespace + test('ws-sel-partial', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -917,7 +989,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with multiple selections', async () => { + // createLineParts render whitespace for selection with multiple selections + test('ws-sel-multiple', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -934,7 +1007,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with multiple, initially unsorted selections', async () => { + // createLineParts render whitespace for selection with multiple, initially unsorted selections + test('ws-sel-unsorted', async () => { const actual = testCreateLineParts( false, ' Hello world!\t', @@ -951,7 +1025,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for selection with selections next to each other', async () => { + // createLineParts render whitespace for selection with selections next to each other + test('ws-sel-adjacent', async () => { const actual = testCreateLineParts( false, ' * S', @@ -966,7 +1041,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for trailing with leading, inner, and without trailing whitespace', async () => { + // createLineParts render whitespace for trailing with leading, inner, and without trailing whitespace + test('ws-trail-no-trail', async () => { const actual = testCreateLineParts( false, ' Hello world!', @@ -983,7 +1059,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for trailing with leading, inner, and trailing whitespace', async () => { + // createLineParts render whitespace for trailing with leading, inner, and trailing whitespace + test('ws-trail-with-trail', async () => { const actual = testCreateLineParts( false, ' Hello world! \t', @@ -1000,7 +1077,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for trailing with 8 leading and 8 trailing whitespaces', async () => { + // createLineParts render whitespace for trailing with 8 leading and 8 trailing whitespaces + test('ws-trail-8-8', async () => { const actual = testCreateLineParts( false, ' Hello world! ', @@ -1017,7 +1095,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts render whitespace for trailing with line containing only whitespaces', async () => { + // createLineParts render whitespace for trailing with line containing only whitespaces + test('ws-trail-only', async () => { const actual = testCreateLineParts( false, ' \t ', @@ -1033,7 +1112,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(actual.mapping); }); - test('createLineParts can handle unsorted inline decorations', async () => { + // createLineParts can handle unsorted inline decorations + test('unsorted-deco', async () => { const actual = renderViewLine(createRenderLineInput({ lineContent: 'Hello world', lineTokens: createViewLineTokens([createPart(11, 0)]), @@ -1055,7 +1135,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #11485: Visible whitespace conflicts with before decorator attachment', async () => { + // issue #11485: Visible whitespace conflicts with before decorator attachment + test('issue-11485', async () => { const lineContent = '\tbla'; @@ -1072,7 +1153,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump"', async () => { + // issue #32436: Non-monospace font + visible whitespace + After decorator causes line to "jump" + test('issue-32436', async () => { const lineContent = '\tbla'; @@ -1089,7 +1171,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #30133: Empty lines don\'t render inline decorations', async () => { + // issue #30133: Empty lines don't render inline decorations + test('issue-30133', async () => { const lineContent = ''; @@ -1106,7 +1189,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character', async () => { + // issue #37208: Collapsing bullet point containing emoji in Markdown document results in [??] character + test('issue-37208', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1123,7 +1207,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #37401 #40127: Allow both before and after decorations on empty line', async () => { + // issue #37401 #40127: Allow both before and after decorations on empty line + test('issue-37401', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1142,7 +1227,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #118759: enable multiple text editor decorations in empty lines', async () => { + // issue #118759: enable multiple text editor decorations in empty lines + test('issue-118759', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1163,7 +1249,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #38935: GitLens end-of-line blame no longer rendering', async () => { + // issue #38935: GitLens end-of-line blame no longer rendering + test('issue-38935', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1181,7 +1268,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #136622: Inline decorations are not rendering on non-ASCII lines when renderControlCharacters is on', async () => { + // issue #136622: Inline decorations are not rendering on non-ASCII lines when renderControlCharacters is on + test('issue-136622', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1201,7 +1289,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #22832: Consider fullwidth characters when rendering tabs', async () => { + // issue #22832: Consider fullwidth characters when rendering tabs + test('issue-22832-1', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1216,7 +1305,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #22832: Consider fullwidth characters when rendering tabs (render whitespace)', async () => { + // issue #22832: Consider fullwidth characters when rendering tabs (render whitespace) + test('issue-22832-2', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1232,7 +1322,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #22352: COMBINING ACUTE ACCENT (U+0301)', async () => { + // issue #22352: COMBINING ACUTE ACCENT (U+0301) + test('issue-22352-1', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1247,7 +1338,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #22352: Partially Broken Complex Script Rendering of Tamil', async () => { + // issue #22352: Partially Broken Complex Script Rendering of Tamil + test('issue-22352-2', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1262,7 +1354,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #42700: Hindi characters are not being rendered properly', async () => { + // issue #42700: Hindi characters are not being rendered properly + test('issue-42700', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, @@ -1277,7 +1370,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped', async () => { + // issue #38123: editor.renderWhitespace: "boundary" renders whitespace at line wrap point when line is wrapped + test('issue-38123', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, lineContent: 'This is a long line which never uses more than two spaces. ', @@ -1292,7 +1386,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #33525: Long line with ligatures takes a long time to paint decorations', async () => { + // issue #33525: Long line with ligatures takes a long time to paint decorations + test('issue-33525-1', async () => { const actual = renderViewLine(createRenderLineInput({ canUseHalfwidthRightwardsArrow: false, lineContent: 'append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to append data to', @@ -1306,7 +1401,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #33525: Long line with ligatures takes a long time to paint decorations - not possible', async () => { + // issue #33525: Long line with ligatures takes a long time to paint decorations - not possible + test('issue-33525-2', async () => { const actual = renderViewLine(createRenderLineInput({ canUseHalfwidthRightwardsArrow: false, lineContent: 'appenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatatoappenddatato', @@ -1320,7 +1416,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #91936: Semantic token color highlighting fails on line with selected text', async () => { + // issue #91936: Semantic token color highlighting fails on line with selected text + test('issue-91936', async () => { const actual = renderViewLine(createRenderLineInput({ lineContent: ' else if ($s = 08) then \'\\b\'', lineTokens: createViewLineTokens([ @@ -1355,7 +1452,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #119416: Delete Control Character (U+007F / ) displayed as space', async () => { + // issue #119416: Delete Control Character (U+007F / ) displayed as space + test('issue-119416', async () => { const actual = renderViewLine(createRenderLineInput({ canUseHalfwidthRightwardsArrow: false, lineContent: '[' + String.fromCharCode(127) + '] [' + String.fromCharCode(0) + ']', @@ -1370,7 +1468,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #116939: Important control characters aren\'t rendered', async () => { + // issue #116939: Important control characters aren't rendered + test('issue-116939', async () => { const actual = renderViewLine(createRenderLineInput({ canUseHalfwidthRightwardsArrow: false, lineContent: `transferBalance(5678,${String.fromCharCode(0x202E)}6776,4321${String.fromCharCode(0x202C)},"USD");`, @@ -1385,7 +1484,8 @@ suite('viewLineRenderer.renderLine 2', () => { await assertSnapshot(inflated.mapping); }); - test('issue #124038: Multiple end-of-line text decorations get merged', async () => { + // issue #124038: Multiple end-of-line text decorations get merged + test('issue-124038', async () => { const actual = renderViewLine(createRenderLineInput({ useMonospaceOptimizations: true, canUseHalfwidthRightwardsArrow: false, diff --git a/src/vs/loader.js b/src/vs/loader.js deleted file mode 100644 index 302fa3441d4..00000000000 --- a/src/vs/loader.js +++ /dev/null @@ -1,1891 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -/*--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - * Please make sure to make edits in the .ts file at https://github.com/microsoft/vscode-loader/ - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------- - *--------------------------------------------------------------------------------------------*/ -const _amdLoaderGlobal = this; -const _commonjsGlobal = typeof global === 'object' ? global : {}; -var AMDLoader; -(function (AMDLoader) { - AMDLoader.global = _amdLoaderGlobal; - class Environment { - get isWindows() { - this._detect(); - return this._isWindows; - } - get isNode() { - this._detect(); - return this._isNode; - } - get isElectronRenderer() { - this._detect(); - return this._isElectronRenderer; - } - get isWebWorker() { - this._detect(); - return this._isWebWorker; - } - get isElectronNodeIntegrationWebWorker() { - this._detect(); - return this._isElectronNodeIntegrationWebWorker; - } - constructor() { - this._detected = false; - this._isWindows = false; - this._isNode = false; - this._isElectronRenderer = false; - this._isWebWorker = false; - this._isElectronNodeIntegrationWebWorker = false; - } - _detect() { - if (this._detected) { - return; - } - this._detected = true; - this._isWindows = Environment._isWindows(); - this._isNode = (typeof module !== 'undefined' && !!module.exports); - this._isElectronRenderer = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'renderer'); - this._isWebWorker = (typeof AMDLoader.global.importScripts === 'function'); - this._isElectronNodeIntegrationWebWorker = this._isWebWorker && (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'worker'); - } - static _isWindows() { - if (typeof navigator !== 'undefined') { - if (navigator.userAgent && navigator.userAgent.indexOf('Windows') >= 0) { - return true; - } - } - if (typeof process !== 'undefined') { - return (process.platform === 'win32'); - } - return false; - } - } - AMDLoader.Environment = Environment; -})(AMDLoader || (AMDLoader = {})); -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var AMDLoader; -(function (AMDLoader) { - class LoaderEvent { - constructor(type, detail, timestamp) { - this.type = type; - this.detail = detail; - this.timestamp = timestamp; - } - } - AMDLoader.LoaderEvent = LoaderEvent; - class LoaderEventRecorder { - constructor(loaderAvailableTimestamp) { - this._events = [new LoaderEvent(1 /* LoaderEventType.LoaderAvailable */, '', loaderAvailableTimestamp)]; - } - record(type, detail) { - this._events.push(new LoaderEvent(type, detail, AMDLoader.Utilities.getHighPerformanceTimestamp())); - } - getEvents() { - return this._events; - } - } - AMDLoader.LoaderEventRecorder = LoaderEventRecorder; - class NullLoaderEventRecorder { - record(type, detail) { - // Nothing to do - } - getEvents() { - return []; - } - } - NullLoaderEventRecorder.INSTANCE = new NullLoaderEventRecorder(); - AMDLoader.NullLoaderEventRecorder = NullLoaderEventRecorder; -})(AMDLoader || (AMDLoader = {})); -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var AMDLoader; -(function (AMDLoader) { - class Utilities { - /** - * This method does not take care of / vs \ - */ - static fileUriToFilePath(isWindows, uri) { - uri = decodeURI(uri).replace(/%23/g, '#'); - if (isWindows) { - if (/^file:\/\/\//.test(uri)) { - // This is a URI without a hostname => return only the path segment - return uri.substr(8); - } - if (/^file:\/\//.test(uri)) { - return uri.substr(5); - } - } - else { - if (/^file:\/\//.test(uri)) { - return uri.substr(7); - } - } - // Not sure... - return uri; - } - static startsWith(haystack, needle) { - return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; - } - static endsWith(haystack, needle) { - return haystack.length >= needle.length && haystack.substr(haystack.length - needle.length) === needle; - } - // only check for "?" before "#" to ensure that there is a real Query-String - static containsQueryString(url) { - return /^[^\#]*\?/gi.test(url); - } - /** - * Does `url` start with http:// or https:// or file:// or / ? - */ - static isAbsolutePath(url) { - return /^((http:\/\/)|(https:\/\/)|(file:\/\/)|(\/))/.test(url); - } - static forEachProperty(obj, callback) { - if (obj) { - let key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - callback(key, obj[key]); - } - } - } - } - static isEmpty(obj) { - let isEmpty = true; - Utilities.forEachProperty(obj, () => { - isEmpty = false; - }); - return isEmpty; - } - static recursiveClone(obj) { - if (!obj || typeof obj !== 'object' || obj instanceof RegExp) { - return obj; - } - if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { - // only clone "simple" objects - return obj; - } - let result = Array.isArray(obj) ? [] : {}; - Utilities.forEachProperty(obj, (key, value) => { - if (value && typeof value === 'object') { - result[key] = Utilities.recursiveClone(value); - } - else { - result[key] = value; - } - }); - return result; - } - static generateAnonymousModule() { - return '===anonymous' + (Utilities.NEXT_ANONYMOUS_ID++) + '==='; - } - static isAnonymousModule(id) { - return Utilities.startsWith(id, '===anonymous'); - } - static getHighPerformanceTimestamp() { - if (!this.PERFORMANCE_NOW_PROBED) { - this.PERFORMANCE_NOW_PROBED = true; - this.HAS_PERFORMANCE_NOW = (AMDLoader.global.performance && typeof AMDLoader.global.performance.now === 'function'); - } - return (this.HAS_PERFORMANCE_NOW ? AMDLoader.global.performance.now() : Date.now()); - } - } - Utilities.NEXT_ANONYMOUS_ID = 1; - Utilities.PERFORMANCE_NOW_PROBED = false; - Utilities.HAS_PERFORMANCE_NOW = false; - AMDLoader.Utilities = Utilities; -})(AMDLoader || (AMDLoader = {})); -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var AMDLoader; -(function (AMDLoader) { - function ensureError(err) { - if (err instanceof Error) { - return err; - } - const result = new Error(err.message || String(err) || 'Unknown Error'); - if (err.stack) { - result.stack = err.stack; - } - return result; - } - AMDLoader.ensureError = ensureError; - ; - class ConfigurationOptionsUtil { - /** - * Ensure configuration options make sense - */ - static validateConfigurationOptions(options) { - function defaultOnError(err) { - if (err.phase === 'loading') { - console.error('Loading "' + err.moduleId + '" failed'); - console.error(err); - console.error('Here are the modules that depend on it:'); - console.error(err.neededBy); - return; - } - if (err.phase === 'factory') { - console.error('The factory function of "' + err.moduleId + '" has thrown an exception'); - console.error(err); - console.error('Here are the modules that depend on it:'); - console.error(err.neededBy); - return; - } - } - options = options || {}; - if (typeof options.baseUrl !== 'string') { - options.baseUrl = ''; - } - if (typeof options.isBuild !== 'boolean') { - options.isBuild = false; - } - if (typeof options.paths !== 'object') { - options.paths = {}; - } - if (typeof options.config !== 'object') { - options.config = {}; - } - if (typeof options.catchError === 'undefined') { - options.catchError = false; - } - if (typeof options.recordStats === 'undefined') { - options.recordStats = false; - } - if (typeof options.urlArgs !== 'string') { - options.urlArgs = ''; - } - if (typeof options.onError !== 'function') { - options.onError = defaultOnError; - } - if (!Array.isArray(options.ignoreDuplicateModules)) { - options.ignoreDuplicateModules = []; - } - if (options.baseUrl.length > 0) { - if (!AMDLoader.Utilities.endsWith(options.baseUrl, '/')) { - options.baseUrl += '/'; - } - } - if (typeof options.cspNonce !== 'string') { - options.cspNonce = ''; - } - if (typeof options.preferScriptTags === 'undefined') { - options.preferScriptTags = false; - } - if (options.nodeCachedData && typeof options.nodeCachedData === 'object') { - if (typeof options.nodeCachedData.seed !== 'string') { - options.nodeCachedData.seed = 'seed'; - } - if (typeof options.nodeCachedData.writeDelay !== 'number' || options.nodeCachedData.writeDelay < 0) { - options.nodeCachedData.writeDelay = 1000 * 7; - } - if (!options.nodeCachedData.path || typeof options.nodeCachedData.path !== 'string') { - const err = ensureError(new Error('INVALID cached data configuration, \'path\' MUST be set')); - err.phase = 'configuration'; - options.onError(err); - options.nodeCachedData = undefined; - } - } - return options; - } - static mergeConfigurationOptions(overwrite = null, base = null) { - let result = AMDLoader.Utilities.recursiveClone(base || {}); - // Merge known properties and overwrite the unknown ones - AMDLoader.Utilities.forEachProperty(overwrite, (key, value) => { - if (key === 'ignoreDuplicateModules' && typeof result.ignoreDuplicateModules !== 'undefined') { - result.ignoreDuplicateModules = result.ignoreDuplicateModules.concat(value); - } - else if (key === 'paths' && typeof result.paths !== 'undefined') { - AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.paths[key2] = value2); - } - else if (key === 'config' && typeof result.config !== 'undefined') { - AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.config[key2] = value2); - } - else { - result[key] = AMDLoader.Utilities.recursiveClone(value); - } - }); - return ConfigurationOptionsUtil.validateConfigurationOptions(result); - } - } - AMDLoader.ConfigurationOptionsUtil = ConfigurationOptionsUtil; - class Configuration { - constructor(env, options) { - this._env = env; - this.options = ConfigurationOptionsUtil.mergeConfigurationOptions(options); - this._createIgnoreDuplicateModulesMap(); - this._createSortedPathsRules(); - if (this.options.baseUrl === '') { - if (this.options.nodeRequire && this.options.nodeRequire.main && this.options.nodeRequire.main.filename && this._env.isNode) { - let nodeMain = this.options.nodeRequire.main.filename; - let dirnameIndex = Math.max(nodeMain.lastIndexOf('/'), nodeMain.lastIndexOf('\\')); - this.options.baseUrl = nodeMain.substring(0, dirnameIndex + 1); - } - } - } - _createIgnoreDuplicateModulesMap() { - // Build a map out of the ignoreDuplicateModules array - this.ignoreDuplicateModulesMap = {}; - for (let i = 0; i < this.options.ignoreDuplicateModules.length; i++) { - this.ignoreDuplicateModulesMap[this.options.ignoreDuplicateModules[i]] = true; - } - } - _createSortedPathsRules() { - // Create an array our of the paths rules, sorted descending by length to - // result in a more specific -> less specific order - this.sortedPathsRules = []; - AMDLoader.Utilities.forEachProperty(this.options.paths, (from, to) => { - if (!Array.isArray(to)) { - this.sortedPathsRules.push({ - from: from, - to: [to] - }); - } - else { - this.sortedPathsRules.push({ - from: from, - to: to - }); - } - }); - this.sortedPathsRules.sort((a, b) => { - return b.from.length - a.from.length; - }); - } - /** - * Clone current configuration and overwrite options selectively. - * @param options The selective options to overwrite with. - * @result A new configuration - */ - cloneAndMerge(options) { - return new Configuration(this._env, ConfigurationOptionsUtil.mergeConfigurationOptions(options, this.options)); - } - /** - * Get current options bag. Useful for passing it forward to plugins. - */ - getOptionsLiteral() { - return this.options; - } - _applyPaths(moduleId) { - let pathRule; - for (let i = 0, len = this.sortedPathsRules.length; i < len; i++) { - pathRule = this.sortedPathsRules[i]; - if (AMDLoader.Utilities.startsWith(moduleId, pathRule.from)) { - let result = []; - for (let j = 0, lenJ = pathRule.to.length; j < lenJ; j++) { - result.push(pathRule.to[j] + moduleId.substr(pathRule.from.length)); - } - return result; - } - } - return [moduleId]; - } - _addUrlArgsToUrl(url) { - if (AMDLoader.Utilities.containsQueryString(url)) { - return url + '&' + this.options.urlArgs; - } - else { - return url + '?' + this.options.urlArgs; - } - } - _addUrlArgsIfNecessaryToUrl(url) { - if (this.options.urlArgs) { - return this._addUrlArgsToUrl(url); - } - return url; - } - _addUrlArgsIfNecessaryToUrls(urls) { - if (this.options.urlArgs) { - for (let i = 0, len = urls.length; i < len; i++) { - urls[i] = this._addUrlArgsToUrl(urls[i]); - } - } - return urls; - } - /** - * Transform a module id to a location. Appends .js to module ids - */ - moduleIdToPaths(moduleId) { - if (this._env.isNode) { - const isNodeModule = (this.options.amdModulesPattern instanceof RegExp - && !this.options.amdModulesPattern.test(moduleId)); - if (isNodeModule) { - // This is a node module... - if (this.isBuild()) { - // ...and we are at build time, drop it - return ['empty:']; - } - else { - // ...and at runtime we create a `shortcut`-path - return ['node|' + moduleId]; - } - } - } - let result = moduleId; - let results; - if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.isAbsolutePath(result)) { - results = this._applyPaths(result); - for (let i = 0, len = results.length; i < len; i++) { - if (this.isBuild() && results[i] === 'empty:') { - continue; - } - if (!AMDLoader.Utilities.isAbsolutePath(results[i])) { - results[i] = this.options.baseUrl + results[i]; - } - if (!AMDLoader.Utilities.endsWith(results[i], '.js') && !AMDLoader.Utilities.containsQueryString(results[i])) { - results[i] = results[i] + '.js'; - } - } - } - else { - if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.containsQueryString(result)) { - result = result + '.js'; - } - results = [result]; - } - return this._addUrlArgsIfNecessaryToUrls(results); - } - /** - * Transform a module id or url to a location. - */ - requireToUrl(url) { - let result = url; - if (!AMDLoader.Utilities.isAbsolutePath(result)) { - result = this._applyPaths(result)[0]; - if (!AMDLoader.Utilities.isAbsolutePath(result)) { - result = this.options.baseUrl + result; - } - } - return this._addUrlArgsIfNecessaryToUrl(result); - } - /** - * Flag to indicate if current execution is as part of a build. - */ - isBuild() { - return this.options.isBuild; - } - shouldInvokeFactory(strModuleId) { - if (!this.options.isBuild) { - // outside of a build, all factories should be invoked - return true; - } - // during a build, only explicitly marked or anonymous modules get their factories invoked - if (AMDLoader.Utilities.isAnonymousModule(strModuleId)) { - return true; - } - if (this.options.buildForceInvokeFactory && this.options.buildForceInvokeFactory[strModuleId]) { - return true; - } - return false; - } - /** - * Test if module `moduleId` is expected to be defined multiple times - */ - isDuplicateMessageIgnoredFor(moduleId) { - return this.ignoreDuplicateModulesMap.hasOwnProperty(moduleId); - } - /** - * Get the configuration settings for the provided module id - */ - getConfigForModule(moduleId) { - if (this.options.config) { - return this.options.config[moduleId]; - } - } - /** - * Should errors be caught when executing module factories? - */ - shouldCatchError() { - return this.options.catchError; - } - /** - * Should statistics be recorded? - */ - shouldRecordStats() { - return this.options.recordStats; - } - /** - * Forward an error to the error handler. - */ - onError(err) { - this.options.onError(err); - } - } - AMDLoader.Configuration = Configuration; -})(AMDLoader || (AMDLoader = {})); -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -var AMDLoader; -(function (AMDLoader) { - /** - * Load `scriptSrc` only once (avoid multiple + `; + + return this._prependToHead(html, cspTag + postMessageRehoist); + } + + private _prependToHead(html: string, content: string): string { + // Try to inject into + const headMatch = html.match(/]*>/i); + if (headMatch) { + const insertIndex = headMatch.index! + headMatch[0].length; + return html.slice(0, insertIndex) + '\n' + content + html.slice(insertIndex); + } + + // If no , try to inject after + const htmlMatch = html.match(/]*>/i); + if (htmlMatch) { + const insertIndex = htmlMatch.index! + htmlMatch[0].length; + return html.slice(0, insertIndex) + '\n' + content + '' + html.slice(insertIndex); + } + + // If no , prepend + return `${content}${html}`; + } + + /** + * Handles incoming JSON-RPC messages from the webview. + */ + private async _handleWebviewMessage(message: McpApps.AppMessage): Promise { + const request = message; + const token = this._disposeCts.token; + + try { + let result: McpApps.HostResult = {}; + + switch (request.method) { + case 'ui/initialize': + result = await this._handleInitialize(request.params); + break; + + case 'tools/call': + result = await this._handleToolsCall(request.params, token); + break; + + case 'resources/read': + result = await this._handleResourcesRead(request.params, token); + break; + + case 'ping': + break; + + case 'ui/notifications/size-changed': + this._handleSizeChanged(request.params); + break; + + case 'ui/open-link': + result = await this._handleOpenLink(request.params); + break; + + case 'ui/request-display-mode': + // VS Code only supports inline display mode + result = { mode: 'inline' } satisfies McpApps.McpUiRequestDisplayModeResult; + break; + + case 'ui/notifications/initialized': + break; + + case 'ui/message': + result = await this._handleUiMessage(request.params); + break; + + case 'ui/update-model-context': + result = await this._handleUpdateModelContext(request.params); + break; + + case 'notifications/message': + await this._mcpToolCallUI.log(request.params); + break; + + case 'ui/notifications/sandbox-wheel': + this._handleSandboxWheel(request.params); + break; + + default: { + softAssertNever(request); + const cast = request as MCP.JSONRPCRequest; + if (cast.id !== undefined) { + await this._sendError(cast.id, -32601, `Method not found: ${cast.method}`); + } + return; + } + } + + // Send response if this was a request (has id) + if (hasKey(request, { id: true })) { + await this._sendResponse(request.id, result); + } + + } catch (error) { + this._logService.error(`[MCP App] Error handling ${request.method}:`, error); + if (hasKey(request, { id: true })) { + const message = error instanceof Error ? error.message : String(error); + await this._sendError(request.id, -32000, message); + } + } + } + + /** + * Handles the ui/initialize request from the MCP App View. + */ + private async _handleInitialize(_params: McpApps.McpUiInitializeRequest['params']): Promise { + this._announcedCapabilities = true; + + // "Host MUST send this notification with the complete tool arguments after the Guest UI's initialize request completes" + // Cast to `any` due to https://github.com/modelcontextprotocol/ext-apps/issues/197 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let args: any; + try { + args = JSON.parse(this.renderData.input); + } catch { + args = this.renderData.input; + } + + const timeout = this._register(disposableTimeout(async () => { + this._store.delete(timeout); + await this._sendNotification({ + method: 'ui/notifications/tool-input', + params: { arguments: args } + }); + + if (this.toolInvocation.kind === 'toolInvocationSerialized') { + this._sendToolResult(this.toolInvocation.resultDetails); + } else if (this.toolInvocation.kind === 'toolInvocation') { + const invocation = this.toolInvocation; + this._register(autorunSelfDisposable(reader => { + const state = invocation.state.read(reader); + if (state.type === IChatToolInvocation.StateKind.Completed) { + this._sendToolResult(state.resultDetails); + reader.dispose(); + } + })); + } + })); + + return { + protocolVersion: McpApps.LATEST_PROTOCOL_VERSION, + hostInfo: { + name: this._productService.nameLong, + version: this._productService.version, + }, + hostCapabilities: { + openLinks: {}, + serverTools: { listChanged: true }, + serverResources: { listChanged: true }, + logging: {}, + sandbox: { + csp: this._latestCsp, + permissions: { clipboardWrite: {} }, + }, + updateModelContext: { + audio: {}, + image: {}, + resourceLink: {}, + resource: {}, + structuredContent: {}, + } + }, + hostContext: this.hostContext.get(), + } satisfies Required; + } + + /** + * Sends the tool result notification when the result becomes available. + */ + private _sendToolResult(resultDetails: IToolResult['toolResultDetails'] | IChatToolInvocationSerialized['resultDetails']): void { + if (isToolResultInputOutputDetails(resultDetails) && resultDetails.mcpOutput) { + this._sendNotification({ + method: 'ui/notifications/tool-result', + params: resultDetails.mcpOutput as MCP.CallToolResult, + }); + } + } + + private async _handleUiMessage(params: McpApps.McpUiMessageRequest['params']): Promise { + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + if (!widget) { + return { isError: true }; + } + + if (!isFalsyOrWhitespace(widget.getInput())) { + return { isError: true }; + } + + widget.setInput(params.content.filter(c => c.type === 'text').map(c => c.text).join('\n\n')); + widget.attachmentModel.clearAndSetContext(...params.content.map((c, i): IChatRequestVariableEntry | undefined => { + const id = `mcpui-${i}-${Date.now()}`; + if (c.type === 'image') { + return { kind: 'image', value: decodeBase64(c.data).buffer, id, name: 'Image' }; + } else if (c.type === 'resource_link') { + const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, c.uri); + return { kind: 'file', value: uri, id, name: basename(uri) }; + } else { + return undefined; + } + }).filter(isDefined)); + widget.focusInput(); + + return { isError: false }; + } + + private async _handleUpdateModelContext(params: McpApps.McpUiUpdateModelContextRequest['params']): Promise { + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + if (!widget) { + return {}; + } + + const idPrefix = `mcpui-context-${hash(this.renderData.serverDefinitionId)}-`; + const toDelete = widget.attachmentModel.getAttachmentIDs(); + const idsToDelete = Array.from(toDelete).filter(id => id.startsWith(idPrefix)); + const entries: IChatRequestVariableEntry[] = []; + let entryIndex = 0; + + if (params.content) { + for (const block of params.content) { + const id = `${idPrefix}${entryIndex++}`; + if (block.type === 'image') { + entries.push({ + kind: 'image', + value: decodeBase64(block.data).buffer, + id, + name: 'Image', + mimeType: block.mimeType, + }); + } else if (block.type === 'resource_link') { + const uri = McpResourceURI.fromServer({ id: this.renderData.serverDefinitionId, label: '' }, block.uri); + entries.push({ + kind: 'file', + value: uri, + id, + name: basename(uri), + }); + } else if (block.type === 'text') { + const preview = block.text.replaceAll(/\s+/g, ' ').trim(); + const truncateTo = 20; + entries.push({ + kind: 'generic', + value: block.text, + id, + tooltip: new MarkdownString().appendCodeblock('plaintext', block.text), + name: preview.length > truncateTo ? preview.slice(0, truncateTo) + '…' : preview, + }); + } + } + } + + if (params.structuredContent && Object.keys(params.structuredContent).length > 0) { + const id = `${idPrefix}structured`; + const value = JSON.stringify(params.structuredContent, null, 2); + entries.push({ + kind: 'generic', + value, + tooltip: new MarkdownString().appendCodeblock('json', value), + id, + name: 'UI Data', + }); + } + + widget.attachmentModel.updateContext(idsToDelete, entries); + + return {}; + } + + private _handleSizeChanged(params: McpApps.McpUiSizeChangedNotification['params']): void { + if (params.height !== undefined && params.height !== this._height) { + this._height = params.height; + ChatMcpAppModel.heightCache.set(this.toolInvocation, params.height); + this._onDidChangeHeight.fire(); + } + } + + private _handleSandboxWheel(params: McpApps.CustomSandboxWheelNotification['params']): void { + let defaultPrevented = false; + const evt: Partial = { + wheelDeltaX: params.deltaX, + wheelDeltaY: -params.deltaY, + wheelDelta: Math.abs(params.deltaY), + + deltaX: params.deltaX, + deltaY: -params.deltaY, + deltaZ: params.deltaZ, + deltaMode: params.deltaMode, + preventDefault: () => { + defaultPrevented = true; + }, + stopPropagation: () => { }, + get defaultPrevented() { + return defaultPrevented; + } + }; + + const widget = this._chatWidgetService.getWidgetBySessionResource(this.renderData.sessionResource); + widget?.delegateScrollFromMouseWheelEvent(evt as IMouseWheelEvent); + } + + private async _handleOpenLink(params: McpApps.McpUiOpenLinkRequest['params']): Promise { + const ok = await this._openerService.open(params.url); + return { isError: !ok }; + } + + /** + * Handles tools/call requests from the MCP App. + */ + private async _handleToolsCall(params: MCP.CallToolRequestParams, token: CancellationToken): Promise { + if (!params?.name) { + throw new Error('Missing tool name in tools/call request'); + } + + return this._mcpToolCallUI.callTool(params.name, params.arguments || {}, token); + } + + /** + * Handles resources/read requests from the MCP App. + */ + private async _handleResourcesRead(params: MCP.ReadResourceRequestParams, token: CancellationToken): Promise { + if (!params?.uri) { + throw new Error('Missing uri in resources/read request'); + } + + return this._mcpToolCallUI.readResource(params.uri, token); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private async _sendResponse(id: number | string, result: any): Promise { + await this._webview.postMessage({ + jsonrpc: '2.0', + id, + result, + } satisfies MCP.JSONRPCResponse); + } + + private async _sendError(id: number | string, code: number, message: string): Promise { + await this._webview.postMessage({ + jsonrpc: '2.0', + id, + error: { code, message }, + } satisfies MCP.JSONRPCErrorResponse); + } + + private async _sendNotification(message: McpApps.HostNotification): Promise { + await this._webview.postMessage({ + jsonrpc: '2.0', + ...message, + }); + } + + public override dispose(): void { + this._disposeCts.dispose(true); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts new file mode 100644 index 00000000000..54776b9e5a3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatMcpAppSubPart.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { Button } from '../../../../../../../base/browser/ui/button/button.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { MutableDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { autorun, observableValue } from '../../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../../nls.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IMarkdownRendererService } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { defaultButtonStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; +import { ChatErrorLevel, IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatErrorWidget } from '../chatErrorContentPart.js'; +import { ChatProgressSubPart } from '../chatProgressContentPart.js'; +import { ChatMcpAppModel, McpAppLoadState } from './chatMcpAppModel.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +/** + * Data needed to render an MCP App, available before tool completion. + */ +export interface IMcpAppRenderData { + /** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */ + readonly resourceUri: string; + /** Reference to the server definition for reconnection */ + readonly serverDefinitionId: string; + /** Reference to the collection containing the server */ + readonly collectionId: string; + /** The tool input arguments as a JSON string */ + readonly input: string; + /** The session resource URI for the chat session */ + readonly sessionResource: URI; +} + +const maxWebviewHeightPct = 0.75; + +/** + * Sub-part for rendering MCP App webviews in chat tool output. + * This is a thin view layer that delegates to ChatMcpAppModel. + */ +export class ChatMcpAppSubPart extends BaseChatToolInvocationSubPart { + + public readonly domNode: HTMLElement; + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + /** The model that owns the webview */ + private readonly _model: ChatMcpAppModel; + + /** The webview container */ + private readonly _webviewContainer: HTMLElement; + + /** Current progress part for loading state */ + private readonly _progressPart = this._register(new MutableDisposable()); + + /** Current error node */ + private _errorNode: HTMLElement | undefined; + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + onDidRemount: Event, + context: IChatContentPartRenderContext, + private readonly _renderData: IMcpAppRenderData, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, + ) { + super(toolInvocation); + + // Create the DOM structure + this.domNode = dom.$('div.mcp-app-part'); + this._webviewContainer = dom.$('div.mcp-app-webview'); + this._webviewContainer.style.maxHeight = `${maxWebviewHeightPct * 100}vh`; + this._webviewContainer.style.minHeight = '100px'; + this._webviewContainer.style.height = '300px'; // Initial height, will be updated by model + this.domNode.appendChild(this._webviewContainer); + + const targetWindow = dom.getWindow(this.domNode); + const getMaxHeight = () => maxWebviewHeightPct * targetWindow.innerHeight; + const maxHeight = observableValue('mcpAppMaxHeight', getMaxHeight()); + dom.addDisposableListener(targetWindow, 'resize', () => maxHeight.set(getMaxHeight(), undefined)); + + // Create the model - it will mount the webview to the container + this._model = this._register(this._instantiationService.createInstance( + ChatMcpAppModel, + toolInvocation, + this._renderData, + this._webviewContainer, + maxHeight, + context.currentWidth, + )); + + // Update container height from model + this._updateContainerHeight(); + + // Set up load state handling + this._register(autorun(reader => { + const loadState = this._model.loadState.read(reader); + this._handleLoadStateChange(this._webviewContainer, loadState); + })); + + // Subscribe to model height changes + this._register(this._model.onDidChangeHeight(() => { + this._updateContainerHeight(); + })); + + this._register(onDidRemount(() => { + this._model.remount(); + })); + + this._register(context.onDidChangeVisibility(visible => { + if (visible) { + this._model.remount(); + } + })); + } + + private _handleLoadStateChange(container: HTMLElement, loadState: McpAppLoadState): void { + // Remove any existing loading/error indicators + if (this._progressPart.value) { + this._progressPart.value.domNode.remove(); + } + this._progressPart.clear(); + if (this._errorNode) { + this._errorNode.remove(); + this._errorNode = undefined; + } + + switch (loadState.status) { + case 'loading': { + // Hide the webview container while loading + container.style.display = 'none'; + + const progressMessage = dom.$('span'); + progressMessage.textContent = localize('loadingMcpApp', 'Loading MCP App...'); + const progressPart = this._instantiationService.createInstance( + ChatProgressSubPart, + progressMessage, + ThemeIcon.modify(Codicon.loading, 'spin'), + undefined + ); + this._progressPart.value = progressPart; + // Append to domNode (parent), not the webview container + this.domNode.appendChild(progressPart.domNode); + break; + } + case 'loaded': { + // Show the webview container + container.style.display = ''; + break; + } + case 'error': { + // Hide the webview container on error + container.style.display = 'none'; + this._showError(this.domNode, loadState.error); + break; + } + } + } + + private _updateContainerHeight(): void { + this._webviewContainer.style.height = `${this._model.height}px`; + } + + /** + * Shows an error message in the container. + */ + private _showError(container: HTMLElement, error: Error): void { + const errorNode = dom.$('.mcp-app-error'); + + // Create error message with markdown + const errorMessage = new MarkdownString(); + errorMessage.appendText(localize('mcpAppError', 'Error loading MCP App: {0}', error.message || String(error))); + + // Use ChatErrorWidget for consistent error styling + const errorWidget = new ChatErrorWidget(ChatErrorLevel.Error, errorMessage, this._markdownRendererService); + errorNode.appendChild(errorWidget.domNode); + + // Add retry button + const buttonContainer = dom.append(errorNode, dom.$('.chat-buttons-container')); + const retryButton = new Button(buttonContainer, defaultButtonStyles); + retryButton.label = localize('retry', 'Retry'); + retryButton.onDidClick(() => { + this._model.retry(); + }); + + container.appendChild(errorNode); + this._errorNode = errorNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts new file mode 100644 index 00000000000..af3c5c31ef7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatResultListSubPart.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { IMarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { Location } from '../../../../../../../editor/common/languages.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized } from '../../../../common/chatService/chatService.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatCollapsibleListContentPart, CollapsibleListPool, IChatCollapsibleListItem } from '../chatReferencesContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { getToolApprovalMessage } from './chatToolPartUtilities.js'; + +export class ChatResultListSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + context: IChatContentPartRenderContext, + message: string | IMarkdownString, + toolDetails: Array, + listPool: CollapsibleListPool, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + const collapsibleListPart = this._register(instantiationService.createInstance( + ChatCollapsibleListContentPart, + toolDetails.map(detail => ({ + kind: 'reference', + reference: detail, + })), + message, + context, + listPool, + getToolApprovalMessage(toolInvocation), + )); + collapsibleListPart.icon = Codicon.check; + this.domNode = collapsibleListPart.domNode; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts new file mode 100644 index 00000000000..f30c20e399b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.ts @@ -0,0 +1,455 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { append, h } from '../../../../../../../base/browser/dom.js'; +import { HoverStyle } from '../../../../../../../base/browser/ui/hover/hover.js'; +import { HoverPosition } from '../../../../../../../base/browser/ui/hover/hoverWidget.js'; +import { Separator } from '../../../../../../../base/common/actions.js'; +import { asArray } from '../../../../../../../base/common/arrays.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { ErrorNoTelemetry } from '../../../../../../../base/common/errors.js'; +import { createCommandUri, MarkdownString, type IMarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { thenRegisterOrDispose, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import Severity from '../../../../../../../base/common/severity.js'; +import { isObject } from '../../../../../../../base/common/types.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../../../base/common/uuid.js'; +import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { ITextModelService } from '../../../../../../../editor/common/services/resolverService.js'; +import { localize } from '../../../../../../../nls.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { IDialogService } from '../../../../../../../platform/dialogs/common/dialogs.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js'; +import { IPreferencesService } from '../../../../../../services/preferences/common/preferences.js'; +import { ITerminalChatService } from '../../../../../terminal/browser/terminal.js'; +import { TerminalContribCommandId, TerminalContribSettingId } from '../../../../../terminal/terminalContribExports.js'; +import { ChatContextKeys } from '../../../../common/actions/chatContextKeys.js'; +import { migrateLegacyTerminalToolSpecificData } from '../../../../common/chat.js'; +import { IChatToolInvocation, ToolConfirmKind, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from '../../../../common/chatService/chatService.js'; +import type { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { AcceptToolConfirmationActionId, SkipToolConfirmationActionId } from '../../../actions/chatToolActions.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { ChatCustomConfirmationWidget, IChatConfirmationButton } from '../chatConfirmationWidget.js'; +import { EditorPool } from '../chatContentCodePools.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatMarkdownContentPart } from '../chatMarkdownContentPart.js'; +import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export const enum TerminalToolConfirmationStorageKeys { + TerminalAutoApproveWarningAccepted = 'chat.tools.terminal.autoApprove.warningAccepted' +} + +export interface ITerminalNewAutoApproveRule { + key: string; + value: boolean | { + approve: boolean; + matchCommandLine?: boolean; + }; + scope: 'session' | 'workspace' | 'user'; +} + +export type TerminalNewAutoApproveButtonData = ( + { type: 'enable' } | + { type: 'configure' } | + { type: 'skip' } | + { type: 'newRule'; rule: ITerminalNewAutoApproveRule | ITerminalNewAutoApproveRule[] } | + { type: 'sessionApproval' } +); + +export class ChatTerminalToolConfirmationSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + public readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation, + terminalData: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly codeBlockStartIndex: number, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IDialogService private readonly dialogService: IDialogService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IPreferencesService private readonly preferencesService: IPreferencesService, + @IStorageService private readonly storageService: IStorageService, + @ITerminalChatService private readonly terminalChatService: ITerminalChatService, + @ITextModelService textModelService: ITextModelService, + @IHoverService hoverService: IHoverService, + ) { + super(toolInvocation); + + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation || !state.confirmationMessages?.title) { + throw new Error('Confirmation messages are missing'); + } + + terminalData = migrateLegacyTerminalToolSpecificData(terminalData); + + const { title, message, disclaimer, terminalCustomActions } = state.confirmationMessages; + + // Use pre-computed confirmation data from runInTerminalTool (cd prefix extraction happens there for localization) + // Use presentationOverrides for display if available (e.g., extracted Python code) + const initialContent = terminalData.presentationOverrides?.commandLine ?? terminalData.confirmation?.commandLine ?? (terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); + const cdPrefix = terminalData.confirmation?.cdPrefix ?? ''; + // When presentationOverrides is set, the editor should be read-only since the displayed content + // differs from the actual command (e.g., extracted Python code vs full python -c command) + const isReadOnly = !!terminalData.presentationOverrides; + + const autoApproveEnabled = this.configurationService.getValue(TerminalContribSettingId.EnableAutoApprove) === true; + const autoApproveWarningAccepted = this.storageService.getBoolean(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, StorageScope.APPLICATION, false); + let moreActions: (IChatConfirmationButton | Separator)[] | undefined = undefined; + if (autoApproveEnabled) { + moreActions = []; + if (!autoApproveWarningAccepted) { + moreActions.push({ + label: localize('autoApprove.enable', 'Enable Auto Approve...'), + data: { + type: 'enable' + } + }); + moreActions.push(new Separator()); + if (terminalCustomActions) { + for (const action of terminalCustomActions) { + if (!(action instanceof Separator)) { + action.disabled = true; + } + } + } + } + if (terminalCustomActions) { + moreActions.push(...terminalCustomActions); + } + if (moreActions.length === 0) { + moreActions = undefined; + } + } + + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on', + readOnly: isReadOnly, + tabFocusMode: true, + ariaLabel: typeof title === 'string' ? title : title.value + } + }; + const languageId = this.languageService.getLanguageIdByLanguageName(terminalData.presentationOverrides?.language ?? terminalData.language ?? 'sh') ?? 'shellscript'; + const model = this._register(this.modelService.createModel( + initialContent, + this.languageService.createById(languageId), + this._getUniqueCodeBlockUri(), + true + )); + thenRegisterOrDispose(textModelService.createModelReference(model.uri), this._store); + const editor = this._register(this.editorPool.get()); + editor.object.render({ + codeBlockIndex: this.codeBlockStartIndex, + codeBlockPartIndex: 0, + element: this.context.element, + languageId, + renderOptions: codeBlockRenderOptions, + textModel: Promise.resolve(model), + chatSessionResource: this.context.element.sessionResource + }, this.currentWidthDelegate()); + this.codeblocks.push({ + codeBlockIndex: this.codeBlockStartIndex, + codemapperUri: undefined, + elementId: this.context.element.id, + focus: () => editor.object.focus(), + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + uriPromise: Promise.resolve(model.uri), + chatSessionResource: this.context.element.sessionResource + }); + this._register(model.onDidChangeContent(e => { + const currentValue = model.getValue(); + // Only set userEdited if the content actually differs from the initial value + // Prepend cd prefix back if it was extracted for display + if (currentValue !== initialContent) { + terminalData.commandLine.userEdited = cdPrefix + currentValue; + } else { + terminalData.commandLine.userEdited = undefined; + } + })); + const elements = h('.chat-confirmation-message-terminal', [ + h('.chat-confirmation-message-terminal-editor@editor'), + h('.chat-confirmation-message-terminal-disclaimer@disclaimer'), + ]); + append(elements.editor, editor.object.element); + this._register(hoverService.setupDelayedHover(elements.editor, { + content: message || '', + style: HoverStyle.Pointer, + position: { hoverPosition: HoverPosition.LEFT }, + })); + const confirmWidget = this._register(this.instantiationService.createInstance( + ChatCustomConfirmationWidget, + this.context, + { + title, + icon: Codicon.terminal, + message: elements.root, + buttons: this._createButtons(moreActions) + }, + )); + + if (disclaimer) { + this._appendMarkdownPart(elements.disclaimer, disclaimer, codeBlockRenderOptions); + } + + const hasToolConfirmationKey = ChatContextKeys.Editing.hasToolConfirmation.bindTo(this.contextKeyService); + hasToolConfirmationKey.set(true); + this._register(toDisposable(() => hasToolConfirmationKey.reset())); + + this._register(confirmWidget.onDidClick(async button => { + let doComplete = true; + const data = button.data; + let toolConfirmKind: ToolConfirmKind = ToolConfirmKind.Denied; + if (typeof data === 'boolean') { + if (data) { + toolConfirmKind = ToolConfirmKind.UserAction; + // Clear out any auto approve info since this was an explicit user action. This + // can happen when the auto approve feature is off. + if (terminalData.autoApproveInfo) { + terminalData.autoApproveInfo = undefined; + } + } + } else if (typeof data !== 'boolean') { + switch (data.type) { + case 'enable': { + const optedIn = await this._showAutoApproveWarning(); + if (optedIn) { + this.storageService.store(TerminalToolConfirmationStorageKeys.TerminalAutoApproveWarningAccepted, true, StorageScope.APPLICATION, StorageTarget.USER); + // If this command would have been auto-approved, approve immediately + if (terminalData.autoApproveInfo) { + toolConfirmKind = ToolConfirmKind.UserAction; + } + // If this would not have been auto approved, enable the options and + // do not complete + else if (terminalCustomActions) { + for (const action of terminalCustomActions) { + if (!(action instanceof Separator)) { + action.disabled = false; + } + } + + confirmWidget.updateButtons(this._createButtons(terminalCustomActions)); + doComplete = false; + } + } else { + doComplete = false; + } + break; + } + case 'skip': { + toolConfirmKind = ToolConfirmKind.Skipped; + break; + } + case 'newRule': { + const newRules = asArray(data.rule); + + // Group rules by scope + const sessionRules = newRules.filter(r => r.scope === 'session'); + const workspaceRules = newRules.filter(r => r.scope === 'workspace'); + const userRules = newRules.filter(r => r.scope === 'user'); + + // Handle session-scoped rules (temporary, in-memory only) + const chatSessionResource = this.context.element.sessionResource; + for (const rule of sessionRules) { + this.terminalChatService.addSessionAutoApproveRule(chatSessionResource, rule.key, rule.value); + } + + // Handle workspace-scoped rules + if (workspaceRules.length > 0) { + const inspect = this.configurationService.inspect(TerminalContribSettingId.AutoApprove); + const oldValue = (inspect.workspaceValue as Record | undefined) ?? {}; + if (isObject(oldValue)) { + const newValue: Record = { ...oldValue }; + for (const rule of workspaceRules) { + newValue[rule.key] = rule.value; + } + await this.configurationService.updateValue(TerminalContribSettingId.AutoApprove, newValue, ConfigurationTarget.WORKSPACE); + } else { + this.preferencesService.openSettings({ + jsonEditor: true, + target: ConfigurationTarget.WORKSPACE, + revealSetting: { key: TerminalContribSettingId.AutoApprove }, + }); + throw new ErrorNoTelemetry(`Cannot add new rule, existing workspace setting is unexpected format`); + } + } + + // Handle user-scoped rules + if (userRules.length > 0) { + const inspect = this.configurationService.inspect(TerminalContribSettingId.AutoApprove); + const oldValue = (inspect.userValue as Record | undefined) ?? {}; + if (isObject(oldValue)) { + const newValue: Record = { ...oldValue }; + for (const rule of userRules) { + newValue[rule.key] = rule.value; + } + await this.configurationService.updateValue(TerminalContribSettingId.AutoApprove, newValue, ConfigurationTarget.USER); + } else { + this.preferencesService.openSettings({ + jsonEditor: true, + target: ConfigurationTarget.USER, + revealSetting: { key: TerminalContribSettingId.AutoApprove }, + }); + throw new ErrorNoTelemetry(`Cannot add new rule, existing setting is unexpected format`); + } + } + + function formatRuleLinks(rules: ITerminalNewAutoApproveRule[], scope: 'session' | 'workspace' | 'user'): string { + return rules.map(e => { + if (scope === 'session') { + return `\`${e.key}\``; + } + const target = scope === 'workspace' ? ConfigurationTarget.WORKSPACE : ConfigurationTarget.USER; + const settingsUri = createCommandUri(TerminalContribCommandId.OpenTerminalSettingsLink, target); + return `[\`${e.key}\`](${settingsUri.toString()} "${localize('ruleTooltip', 'View rule in settings')}")`; + }).join(', '); + } + const mdTrustSettings = { + isTrusted: { + enabledCommands: [TerminalContribCommandId.OpenTerminalSettingsLink] + } + }; + const parts: string[] = []; + if (sessionRules.length > 0) { + parts.push(sessionRules.length === 1 + ? localize('newRule.session', 'Session auto approve rule {0} added', formatRuleLinks(sessionRules, 'session')) + : localize('newRule.session.plural', 'Session auto approve rules {0} added', formatRuleLinks(sessionRules, 'session'))); + } + if (workspaceRules.length > 0) { + parts.push(workspaceRules.length === 1 + ? localize('newRule.workspace', 'Workspace auto approve rule {0} added', formatRuleLinks(workspaceRules, 'workspace')) + : localize('newRule.workspace.plural', 'Workspace auto approve rules {0} added', formatRuleLinks(workspaceRules, 'workspace'))); + } + if (userRules.length > 0) { + parts.push(userRules.length === 1 + ? localize('newRule.user', 'User auto approve rule {0} added', formatRuleLinks(userRules, 'user')) + : localize('newRule.user.plural', 'User auto approve rules {0} added', formatRuleLinks(userRules, 'user'))); + } + if (parts.length > 0) { + terminalData.autoApproveInfo = new MarkdownString(parts.join(', '), mdTrustSettings); + } + toolConfirmKind = ToolConfirmKind.UserAction; + break; + } + case 'configure': { + this.preferencesService.openSettings({ + target: ConfigurationTarget.USER, + query: `@id:${TerminalContribSettingId.AutoApprove}`, + }); + doComplete = false; + break; + } + case 'sessionApproval': { + const sessionResource = this.context.element.sessionResource; + this.terminalChatService.setChatSessionAutoApproval(sessionResource, true); + const disableUri = createCommandUri(TerminalContribCommandId.DisableSessionAutoApproval, sessionResource); + const mdTrustSettings = { + isTrusted: { + enabledCommands: [TerminalContribCommandId.DisableSessionAutoApproval] + } + }; + terminalData.autoApproveInfo = new MarkdownString(`${localize('sessionApproval', 'All commands will be auto approved for this session')} ([${localize('sessionApproval.disable', 'Disable')}](${disableUri.toString()}))`, mdTrustSettings); + toolConfirmKind = ToolConfirmKind.UserAction; + break; + } + } + } + + if (doComplete) { + IChatToolInvocation.confirmWith(toolInvocation, { type: toolConfirmKind }); + this.chatWidgetService.getWidgetBySessionResource(this.context.element.sessionResource)?.focusInput(); + } + })); + + this.domNode = confirmWidget.domNode; + } + + private _createButtons(moreActions: (IChatConfirmationButton | Separator)[] | undefined): IChatConfirmationButton[] { + const getLabelAndTooltip = (label: string, actionId: string, tooltipDetail: string = label): { label: string; tooltip: string } => { + const tooltip = this.keybindingService.appendKeybinding(tooltipDetail, actionId); + return { label, tooltip }; + }; + return [ + { + ...getLabelAndTooltip(localize('tool.allow', "Allow"), AcceptToolConfirmationActionId), + data: true, + moreActions, + }, + { + ...getLabelAndTooltip(localize('tool.skip', "Skip"), SkipToolConfirmationActionId, localize('skip.detail', 'Proceed without executing this command')), + data: { type: 'skip' }, + isSecondary: true, + }, + ]; + } + + private async _showAutoApproveWarning(): Promise { + const promptResult = await this.dialogService.prompt({ + type: Severity.Info, + message: localize('autoApprove.title', 'Enable terminal auto approve?'), + buttons: [{ + label: localize('autoApprove.button.enable', 'Enable'), + run: () => true + }], + cancelButton: true, + custom: { + icon: Codicon.shield, + markdownDetails: [{ + markdown: new MarkdownString(localize('autoApprove.markdown', 'This will enable a configurable subset of commands to run in the terminal autonomously. It provides *best effort protections* and assumes the agent is not acting maliciously.')), + }, { + markdown: new MarkdownString(`[${localize('autoApprove.markdown2', 'Learn more about the potential risks and how to avoid them.')}](https://code.visualstudio.com/docs/copilot/security#_security-considerations)`) + }], + } + }); + return promptResult.result === true; + } + + private _getUniqueCodeBlockUri() { + return URI.from({ + scheme: Schemas.vscodeChatCodeBlock, + path: generateUuid(), + }); + } + + private _appendMarkdownPart(container: HTMLElement, message: string | IMarkdownString, codeBlockRenderOptions: ICodeBlockRenderOptions) { + const part = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, + { + kind: 'markdownContent', + content: typeof message === 'string' ? new MarkdownString().appendMarkdown(message) : message + }, + this.context, + this.editorPool, + false, + this.codeBlockStartIndex, + this.renderer, + undefined, + this.currentWidthDelegate(), + this.codeBlockModelCollection, + { codeBlockRenderOptions }, + )); + append(container, part.domNode); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts new file mode 100644 index 00000000000..c2e85e9e4d5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.ts @@ -0,0 +1,1595 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ActionBar } from '../../../../../../../base/browser/ui/actionbar/actionbar.js'; +import { isMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ChatConfiguration } from '../../../../common/constants.js'; +import { migrateLegacyTerminalToolSpecificData } from '../../../../common/chat.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized, type IChatMarkdownContent, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from '../../../../common/chatService/chatService.js'; +import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { ChatTreeItem, IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { ChatQueryTitlePart } from '../chatConfirmationWidget.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatMarkdownContentPart, type IChatMarkdownContentPartOptions } from '../chatMarkdownContentPart.js'; +import { ChatProgressSubPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { TerminalToolAutoExpand } from './terminalToolAutoExpand.js'; +import { ChatCollapsibleContentPart } from '../chatCollapsibleContentPart.js'; +import { IChatRendererContent } from '../../../../common/model/chatViewModel.js'; +import '../media/chatTerminalToolProgressPart.css'; +import type { ICodeBlockRenderOptions } from '../codeBlockPart.js'; +import { Action, IAction } from '../../../../../../../base/common/actions.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../../../terminal/browser/terminal.js'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { DecorationSelector, getTerminalCommandDecorationState, getTerminalCommandDecorationTooltip } from '../../../../../terminal/browser/xterm/decorationStyles.js'; +import * as dom from '../../../../../../../base/browser/dom.js'; +import { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; +import { ScrollbarVisibility } from '../../../../../../../base/common/scrollable.js'; +import { localize } from '../../../../../../../nls.js'; +import { ITerminalCommand, TerminalCapability, type ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { stripIcons } from '../../../../../../../base/common/iconLabels.js'; +import { IAccessibleViewService } from '../../../../../../../platform/accessibility/browser/accessibleView.js'; +import { IContextKey, IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { AccessibilityVerbositySettingId } from '../../../../../accessibility/browser/accessibilityConfiguration.js'; +import { ChatContextKeys } from '../../../../common/actions/chatContextKeys.js'; +import { EditorPool } from '../chatContentCodePools.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { DetachedTerminalCommandMirror, DetachedTerminalSnapshotMirror } from '../../../../../terminal/browser/chatTerminalCommandMirror.js'; +import { TerminalLocation } from '../../../../../../../platform/terminal/common/terminal.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { TerminalContribCommandId } from '../../../../../terminal/terminalContribExports.js'; +import { ITelemetryService } from '../../../../../../../platform/telemetry/common/telemetry.js'; +import { isNumber } from '../../../../../../../base/common/types.js'; +import { removeAnsiEscapeCodes } from '../../../../../../../base/common/strings.js'; +import { PANEL_BACKGROUND } from '../../../../../../common/theme.js'; +import { editorBackground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; + +const MIN_OUTPUT_ROWS = 1; +const MAX_OUTPUT_ROWS = 10; + +/** + * Remembers whether a tool invocation was last expanded so state survives virtualization re-renders. + */ +const expandedStateByInvocation = new WeakMap(); + +/** + * Options for configuring a terminal command decoration. + */ +interface ITerminalCommandDecorationOptions { + /** + * The terminal data associated with the tool invocation. + */ + readonly terminalData: IChatTerminalToolInvocationData; + + /** + * Returns the HTML element representing the command block in the terminal output. + * May return `undefined` if the command block is not currently rendered. + * Called when attaching the decoration to the command block container. + */ + getCommandBlock(): HTMLElement | undefined; + + /** + * Returns the HTML element representing the icon for the command, if any. + * May return `undefined` if no icon is present. + * Used to determine where to insert the decoration relative to the icon. + */ + getIconElement(): HTMLElement | undefined; + + /** + * Returns the resolved terminal command associated with this decoration, if available. + * May return `undefined` if the command has not been resolved yet. + * Used to access command metadata for the decoration. + */ + getResolvedCommand(): ITerminalCommand | undefined; +} + +class TerminalCommandDecoration extends Disposable { + private readonly _element: HTMLElement; + private _interactionElement: HTMLElement | undefined; + + constructor( + private readonly _options: ITerminalCommandDecorationOptions, + @IHoverService private readonly _hoverService: IHoverService + ) { + super(); + const decorationElements = h('span.chat-terminal-command-decoration@decoration', { role: 'img', tabIndex: 0 }); + this._element = decorationElements.decoration; + this._attachElementToContainer(); + } + + private _attachElementToContainer(): void { + const container = this._options.getCommandBlock(); + if (!container) { + return; + } + + const decoration = this._element; + if (!decoration.isConnected || decoration.parentElement !== container) { + const icon = this._options.getIconElement(); + if (icon && icon.parentElement === container) { + icon.insertAdjacentElement('afterend', decoration); + } else { + container.insertBefore(decoration, container.firstElementChild ?? null); + } + } + + this._register(this._hoverService.setupDelayedHover(decoration, () => ({ + content: this._getHoverText() + }))); + this._attachInteractionHandlers(decoration); + } + + private _getHoverText(): string { + const command = this._options.getResolvedCommand(); + const storedState = this._options.terminalData.terminalCommandState; + return getTerminalCommandDecorationTooltip(command, storedState) || ''; + } + + public update(command?: ITerminalCommand): void { + this._attachElementToContainer(); + const decoration = this._element; + const resolvedCommand = command ?? this._options.getResolvedCommand(); + this._apply(decoration, resolvedCommand); + } + + private _apply(decoration: HTMLElement, command: ITerminalCommand | undefined): void { + const terminalData = this._options.terminalData; + let storedState = terminalData.terminalCommandState; + + if (command) { + const existingState = terminalData.terminalCommandState ?? {}; + terminalData.terminalCommandState = { + ...existingState, + exitCode: command.exitCode, + timestamp: command.timestamp ?? existingState.timestamp, + duration: command.duration ?? existingState.duration + }; + storedState = terminalData.terminalCommandState; + } else if (!storedState) { + const now = Date.now(); + terminalData.terminalCommandState = { exitCode: undefined, timestamp: now }; + storedState = terminalData.terminalCommandState; + } + + const decorationState = getTerminalCommandDecorationState(command, storedState); + const tooltip = getTerminalCommandDecorationTooltip(command, storedState); + + decoration.className = `chat-terminal-command-decoration ${DecorationSelector.CommandDecoration}`; + decoration.classList.add(DecorationSelector.Codicon); + for (const className of decorationState.classNames) { + decoration.classList.add(className); + } + decoration.classList.add(...ThemeIcon.asClassNameArray(decorationState.icon)); + const isInteractive = !decoration.classList.contains(DecorationSelector.Default); + decoration.tabIndex = isInteractive ? 0 : -1; + if (isInteractive) { + decoration.removeAttribute('aria-disabled'); + } else { + decoration.setAttribute('aria-disabled', 'true'); + } + const hoverText = tooltip || decorationState.hoverMessage; + if (hoverText) { + decoration.setAttribute('aria-label', hoverText); + } else { + decoration.removeAttribute('aria-label'); + } + } + + private _attachInteractionHandlers(decoration: HTMLElement): void { + if (this._interactionElement === decoration) { + return; + } + this._interactionElement = decoration; + } +} + +export class ChatTerminalToolProgressPart extends BaseChatToolInvocationSubPart implements IChatTerminalToolProgressPart { + public readonly domNode: HTMLElement; + + private readonly _actionBar: ActionBar; + + private readonly _titleElement: HTMLElement; + private readonly _outputView: ChatTerminalToolOutputSection; + private readonly _terminalOutputContextKey: IContextKey; + private _terminalSessionRegistration: IDisposable | undefined; + private readonly _elementIndex: number; + private readonly _contentIndex: number; + private readonly _sessionResource: URI; + + private readonly _showOutputAction = this._register(new MutableDisposable()); + private _showOutputActionAdded = false; + private readonly _focusAction = this._register(new MutableDisposable()); + private readonly _continueInBackgroundAction = this._register(new MutableDisposable()); + + private readonly _terminalData: IChatTerminalToolInvocationData; + private _terminalCommandUri: URI | undefined; + private _storedCommandId: string | undefined; + private readonly _commandText: string; + private readonly _isSerializedInvocation: boolean; + private _terminalInstance: ITerminalInstance | undefined; + private readonly _decoration: TerminalCommandDecoration; + private _userToggledOutput: boolean = false; + private _isInThinkingContainer: boolean = false; + private _thinkingCollapsibleWrapper: ChatTerminalThinkingCollapsibleWrapper | undefined; + + private markdownPart: ChatMarkdownContentPart | undefined; + public get codeblocks(): IChatCodeBlockInfo[] { + return this.markdownPart?.codeblocks ?? []; + } + + public get elementIndex(): number { + return this._elementIndex; + } + + public get contentIndex(): number { + return this._contentIndex; + } + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + terminalData: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData, + context: IChatContentPartRenderContext, + renderer: IMarkdownRenderer, + editorPool: EditorPool, + currentWidthDelegate: () => number, + codeBlockStartIndex: number, + codeBlockModelCollection: CodeBlockModelCollection, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, + @ITerminalService private readonly _terminalService: ITerminalService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + ) { + super(toolInvocation); + + this._elementIndex = context.elementIndex; + this._contentIndex = context.contentIndex; + this._sessionResource = context.element.sessionResource; + + terminalData = migrateLegacyTerminalToolSpecificData(terminalData); + this._terminalData = terminalData; + this._terminalCommandUri = terminalData.terminalCommandUri ? URI.revive(terminalData.terminalCommandUri) : undefined; + this._storedCommandId = this._terminalCommandUri ? new URLSearchParams(this._terminalCommandUri.query ?? '').get('command') ?? undefined : undefined; + this._isSerializedInvocation = (toolInvocation.kind === 'toolInvocationSerialized'); + + const elements = h('.chat-terminal-content-part@container', [ + h('.chat-terminal-content-title@title', [ + h('.chat-terminal-command-block@commandBlock') + ]), + h('.chat-terminal-content-message@message') + ]); + this._titleElement = elements.title; + + const command = (terminalData.commandLine.forDisplay ?? terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original).trimStart(); + this._commandText = command; + this._terminalOutputContextKey = ChatContextKeys.inChatTerminalToolOutput.bindTo(this._contextKeyService); + + this._decoration = this._register(this._instantiationService.createInstance(TerminalCommandDecoration, { + terminalData: this._terminalData, + getCommandBlock: () => elements.commandBlock, + getIconElement: () => undefined, + getResolvedCommand: () => this._getResolvedCommand() + })); + + // Use presentationOverrides for display if available (e.g., extracted Python code with syntax highlighting) + const displayCommand = terminalData.presentationOverrides?.commandLine ?? command; + const displayLanguage = terminalData.presentationOverrides?.language ?? terminalData.language; + const titlePart = this._register(_instantiationService.createInstance( + ChatQueryTitlePart, + elements.commandBlock, + new MarkdownString([ + `\`\`\`${displayLanguage}`, + `${displayCommand.replaceAll('```', '\\`\\`\\`')}`, + `\`\`\`` + ].join('\n'), { supportThemeIcons: true }), + undefined, + )); + this._register(titlePart.onDidChangeHeight(() => { + this._decoration.update(); + })); + + this._outputView = this._register(this._instantiationService.createInstance( + ChatTerminalToolOutputSection, + () => this._ensureTerminalInstance(), + () => this._getResolvedCommand(), + () => this._terminalData.terminalCommandOutput, + () => this._commandText, + () => this._terminalData.terminalTheme, + )); + elements.container.append(this._outputView.domNode); + this._register(this._outputView.onDidFocus(() => this._handleOutputFocus())); + this._register(this._outputView.onDidBlur(e => this._handleOutputBlur(e))); + this._register(toDisposable(() => this._handleDispose())); + this._register(this._keybindingService.onDidUpdateKeybindings(() => { + this._focusAction.value?.refreshKeybindingTooltip(); + this._showOutputAction.value?.refreshKeybindingTooltip(); + })); + + + const actionBarEl = h('.chat-terminal-action-bar@actionBar'); + elements.title.append(actionBarEl.root); + this._actionBar = this._register(new ActionBar(actionBarEl.actionBar, {})); + this._initializeTerminalActions(); + this._terminalService.whenConnected.then(() => this._initializeTerminalActions()); + let pastTenseMessage: string | undefined; + if (toolInvocation.pastTenseMessage) { + pastTenseMessage = `${typeof toolInvocation.pastTenseMessage === 'string' ? toolInvocation.pastTenseMessage : toolInvocation.pastTenseMessage.value}`; + } + const markdownContent = new MarkdownString(pastTenseMessage, { + supportThemeIcons: true, + isTrusted: isMarkdownString(toolInvocation.pastTenseMessage) ? toolInvocation.pastTenseMessage.isTrusted : false, + }); + const chatMarkdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: markdownContent, + }; + + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + wordWrap: 'on' + } + }; + + const markdownOptions: IChatMarkdownContentPartOptions = { + codeBlockRenderOptions, + accessibilityOptions: pastTenseMessage ? { + statusMessage: localize('terminalToolCommand', '{0}', stripIcons(pastTenseMessage)) + } : undefined + }; + + this.markdownPart = this._register(_instantiationService.createInstance(ChatMarkdownContentPart, chatMarkdownContent, context, editorPool, false, codeBlockStartIndex, renderer, {}, currentWidthDelegate(), codeBlockModelCollection, markdownOptions)); + + elements.message.append(this.markdownPart.domNode); + const progressPart = this._register(_instantiationService.createInstance(ChatProgressSubPart, elements.container, this.getIcon(), terminalData.autoApproveInfo)); + this._decoration.update(); + + // wrap terminal when thinking setting enabled + const terminalToolsInThinking = this._configurationService.getValue(ChatConfiguration.TerminalToolsInThinking); + const requiresConfirmation = toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.getConfirmationMessages(toolInvocation); + + if (terminalToolsInThinking && !requiresConfirmation) { + this._isInThinkingContainer = true; + this.domNode = this._createCollapsibleWrapper(progressPart.domNode, command, toolInvocation, context); + } else { + this.domNode = progressPart.domNode; + } + + // Only auto-expand in thinking containers if there's actual output to show + const hasStoredOutput = !!terminalData.terminalCommandOutput; + if (expandedStateByInvocation.get(toolInvocation) || (this._isInThinkingContainer && IChatToolInvocation.isComplete(toolInvocation) && hasStoredOutput)) { + void this._toggleOutput(true); + } + this._register(this._terminalChatService.registerProgressPart(this)); + } + + private _createCollapsibleWrapper(contentElement: HTMLElement, commandText: string, toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, context: IChatContentPartRenderContext): HTMLElement { + // truncate header when it's too long + const maxCommandLength = 50; + const truncatedCommand = commandText.length > maxCommandLength + ? commandText.substring(0, maxCommandLength) + '...' + : commandText; + + const isComplete = IChatToolInvocation.isComplete(toolInvocation); + const autoExpandFailures = this._configurationService.getValue(ChatConfiguration.AutoExpandToolFailures); + const hasError = autoExpandFailures && this._terminalData.terminalCommandState?.exitCode !== undefined && this._terminalData.terminalCommandState.exitCode !== 0; + const initialExpanded = !isComplete || hasError; + + const wrapper = this._register(this._instantiationService.createInstance( + ChatTerminalThinkingCollapsibleWrapper, + truncatedCommand, + contentElement, + context, + initialExpanded, + isComplete + )); + this._thinkingCollapsibleWrapper = wrapper; + + return wrapper.domNode; + } + + public expandCollapsibleWrapper(): void { + this._thinkingCollapsibleWrapper?.expand(); + } + + public markCollapsibleWrapperComplete(): void { + this._thinkingCollapsibleWrapper?.markComplete(); + } + + private async _initializeTerminalActions(): Promise { + if (this._store.isDisposed) { + return; + } + const terminalToolSessionId = this._terminalData.terminalToolSessionId; + if (!terminalToolSessionId) { + this._addActions(); + return; + } + + const attachInstance = async (instance: ITerminalInstance | undefined) => { + if (this._store.isDisposed) { + return; + } + if (!instance) { + if (this._isSerializedInvocation) { + this._clearCommandAssociation(); + } + this._addActions(undefined, terminalToolSessionId); + return; + } + const isNewInstance = this._terminalInstance !== instance; + if (isNewInstance) { + this._terminalInstance = instance; + this._registerInstanceListener(instance); + } + // Always call _addActions to ensure actions are added, even if instance was set earlier + // (e.g., by the output view during expanded state restoration) + this._addActions(instance, terminalToolSessionId); + }; + + const initialInstance = await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId); + await attachInstance(initialInstance); + + if (!initialInstance) { + this._addActions(undefined, terminalToolSessionId); + } + + if (this._store.isDisposed) { + return; + } + + if (!this._terminalSessionRegistration) { + const listener = this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession(async instance => { + const registeredInstance = await this._terminalChatService.getTerminalInstanceByToolSessionId(terminalToolSessionId); + if (instance !== registeredInstance) { + return; + } + this._terminalSessionRegistration?.dispose(); + this._terminalSessionRegistration = undefined; + await attachInstance(instance); + }); + this._terminalSessionRegistration = this._store.add(listener); + } + + // Listen for continue in background to remove the button + this._store.add(this._terminalChatService.onDidContinueInBackground(sessionId => { + if (sessionId === terminalToolSessionId) { + this._terminalData.didContinueInBackground = true; + this._removeContinueInBackgroundAction(); + } + })); + } + + private _addActions(terminalInstance?: ITerminalInstance, terminalToolSessionId?: string): void { + if (this._store.isDisposed) { + return; + } + const actionBar = this._actionBar; + this._removeFocusAction(); + const resolvedCommand = this._getResolvedCommand(terminalInstance); + + this._removeContinueInBackgroundAction(); + if (terminalInstance) { + const isTerminalHidden = terminalInstance && terminalToolSessionId ? this._terminalChatService.isBackgroundTerminal(terminalToolSessionId) : false; + const focusAction = this._instantiationService.createInstance(FocusChatInstanceAction, terminalInstance, resolvedCommand, this._terminalCommandUri, this._storedCommandId, isTerminalHidden); + this._focusAction.value = focusAction; + actionBar.push(focusAction, { icon: true, label: false, index: 0 }); + + // Add continue in background action - only for foreground executions with running commands + // Note: isBackground refers to whether the tool was invoked with isBackground=true (background execution), + // not whether the terminal is hidden from the user + if (terminalToolSessionId && !this._terminalData.isBackground && !this._terminalData.didContinueInBackground) { + const isStillRunning = resolvedCommand?.exitCode === undefined && this._terminalData.terminalCommandState?.exitCode === undefined; + if (isStillRunning) { + const continueAction = this._instantiationService.createInstance(ContinueInBackgroundAction, terminalToolSessionId); + this._continueInBackgroundAction.value = continueAction; + actionBar.push(continueAction, { icon: true, label: false, index: 0 }); + } + } + } + + this._ensureShowOutputAction(resolvedCommand); + this._decoration.update(resolvedCommand); + } + + private _getResolvedCommand(instance?: ITerminalInstance): ITerminalCommand | undefined { + const target = instance ?? this._terminalInstance; + if (!target) { + return undefined; + } + return this._resolveCommand(target); + } + + private _ensureShowOutputAction(command?: ITerminalCommand): void { + if (this._store.isDisposed) { + return; + } + // don't show dropdown when in thinking container + if (this._isInThinkingContainer) { + return; + } + const resolvedCommand = command ?? this._getResolvedCommand(); + const hasSnapshot = !!this._terminalData.terminalCommandOutput; + if (!resolvedCommand && !hasSnapshot) { + return; + } + let showOutputAction = this._showOutputAction.value; + if (!showOutputAction) { + showOutputAction = this._instantiationService.createInstance(ToggleChatTerminalOutputAction, () => this._toggleOutputFromAction()); + this._showOutputAction.value = showOutputAction; + const autoExpandFailures = this._configurationService.getValue(ChatConfiguration.AutoExpandToolFailures); + const exitCode = resolvedCommand?.exitCode ?? this._terminalData.terminalCommandState?.exitCode; + if (exitCode !== undefined && exitCode !== 0 && autoExpandFailures) { + this._toggleOutput(true); + } + } + showOutputAction.syncPresentation(this._outputView.isExpanded); + + const actionBar = this._actionBar; + if (this._showOutputActionAdded) { + const existingIndex = actionBar.viewItems.findIndex(item => item.action === showOutputAction); + if (existingIndex >= 0 && existingIndex !== actionBar.length() - 1) { + actionBar.pull(existingIndex); + this._showOutputActionAdded = false; + } else if (existingIndex >= 0) { + return; + } + } + + if (this._showOutputActionAdded) { + return; + } + actionBar.push([showOutputAction], { icon: true, label: false }); + this._showOutputActionAdded = true; + } + + private _clearCommandAssociation(options?: { clearPersistentData?: boolean }): void { + this._terminalCommandUri = undefined; + this._storedCommandId = undefined; + if (options?.clearPersistentData) { + if (this._terminalData.terminalCommandUri) { + delete this._terminalData.terminalCommandUri; + } + if (this._terminalData.terminalToolSessionId) { + delete this._terminalData.terminalToolSessionId; + } + } + this._decoration.update(); + } + + private _registerInstanceListener(terminalInstance: ITerminalInstance): void { + const commandDetectionListener = this._register(new MutableDisposable()); + const tryResolveCommand = async (): Promise => { + const resolvedCommand = this._resolveCommand(terminalInstance); + this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); + return resolvedCommand; + }; + + const attachCommandDetection = async (commandDetection: ICommandDetectionCapability | undefined) => { + commandDetectionListener.clear(); + if (!commandDetection) { + await tryResolveCommand(); + return; + } + + const store = new DisposableStore(); + let receivedDataCount = 0; + + const hasRealOutput = (): boolean => { + // Check for snapshot output + if (this._terminalData.terminalCommandOutput?.text?.trim()) { + return true; + } + // Check for live output (cursor moved past executed marker) + const command = this._getResolvedCommand(terminalInstance); + if (!command?.executedMarker || terminalInstance.isDisposed) { + return false; + } + const buffer = terminalInstance.xterm?.raw.buffer.active; + if (!buffer) { + return false; + } + const cursorLine = buffer.baseY + buffer.cursorY; + if (cursorLine > command.executedMarker.line) { + return true; + } + // If we've received many data events, treat it as real output even if cursor + // hasn't moved past the marker (e.g., progress bars updating on same line) + // Shell integration sequences fire a couple times per command (PromptStart, CommandStart, + // CommandExecuted), so we need a small threshold to filter those out + return receivedDataCount > 2; + }; + + // Use the extracted auto-expand logic + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection, + onWillData: terminalInstance.onWillData, + shouldAutoExpand: () => !this._outputView.isExpanded && !this._userToggledOutput && !this._store.isDisposed && !expandedStateByInvocation.get(this.toolInvocation), + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => { + if (this._isInThinkingContainer) { + this.expandCollapsibleWrapper(); + } + this._toggleOutput(true); + })); + + // Track data events to help hasRealOutput detect progress-style output + store.add(terminalInstance.onWillData(() => { + receivedDataCount++; + })); + + store.add(commandDetection.onCommandExecuted(() => { + this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); + })); + + store.add(commandDetection.onCommandFinished(() => { + this._addActions(terminalInstance, this._terminalData.terminalToolSessionId); + const resolvedCommand = this._getResolvedCommand(terminalInstance); + + // update title + this.markCollapsibleWrapperComplete(); + + // Auto-collapse on success + if (resolvedCommand?.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) { + this._toggleOutput(false); + } + // keep outer wrapper expanded on error + const autoExpandFailures = this._configurationService.getValue(ChatConfiguration.AutoExpandToolFailures); + if (autoExpandFailures && resolvedCommand?.exitCode !== undefined && resolvedCommand.exitCode !== 0 && this._thinkingCollapsibleWrapper) { + this.expandCollapsibleWrapper(); + } + if (resolvedCommand?.endMarker) { + commandDetectionListener.clear(); + } + })); + commandDetectionListener.value = store; + + const resolvedImmediately = await tryResolveCommand(); + if (resolvedImmediately?.endMarker) { + commandDetectionListener.clear(); + // update title + this.markCollapsibleWrapperComplete(); + // Auto-collapse on success + if (resolvedImmediately.exitCode === 0 && this._outputView.isExpanded && !this._userToggledOutput) { + this._toggleOutput(false); + } + // keep outer wrapper expanded on error + const autoExpandFailures = this._configurationService.getValue(ChatConfiguration.AutoExpandToolFailures); + if (autoExpandFailures && resolvedImmediately.exitCode !== undefined && resolvedImmediately.exitCode !== 0 && this._thinkingCollapsibleWrapper) { + this.expandCollapsibleWrapper(); + } + return; + } + }; + + attachCommandDetection(terminalInstance.capabilities.get(TerminalCapability.CommandDetection)); + this._register(terminalInstance.capabilities.onDidAddCommandDetectionCapability(cd => attachCommandDetection(cd))); + + const instanceListener = this._register(terminalInstance.onDisposed(() => { + if (this._terminalInstance === terminalInstance) { + this._terminalInstance = undefined; + } + this._clearCommandAssociation({ clearPersistentData: true }); + commandDetectionListener.clear(); + if (!this._store.isDisposed) { + this._actionBar.clear(); + } + this._removeFocusAction(); + this._showOutputActionAdded = false; + this._showOutputAction.clear(); + this._addActions(undefined, this._terminalData.terminalToolSessionId); + instanceListener.dispose(); + })); + } + + private _removeFocusAction(): void { + if (this._store.isDisposed) { + return; + } + const actionBar = this._actionBar; + const focusAction = this._focusAction.value; + if (actionBar && focusAction) { + const existingIndex = actionBar.viewItems.findIndex(item => item.action === focusAction); + if (existingIndex >= 0) { + actionBar.pull(existingIndex); + } + } + this._focusAction.clear(); + } + + private _removeContinueInBackgroundAction(): void { + if (this._store.isDisposed) { + return; + } + const actionBar = this._actionBar; + const continueAction = this._continueInBackgroundAction.value; + if (actionBar && continueAction) { + const existingIndex = actionBar.viewItems.findIndex(item => item.action === continueAction); + if (existingIndex >= 0) { + actionBar.pull(existingIndex); + } + } + this._continueInBackgroundAction.clear(); + } + + private async _toggleOutput(expanded: boolean): Promise { + const didChange = await this._outputView.toggle(expanded); + const isExpanded = this._outputView.isExpanded; + this._titleElement.classList.toggle('chat-terminal-content-title-no-bottom-radius', isExpanded); + this._showOutputAction.value?.syncPresentation(isExpanded); + if (didChange) { + expandedStateByInvocation.set(this.toolInvocation, isExpanded); + } + return didChange; + } + + private async _ensureTerminalInstance(): Promise { + if (this._terminalInstance?.isDisposed) { + this._terminalInstance = undefined; + } + if (!this._terminalInstance && this._terminalData.terminalToolSessionId) { + this._terminalInstance = await this._terminalChatService.getTerminalInstanceByToolSessionId(this._terminalData.terminalToolSessionId); + if (this._terminalInstance?.isDisposed) { + this._terminalInstance = undefined; + } + } + return this._terminalInstance; + } + + private _handleOutputFocus(): void { + this._terminalOutputContextKey.set(true); + this._terminalChatService.setFocusedProgressPart(this); + this._outputView.updateAriaLabel(); + } + + private _handleOutputBlur(event: FocusEvent): void { + const nextTarget = event.relatedTarget as HTMLElement | null; + if (this._outputView.containsElement(nextTarget)) { + return; + } + this._terminalOutputContextKey.reset(); + this._terminalChatService.clearFocusedProgressPart(this); + } + + private _handleDispose(): void { + this._terminalOutputContextKey.reset(); + this._terminalChatService.clearFocusedProgressPart(this); + } + + public getCommandAndOutputAsText(): string | undefined { + return this._outputView.getCommandAndOutputAsText(); + } + + public focusOutput(): void { + this._outputView.focus(); + } + + private _focusChatInput(): void { + const widget = this._chatWidgetService.getWidgetBySessionResource(this._sessionResource); + widget?.focusInput(); + } + + public async focusTerminal(): Promise { + if (this._focusAction.value) { + await this._focusAction.value.run(); + return; + } + if (this._terminalCommandUri) { + this._terminalService.openResource(this._terminalCommandUri); + } + } + + public async toggleOutputFromKeyboard(): Promise { + this._userToggledOutput = true; + if (!this._outputView.isExpanded) { + await this._toggleOutput(true); + this.focusOutput(); + return; + } + await this._collapseOutputAndFocusInput(); + } + + private async _toggleOutputFromAction(): Promise { + this._userToggledOutput = true; + if (!this._outputView.isExpanded) { + await this._toggleOutput(true); + return; + } + await this._toggleOutput(false); + } + + private async _collapseOutputAndFocusInput(): Promise { + if (this._outputView.isExpanded) { + await this._toggleOutput(false); + } + this._focusChatInput(); + } + + private _resolveCommand(instance: ITerminalInstance): ITerminalCommand | undefined { + if (instance.isDisposed) { + return undefined; + } + const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); + if (!commandDetection) { + return undefined; + } + + const targetId = this._terminalData.terminalCommandId; + if (!targetId) { + return undefined; + } + + const commands = commandDetection.commands; + if (commands && commands.length > 0) { + const fromHistory = commands.find(c => c.id === targetId); + if (fromHistory) { + return fromHistory; + } + } + + const executing = commandDetection.executingCommandObject; + if (executing && executing.id === targetId) { + return executing; + } + + return undefined; + } +} + +class ChatTerminalToolOutputSection extends Disposable { + public readonly domNode: HTMLElement; + + public get isExpanded(): boolean { + return this.domNode.classList.contains('expanded'); + } + + private readonly _outputBody: HTMLElement; + private _scrollableContainer: DomScrollableElement | undefined; + private _isAtBottom: boolean = true; + private _isProgrammaticScroll: boolean = false; + private _mirror: DetachedTerminalCommandMirror | undefined; + private _snapshotMirror: DetachedTerminalSnapshotMirror | undefined; + private readonly _contentContainer: HTMLElement; + private readonly _terminalContainer: HTMLElement; + private readonly _emptyElement: HTMLElement; + private _lastRenderedLineCount: number | undefined; + private _lastRenderedMaxColumnWidth: number | undefined; + + private readonly _onDidFocusEmitter = this._register(new Emitter()); + public get onDidFocus() { return this._onDidFocusEmitter.event; } + private readonly _onDidBlurEmitter = this._register(new Emitter()); + public get onDidBlur() { return this._onDidBlurEmitter.event; } + + constructor( + private readonly _ensureTerminalInstance: () => Promise, + private readonly _resolveCommand: () => ITerminalCommand | undefined, + private readonly _getTerminalCommandOutput: () => IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined, + private readonly _getCommandText: () => string, + private readonly _getStoredTheme: () => IChatTerminalToolInvocationData['terminalTheme'] | undefined, + @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, + @IThemeService private readonly _themeService: IThemeService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + + const containerElements = h('.chat-terminal-output-container@container', [ + h('.chat-terminal-output-body@body', [ + h('.chat-terminal-output-content@content', [ + h('.chat-terminal-output-terminal@terminal'), + h('.chat-terminal-output-empty@empty') + ]) + ]) + ]); + this.domNode = containerElements.container; + this.domNode.classList.add('collapsed'); + this._outputBody = containerElements.body; + this._contentContainer = containerElements.content; + this._terminalContainer = containerElements.terminal; + + this._emptyElement = containerElements.empty; + this._contentContainer.appendChild(this._emptyElement); + + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_IN, () => this._onDidFocusEmitter.fire())); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_OUT, event => this._onDidBlurEmitter.fire(event))); + + const resizeObserver = new ResizeObserver(() => this._handleResize()); + resizeObserver.observe(this.domNode); + this._register(toDisposable(() => resizeObserver.disconnect())); + + this._applyBackgroundColor(); + this._register(this._themeService.onDidColorThemeChange(() => this._applyBackgroundColor())); + } + + public async toggle(expanded: boolean): Promise { + const currentlyExpanded = this.isExpanded; + if (expanded === currentlyExpanded) { + if (expanded) { + await this._updateTerminalContent(); + } + return false; + } + + if (!expanded) { + this._setExpanded(false); + this._isAtBottom = true; + return true; + } + + if (!this._scrollableContainer) { + await this._createScrollableContainer(); + } + await this._updateTerminalContent(); + + // Only now show the expanded state (after content is ready) + this._setExpanded(true); + this._layoutOutput(); + this._scrollOutputToBottom(); + this._scheduleOutputRelayout(); + return true; + } + + public focus(): void { + this._scrollableContainer?.getDomNode().focus(); + } + + public containsElement(element: HTMLElement | null): boolean { + return !!element && this.domNode.contains(element); + } + + public updateAriaLabel(): void { + if (!this._scrollableContainer) { + return; + } + const command = this._resolveCommand(); + const commandText = command?.command ?? this._getCommandText(); + if (!commandText) { + return; + } + const ariaLabel = localize('chatTerminalOutputAriaLabel', 'Terminal output for {0}', commandText); + const scrollableDomNode = this._scrollableContainer.getDomNode(); + scrollableDomNode.setAttribute('role', 'region'); + const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.TerminalChatOutput); + const label = accessibleViewHint + ? ariaLabel + ', ' + accessibleViewHint + : ariaLabel; + scrollableDomNode.setAttribute('aria-label', label); + } + + public getCommandAndOutputAsText(): string | undefined { + const command = this._resolveCommand(); + const commandText = command?.command ?? this._getCommandText(); + if (!commandText) { + return undefined; + } + const commandHeader = localize('chatTerminalOutputAccessibleViewHeader', 'Command: {0}', commandText); + if (command) { + const rawOutput = command.getOutput(); + if (!rawOutput || rawOutput.trim().length === 0) { + return `${commandHeader}\n${localize('chat.terminalOutputEmpty', 'No output was produced by the command.')}`; + } + const lines = rawOutput.split('\n'); + return `${commandHeader}\n${lines.join('\n').trimEnd()}`; + } + + const snapshot = this._getTerminalCommandOutput(); + if (!snapshot) { + return `${commandHeader}\n${localize('chatTerminalOutputUnavailable', 'Command output is no longer available.')}`; + } + const plain = removeAnsiEscapeCodes((snapshot.text ?? '')); + if (!plain.trim().length) { + return `${commandHeader}\n${localize('chat.terminalOutputEmpty', 'No output was produced by the command.')}`; + } + let outputText = plain.trimEnd(); + if (snapshot.truncated) { + outputText += `\n${localize('chatTerminalOutputTruncated', 'Output truncated.')}`; + } + return `${commandHeader}\n${outputText}`; + } + + private _setExpanded(expanded: boolean): void { + this.domNode.classList.toggle('expanded', expanded); + this.domNode.classList.toggle('collapsed', !expanded); + } + + private async _createScrollableContainer(): Promise { + this._scrollableContainer = this._register(new DomScrollableElement(this._outputBody, { + vertical: ScrollbarVisibility.Hidden, + horizontal: ScrollbarVisibility.Hidden, + handleMouseWheel: true + })); + const scrollableDomNode = this._scrollableContainer.getDomNode(); + scrollableDomNode.tabIndex = 0; + this.domNode.appendChild(scrollableDomNode); + this.updateAriaLabel(); + + // Show horizontal scrollbar on hover/focus, hide otherwise to prevent flickering during streaming + this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_ENTER, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Auto }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_LEAVE, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Hidden }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_IN, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Auto }); + })); + this._register(dom.addDisposableListener(this.domNode, dom.EventType.FOCUS_OUT, () => { + this._scrollableContainer?.updateOptions({ horizontal: ScrollbarVisibility.Hidden }); + })); + + // Track scroll state to enable scroll lock behavior (only for user scrolls) + this._register(this._scrollableContainer.onScroll(() => { + if (this._isProgrammaticScroll) { + return; + } + this._isAtBottom = this._computeIsAtBottom(); + })); + } + + private async _updateTerminalContent(): Promise { + const liveTerminalInstance = await this._resolveLiveTerminal(); + const command = liveTerminalInstance ? this._resolveCommand() : undefined; + const snapshot = this._getTerminalCommandOutput(); + + if (liveTerminalInstance && command) { + const handled = await this._renderLiveOutput(liveTerminalInstance, command); + if (handled) { + return; + } + } + + this._disposeLiveMirror(); + + if (snapshot) { + await this._renderSnapshotOutput(snapshot); + return; + } + + this._renderUnavailableMessage(liveTerminalInstance); + } + + private async _renderLiveOutput(liveTerminalInstance: ITerminalInstance, command: ITerminalCommand): Promise { + if (this._mirror) { + return true; + } + await liveTerminalInstance.xtermReadyPromise; + if (this._store.isDisposed || liveTerminalInstance.isDisposed || !liveTerminalInstance.xterm) { + this._disposeLiveMirror(); + return false; + } + const mirror = this._register(this._instantiationService.createInstance(DetachedTerminalCommandMirror, liveTerminalInstance.xterm, command)); + this._mirror = mirror; + this._register(mirror.onDidUpdate(result => { + // Hide empty message as soon as we get output + if (result.lineCount && result.lineCount > 0) { + this._hideEmptyMessage(); + } + this._layoutOutput(result.lineCount, result.maxColumnWidth); + if (this._isAtBottom) { + this._scrollOutputToBottom(); + } + })); + // Forward input from the mirror terminal to the live terminal instance + this._register(mirror.onDidInput(data => { + if (!liveTerminalInstance.isDisposed) { + liveTerminalInstance.sendText(data, false); + } + })); + await mirror.attach(this._terminalContainer); + let result = await mirror.renderCommand(); + // Only show "No output" message if: + // 1. Command has finished (has endMarker), AND + // 2. There's no output after retrying + // If command is still running, don't show the message - output may come later + let commandFinished = !!command.endMarker; + let hasOutput = result && result.lineCount && result.lineCount > 0; + + // If we got no output, poll until either output appears or command finishes + // This handles cases where: + // 1. Command is running but executedMarker isn't set yet (renderCommand returns undefined) + // 2. Command finished quickly but buffer isn't ready yet + if (!hasOutput) { + const maxRetries = 10; + for (let retry = 0; retry < maxRetries && !hasOutput; retry++) { + await timeout(100); + if (this._store.isDisposed) { + return true; + } + result = await mirror.renderCommand(); + hasOutput = result && result.lineCount && result.lineCount > 0; + commandFinished = !!command.endMarker; + // Stop polling if command finished (we'll show "no output" or output) + if (commandFinished) { + break; + } + } + } + + if (!hasOutput) { + if (commandFinished) { + this._showEmptyMessage(localize('chat.terminalOutputEmpty', 'No output was produced by the command.')); + } + // If command is still running, leave content empty but don't show "no output" message + } else { + this._hideEmptyMessage(); + } + this._layoutOutput(result?.lineCount ?? 0, result?.maxColumnWidth); + return true; + } + + private async _renderSnapshotOutput(snapshot: NonNullable): Promise { + if (this._snapshotMirror) { + this._layoutOutput(snapshot.lineCount ?? 0, this._lastRenderedMaxColumnWidth); + return; + } + if (this._store.isDisposed) { + return; + } + dom.clearNode(this._terminalContainer); + this._snapshotMirror = this._register(this._instantiationService.createInstance(DetachedTerminalSnapshotMirror, snapshot, this._getStoredTheme)); + await this._snapshotMirror.attach(this._terminalContainer); + this._snapshotMirror.setOutput(snapshot); + const result = await this._snapshotMirror.render(); + const hasText = !!snapshot.text && snapshot.text.length > 0; + if (hasText) { + this._hideEmptyMessage(); + } else { + this._showEmptyMessage(localize('chat.terminalOutputEmpty', 'No output was produced by the command.')); + } + const lineCount = result?.lineCount ?? snapshot.lineCount ?? 0; + this._layoutOutput(lineCount, result?.maxColumnWidth); + } + + private _renderUnavailableMessage(liveTerminalInstance: ITerminalInstance | undefined): void { + dom.clearNode(this._terminalContainer); + this._lastRenderedLineCount = undefined; + if (!liveTerminalInstance) { + this._showEmptyMessage(localize('chat.terminalOutputTerminalMissing', 'Terminal is no longer available.')); + } else { + this._showEmptyMessage(localize('chat.terminalOutputCommandMissing', 'Command information is not available.')); + } + } + + private async _resolveLiveTerminal(): Promise { + const instance = await this._ensureTerminalInstance(); + return instance && !instance.isDisposed ? instance : undefined; + } + + private _showEmptyMessage(message: string): void { + this._emptyElement.textContent = message; + this._terminalContainer.classList.add('chat-terminal-output-terminal-no-output'); + this.domNode.classList.add('chat-terminal-output-container-no-output'); + } + + private _hideEmptyMessage(): void { + this._emptyElement.textContent = ''; + this._terminalContainer.classList.remove('chat-terminal-output-terminal-no-output'); + this.domNode.classList.remove('chat-terminal-output-container-no-output'); + } + + private _disposeLiveMirror(): void { + if (this._mirror) { + this._mirror.dispose(); + this._mirror = undefined; + } + } + + private _scheduleOutputRelayout(): void { + dom.getActiveWindow().requestAnimationFrame(() => { + this._layoutOutput(); + this._scrollOutputToBottom(); + }); + } + + private _handleResize(): void { + if (!this._scrollableContainer) { + return; + } + if (this.isExpanded) { + this._layoutOutput(); + this._scrollOutputToBottom(); + } else { + this._scrollableContainer.scanDomNode(); + } + } + + private _layoutOutput(lineCount?: number, maxColumnWidth?: number): void { + if (!this._scrollableContainer) { + return; + } + + if (lineCount !== undefined) { + this._lastRenderedLineCount = lineCount; + } else { + lineCount = this._lastRenderedLineCount; + } + + if (maxColumnWidth !== undefined) { + this._lastRenderedMaxColumnWidth = maxColumnWidth; + } else { + maxColumnWidth = this._lastRenderedMaxColumnWidth; + } + + this._scrollableContainer.scanDomNode(); + if (!this.isExpanded || lineCount === undefined) { + return; + } + + const scrollableDomNode = this._scrollableContainer.getDomNode(); + + // Calculate and apply width based on content + this._applyContentWidth(maxColumnWidth); + + const rowHeight = this._computeRowHeightPx(); + const padding = this._getOutputPadding(); + const minHeight = rowHeight * MIN_OUTPUT_ROWS + padding; + const maxHeight = rowHeight * MAX_OUTPUT_ROWS + padding; + const contentHeight = this._getOutputContentHeight(lineCount, rowHeight, padding); + const clampedHeight = Math.min(contentHeight, maxHeight); + const measuredBodyHeight = Math.max(this._outputBody.clientHeight, minHeight); + const appliedHeight = Math.min(clampedHeight, measuredBodyHeight); + scrollableDomNode.style.height = appliedHeight < maxHeight ? `${appliedHeight}px` : ''; + this._scrollableContainer.scanDomNode(); + } + + private _computeIsAtBottom(): boolean { + if (!this._scrollableContainer) { + return true; + } + const dimensions = this._scrollableContainer.getScrollDimensions(); + const scrollPosition = this._scrollableContainer.getScrollPosition(); + // Consider "at bottom" if within a small threshold to account for rounding + const threshold = 5; + return scrollPosition.scrollTop >= dimensions.scrollHeight - dimensions.height - threshold; + } + + private _scrollOutputToBottom(): void { + if (!this._scrollableContainer) { + return; + } + this._isProgrammaticScroll = true; + const dimensions = this._scrollableContainer.getScrollDimensions(); + this._scrollableContainer.setScrollPosition({ scrollTop: dimensions.scrollHeight }); + this._isProgrammaticScroll = false; + } + + private _getOutputContentHeight(lineCount: number, rowHeight: number, padding: number): number { + const contentRows = Math.max(lineCount, MIN_OUTPUT_ROWS); + // Always add an extra row for buffer space to prevent the last line from being cut off during streaming + const adjustedRows = contentRows + 1; + return (adjustedRows * rowHeight) + padding; + } + + private _getOutputPadding(): number { + const style = dom.getComputedStyle(this._outputBody); + const paddingTop = Number.parseFloat(style.paddingTop || '0'); + const paddingBottom = Number.parseFloat(style.paddingBottom || '0'); + return paddingTop + paddingBottom; + } + + private _applyContentWidth(maxColumnWidth?: number): void { + if (!this._scrollableContainer) { + return; + } + + const window = dom.getActiveWindow(); + const font = this._terminalConfigurationService.getFont(window); + const charWidth = font.charWidth; + + if (!charWidth || !maxColumnWidth || maxColumnWidth <= 0) { + // No content width info, leave existing width unchanged + return; + } + + // Calculate the pixel width needed for the content + // Add some padding for scrollbar and visual comfort + // Account for container padding + // Add one extra character width (cursorWidth) to account for the cursor position + // which may be one character beyond the last content character during streaming + const horizontalPadding = 24; + const cursorWidth = charWidth; + const contentWidth = Math.ceil(maxColumnWidth * charWidth) + horizontalPadding + cursorWidth; + + // Get the max available width (container's parent width) + const parentWidth = this.domNode.parentElement?.clientWidth ?? 0; + + const scrollableDomNode = this._scrollableContainer.getDomNode(); + + if (parentWidth > 0 && contentWidth < parentWidth) { + // Content is smaller than available space - shrink to fit + // Apply width to both the scrollable container and the content body + // The xterm element renders at full column width, so we need to clip it + scrollableDomNode.style.width = `${contentWidth}px`; + this._outputBody.style.width = `${contentWidth}px`; + this._terminalContainer.style.width = `${contentWidth}px`; + this._terminalContainer.classList.add('chat-terminal-output-terminal-clipped'); + } else { + // Content needs full width or more (scrollbar will show) + scrollableDomNode.style.width = ''; + this._outputBody.style.width = ''; + this._terminalContainer.style.width = ''; + this._terminalContainer.classList.remove('chat-terminal-output-terminal-clipped'); + } + + this._scrollableContainer.scanDomNode(); + } + + private _computeRowHeightPx(): number { + const window = dom.getActiveWindow(); + const font = this._terminalConfigurationService.getFont(window); + const hasCharHeight = isNumber(font.charHeight) && font.charHeight > 0; + const hasFontSize = isNumber(font.fontSize) && font.fontSize > 0; + const hasLineHeight = isNumber(font.lineHeight) && font.lineHeight > 0; + const charHeight = (hasCharHeight ? font.charHeight : (hasFontSize ? font.fontSize : 1)) ?? 1; + const lineHeight = hasLineHeight ? font.lineHeight : 1; + const rowHeight = Math.ceil(charHeight * lineHeight); + return Math.max(rowHeight, 1); + } + + private _applyBackgroundColor(): void { + const theme = this._themeService.getColorTheme(); + const isInEditor = ChatContextKeys.inChatEditor.getValue(this._contextKeyService); + const backgroundColor = theme.getColor(isInEditor ? editorBackground : PANEL_BACKGROUND); + if (backgroundColor) { + this.domNode.style.backgroundColor = backgroundColor.toString(); + } + } +} + +export class ToggleChatTerminalOutputAction extends Action implements IAction { + private _expanded = false; + + constructor( + private readonly _toggle: () => Promise, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super( + TerminalContribCommandId.ToggleChatTerminalOutput, + localize('showTerminalOutput', 'Show Output'), + ThemeIcon.asClassName(Codicon.chevronRight), + true, + ); + this._updateTooltip(); + } + + public override async run(): Promise { + type ToggleChatTerminalOutputTelemetryEvent = { + previousExpanded: boolean; + }; + + type ToggleChatTerminalOutputTelemetryClassification = { + owner: 'meganrogge'; + comment: 'Track usage of the toggle chat terminal output action.'; + previousExpanded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the terminal output was expanded before the toggle.' }; + }; + this._telemetryService.publicLog2('terminal/chatToggleOutput', { + previousExpanded: this._expanded + }); + await this._toggle(); + } + + public syncPresentation(expanded: boolean): void { + this._expanded = expanded; + this._updatePresentation(); + this._updateTooltip(); + } + + public refreshKeybindingTooltip(): void { + this._updateTooltip(); + } + + private _updatePresentation(): void { + if (this._expanded) { + this.label = localize('hideTerminalOutput', 'Hide Output'); + this.class = ThemeIcon.asClassName(Codicon.chevronDown); + } else { + this.label = localize('showTerminalOutput', 'Show Output'); + this.class = ThemeIcon.asClassName(Codicon.chevronRight); + } + } + + private _updateTooltip(): void { + this.tooltip = this._keybindingService.appendKeybinding(this.label, TerminalContribCommandId.FocusMostRecentChatTerminalOutput); + } +} + +export class FocusChatInstanceAction extends Action implements IAction { + constructor( + private _instance: ITerminalInstance | undefined, + private _command: ITerminalCommand | undefined, + private readonly _commandUri: URI | undefined, + private readonly _commandId: string | undefined, + isTerminalHidden: boolean, + @ITerminalService private readonly _terminalService: ITerminalService, + @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, + @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + ) { + super( + TerminalContribCommandId.FocusChatInstanceAction, + isTerminalHidden ? localize('showTerminal', 'Show and Focus Terminal') : localize('focusTerminal', 'Focus Terminal'), + ThemeIcon.asClassName(Codicon.openInProduct), + true, + ); + this._updateTooltip(); + } + + public override async run() { + this.label = this._instance?.shellLaunchConfig.hideFromUser ? localize('showAndFocusTerminal', 'Show and Focus Terminal') : localize('focusTerminal', 'Focus Terminal'); + this._updateTooltip(); + + let target: FocusChatInstanceTelemetryEvent['target'] = 'none'; + let location: FocusChatInstanceTelemetryEvent['location'] = 'panel'; + if (this._instance) { + target = 'instance'; + location = this._instance.target === TerminalLocation.Editor ? 'editor' : 'panel'; + } else if (this._commandUri) { + target = 'commandUri'; + } + + type FocusChatInstanceTelemetryEvent = { + target: 'instance' | 'commandUri' | 'none'; + location: 'panel' | 'editor'; + }; + + type FocusChatInstanceTelemetryClassification = { + owner: 'meganrogge'; + comment: 'Track usage of the focus chat terminal action.'; + target: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether focusing targeted an existing instance or opened a command URI.' }; + location: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Location of the terminal instance when focusing.' }; + }; + this._telemetryService.publicLog2('terminal/chatFocusInstance', { + target, + location + }); + + if (this._instance) { + this._terminalService.setActiveInstance(this._instance); + if (this._instance.target === TerminalLocation.Editor) { + this._terminalEditorService.openEditor(this._instance); + } else { + await this._terminalGroupService.showPanel(true); + } + this._terminalService.setActiveInstance(this._instance); + await this._instance.focusWhenReady(true); + const command = this._resolveCommand(); + if (command) { + this._instance.xterm?.markTracker.revealCommand(command); + } + return; + } + + if (this._commandUri) { + this._terminalService.openResource(this._commandUri); + } + } + + public refreshKeybindingTooltip(): void { + this._updateTooltip(); + } + + private _resolveCommand(): ITerminalCommand | undefined { + if (this._command && !this._command.endMarker?.isDisposed) { + return this._command; + } + if (!this._instance || !this._commandId) { + return this._command; + } + const commandDetection = this._instance.capabilities.get(TerminalCapability.CommandDetection); + const resolved = commandDetection?.commands.find(c => c.id === this._commandId); + if (resolved) { + this._command = resolved; + } + return this._command; + } + + private _updateTooltip(): void { + this.tooltip = this._keybindingService.appendKeybinding(this.label, TerminalContribCommandId.FocusMostRecentChatTerminal); + } +} + +export class ContinueInBackgroundAction extends Action implements IAction { + constructor( + private readonly _terminalToolSessionId: string, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, + ) { + super( + TerminalContribCommandId.ContinueInBackground, + localize('continueInBackground', 'Continue in Background'), + ThemeIcon.asClassName(Codicon.debugContinue), + true, + ); + } + + public override async run(): Promise { + this._terminalChatService.continueInBackground(this._terminalToolSessionId); + } +} + +class ChatTerminalThinkingCollapsibleWrapper extends ChatCollapsibleContentPart { + private readonly _terminalContentElement: HTMLElement; + private readonly _commandText: string; + private _isComplete: boolean; + + constructor( + commandText: string, + contentElement: HTMLElement, + context: IChatContentPartRenderContext, + initialExpanded: boolean, + isComplete: boolean, + @IHoverService hoverService: IHoverService, + ) { + const title = isComplete ? `Ran \`${commandText}\`` : `Running \`${commandText}\``; + super(title, context, undefined, hoverService); + + this._terminalContentElement = contentElement; + this._commandText = commandText; + this._isComplete = isComplete; + + this.domNode.classList.add('chat-terminal-thinking-collapsible'); + + this._setCodeFormattedTitle(); + this.setExpanded(initialExpanded); + } + + private _setCodeFormattedTitle(): void { + if (!this._collapseButton) { + return; + } + + const labelElement = this._collapseButton.labelElement; + labelElement.textContent = ''; + + const prefixText = this._isComplete + ? localize('chat.terminal.ran.prefix', "Ran ") + : localize('chat.terminal.running.prefix', "Running "); + const ranText = document.createTextNode(prefixText); + const codeElement = document.createElement('code'); + codeElement.textContent = this._commandText; + + labelElement.appendChild(ranText); + labelElement.appendChild(codeElement); + } + + public markComplete(): void { + if (this._isComplete) { + return; + } + this._isComplete = true; + this._setCodeFormattedTitle(); + } + + protected override initContent(): HTMLElement { + const listWrapper = dom.$('.chat-used-context-list.chat-terminal-thinking-content'); + listWrapper.appendChild(this._terminalContentElement); + return listWrapper; + } + + public expand(): void { + this.setExpanded(true); + } + + override hasSameContent(_other: IChatRendererContent, _followingContent: IChatRendererContent[], _element: ChatTreeItem): boolean { + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts new file mode 100644 index 00000000000..ffecd3be045 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolConfirmationSubPart.ts @@ -0,0 +1,341 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { Separator } from '../../../../../../../base/common/actions.js'; +import { RunOnceScheduler } from '../../../../../../../base/common/async.js'; +import { IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { count } from '../../../../../../../base/common/strings.js'; +import { isEmptyObject } from '../../../../../../../base/common/types.js'; +import { generateUuid } from '../../../../../../../base/common/uuid.js'; +import { ElementSizeObserver } from '../../../../../../../editor/browser/config/elementSizeObserver.js'; +import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../../nls.js'; +import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IMarkerData, IMarkerService, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; +import { IChatToolInvocation, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { createToolInputUri, createToolSchemaUri, ILanguageModelToolsService, IToolConfirmationMessages } from '../../../../common/tools/languageModelToolsService.js'; +import { ILanguageModelToolsConfirmationService } from '../../../../common/tools/languageModelToolsConfirmationService.js'; +import { AcceptToolConfirmationActionId, SkipToolConfirmationActionId } from '../../../actions/chatToolActions.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { renderFileWidgets } from '../chatInlineAnchorWidget.js'; +import { ICodeBlockRenderOptions } from '../codeBlockPart.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { IChatMarkdownAnchorService } from '../chatMarkdownAnchorService.js'; +import { ChatMarkdownContentPart } from '../chatMarkdownContentPart.js'; +import { AbstractToolConfirmationSubPart } from './abstractToolConfirmationSubPart.js'; +import { EditorPool } from '../chatContentCodePools.js'; + +const SHOW_MORE_MESSAGE_HEIGHT_TRIGGER = 45; + +export class ToolConfirmationSubPart extends AbstractToolConfirmationSubPart { + private markdownParts: ChatMarkdownContentPart[] = []; + public get codeblocks(): IChatCodeBlockInfo[] { + return this.markdownParts.flatMap(part => part.codeblocks); + } + + constructor( + toolInvocation: IChatToolInvocation, + context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly codeBlockStartIndex: number, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatWidgetService chatWidgetService: IChatWidgetService, + @ICommandService private readonly commandService: ICommandService, + @IMarkerService private readonly markerService: IMarkerService, + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @IChatMarkdownAnchorService private readonly chatMarkdownAnchorService: IChatMarkdownAnchorService, + @ILanguageModelToolsConfirmationService private readonly confirmationService: ILanguageModelToolsConfirmationService, + ) { + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation || !state.confirmationMessages?.title) { + throw new Error('Confirmation messages are missing'); + } + + super(toolInvocation, context, instantiationService, keybindingService, contextKeyService, chatWidgetService, languageModelToolsService); + + this.render({ + allowActionId: AcceptToolConfirmationActionId, + skipActionId: SkipToolConfirmationActionId, + allowLabel: state.confirmationMessages.confirmResults ? localize('allowReview', "Allow and Review") : localize('allow', "Allow"), + skipLabel: localize('skip.detail', 'Proceed without running this tool'), + partType: 'chatToolConfirmation', + subtitle: typeof toolInvocation.originMessage === 'string' ? toolInvocation.originMessage : toolInvocation.originMessage?.value, + }); + } + + protected override additionalPrimaryActions() { + const actions = super.additionalPrimaryActions(); + + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return actions; + } + + if (state.confirmationMessages?.allowAutoConfirm !== false) { + // Get actions from confirmation service + const confirmActions = this.confirmationService.getPreConfirmActions({ + toolId: this.toolInvocation.toolId, + source: this.toolInvocation.source, + parameters: state.parameters, + chatSessionResource: this.context.element.sessionResource + }); + + for (const action of confirmActions) { + if (action.divider) { + actions.push(new Separator()); + } + actions.push({ + label: action.label, + tooltip: action.detail, + data: async () => { + const shouldConfirm = await action.select(); + if (shouldConfirm) { + this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction }); + } + } + }); + } + } + if (state.confirmationMessages?.confirmResults) { + actions.unshift( + { + label: localize('allowSkip', 'Allow and Skip Reviewing Result'), + data: () => { + (state.confirmationMessages as IToolConfirmationMessages).confirmResults = undefined; + this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction }); + } + }, + new Separator(), + ); + } + + return actions; + } + + protected createContentElement(): HTMLElement | string { + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + const { message, disclaimer } = state.confirmationMessages!; + const toolInvocation = this.toolInvocation as IChatToolInvocation; + + if (typeof message === 'string' && !disclaimer) { + return message; + } else { + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + verticalPadding: 5, + editorOptions: { + tabFocusMode: true, + ariaLabel: this.getTitle(), + }, + }; + + const elements = dom.h('div', [ + dom.h('.message@messageContainer', [ + dom.h('.message-wrapper@message'), + dom.h('.see-more@showMore', [ + dom.h('a', [localize('showMore', "Show More")]) + ]), + ]), + dom.h('.editor@editor'), + dom.h('.disclaimer@disclaimer'), + ]); + + if (toolInvocation.toolSpecificData?.kind === 'input' && toolInvocation.toolSpecificData.rawInput && !isEmptyObject(toolInvocation.toolSpecificData.rawInput)) { + + const titleEl = document.createElement('h3'); + titleEl.textContent = localize('chat.input', "Input"); + elements.editor.appendChild(titleEl); + + const inputData = toolInvocation.toolSpecificData; + + const codeBlockRenderOptions: ICodeBlockRenderOptions = { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { + wordWrap: 'off', + readOnly: false, + ariaLabel: this.getTitle(), + } + }; + + const langId = this.languageService.getLanguageIdByLanguageName('json'); + const rawJsonInput = JSON.stringify(inputData.rawInput ?? {}, null, 1); + const canSeeMore = count(rawJsonInput, '\n') > 2; // if more than one key:value + const model = this._register(this.modelService.createModel( + // View a single JSON line by default until they 'see more' + rawJsonInput.replace(/\n */g, ' '), + this.languageService.createById(langId), + createToolInputUri(toolInvocation.toolCallId), + true + )); + + const markerOwner = generateUuid(); + const schemaUri = createToolSchemaUri(toolInvocation.toolId); + const validator = new RunOnceScheduler(async () => { + + const newMarker: IMarkerData[] = []; + + type JsonDiagnostic = { + message: string; + range: { line: number; character: number }[]; + severity: string; + code?: string | number; + }; + + const result = await this.commandService.executeCommand('json.validate', schemaUri, model.getValue()); + for (const item of result ?? []) { + if (item.range && item.message) { + newMarker.push({ + severity: item.severity === 'Error' ? MarkerSeverity.Error : MarkerSeverity.Warning, + message: item.message, + startLineNumber: item.range[0].line + 1, + startColumn: item.range[0].character + 1, + endLineNumber: item.range[1].line + 1, + endColumn: item.range[1].character + 1, + code: item.code ? String(item.code) : undefined + }); + } + } + + this.markerService.changeOne(markerOwner, model.uri, newMarker); + }, 500); + + validator.schedule(); + this._register(model.onDidChangeContent(() => validator.schedule())); + this._register(toDisposable(() => this.markerService.remove(markerOwner, [model.uri]))); + this._register(validator); + + const editor = this._register(this.editorPool.get()); + editor.object.render({ + codeBlockIndex: this.codeBlockStartIndex, + codeBlockPartIndex: 0, + element: this.context.element, + languageId: langId ?? 'json', + renderOptions: codeBlockRenderOptions, + textModel: Promise.resolve(model), + chatSessionResource: this.context.element.sessionResource + }, this.currentWidthDelegate()); + this.codeblocks.push({ + codeBlockIndex: this.codeBlockStartIndex, + codemapperUri: undefined, + elementId: this.context.element.id, + focus: () => editor.object.focus(), + ownerMarkdownPartId: this.codeblocksPartId, + uri: model.uri, + uriPromise: Promise.resolve(model.uri), + chatSessionResource: this.context.element.sessionResource + }); + this._register(model.onDidChangeContent(e => { + try { + inputData.rawInput = JSON.parse(model.getValue()); + } catch { + // ignore + } + })); + + elements.editor.append(editor.object.element); + + if (canSeeMore) { + const seeMore = dom.h('div.see-more', [dom.h('a@link')]); + seeMore.link.textContent = localize('seeMore', "See more"); + this._register(dom.addDisposableGenericMouseDownListener(seeMore.link, () => { + try { + const parsed = JSON.parse(model.getValue()); + model.setValue(JSON.stringify(parsed, null, 2)); + editor.object.editor.updateOptions({ tabFocusMode: false }); + editor.object.editor.updateOptions({ wordWrap: 'on' }); + } catch { + // ignored + } + seeMore.root.remove(); + })); + elements.editor.append(seeMore.root); + } + } + + const mdPart = this._makeMarkdownPart(elements.message, message!, codeBlockRenderOptions); + + const messageSeeMoreObserver = this._register(new ElementSizeObserver(mdPart.domNode, undefined)); + const updateSeeMoreDisplayed = () => { + const show = messageSeeMoreObserver.getHeight() > SHOW_MORE_MESSAGE_HEIGHT_TRIGGER; + if (elements.messageContainer.classList.contains('can-see-more') !== show) { + elements.messageContainer.classList.toggle('can-see-more', show); + } + }; + + this._register(dom.addDisposableListener(elements.showMore, 'click', () => { + elements.messageContainer.classList.toggle('can-see-more', false); + messageSeeMoreObserver.dispose(); + })); + + + this._register(messageSeeMoreObserver.onDidChange(updateSeeMoreDisplayed)); + messageSeeMoreObserver.startObserving(); + + if (disclaimer) { + this._makeMarkdownPart(elements.disclaimer, disclaimer, codeBlockRenderOptions); + } else { + elements.disclaimer.remove(); + } + + return elements.root; + } + } + + protected getTitle(): string { + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForConfirmation) { + return ''; + } + const title = state.confirmationMessages?.title; + if (!title) { + return ''; + } + return typeof title === 'string' ? title : title.value; + } + + private _makeMarkdownPart(container: HTMLElement, message: string | IMarkdownString, codeBlockRenderOptions: ICodeBlockRenderOptions) { + const part = this._register(this.instantiationService.createInstance(ChatMarkdownContentPart, + { + kind: 'markdownContent', + content: typeof message === 'string' ? new MarkdownString().appendMarkdown(message) : message, + }, + this.context, + this.editorPool, + false, + this.codeBlockStartIndex, + this.renderer, + undefined, + this.currentWidthDelegate(), + this.codeBlockModelCollection, + { codeBlockRenderOptions }, + )); + renderFileWidgets(part.domNode, this.instantiationService, this.chatMarkdownAnchorService, this._store); + container.append(part.domNode); + + return part; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts new file mode 100644 index 00000000000..eaa071c1b8d --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationPart.ts @@ -0,0 +1,245 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { autorun, derived } from '../../../../../../../base/common/observable.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { IChatRendererContent } from '../../../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { isToolResultInputOutputDetails, isToolResultOutputDetails, ToolInvocationPresentation } from '../../../../common/tools/languageModelToolsService.js'; +import { ChatTreeItem, IChatCodeBlockInfo } from '../../../chat.js'; +import { EditorPool } from '../chatContentCodePools.js'; +import { IChatContentPart, IChatContentPartRenderContext } from '../chatContentParts.js'; +import { CollapsibleListPool } from '../chatReferencesContentPart.js'; +import { ExtensionsInstallConfirmationWidgetSubPart } from './chatExtensionsInstallToolSubPart.js'; +import { ChatInputOutputMarkdownProgressPart } from './chatInputOutputMarkdownProgressPart.js'; +import { ChatMcpAppSubPart, IMcpAppRenderData } from './chatMcpAppSubPart.js'; +import { ChatResultListSubPart } from './chatResultListSubPart.js'; +import { ChatTerminalToolConfirmationSubPart } from './chatTerminalToolConfirmationSubPart.js'; +import { ChatTerminalToolProgressPart } from './chatTerminalToolProgressPart.js'; +import { ToolConfirmationSubPart } from './chatToolConfirmationSubPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { ChatToolOutputSubPart } from './chatToolOutputPart.js'; +import { ChatToolPostExecuteConfirmationPart } from './chatToolPostExecuteConfirmationPart.js'; +import { ChatToolProgressSubPart } from './chatToolProgressPart.js'; +import { ChatToolStreamingSubPart } from './chatToolStreamingSubPart.js'; + +export class ChatToolInvocationPart extends Disposable implements IChatContentPart { + public readonly domNode: HTMLElement; + + public get codeblocks(): IChatCodeBlockInfo[] { + const codeblocks = this.subPart?.codeblocks ?? []; + if (this.mcpAppPart) { + codeblocks.push(...this.mcpAppPart.codeblocks); + } + return codeblocks; + } + + public get codeblocksPartId(): string | undefined { + return this.subPart?.codeblocksPartId; + } + + private subPart!: BaseChatToolInvocationSubPart; + private mcpAppPart: ChatMcpAppSubPart | undefined; + + private readonly _onDidRemount = this._register(new Emitter()); + + constructor( + private readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + private readonly listPool: CollapsibleListPool, + private readonly editorPool: EditorPool, + private readonly currentWidthDelegate: () => number, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly announcedToolProgressKeys: Set | undefined, + private readonly codeBlockStartIndex: number, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.domNode = dom.$('.chat-tool-invocation-part'); + if (toolInvocation.presentation === 'hidden') { + return; + } + + if (toolInvocation.kind === 'toolInvocation') { + const initialState = toolInvocation.state.get().type; + this._register(autorun(reader => { + if (toolInvocation.state.read(reader).type !== initialState) { + render(); + } + })); + } + + // This part is a bit different, since IChatToolInvocation is not an immutable model object. So this part is able to rerender itself. + // If this turns out to be a typical pattern, we could come up with a more reusable pattern, like telling the list to rerender an element + // when the model changes, or trying to make the model immutable and swap out one content part for a new one based on user actions in the view. + // Note that `node.replaceWith` is used to ensure order is preserved when an mpc app is present. + const partStore = this._register(new DisposableStore()); + let subPartDomNode: HTMLElement = document.createElement('div'); + this.domNode.appendChild(subPartDomNode); + + const render = () => { + partStore.clear(); + + if (toolInvocation.presentation === ToolInvocationPresentation.HiddenAfterComplete && IChatToolInvocation.isComplete(toolInvocation)) { + return; + } + + this.subPart = partStore.add(this.createToolInvocationSubPart()); + subPartDomNode.replaceWith(this.subPart.domNode); + subPartDomNode = this.subPart.domNode; + + // Add class when displaying a confirmation widget + const isConfirmation = this.subPart instanceof ToolConfirmationSubPart || + this.subPart instanceof ChatTerminalToolConfirmationSubPart || + this.subPart instanceof ExtensionsInstallConfirmationWidgetSubPart || + this.subPart instanceof ChatToolPostExecuteConfirmationPart; + this.domNode.classList.toggle('has-confirmation', isConfirmation); + + partStore.add(this.subPart.onNeedsRerender(render)); + }; + + const mcpAppRenderData = this.getMcpAppRenderData(); + if (mcpAppRenderData) { + const shouldRender = derived(r => { + const outcome = IChatToolInvocation.executionConfirmedOrDenied(toolInvocation, r); + return !!outcome && outcome.type !== ToolConfirmKind.Denied && outcome.type !== ToolConfirmKind.Skipped; + }); + + let appDomNode: HTMLElement = document.createElement('div'); + this.domNode.appendChild(appDomNode); + + this._register(autorun(r => { + if (shouldRender.read(r)) { + this.mcpAppPart = r.store.add(this.instantiationService.createInstance( + ChatMcpAppSubPart, + this.toolInvocation, + this._onDidRemount.event, + context, + mcpAppRenderData, + )); + appDomNode.replaceWith(this.mcpAppPart.domNode); + appDomNode = this.mcpAppPart.domNode; + } else { + this.mcpAppPart = undefined; + dom.clearNode(appDomNode); + } + })); + } + + render(); + } + + private createToolInvocationSubPart(): BaseChatToolInvocationSubPart { + if (this.toolInvocation.kind === 'toolInvocation') { + if (this.toolInvocation.toolSpecificData?.kind === 'extensions') { + return this.instantiationService.createInstance(ExtensionsInstallConfirmationWidgetSubPart, this.toolInvocation, this.context); + } + const state = this.toolInvocation.state.get(); + + // Handle streaming state - show streaming progress + if (state.type === IChatToolInvocation.StateKind.Streaming) { + return this.instantiationService.createInstance(ChatToolStreamingSubPart, this.toolInvocation, this.context, this.renderer); + } + + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { + return this.instantiationService.createInstance(ChatTerminalToolConfirmationSubPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); + } else { + return this.instantiationService.createInstance(ToolConfirmationSubPart, this.toolInvocation, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockModelCollection, this.codeBlockStartIndex); + } + } + if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return this.instantiationService.createInstance(ChatToolPostExecuteConfirmationPart, this.toolInvocation, this.context); + } + } + + if (this.toolInvocation.toolSpecificData?.kind === 'terminal') { + return this.instantiationService.createInstance(ChatTerminalToolProgressPart, this.toolInvocation, this.toolInvocation.toolSpecificData, this.context, this.renderer, this.editorPool, this.currentWidthDelegate, this.codeBlockStartIndex, this.codeBlockModelCollection); + } + + const resultDetails = IChatToolInvocation.resultDetails(this.toolInvocation); + if (Array.isArray(resultDetails) && resultDetails.length) { + return this.instantiationService.createInstance(ChatResultListSubPart, this.toolInvocation, this.context, this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, resultDetails, this.listPool); + } + + if (isToolResultOutputDetails(resultDetails)) { + return this.instantiationService.createInstance(ChatToolOutputSubPart, this.toolInvocation, this.context, this._onDidRemount.event); + } + + if (isToolResultInputOutputDetails(resultDetails)) { + return this.instantiationService.createInstance( + ChatInputOutputMarkdownProgressPart, + this.toolInvocation, + this.context, + this.codeBlockStartIndex, + this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage, + this.toolInvocation.originMessage, + resultDetails.input, + resultDetails.output, + !!resultDetails.isError, + ); + } + + if (this.toolInvocation.kind === 'toolInvocation' && this.toolInvocation.toolSpecificData?.kind === 'input' && !IChatToolInvocation.isComplete(this.toolInvocation)) { + return this.instantiationService.createInstance( + ChatInputOutputMarkdownProgressPart, + this.toolInvocation, + this.context, + this.codeBlockStartIndex, + this.toolInvocation.invocationMessage, + this.toolInvocation.originMessage, + typeof this.toolInvocation.toolSpecificData.rawInput === 'string' ? this.toolInvocation.toolSpecificData.rawInput : JSON.stringify(this.toolInvocation.toolSpecificData.rawInput, null, 2), + undefined, + false, + ); + } + + return this.instantiationService.createInstance(ChatToolProgressSubPart, this.toolInvocation, this.context, this.renderer, this.announcedToolProgressKeys); + } + + /** + * Gets MCP App render data if this tool invocation has MCP App UI. + * Returns data from either: + * - toolSpecificData.mcpAppData (for in-progress tools) + * - result details mcpOutput (for completed tools) + */ + private getMcpAppRenderData(): IMcpAppRenderData | undefined { + const toolSpecificData = this.toolInvocation.toolSpecificData; + if (toolSpecificData?.kind === 'input' && toolSpecificData.mcpAppData) { + const rawInput = typeof toolSpecificData.rawInput === 'string' + ? toolSpecificData.rawInput + : JSON.stringify(toolSpecificData.rawInput, null, 2); + + return { + resourceUri: toolSpecificData.mcpAppData.resourceUri, + serverDefinitionId: toolSpecificData.mcpAppData.serverDefinitionId, + collectionId: toolSpecificData.mcpAppData.collectionId, + input: rawInput, + sessionResource: this.context.element.sessionResource, + }; + } + + return undefined; + } + + onDidRemount(): void { + this._onDidRemount.fire(); + } + + hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { + return (other.kind === 'toolInvocation' || other.kind === 'toolInvocationSerialized') && this.toolInvocation.toolCallId === other.toolCallId; + } + + addDisposable(disposable: IDisposable): void { + this._register(disposable); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts new file mode 100644 index 00000000000..036d5e01d63 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolInvocationSubPart.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { Emitter } from '../../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; + +export abstract class BaseChatToolInvocationSubPart extends Disposable { + protected static idPool = 0; + public abstract readonly domNode: HTMLElement; + + protected _onNeedsRerender = this._register(new Emitter()); + public readonly onNeedsRerender = this._onNeedsRerender.event; + + public abstract codeblocks: IChatCodeBlockInfo[]; + + private readonly _codeBlocksPartId = 'tool-' + (BaseChatToolInvocationSubPart.idPool++); + + public get codeblocksPartId() { + return this._codeBlocksPartId; + } + + constructor( + protected readonly toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + ) { + super(); + } + + protected getIcon() { + const toolInvocation = this.toolInvocation; + const confirmState = IChatToolInvocation.executionConfirmedOrDenied(toolInvocation); + const isSkipped = confirmState?.type === ToolConfirmKind.Skipped; + if (isSkipped) { + return Codicon.circleSlash; + } + + return confirmState?.type === ToolConfirmKind.Denied ? + Codicon.error : + IChatToolInvocation.isComplete(toolInvocation) ? + Codicon.check : ThemeIcon.modify(Codicon.loading, 'spin'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts new file mode 100644 index 00000000000..c572c8886b8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputPart.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { renderMarkdown } from '../../../../../../../base/browser/markdownRenderer.js'; +import { decodeBase64 } from '../../../../../../../base/common/buffer.js'; +import { CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { isCancellationError } from '../../../../../../../base/common/errors.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { generateUuid } from '../../../../../../../base/common/uuid.js'; +import { localize } from '../../../../../../../nls.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatToolInvocation, IChatToolInvocationSerialized, IToolResultOutputDetailsSerialized } from '../../../../common/chatService/chatService.js'; +import { IToolResultOutputDetails } from '../../../../common/tools/languageModelToolsService.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { IChatOutputRendererService } from '../../../chatOutputItemRenderer.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatProgressSubPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; +import { IChatToolOutputStateCache, IOutputState } from './chatToolOutputStateCache.js'; + +// TODO: see if we can reuse existing types instead of adding ChatToolOutputSubPart +export class ChatToolOutputSubPart extends BaseChatToolInvocationSubPart { + + public readonly domNode: HTMLElement; + + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + private readonly _disposeCts = this._register(new CancellationTokenSource()); + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + private readonly context: IChatContentPartRenderContext, + private readonly onDidRemount: Event, + @IChatOutputRendererService private readonly chatOutputItemRendererService: IChatOutputRendererService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatToolOutputStateCache private readonly stateCache: IChatToolOutputStateCache, + ) { + super(toolInvocation); + + const details: IToolResultOutputDetails = toolInvocation.kind === 'toolInvocation' + ? IChatToolInvocation.resultDetails(toolInvocation) as IToolResultOutputDetails + : { + output: { + type: 'data', + mimeType: (toolInvocation.resultDetails as IToolResultOutputDetailsSerialized).output.mimeType, + value: decodeBase64((toolInvocation.resultDetails as IToolResultOutputDetailsSerialized).output.base64Data), + }, + }; + + this.domNode = dom.$('div.tool-output-part'); + + if (toolInvocation.invocationMessage) { + const titleEl = dom.$('.output-title'); + this.domNode.appendChild(titleEl); + if (typeof toolInvocation.invocationMessage === 'string') { + titleEl.textContent = toolInvocation.invocationMessage; + } else { + const md = this._register(renderMarkdown(toolInvocation.invocationMessage)); + titleEl.appendChild(md.element); + } + } + + this.domNode.appendChild(this.createOutputPart(toolInvocation, details)); + } + + public override dispose(): void { + this._disposeCts.dispose(true); + super.dispose(); + } + + private createOutputPart(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, details: IToolResultOutputDetails): HTMLElement { + const parent = dom.$('div.webview-output'); + parent.style.maxHeight = '80vh'; + + // Try to restore cached state, or create new state + const partState: IOutputState = this.stateCache.get(toolInvocation.toolCallId) ?? { height: 0, webviewOrigin: generateUuid() }; + + // Always update the cache with the current state reference + this.stateCache.set(toolInvocation.toolCallId, partState); + + if (partState.height) { + parent.style.height = `${partState.height}px`; + } + if (partState.webviewOrigin) { + partState.webviewOrigin = partState.webviewOrigin; + } + + const progressMessage = dom.$('span'); + progressMessage.textContent = localize('loading', 'Rendering tool output...'); + const progressPart = this._register(this.instantiationService.createInstance(ChatProgressSubPart, progressMessage, ThemeIcon.modify(Codicon.loading, 'spin'), undefined)); + parent.appendChild(progressPart.domNode); + + // TODO: we also need to show the tool output in the UI + this.chatOutputItemRendererService.renderOutputPart(details.output.mimeType, details.output.value.buffer, parent, { origin: partState.webviewOrigin, webviewState: partState.webviewState }, this._disposeCts.token).then((renderedItem) => { + if (this._disposeCts.token.isCancellationRequested) { + return; + } + + this._register(renderedItem); + + progressPart.domNode.remove(); + + this._register(renderedItem.webview.onDidUpdateState(e => { + partState.webviewState = e; + })); + + this._register(renderedItem.onDidChangeHeight(newHeight => { + partState.height = newHeight; + })); + + this._register(renderedItem.webview.onDidWheel(e => { + this.chatWidgetService.getWidgetBySessionResource(this.context.element.sessionResource)?.delegateScrollFromMouseWheelEvent({ + ...e, + preventDefault: () => { }, + stopPropagation: () => { } + }); + })); + + // When the webview is disconnected from the DOM due to being hidden, we need to reload it when it is shown again. + this._register(this.context.onDidChangeVisibility(visible => { + if (visible) { + renderedItem.reinitialize(); + } + })); + + this._register(this.onDidRemount(() => { + renderedItem.reinitialize(); + })); + }, (error) => { + if (isCancellationError(error)) { + return; + } + + console.error('Error rendering tool output:', error); + + const errorNode = dom.$('.output-error'); + + const errorHeaderNode = dom.$('.output-error-header'); + dom.append(errorNode, errorHeaderNode); + + const iconElement = dom.$('div'); + iconElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.error)); + errorHeaderNode.append(iconElement); + + const errorTitleNode = dom.$('.output-error-title'); + errorTitleNode.textContent = localize('chat.toolOutputError', "Error rendering the tool output"); + errorHeaderNode.append(errorTitleNode); + + const errorMessageNode = dom.$('.output-error-details'); + errorMessageNode.textContent = error?.message || String(error); + errorNode.append(errorMessageNode); + + progressPart.domNode.replaceWith(errorNode); + }); + + return parent; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputStateCache.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputStateCache.ts new file mode 100644 index 00000000000..a95bd38425e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolOutputStateCache.ts @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../../../../base/common/event.js'; +import { LRUCache } from '../../../../../../../base/common/map.js'; +import { createDecorator } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget, WillSaveStateReason } from '../../../../../../../platform/storage/common/storage.js'; +import { InstantiationType, registerSingleton } from '../../../../../../../platform/instantiation/common/extensions.js'; + +export interface IOutputState { + webviewOrigin: string; + height: number; + webviewState?: string; +} + +export const IChatToolOutputStateCache = createDecorator('IChatToolOutputStateCache'); + +export interface IChatToolOutputStateCache { + readonly _serviceBrand: undefined; + + get(toolCallId: string): IOutputState | undefined; + set(toolCallId: string, state: IOutputState): void; +} + +const CACHE_STORAGE_KEY = 'chat/toolOutputStateCache'; +const CACHE_LIMIT = 100; + +export class ChatToolOutputStateCache implements IChatToolOutputStateCache { + + declare readonly _serviceBrand: undefined; + + private readonly _cache = new LRUCache(CACHE_LIMIT, 0.75); + + constructor(@IStorageService storageService: IStorageService) { + // Restore cached states from storage + const raw = storageService.get(CACHE_STORAGE_KEY, StorageScope.WORKSPACE, '{}'); + this._deserialize(raw); + + // Store cached states on shutdown + const onWillSaveStateBecauseOfShutdown = Event.filter(storageService.onWillSaveState, e => e.reason === WillSaveStateReason.SHUTDOWN); + Event.once(onWillSaveStateBecauseOfShutdown)(() => { + storageService.store(CACHE_STORAGE_KEY, this._serialize(), StorageScope.WORKSPACE, StorageTarget.MACHINE); + }); + } + + get(toolCallId: string): IOutputState | undefined { + return this._cache.get(toolCallId); + } + + set(toolCallId: string, state: IOutputState): void { + this._cache.set(toolCallId, state); + } + + private _serialize(): string { + const data: Record = Object.create(null); + for (const [key, value] of this._cache) { + data[key] = value; + } + return JSON.stringify(data); + } + + private _deserialize(raw: string): void { + try { + const data: Record = JSON.parse(raw); + for (const key in data) { + const state = data[key]; + // Validate the shape of the cached data + if (typeof state.webviewOrigin === 'string' && typeof state.height === 'number') { + this._cache.set(key, state); + } + } + } catch { + // ignore parse errors + } + } +} + +registerSingleton(IChatToolOutputStateCache, ChatToolOutputStateCache, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts new file mode 100644 index 00000000000..aa7a82177c6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPartUtilities.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createMarkdownCommandLink, IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { localize } from '../../../../../../../nls.js'; +import { ConfirmedReason, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; + +/** + * Creates a markdown message explaining why a tool was auto-approved. + * @param toolInvocation The tool invocation to get the approval message for + * @returns A markdown string with the approval message, or undefined if no message should be shown + */ +export function getToolApprovalMessage(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): IMarkdownString | undefined { + const reason = IChatToolInvocation.executionConfirmedOrDenied(toolInvocation); + if (!reason || typeof reason === 'boolean') { + return undefined; + } + + return getApprovalMessageFromReason(reason); +} + +/** + * Creates a markdown message from a ConfirmedReason explaining why a tool was auto-approved. + * @param reason The confirmation reason + * @returns A markdown string with the approval message, or undefined if no message should be shown + */ +export function getApprovalMessageFromReason(reason: ConfirmedReason): IMarkdownString | undefined { + let md: string; + switch (reason.type) { + case ToolConfirmKind.Setting: + md = localize('chat.autoapprove.setting', 'Auto approved by {0}', createMarkdownCommandLink({ title: '`' + reason.id + '`', id: 'workbench.action.openSettings', arguments: [reason.id] }, false)); + break; + case ToolConfirmKind.LmServicePerTool: + md = reason.scope === 'session' + ? localize('chat.autoapprove.lmServicePerTool.session', 'Auto approved for this session') + : reason.scope === 'workspace' + ? localize('chat.autoapprove.lmServicePerTool.workspace', 'Auto approved for this workspace') + : localize('chat.autoapprove.lmServicePerTool.profile', 'Auto approved for this profile'); + md += ' (' + createMarkdownCommandLink({ title: localize('edit', 'Edit'), id: 'workbench.action.chat.editToolApproval', arguments: [reason.scope] }) + ')'; + break; + case ToolConfirmKind.ConfirmationNotNeeded: + if (reason.reason) { + return typeof reason.reason === 'string' + ? new MarkdownString(reason.reason, { isTrusted: true }) + : reason.reason; + } + return undefined; + case ToolConfirmKind.UserAction: + case ToolConfirmKind.Denied: + default: + return undefined; + } + + return new MarkdownString(md, { isTrusted: true }); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts new file mode 100644 index 00000000000..dfe354e2514 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolPostExecuteConfirmationPart.ts @@ -0,0 +1,216 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { Separator } from '../../../../../../../base/common/actions.js'; +import { getExtensionForMimeType } from '../../../../../../../base/common/mime.js'; +import { localize } from '../../../../../../../nls.js'; +import { IContextKeyService } from '../../../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; +import { ChatResponseResource } from '../../../../common/model/chatModel.js'; +import { IChatToolInvocation, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { ILanguageModelToolsConfirmationService } from '../../../../common/tools/languageModelToolsConfirmationService.js'; +import { ILanguageModelToolsService, IToolResultDataPart, IToolResultPromptTsxPart, IToolResultTextPart, stringifyPromptTsxPart } from '../../../../common/tools/languageModelToolsService.js'; +import { AcceptToolPostConfirmationActionId, SkipToolPostConfirmationActionId } from '../../../actions/chatToolActions.js'; +import { IChatCodeBlockInfo, IChatWidgetService } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatCollapsibleIOPart } from '../chatToolInputOutputContentPart.js'; +import { ChatToolOutputContentSubPart } from '../chatToolOutputContentSubPart.js'; +import { AbstractToolConfirmationSubPart } from './abstractToolConfirmationSubPart.js'; + +export class ChatToolPostExecuteConfirmationPart extends AbstractToolConfirmationSubPart { + private _codeblocks: IChatCodeBlockInfo[] = []; + public get codeblocks(): IChatCodeBlockInfo[] { + return this._codeblocks; + } + + constructor( + toolInvocation: IChatToolInvocation, + context: IChatContentPartRenderContext, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatWidgetService chatWidgetService: IChatWidgetService, + @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, + @ILanguageModelToolsConfirmationService private readonly confirmationService: ILanguageModelToolsConfirmationService, + ) { + super(toolInvocation, context, instantiationService, keybindingService, contextKeyService, chatWidgetService, languageModelToolsService); + const subtitle = toolInvocation.pastTenseMessage || toolInvocation.invocationMessage; + this.render({ + allowActionId: AcceptToolPostConfirmationActionId, + skipActionId: SkipToolPostConfirmationActionId, + allowLabel: localize('allow', "Allow"), + skipLabel: localize('skip.post', 'Skip Results'), + partType: 'chatToolPostConfirmation', + subtitle: typeof subtitle === 'string' ? subtitle : subtitle?.value, + }); + } + + protected createContentElement(): HTMLElement { + if (this.toolInvocation.kind !== 'toolInvocation') { + throw new Error('post-approval not supported for serialized data'); + } + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForPostApproval) { + throw new Error('Tool invocation is not waiting for post-approval'); + } + + return this.createResultsDisplay(this.toolInvocation, state.contentForModel); + } + + protected getTitle(): string { + return localize('approveToolResult', "Approve Tool Result"); + } + + protected override additionalPrimaryActions() { + const actions = super.additionalPrimaryActions(); + + const state = this.toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.WaitingForPostApproval) { + return actions; + } + + // Get actions from confirmation service + const confirmActions = this.confirmationService.getPostConfirmActions({ + toolId: this.toolInvocation.toolId, + source: this.toolInvocation.source, + parameters: state.parameters + }); + + for (const action of confirmActions) { + if (action.divider) { + actions.push(new Separator()); + } + actions.push({ + label: action.label, + tooltip: action.detail, + data: async () => { + const shouldConfirm = await action.select(); + if (shouldConfirm) { + this.confirmWith(this.toolInvocation, { type: ToolConfirmKind.UserAction }); + } + } + }); + } + + return actions; + } + + private createResultsDisplay(toolInvocation: IChatToolInvocation, contentForModel: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[]): HTMLElement { + const container = dom.$('.tool-postconfirm-display'); + + if (!contentForModel || contentForModel.length === 0) { + container.textContent = localize('noResults', 'No results to display'); + return container; + } + + const parts: ChatCollapsibleIOPart[] = []; + + for (const [i, part] of contentForModel.entries()) { + if (part.kind === 'text') { + // Display text parts + parts.push({ + kind: 'code', + title: part.title, + data: part.value, + languageId: 'plaintext', + codeBlockIndex: i, + ownerMarkdownPartId: this.codeblocksPartId, + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { wordWrap: 'on', readOnly: true } + } + }); + } else if (part.kind === 'promptTsx') { + // Display TSX parts as JSON-stringified + const stringified = stringifyPromptTsxPart(part); + + parts.push({ + kind: 'code', + data: stringified, + languageId: 'json', + codeBlockIndex: i, + ownerMarkdownPartId: this.codeblocksPartId, + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { wordWrap: 'on', readOnly: true } + } + }); + } else if (part.kind === 'data') { + // Display data parts + const mimeType = part.value.mimeType; + const data = part.value.data; + + // Check if it's an image + if (mimeType?.startsWith('image/')) { + const permalinkBasename = getExtensionForMimeType(mimeType) ? `image${getExtensionForMimeType(mimeType)}` : 'image.bin'; + const permalinkUri = ChatResponseResource.createUri(this.context.element.sessionResource, toolInvocation.toolCallId, i, permalinkBasename); + parts.push({ kind: 'data', value: data.buffer, mimeType, uri: permalinkUri, audience: part.audience }); + } else { + // Try to display as UTF-8 text, otherwise base64 + const decoder = new TextDecoder('utf-8', { fatal: true }); + try { + const text = decoder.decode(data.buffer); + + parts.push({ + kind: 'code', + data: text, + languageId: 'plaintext', + codeBlockIndex: i, + ownerMarkdownPartId: this.codeblocksPartId, + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { wordWrap: 'on', readOnly: true } + } + }); + } catch { + // Not valid UTF-8, show base64 + const base64 = data.toString(); + + parts.push({ + kind: 'code', + data: base64, + languageId: 'plaintext', + codeBlockIndex: i, + ownerMarkdownPartId: this.codeblocksPartId, + options: { + hideToolbar: true, + reserveWidth: 19, + maxHeightInLines: 13, + verticalPadding: 5, + editorOptions: { wordWrap: 'on', readOnly: true } + } + }); + } + } + } + } + + if (parts.length > 0) { + const outputSubPart = this._register(this.instantiationService.createInstance( + ChatToolOutputContentSubPart, + this.context, + parts, + )); + + this._codeblocks.push(...outputSubPart.codeblocks); + outputSubPart.domNode.classList.add('tool-postconfirm-display'); + return outputSubPart.domNode; + } + + container.textContent = localize('noDisplayableResults', 'No displayable results'); + return container; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts new file mode 100644 index 00000000000..9e35ac04658 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolProgressPart.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../../../../base/browser/markdownRenderer.js'; +import { status } from '../../../../../../../base/browser/ui/aria/aria.js'; +import { IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { stripIcons } from '../../../../../../../base/common/iconLabels.js'; +import { autorun } from '../../../../../../../base/common/observable.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatProgressMessage, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { AccessibilityWorkbenchSettingId } from '../../../../../accessibility/browser/accessibilityConfiguration.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatProgressContentPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +export class ChatToolProgressSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + private readonly announcedToolProgressKeys: Set | undefined, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(toolInvocation); + + this.domNode = this.createProgressPart(); + } + + private createProgressPart(): HTMLElement { + const isComplete = IChatToolInvocation.isComplete(this.toolInvocation); + + if (isComplete && this.toolIsConfirmed && this.toolInvocation.pastTenseMessage) { + const key = this.getAnnouncementKey('complete'); + const completionContent = this.toolInvocation.pastTenseMessage ?? this.toolInvocation.invocationMessage; + // Don't render anything if there's no meaningful content + if (!this.hasMeaningfulContent(completionContent)) { + return document.createElement('div'); + } + const shouldAnnounce = this.toolInvocation.kind === 'toolInvocation' && this.hasMeaningfulContent(completionContent) ? this.computeShouldAnnounce(key) : false; + const part = this.renderProgressContent(completionContent, shouldAnnounce); + this._register(part); + return part.domNode; + } else { + const container = document.createElement('div'); + const progressObservable = this.toolInvocation.kind === 'toolInvocation' ? this.toolInvocation.state.map((s, r) => s.type === IChatToolInvocation.StateKind.Executing ? s.progress.read(r) : undefined) : undefined; + this._register(autorun(reader => { + const progress = progressObservable?.read(reader); + const key = this.getAnnouncementKey('progress'); + const progressContent = progress?.message ?? this.toolInvocation.invocationMessage; + // Don't render anything if there's no meaningful content + if (!this.hasMeaningfulContent(progressContent)) { + dom.clearNode(container); + return; + } + const shouldAnnounce = this.toolInvocation.kind === 'toolInvocation' && this.hasMeaningfulContent(progressContent) ? this.computeShouldAnnounce(key) : false; + const part = reader.store.add(this.renderProgressContent(progressContent, shouldAnnounce)); + dom.reset(container, part.domNode); + })); + return container; + } + } + + private get toolIsConfirmed() { + const c = IChatToolInvocation.executionConfirmedOrDenied(this.toolInvocation); + return !!c && c.type !== ToolConfirmKind.Denied; + } + + private renderProgressContent(content: IMarkdownString | string, shouldAnnounce: boolean) { + if (typeof content === 'string') { + content = new MarkdownString().appendText(content); + } + + const progressMessage: IChatProgressMessage = { + kind: 'progressMessage', + content + }; + + if (shouldAnnounce) { + this.provideScreenReaderStatus(content); + } + + return this.instantiationService.createInstance(ChatProgressContentPart, progressMessage, this.renderer, this.context, undefined, true, this.getIcon(), this.toolInvocation); + } + + private getAnnouncementKey(kind: 'progress' | 'complete'): string { + return `${kind}:${this.toolInvocation.toolCallId}`; + } + + private computeShouldAnnounce(key: string): boolean { + if (!this.announcedToolProgressKeys) { + return false; + } + if (!this.configurationService.getValue(AccessibilityWorkbenchSettingId.VerboseChatProgressUpdates)) { + return false; + } + if (this.announcedToolProgressKeys.has(key)) { + return false; + } + this.announcedToolProgressKeys.add(key); + return true; + } + + private provideScreenReaderStatus(content: IMarkdownString | string): void { + const message = typeof content === 'string' ? content : stripIcons(renderAsPlaintext(content, { useLinkFormatter: true })); + status(message); + } + + private hasMeaningfulContent(content: IMarkdownString | string | undefined): boolean { + if (!content) { + return false; + } + + const text = typeof content === 'string' ? content : content.value; + return text.trim().length > 0; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts new file mode 100644 index 00000000000..3e3e3cc12d0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/chatToolStreamingSubPart.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../../base/browser/dom.js'; +import { IMarkdownString, MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { autorun } from '../../../../../../../base/common/observable.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatProgressMessage, IChatToolInvocation } from '../../../../common/chatService/chatService.js'; +import { IChatCodeBlockInfo } from '../../../chat.js'; +import { IChatContentPartRenderContext } from '../chatContentParts.js'; +import { ChatProgressContentPart } from '../chatProgressContentPart.js'; +import { BaseChatToolInvocationSubPart } from './chatToolInvocationSubPart.js'; + +/** + * Sub-part for rendering a tool invocation in the streaming state. + * This shows progress while the tool arguments are being streamed from the LM. + */ +export class ChatToolStreamingSubPart extends BaseChatToolInvocationSubPart { + public readonly domNode: HTMLElement; + + public override readonly codeblocks: IChatCodeBlockInfo[] = []; + + constructor( + toolInvocation: IChatToolInvocation, + private readonly context: IChatContentPartRenderContext, + private readonly renderer: IMarkdownRenderer, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(toolInvocation); + + this.domNode = this.createStreamingPart(); + } + + private createStreamingPart(): HTMLElement { + const container = document.createElement('div'); + + if (this.toolInvocation.kind !== 'toolInvocation') { + return container; + } + + const toolInvocation = this.toolInvocation; + const state = toolInvocation.state.get(); + if (state.type !== IChatToolInvocation.StateKind.Streaming) { + return container; + } + + // Observe streaming message changes + this._register(autorun(reader => { + const currentState = toolInvocation.state.read(reader); + if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { + // State changed - clear the container DOM before triggering re-render + // This prevents the old streaming message from lingering + dom.clearNode(container); + this._onNeedsRerender.fire(); + return; + } + + // Read the streaming message + const streamingMessage = currentState.streamingMessage.read(reader); + const displayMessage = streamingMessage ?? toolInvocation.invocationMessage; + + // Don't render anything if there's no meaningful content + const messageText = typeof displayMessage === 'string' ? displayMessage : displayMessage.value; + if (!messageText || messageText.trim().length === 0) { + dom.clearNode(container); + return; + } + + const content: IMarkdownString = typeof displayMessage === 'string' + ? new MarkdownString().appendText(displayMessage) + : displayMessage; + + const progressMessage: IChatProgressMessage = { + kind: 'progressMessage', + content + }; + + const part = reader.store.add(this.instantiationService.createInstance( + ChatProgressContentPart, + progressMessage, + this.renderer, + this.context, + undefined, + true, + this.getIcon(), + toolInvocation + )); + + dom.reset(container, part.domNode); + })); + + return container; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts new file mode 100644 index 00000000000..845ff711421 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from '../../../../../../../base/common/lifecycle.js'; +import { Emitter, Event } from '../../../../../../../base/common/event.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { disposableTimeout } from '../../../../../../../base/common/async.js'; + +/** + * The auto-expand algorithm for terminal tool progress parts. + * + * The algorithm is: + * 1. When command executes, kick off 500ms timeout - expand if there's real output (data events + * may fire before onCommandExecuted due to shell integration sequences, so we can't rely on + * receivedData to skip this path) + * 2. On first data event, wait 50ms and expand if command not yet finished and has real output + * 3. Fast commands (finishing quickly) should NOT auto-expand to prevent flickering + */ +export interface ITerminalToolAutoExpandOptions { + /** + * The command detection capability to listen for command events. + */ + readonly commandDetection: ICommandDetectionCapability; + + /** + * Event fired when data is received from the terminal. + */ + readonly onWillData: Event; + + /** + * Check if the output should auto-expand (e.g. not already expanded, user hasn't toggled). + */ + shouldAutoExpand(): boolean; + + /** + * Check if there is real output (not just shell integration sequences). + */ + hasRealOutput(): boolean; +} + +/** + * Timeout constants for the auto-expand algorithm. + */ +export const enum TerminalToolAutoExpandTimeout { + /** + * Timeout in milliseconds to wait when no data events are received before checking for auto-expand. + */ + NoData = 500, + /** + * Timeout in milliseconds to wait after first data event before checking for auto-expand. + * This prevents flickering for fast commands like `ls` that finish quickly. + */ + DataEvent = 50, +} + +export class TerminalToolAutoExpand extends Disposable { + private _commandFinished = false; + private _receivedData = false; + private _dataEventTimeout: IDisposable | undefined; + private _noDataTimeout: IDisposable | undefined; + + private readonly _onDidRequestExpand = this._register(new Emitter()); + readonly onDidRequestExpand: Event = this._onDidRequestExpand.event; + + constructor( + private readonly _options: ITerminalToolAutoExpandOptions, + ) { + super(); + this._setupListeners(); + } + + private _setupListeners(): void { + const store = this._register(new DisposableStore()); + + const commandDetection = this._options.commandDetection; + + store.add(commandDetection.onCommandExecuted(() => { + // Auto-expand for long-running commands: + if (this._options.shouldAutoExpand() && !this._noDataTimeout) { + this._noDataTimeout = disposableTimeout(() => { + this._noDataTimeout = undefined; + const shouldExpand = this._options.shouldAutoExpand(); + const hasOutput = this._options.hasRealOutput(); + // Don't check receivedData here - data events can fire before onCommandExecuted + // (shell integration sequences), and the DataEvent path may not have expanded + // if hasRealOutput was false at that time + if (shouldExpand && hasOutput) { + // Cancel the DataEvent timeout since we're expanding via the NoData path + this._dataEventTimeout?.dispose(); + this._dataEventTimeout = undefined; + this._onDidRequestExpand.fire(); + } + }, TerminalToolAutoExpandTimeout.NoData, store); + } + })); + + // 2. Wait for first data event - when hit, wait 50ms and expand if command not yet finished + // Also checks for real output since shell integration sequences trigger onWillData + // Important: We don't cancel _noDataTimeout here because early data might just be shell + // integration sequences. The NoData path should still run if the DataEvent path doesn't + // find real output. + store.add(this._options.onWillData(() => { + if (this._receivedData) { + return; + } + this._receivedData = true; + // Wait 50ms and expand if command hasn't finished yet and has real output + if (this._options.shouldAutoExpand() && !this._dataEventTimeout) { + this._dataEventTimeout = disposableTimeout(() => { + this._dataEventTimeout = undefined; + const shouldExpand = this._options.shouldAutoExpand(); + const hasOutput = this._options.hasRealOutput(); + if (!this._commandFinished && shouldExpand && hasOutput) { + // Cancel the NoData timeout since we're expanding via the DataEvent path + this._noDataTimeout?.dispose(); + this._noDataTimeout = undefined; + this._onDidRequestExpand.fire(); + } + }, TerminalToolAutoExpandTimeout.DataEvent, store); + } + })); + + store.add(commandDetection.onCommandFinished(() => { + this._commandFinished = true; + this._clearAutoExpandTimeouts(); + })); + } + + private _clearAutoExpandTimeouts(): void { + this._dataEventTimeout?.dispose(); + this._dataEventTimeout = undefined; + this._noDataTimeout?.dispose(); + this._noDataTimeout = undefined; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts b/src/vs/workbench/contrib/chat/browser/widget/chatDragAndDrop.ts similarity index 88% rename from src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts rename to src/vs/workbench/contrib/chat/browser/widget/chatDragAndDrop.ts index dc847f8e8ab..fa166677003 100644 --- a/src/vs/workbench/contrib/chat/browser/chatDragAndDrop.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatDragAndDrop.ts @@ -3,29 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DataTransfers } from '../../../../base/browser/dnd.js'; -import { $, DragAndDropObserver } from '../../../../base/browser/dom.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { coalesce } from '../../../../base/common/arrays.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { UriList } from '../../../../base/common/dataTransfer.js'; -import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Mimes } from '../../../../base/common/mime.js'; -import { URI } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; -import { CodeDataTransfers, containsDragType, extractEditorsDropData, extractMarkerDropData, extractNotebookCellOutputDropData, extractSymbolDropData } from '../../../../platform/dnd/browser/dnd.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; -import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; -import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; -import { IChatRequestVariableEntry } from '../common/chatVariableEntries.js'; -import { IChatWidgetService } from './chat.js'; -import { IChatAttachmentResolveService, ImageTransferData } from './chatAttachmentResolveService.js'; -import { ChatAttachmentModel } from './chatAttachmentModel.js'; -import { IChatInputStyles } from './chatInputPart.js'; -import { convertStringToUInt8Array } from './imageUtils.js'; -import { extractSCMHistoryItemDropData } from '../../scm/browser/scmHistoryChatContext.js'; +import { DataTransfers } from '../../../../../base/browser/dnd.js'; +import { $, DragAndDropObserver } from '../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { coalesce } from '../../../../../base/common/arrays.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { UriList } from '../../../../../base/common/dataTransfer.js'; +import { IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Mimes } from '../../../../../base/common/mime.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { CodeDataTransfers, containsDragType, extractEditorsDropData, extractMarkerDropData, extractNotebookCellOutputDropData, extractSymbolDropData } from '../../../../../platform/dnd/browser/dnd.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IThemeService, Themable } from '../../../../../platform/theme/common/themeService.js'; +import { ISharedWebContentExtractorService } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { IExtensionService, isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; +import { extractSCMHistoryItemDropData } from '../../../scm/browser/scmHistoryChatContext.js'; +import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { IChatWidget } from '../chat.js'; +import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js'; +import { IChatAttachmentResolveService, ImageTransferData } from '../attachments/chatAttachmentResolveService.js'; +import { IChatInputStyles } from './input/chatInputPart.js'; +import { convertStringToUInt8Array } from '../chatImageUtils.js'; enum ChatDragAndDropType { FILE_INTERNAL, @@ -50,12 +50,12 @@ export class ChatDragAndDrop extends Themable { private disableOverlay: boolean = false; constructor( + private readonly widgetRef: () => IChatWidget | undefined, private readonly attachmentModel: ChatAttachmentModel, private readonly styles: IChatInputStyles, @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @ISharedWebContentExtractorService private readonly webContentExtractorService: ISharedWebContentExtractorService, - @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @ILogService private readonly logService: ILogService, @IChatAttachmentResolveService private readonly chatAttachmentResolveService: IChatAttachmentResolveService ) { @@ -299,9 +299,10 @@ export class ChatDragAndDrop extends Themable { } // TODO: use dnd provider to insert text @justschen - const selection = this.chatWidgetService.lastFocusedWidget?.inputEditor.getSelection(); - if (selection && this.chatWidgetService.lastFocusedWidget) { - this.chatWidgetService.lastFocusedWidget.inputEditor.executeEdits('chatInsertUrl', [{ range: selection, text: url }]); + const widget = this.widgetRef(); + const selection = widget?.inputEditor.getSelection(); + if (selection && widget) { + widget.inputEditor.executeEdits('chatInsertUrl', [{ range: selection, text: url }]); } this.logService.warn(`Image URLs must end in .jpg, .png, .gif, .webp, or .bmp. Failed to fetch image from this URL: ${url}`); diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatLayoutService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatLayoutService.ts new file mode 100644 index 00000000000..3a53810cb5e --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatLayoutService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { derived, IObservable } from '../../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; +import { IChatLayoutService } from '../../common/widget/chatLayoutService.js'; + +const FONT_SIZE = 13; + +export class ChatLayoutService extends Disposable implements IChatLayoutService { + declare readonly _serviceBrand: undefined; + + readonly fontFamily: IObservable; + readonly fontSize: IObservable; + + constructor(@IConfigurationService configurationService: IConfigurationService) { + super(); + + const chatFontFamily = observableConfigValue('chat.fontFamily', 'default', configurationService); + this.fontFamily = derived(reader => { + const fontFamily = chatFontFamily.read(reader); + return fontFamily === 'default' ? null : fontFamily; + }); + + this.fontSize = observableConfigValue('chat.fontSize', FONT_SIZE, configurationService); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts new file mode 100644 index 00000000000..48bee4cd08a --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListRenderer.ts @@ -0,0 +1,2276 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { renderFormattedText } from '../../../../../base/browser/formattedTextRenderer.js'; +import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; +import { IActionViewItemOptions } from '../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { alert } from '../../../../../base/browser/ui/aria/aria.js'; +import { DropdownMenuActionViewItem, IDropdownMenuActionViewItemOptions } from '../../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; +import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { CachedListVirtualDelegate, IListElementRenderDetails } from '../../../../../base/browser/ui/list/list.js'; +import { ITreeNode, ITreeRenderer } from '../../../../../base/browser/ui/tree/tree.js'; +import { IAction } from '../../../../../base/common/actions.js'; +import { coalesce, distinct } from '../../../../../base/common/arrays.js'; +import { findLast } from '../../../../../base/common/arraysFind.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { canceledName } from '../../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { FuzzyScore } from '../../../../../base/common/filters.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { KeyCode } from '../../../../../base/common/keyCodes.js'; +import { Disposable, DisposableStore, IDisposable, dispose, thenIfNotDisposed, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { ScrollEvent } from '../../../../../base/common/scrollable.js'; +import { FileAccess, Schemas } from '../../../../../base/common/network.js'; +import { clamp } from '../../../../../base/common/numbers.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IMenuEntryActionViewItemOptions, createActionViewItem } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IMarkdownRenderer } from '../../../../../platform/markdown/browser/markdownRenderer.js'; +import { isDark } from '../../../../../platform/theme/common/theme.js'; +import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IWorkbenchIssueService } from '../../../issue/common/issue.js'; +import { CodiconActionViewItem } from '../../../notebook/browser/view/cellParts/cellActionView.js'; +import { annotateSpecialMarkdownContent, extractSubAgentInvocationIdFromText, hasCodeblockUriTag } from '../../common/widget/annotations.js'; +import { checkModeOption } from '../../common/chat.js'; +import { IChatAgentMetadata } from '../../common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatTextEditGroup } from '../../common/model/chatModel.js'; +import { chatSubcommandLeader } from '../../common/requestParser/chatParserTypes.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatErrorLevel, IChatConfirmation, IChatContentReference, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatPullRequestContent, IChatQuestionCarousel, IChatService, IChatTask, IChatTaskSerialized, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, isChatFollowup } from '../../common/chatService/chatService.js'; +import { localChatSessionType } from '../../common/chatSessionsService.js'; +import { getChatSessionType } from '../../common/model/chatUri.js'; +import { IChatRequestVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { IChatChangesSummaryPart, IChatCodeCitations, IChatErrorDetailsPart, IChatReferences, IChatRendererContent, IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; +import { getNWords } from '../../common/model/chatWordCounter.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, CollapsedToolsDisplayMode, ThinkingDisplayMode } from '../../common/constants.js'; +import { MarkUnhelpfulActionId } from '../actions/chatTitleActions.js'; +import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidgetService } from '../chat.js'; +import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; +import { ChatContentMarkdownRenderer } from './chatContentMarkdownRenderer.js'; +import { ChatAgentCommandContentPart } from './chatContentParts/chatAgentCommandContentPart.js'; +import { ChatAnonymousRateLimitedPart } from './chatContentParts/chatAnonymousRateLimitedPart.js'; +import { ChatAttachmentsContentPart } from './chatContentParts/chatAttachmentsContentPart.js'; +import { ChatCheckpointFileChangesSummaryContentPart } from './chatContentParts/chatChangesSummaryPart.js'; +import { ChatCodeCitationContentPart } from './chatContentParts/chatCodeCitationContentPart.js'; +import { ChatCommandButtonContentPart } from './chatContentParts/chatCommandContentPart.js'; +import { ChatConfirmationContentPart } from './chatContentParts/chatConfirmationContentPart.js'; +import { DiffEditorPool, EditorPool } from './chatContentParts/chatContentCodePools.js'; +import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts/chatContentParts.js'; +import { ChatElicitationContentPart } from './chatContentParts/chatElicitationContentPart.js'; +import { ChatErrorConfirmationContentPart } from './chatContentParts/chatErrorConfirmationPart.js'; +import { ChatErrorContentPart } from './chatContentParts/chatErrorContentPart.js'; +import { ChatQuestionCarouselPart } from './chatContentParts/chatQuestionCarouselPart.js'; +import { ChatExtensionsContentPart } from './chatContentParts/chatExtensionsContentPart.js'; +import { ChatMarkdownContentPart, codeblockHasClosingBackticks } from './chatContentParts/chatMarkdownContentPart.js'; +import { ChatMcpServersInteractionContentPart } from './chatContentParts/chatMcpServersInteractionContentPart.js'; +import { ChatMultiDiffContentPart } from './chatContentParts/chatMultiDiffContentPart.js'; +import { ChatProgressContentPart, ChatWorkingProgressContentPart } from './chatContentParts/chatProgressContentPart.js'; +import { ChatPullRequestContentPart } from './chatContentParts/chatPullRequestContentPart.js'; +import { ChatQuotaExceededPart } from './chatContentParts/chatQuotaExceededPart.js'; +import { ChatCollapsibleListContentPart, ChatUsedReferencesListContentPart, CollapsibleListPool } from './chatContentParts/chatReferencesContentPart.js'; +import { ChatTaskContentPart } from './chatContentParts/chatTaskContentPart.js'; +import { ChatTextEditContentPart } from './chatContentParts/chatTextEditContentPart.js'; +import { ChatThinkingContentPart } from './chatContentParts/chatThinkingContentPart.js'; +import { ChatSubagentContentPart } from './chatContentParts/chatSubagentContentPart.js'; +import { ChatTipContentPart } from './chatContentParts/chatTipContentPart.js'; +import { ChatTreeContentPart, TreePool } from './chatContentParts/chatTreeContentPart.js'; +import { ChatWorkspaceEditContentPart } from './chatContentParts/chatWorkspaceEditContentPart.js'; +import { ChatToolInvocationPart } from './chatContentParts/toolInvocationParts/chatToolInvocationPart.js'; +import { ChatMarkdownDecorationsRenderer } from './chatContentParts/chatMarkdownDecorationsRenderer.js'; +import { ChatEditorOptions } from './chatOptions.js'; +import { ChatCodeBlockContentProvider, CodeBlockPart } from './chatContentParts/codeBlockPart.js'; +import { autorun, observableValue } from '../../../../../base/common/observable.js'; +import { RunSubagentTool } from '../../common/tools/builtinTools/runSubagentTool.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { IChatTipService } from '../chatTipService.js'; + +const $ = dom.$; + +const COPILOT_USERNAME = 'GitHub Copilot'; + +export interface IChatListItemTemplate { + currentElement?: ChatTreeItem; + /** + * The parts that are currently rendered in the template. Note that these are purposely not added to elementDisposables- + * they are disposed in a separate cycle after diffing with the next content to render. + */ + renderedParts?: IChatContentPart[]; + /** + * Element used to track whether the template is mounted in the DOM. + */ + renderedPartsMounted?: boolean; + + readonly rowContainer: HTMLElement; + readonly titleToolbar?: MenuWorkbenchToolBar; + readonly header?: HTMLElement; + readonly footerToolbar: MenuWorkbenchToolBar; + readonly footerDetailsContainer: HTMLElement; + readonly avatarContainer: HTMLElement; + readonly username: HTMLElement; + readonly detail: HTMLElement; + readonly value: HTMLElement; + readonly contextKeyService: IContextKeyService; + readonly instantiationService: IInstantiationService; + readonly templateDisposables: IDisposable; + readonly elementDisposables: DisposableStore; + readonly agentHover: ChatAgentHover; + readonly requestHover: HTMLElement; + readonly disabledOverlay: HTMLElement; + readonly checkpointToolbar: MenuWorkbenchToolBar; + readonly checkpointRestoreToolbar: MenuWorkbenchToolBar; + readonly checkpointContainer: HTMLElement; + readonly checkpointRestoreContainer: HTMLElement; +} + +interface IItemHeightChangeParams { + element: ChatTreeItem; + height: number; +} + +const forceVerboseLayoutTracing = false + // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed + ; + +export interface IChatRendererDelegate { + container: HTMLElement; + getListLength(): number; + currentChatMode(): ChatModeKind; + + readonly onDidScroll?: Event; +} + +const mostRecentResponseClassName = 'chat-most-recent-response'; + +export class ChatListItemRenderer extends Disposable implements ITreeRenderer { + static readonly ID = 'item'; + + private readonly codeBlocksByResponseId = new Map(); + private readonly codeBlocksByEditorUri = new ResourceMap(); + + private readonly fileTreesByResponseId = new Map(); + private readonly focusedFileTreesByResponseId = new Map(); + + private readonly templateDataByRequestId = new Map(); + + /** Track pending question carousels by session resource for auto-skip on chat submission */ + private readonly pendingQuestionCarousels = new ResourceMap>(); + + private readonly chatContentMarkdownRenderer: IMarkdownRenderer; + private readonly markdownDecorationsRenderer: ChatMarkdownDecorationsRenderer; + protected readonly _onDidClickFollowup = this._register(new Emitter()); + readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + + private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter<{ readonly sessionResource: URI; readonly requestId: string }>(); + readonly onDidClickRerunWithAgentOrCommandDetection = this._onDidClickRerunWithAgentOrCommandDetection.event; + + + private readonly _onDidClickRequest = this._register(new Emitter()); + readonly onDidClickRequest: Event = this._onDidClickRequest.event; + + private readonly _onDidRerender = this._register(new Emitter()); + readonly onDidRerender: Event = this._onDidRerender.event; + + private readonly _onDidDispose = this._register(new Emitter()); + readonly onDidDispose: Event = this._onDidDispose.event; + + private readonly _onDidFocusOutside = this._register(new Emitter()); + readonly onDidFocusOutside: Event = this._onDidFocusOutside.event; + + protected readonly _onDidChangeItemHeight = this._register(new Emitter()); + readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; + + private readonly _onDidUpdateViewModel = this._register(new Emitter()); + + private readonly _editorPool: EditorPool; + private readonly _toolEditorPool: EditorPool; + private readonly _diffEditorPool: DiffEditorPool; + private readonly _treePool: TreePool; + private readonly _contentReferencesListPool: CollapsibleListPool; + + private _currentLayoutWidth = observableValue(this, 0); + private _isVisible = true; + private _elementBeingRendered: ChatTreeItem | undefined; + private _onDidChangeVisibility = this._register(new Emitter()); + + /** + * Tool invocations get their own so that the ChatViewModel doesn't overwrite it. + * TODO@roblourens shouldn't use the CodeBlockModelCollection at all + */ + private readonly _toolInvocationCodeBlockCollection: CodeBlockModelCollection; + + /** + * Prevents re-announcement of already rendered chat progress + * by screen readers + */ + private readonly _announcedToolProgressKeys = new Set(); + + constructor( + editorOptions: ChatEditorOptions, + private rendererOptions: IChatListItemRendererOptions, + private readonly delegate: IChatRendererDelegate, + private readonly codeBlockModelCollection: CodeBlockModelCollection, + overflowWidgetsDomNode: HTMLElement | undefined, + private viewModel: IChatViewModel | undefined, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configService: IConfigurationService, + @ILogService private readonly logService: ILogService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IThemeService private readonly themeService: IThemeService, + @ICommandService private readonly commandService: ICommandService, + @IHoverService private readonly hoverService: IHoverService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @IChatService private readonly chatService: IChatService, + @IChatTipService private readonly chatTipService: IChatTipService, + ) { + super(); + + this.chatContentMarkdownRenderer = this.instantiationService.createInstance(ChatContentMarkdownRenderer); + this.markdownDecorationsRenderer = this.instantiationService.createInstance(ChatMarkdownDecorationsRenderer); + this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, editorOptions, delegate, overflowWidgetsDomNode, false)); + this._toolEditorPool = this._register(this.instantiationService.createInstance(EditorPool, editorOptions, delegate, overflowWidgetsDomNode, true)); + this._diffEditorPool = this._register(this.instantiationService.createInstance(DiffEditorPool, editorOptions, delegate, overflowWidgetsDomNode, false)); + this._treePool = this._register(this.instantiationService.createInstance(TreePool, this._onDidChangeVisibility.event)); + this._contentReferencesListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, undefined, undefined)); + + this._register(this.instantiationService.createInstance(ChatCodeBlockContentProvider)); + this._toolInvocationCodeBlockCollection = this._register(this.instantiationService.createInstance(CodeBlockModelCollection, 'tools')); + + // Auto-skip pending question carousels when user submits a new chat message + this._register(this.chatService.onDidSubmitRequest(e => { + const carousels = this.pendingQuestionCarousels.get(e.chatSessionResource); + if (carousels) { + for (const carousel of carousels) { + carousel.skip(); + } + carousels.clear(); + } + })); + } + + public updateOptions(options: IChatListItemRendererOptions): void { + this.rendererOptions = { ...this.rendererOptions, ...options }; + } + + get templateId(): string { + return ChatListItemRenderer.ID; + } + + editorsInUse(): Iterable { + return Iterable.concat(this._editorPool.inUse(), this._toolEditorPool.inUse()); + } + + private traceLayout(method: string, message: string) { + if (forceVerboseLayoutTracing) { + this.logService.info(`ChatListItemRenderer#${method}: ${message}`); + } else { + this.logService.trace(`ChatListItemRenderer#${method}: ${message}`); + } + } + + /** + * Compute a rate to render at in words/s. + */ + private getProgressiveRenderRate(element: IChatResponseViewModel): number { + const enum Rate { + Min = 5, + Max = 2000, + } + + const minAfterComplete = 80; + + const rate = element.contentUpdateTimings?.impliedWordLoadRate; + if (element.isComplete) { + if (typeof rate === 'number') { + return clamp(rate, minAfterComplete, Rate.Max); + } else { + return minAfterComplete; + } + } + + if (typeof rate === 'number') { + return clamp(rate, Rate.Min, Rate.Max); + } + + return 8; + } + + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { + const codeBlocks = this.codeBlocksByResponseId.get(response.id); + return codeBlocks ?? []; + } + + updateViewModel(viewModel: IChatViewModel | undefined): void { + this.viewModel = viewModel; + this._announcedToolProgressKeys.clear(); + this.codeBlocksByEditorUri.clear(); + this.codeBlocksByResponseId.clear(); + this.fileTreesByResponseId.clear(); + this.focusedFileTreesByResponseId.clear(); + this._editorPool.clear(); + this._toolEditorPool.clear(); + this._diffEditorPool.clear(); + this._treePool.clear(); + this._contentReferencesListPool.clear(); + this._onDidUpdateViewModel.fire(); + } + + getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { + return this.codeBlocksByEditorUri.get(uri); + } + + getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { + const fileTrees = this.fileTreesByResponseId.get(response.id); + return fileTrees ?? []; + } + + getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { + const fileTrees = this.fileTreesByResponseId.get(response.id); + const lastFocusedFileTreeIndex = this.focusedFileTreesByResponseId.get(response.id); + if (fileTrees?.length && lastFocusedFileTreeIndex !== undefined && lastFocusedFileTreeIndex < fileTrees.length) { + return fileTrees[lastFocusedFileTreeIndex]; + } + return undefined; + } + + getTemplateDataForRequestId(requestId?: string): IChatListItemTemplate | undefined { + if (!requestId) { + return undefined; + } + const templateData = this.templateDataByRequestId.get(requestId); + if (templateData && templateData.currentElement?.id === requestId) { + return templateData; + } + if (templateData) { + this.templateDataByRequestId.delete(requestId); + } + return undefined; + } + + setVisible(visible: boolean): void { + this._isVisible = visible; + this._onDidChangeVisibility.fire(visible); + } + + layout(width: number): void { + const newWidth = width - 40; // padding + if (newWidth !== this._currentLayoutWidth.get()) { + this._currentLayoutWidth.set(newWidth, undefined); + for (const editor of this._editorPool.inUse()) { + editor.layout(newWidth); + } + for (const toolEditor of this._toolEditorPool.inUse()) { + toolEditor.layout(newWidth); + } + for (const diffEditor of this._diffEditorPool.inUse()) { + diffEditor.layout(newWidth); + } + } + } + + renderTemplate(container: HTMLElement): IChatListItemTemplate { + const templateDisposables = new DisposableStore(); + const disabledOverlay = dom.append(container, $('.chat-row-disabled-overlay')); + const rowContainer = dom.append(container, $('.interactive-item-container')); + if (this.rendererOptions.renderStyle === 'compact') { + rowContainer.classList.add('interactive-item-compact'); + } + + let headerParent = rowContainer; + let valueParent = rowContainer; + let detailContainerParent: HTMLElement | undefined; + + if (this.rendererOptions.renderStyle === 'minimal') { + rowContainer.classList.add('interactive-item-compact'); + rowContainer.classList.add('minimal'); + // ----------------------------------------------------- + // icon | details + // | references + // | value + // ----------------------------------------------------- + const lhsContainer = dom.append(rowContainer, $('.column.left')); + const rhsContainer = dom.append(rowContainer, $('.column.right')); + + headerParent = lhsContainer; + detailContainerParent = rhsContainer; + valueParent = rhsContainer; + } + + const header = dom.append(headerParent, $('.header')); + const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer)); + const scopedInstantiationService = templateDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); + + const requestHover = dom.append(rowContainer, $('.request-hover')); + let titleToolbar: MenuWorkbenchToolBar | undefined; + if (this.rendererOptions.noHeader) { + header.classList.add('hidden'); + } else { + titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, requestHover, MenuId.ChatMessageTitle, { + menuOptions: { + shouldForwardArgs: true + }, + toolbarOptions: { + shouldInlineSubmenu: submenu => submenu.actions.length <= 1 + }, + })); + } + this.hoverHidden(requestHover); + + const checkpointContainer = dom.append(rowContainer, $('.checkpoint-container')); + const codiconContainer = dom.append(checkpointContainer, $('.codicon-container')); + dom.append(codiconContainer, $('span.codicon.codicon-bookmark')); + + const checkpointToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, checkpointContainer, MenuId.ChatMessageCheckpoint, { + actionViewItemProvider: (action, options) => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(CodiconActionViewItem, action, { hoverDelegate: options.hoverDelegate }); + } + return undefined; + }, + renderDropdownAsChildElement: true, + menuOptions: { + shouldForwardArgs: true + }, + toolbarOptions: { + shouldInlineSubmenu: submenu => submenu.actions.length <= 1 + }, + })); + + dom.append(checkpointContainer, $('.checkpoint-divider')); + + const user = dom.append(header, $('.user')); + const avatarContainer = dom.append(user, $('.avatar-container')); + const username = dom.append(user, $('h3.username')); + username.tabIndex = 0; + const detailContainer = dom.append(detailContainerParent ?? user, $('span.detail-container')); + const detail = dom.append(detailContainer, $('span.detail')); + dom.append(detailContainer, $('span.chat-animated-ellipsis')); + const value = dom.append(valueParent, $('.value')); + const elementDisposables = templateDisposables.add(new DisposableStore()); + + const footerToolbarContainer = dom.append(rowContainer, $('.chat-footer-toolbar')); + if (this.rendererOptions.noFooter) { + footerToolbarContainer.classList.add('hidden'); + } + + const footerToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, footerToolbarContainer, MenuId.ChatMessageFooter, { + eventDebounceDelay: 0, + menuOptions: { shouldForwardArgs: true, renderShortTitle: true }, + toolbarOptions: { shouldInlineSubmenu: submenu => submenu.actions.length <= 1 }, + actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { + if (action instanceof MenuItemAction && action.item.id === MarkUnhelpfulActionId) { + return scopedInstantiationService.createInstance(ChatVoteDownButton, action, options as IMenuEntryActionViewItemOptions); + } + return createActionViewItem(scopedInstantiationService, action, options); + } + })); + + // Insert the details container into the toolbar's internal element structure + const footerDetailsContainer = dom.append(footerToolbar.getElement(), $('.chat-footer-details')); + footerDetailsContainer.tabIndex = 0; + + const checkpointRestoreContainer = dom.append(rowContainer, $('.checkpoint-restore-container')); + const codiconRestoreContainer = dom.append(checkpointRestoreContainer, $('.codicon-container')); + dom.append(codiconRestoreContainer, $('span.codicon.codicon-bookmark')); + const label = dom.append(checkpointRestoreContainer, $('span.checkpoint-label-text')); + label.textContent = localize('checkpointRestore', 'Checkpoint Restored'); + const checkpointRestoreToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, checkpointRestoreContainer, MenuId.ChatMessageRestoreCheckpoint, { + actionViewItemProvider: (action, options) => { + if (action instanceof MenuItemAction) { + return this.instantiationService.createInstance(CodiconActionViewItem, action, { hoverDelegate: options.hoverDelegate }); + } + return undefined; + }, + renderDropdownAsChildElement: true, + menuOptions: { + shouldForwardArgs: true + }, + toolbarOptions: { + shouldInlineSubmenu: submenu => submenu.actions.length <= 1 + }, + })); + + dom.append(checkpointRestoreContainer, $('.checkpoint-divider')); + + + const agentHover = templateDisposables.add(this.instantiationService.createInstance(ChatAgentHover)); + const hoverContent = () => { + if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) { + agentHover.setAgent(template.currentElement.agent.id); + return agentHover.domNode; + } + + return undefined; + }; + const hoverOptions = getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); + templateDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); + templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { + const ev = new StandardKeyboardEvent(e); + if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { + const content = hoverContent(); + if (content) { + this.hoverService.showInstantHover({ content, target: user, trapFocus: true, actions: hoverOptions.actions }, true); + } + } else if (ev.equals(KeyCode.Escape)) { + this.hoverService.hideHover(); + } + })); + const connectionObserver = document.createElement('connection-observer') as dom.ConnectionObserverElement; + dom.append(container, connectionObserver); + const template: IChatListItemTemplate = { header, avatarContainer, requestHover, username, detail, value, rowContainer, elementDisposables, templateDisposables, contextKeyService, instantiationService: scopedInstantiationService, agentHover, titleToolbar, footerToolbar, footerDetailsContainer, disabledOverlay, checkpointToolbar, checkpointRestoreToolbar, checkpointContainer, checkpointRestoreContainer }; + + connectionObserver.onDidDisconnect = () => { + template.renderedPartsMounted = false; + }; + + templateDisposables.add(this._onDidUpdateViewModel.event(() => { + if (!template.currentElement || !this.viewModel?.sessionResource || !isEqual(template.currentElement.sessionResource, this.viewModel.sessionResource)) { + this.clearRenderedParts(template); + } + })); + + templateDisposables.add(dom.addDisposableListener(disabledOverlay, dom.EventType.CLICK, e => { + if (!this.viewModel?.editing) { + return; + } + const current = template.currentElement; + if (!current || current.id === this.viewModel.editing.id) { + return; + } + + if (disabledOverlay.classList.contains('disabled')) { + e.preventDefault(); + e.stopPropagation(); + this._onDidFocusOutside.fire(); + } + })); + + const resizeObserver = templateDisposables.add(new dom.DisposableResizeObserver((entries) => { + if (!template.currentElement) { + return; + } + + const entry = entries[0]; + if (entry) { + const height = entry.borderBoxSize.at(0)?.blockSize; + if (height === 0 || !height || !template.rowContainer.isConnected) { + // Don't fire for changes that happen from the row being removed from the DOM + return; + } + + const normalizedHeight = Math.ceil(height); + template.currentElement.currentRenderedHeight = normalizedHeight; + if (template.currentElement !== this._elementBeingRendered) { + this._onDidChangeItemHeight.fire({ element: template.currentElement, height: normalizedHeight }); + } + } + })); + templateDisposables.add(resizeObserver.observe(rowContainer)); + + return template; + } + + renderElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { + this._elementBeingRendered = node.element; + try { + this.renderChatTreeItem(node.element, index, templateData); + } finally { + this._elementBeingRendered = undefined; + } + } + + /** + * Dispose the rendered parts in the template, which aren't done in disposeElement + * so they can be reused when a new render is started. + */ + private clearRenderedParts(templateData: IChatListItemTemplate): void { + if (templateData.renderedParts) { + dispose(coalesce(templateData.renderedParts)); + templateData.renderedParts = undefined; + dom.clearNode(templateData.value); + } + + // This template item is no longer in use, or having another element rendered into it, + // clear the context on toolbars so it doesn't retain the viewmodel. + if (templateData.titleToolbar) { + templateData.titleToolbar.context = undefined; + } + templateData.footerToolbar.context = undefined; + templateData.checkpointToolbar.context = undefined; + templateData.checkpointRestoreToolbar.context = undefined; + } + + private renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void { + if (templateData.currentElement && templateData.currentElement.id !== element.id) { + this.traceLayout('renderChatTreeItem', `Rendering a different element into the template, index=${index}`); + const mappedTemplateData = this.templateDataByRequestId.get(templateData.currentElement.id); + if (mappedTemplateData && (mappedTemplateData.currentElement?.id !== templateData.currentElement.id)) { + this.templateDataByRequestId.delete(templateData.currentElement.id); + } + + this.clearRenderedParts(templateData); + } + + templateData.currentElement = element; + this.templateDataByRequestId.set(element.id, templateData); + const kind = isRequestVM(element) ? 'request' : + isResponseVM(element) ? 'response' : + 'welcome'; + this.traceLayout('renderElement', `${kind}, index=${index}`); + + ChatContextKeys.isResponse.bindTo(templateData.contextKeyService).set(isResponseVM(element)); + ChatContextKeys.itemId.bindTo(templateData.contextKeyService).set(element.id); + ChatContextKeys.isRequest.bindTo(templateData.contextKeyService).set(isRequestVM(element)); + ChatContextKeys.responseDetectedAgentCommand.bindTo(templateData.contextKeyService).set(isResponseVM(element) && element.agentOrSlashCommandDetected); + if (isResponseVM(element)) { + ChatContextKeys.responseSupportsIssueReporting.bindTo(templateData.contextKeyService).set(!!element.agent?.metadata.supportIssueReporting); + ChatContextKeys.responseVote.bindTo(templateData.contextKeyService).set(element.vote === ChatAgentVoteDirection.Up ? 'up' : element.vote === ChatAgentVoteDirection.Down ? 'down' : ''); + } else { + ChatContextKeys.responseVote.bindTo(templateData.contextKeyService).set(''); + } + + if (templateData.titleToolbar) { + templateData.titleToolbar.context = element; + } + templateData.footerToolbar.context = element; + + // Render result details in footer if available + if (isResponseVM(element) && element.result?.details) { + templateData.footerDetailsContainer.textContent = element.result.details; + templateData.footerDetailsContainer.classList.remove('hidden'); + } else { + templateData.footerDetailsContainer.classList.add('hidden'); + } + + ChatContextKeys.responseHasError.bindTo(templateData.contextKeyService).set(isResponseVM(element) && !!element.errorDetails); + const isFiltered = !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered); + ChatContextKeys.responseIsFiltered.bindTo(templateData.contextKeyService).set(isFiltered); + + const location = this.chatWidgetService.getWidgetBySessionResource(element.sessionResource)?.location; + templateData.rowContainer.classList.toggle('editing-session', location === ChatAgentLocation.Chat); + templateData.rowContainer.classList.toggle('interactive-request', isRequestVM(element)); + templateData.rowContainer.classList.toggle('interactive-response', isResponseVM(element)); + const progressMessageAtBottomOfResponse = checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.progressMessageAtBottomOfResponse); + templateData.rowContainer.classList.toggle('show-detail-progress', isResponseVM(element) && !element.isComplete && !element.progressMessages.length && !progressMessageAtBottomOfResponse); + if (!this.rendererOptions.noHeader) { + this.renderAvatar(element, templateData); + } + + templateData.username.textContent = element.username; + templateData.username.classList.toggle('hidden', element.username === COPILOT_USERNAME); + templateData.avatarContainer.classList.toggle('hidden', element.username === COPILOT_USERNAME); + + this.hoverHidden(templateData.requestHover); + dom.clearNode(templateData.detail); + if (isResponseVM(element)) { + this.renderDetail(element, templateData); + } + + templateData.checkpointToolbar.context = element; + const checkpointEnabled = this.configService.getValue(ChatConfiguration.CheckpointsEnabled) + && (this.rendererOptions.restorable ?? true); + + templateData.checkpointContainer.classList.toggle('hidden', isResponseVM(element) || !(checkpointEnabled)); + + // Only show restore container when we have a checkpoint and not editing + const shouldShowRestore = this.viewModel?.model.checkpoint && !this.viewModel?.editing && (index === this.delegate.getListLength() - 1); + templateData.checkpointRestoreContainer.classList.toggle('hidden', !(shouldShowRestore && checkpointEnabled)); + + const editing = element.id === this.viewModel?.editing?.id; + const isInput = this.configService.getValue('chat.editRequests') === 'input'; + + templateData.elementDisposables.add(autorun(r => { + const shouldBeBlocked = element.shouldBeBlocked.read(r); + templateData.disabledOverlay.classList.toggle('disabled', shouldBeBlocked && !editing && this.viewModel?.editing !== undefined); + })); + templateData.rowContainer.classList.toggle('editing', editing && !isInput); + templateData.rowContainer.classList.toggle('editing-input', editing && isInput); + templateData.requestHover.classList.toggle('editing', editing && isInput); + templateData.requestHover.classList.toggle('hidden', (!!this.viewModel?.editing && !editing) || isResponseVM(element) || !this.rendererOptions.editable); + templateData.requestHover.classList.toggle('expanded', this.configService.getValue('chat.editRequests') === 'hover'); + templateData.requestHover.classList.toggle('checkpoints-enabled', checkpointEnabled); + templateData.elementDisposables.add(dom.addStandardDisposableListener(templateData.rowContainer, dom.EventType.CLICK, (e) => { + const current = templateData.currentElement; + if (current && this.viewModel?.editing && current.id !== this.viewModel.editing.id) { + e.stopPropagation(); + e.preventDefault(); + this._onDidFocusOutside.fire(); + } + })); + + // Overlay click listener removed: overlay is non-interactive in cancel-on-any-row mode. + + // hack @joaomoreno + templateData.rowContainer.parentElement?.parentElement?.parentElement?.classList.toggle('request', isRequestVM(element)); + templateData.rowContainer.classList.toggle(mostRecentResponseClassName, index === this.delegate.getListLength() - 1); + templateData.rowContainer.classList.toggle('confirmation-message', isRequestVM(element) && !!element.confirmation); + + // TODO: @justschen decide if we want to hide the header for requests or not + const shouldShowHeader = isResponseVM(element) && !this.rendererOptions.noHeader; + templateData.header?.classList.toggle('header-disabled', !shouldShowHeader); + + if (isRequestVM(element) && element.confirmation) { + this.renderConfirmationAction(element, templateData); + } + + // Do a progressive render if + // - This the last response in the list + // - And it has some content + // - And the response is not complete + // - Or, we previously started a progressive rendering of this element (if the element is complete, we will finish progressive rendering with a very fast rate) + if (isResponseVM(element) && index === this.delegate.getListLength() - 1 && (!element.isComplete || element.renderData)) { + this.traceLayout('renderElement', `start progressive render, index=${index}`); + + const timer = templateData.elementDisposables.add(new dom.WindowIntervalTimer()); + const runProgressiveRender = (initial?: boolean) => { + try { + if (this.doNextProgressiveRender(element, index, templateData, !!initial)) { + timer.cancel(); + } + } catch (err) { + // Kill the timer if anything went wrong, avoid getting stuck in a nasty rendering loop. + timer.cancel(); + this.logService.error(err); + } + }; + timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer)); + runProgressiveRender(true); + } else { + if (isResponseVM(element)) { + this.renderChatResponseBasic(element, index, templateData); + } else if (isRequestVM(element)) { + this.renderChatRequest(element, index, templateData); + } + } + templateData.renderedPartsMounted = true; + } + + private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { + dom.clearNode(templateData.detail); + + if (element.agentOrSlashCommandDetected) { + const msg = element.slashCommand ? localize('usedAgentSlashCommand', "used {0} [[(rerun without)]]", `${chatSubcommandLeader}${element.slashCommand.name}`) : localize('usedAgent', "[[(rerun without)]]"); + dom.reset(templateData.detail, renderFormattedText(msg, { + actionHandler: { + disposables: templateData.elementDisposables, + callback: (content) => { + this._onDidClickRerunWithAgentOrCommandDetection.fire(element); + }, + } + }, $('span.agentOrSlashCommandDetected'))); + + } else if (this.rendererOptions.renderStyle !== 'minimal' && !element.isComplete && !checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.progressMessageAtBottomOfResponse)) { + templateData.detail.textContent = localize('working', "Working"); + } + } + + private renderConfirmationAction(element: IChatRequestViewModel, templateData: IChatListItemTemplate) { + dom.clearNode(templateData.detail); + if (element.confirmation) { + dom.append(templateData.detail, $('span.codicon.codicon-check', { 'aria-hidden': 'true' })); + dom.append(templateData.detail, $('span.confirmation-text', undefined, localize('chatConfirmationAction', 'Selected "{0}"', element.confirmation))); + templateData.header?.classList.remove('header-disabled'); + templateData.header?.classList.add('partially-disabled'); + } + } + + private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void { + const icon = isResponseVM(element) ? + this.getAgentIcon(element.agent?.metadata) : + (element.avatarIcon ?? Codicon.account); + if (icon instanceof URI) { + const avatarIcon = dom.$('img.icon'); + avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); + templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); + } else { + const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); + templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); + } + } + + private getAgentIcon(agent: IChatAgentMetadata | undefined): URI | ThemeIcon { + if (agent?.themeIcon) { + return agent.themeIcon; + } else if (agent?.iconDark && isDark(this.themeService.getColorTheme().type)) { + return agent.iconDark; + } else if (agent?.icon) { + return agent.icon; + } else { + return Codicon.chatSparkle; + } + } + + private renderChatResponseBasic(element: IChatResponseViewModel, index: number, templateData: IChatListItemTemplate) { + templateData.rowContainer.classList.toggle('chat-response-loading', (isResponseVM(element) && !element.isComplete)); + + if (element.isComplete || element.isCanceled) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + if (lastThinking?.domNode && lastThinking.getIsActive()) { + lastThinking.finalizeTitleIfDefault(); + lastThinking.markAsInactive(); + } + this.finalizeAllSubagentParts(templateData); + } + + const content: IChatRendererContent[] = []; + const isFiltered = !!element.errorDetails?.responseIsFiltered; + if (!isFiltered) { + // Always add the references to avoid shifting the content parts when a reference is added, and having to re-diff all the content. + // The part will hide itself if the list is empty. + content.push({ kind: 'references', references: element.contentReferences }); + content.push(...annotateSpecialMarkdownContent(element.response.value)); + if (element.codeCitations.length) { + content.push({ kind: 'codeCitations', citations: element.codeCitations }); + } + } + + if (element.model.response === element.model.entireResponse && element.errorDetails?.message && element.errorDetails.message !== canceledName) { + content.push({ kind: 'errorDetails', errorDetails: element.errorDetails, isLast: index === this.delegate.getListLength() - 1 }); + } + + const fileChangesSummaryPart = this.getChatFileChangesSummaryPart(element); + if (fileChangesSummaryPart) { + content.push(fileChangesSummaryPart); + } + + const diff = this.diff(templateData.renderedParts ?? [], content, element); + this.renderChatContentDiff(diff, content, element, index, templateData); + } + + private shouldShowWorkingProgress(element: IChatResponseViewModel, partsToRender: IChatRendererContent[], templateData: IChatListItemTemplate): boolean { + if (element.agentOrSlashCommandDetected || this.rendererOptions.renderStyle === 'minimal' || element.isComplete || !checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.progressMessageAtBottomOfResponse)) { + return false; + } + + // Don't show working if a streaming tool invocation is already present + if (partsToRender.some(part => part.kind === 'toolInvocation' && IChatToolInvocation.isStreaming(part))) { + return false; + } + + // Show if no content, only "used references", ends with a complete tool call, or ends with complete text edits and there is no incomplete tool call (edits are still being applied some time after they are all generated) + const lastPart = findLast(partsToRender, part => part.kind !== 'markdownContent' || part.content.value.trim().length > 0); + + // don't show working progress when there is thinking content in partsToRender (about to be rendered) + if (partsToRender.some(part => part.kind === 'thinking')) { + return false; + } + + // never show working progress when there is an active thinking piece + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + if (lastThinking) { + return false; + } + + const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); + if (collapsedToolsMode !== CollapsedToolsDisplayMode.Off && + partsToRender.some(part => + (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && + part.presentation !== 'hidden' && + this.shouldPinPart(part, element) + )) { + return false; + } + + // Don't show working spinner when there's any active subagent - subagents have their own progress indicator + if (this.getSubagentPart(templateData.renderedParts)) { + return false; + } + + if ( + !lastPart || + lastPart.kind === 'references' || + ((lastPart.kind === 'toolInvocation' || lastPart.kind === 'toolInvocationSerialized') && (IChatToolInvocation.isComplete(lastPart) || lastPart.presentation === 'hidden')) || + ((lastPart.kind === 'textEditGroup' || lastPart.kind === 'notebookEditGroup') && lastPart.done && !partsToRender.some(part => part.kind === 'toolInvocation' && !IChatToolInvocation.isComplete(part))) || + (lastPart.kind === 'progressTask' && lastPart.deferred.isSettled) || + lastPart.kind === 'mcpServersStarting' + ) { + return true; + } + + return false; + } + + + private getChatFileChangesSummaryPart(element: IChatResponseViewModel): IChatChangesSummaryPart | undefined { + if (!this.shouldShowFileChangesSummary(element)) { + return undefined; + } + if (!element.model.entireResponse.value.some(part => part.kind === 'textEditGroup' || part.kind === 'notebookEditGroup')) { + return undefined; + } + + return { kind: 'changesSummary', requestId: element.requestId, sessionResource: element.sessionResource }; + } + + private renderChatRequest(element: IChatRequestViewModel, index: number, templateData: IChatListItemTemplate) { + templateData.rowContainer.classList.toggle('chat-response-loading', false); + if (element.id === this.viewModel?.editing?.id) { + this._onDidRerender.fire(templateData); + } + + if (this.configService.getValue('chat.editRequests') !== 'none' && this.rendererOptions.editable) { + templateData.elementDisposables.add(dom.addDisposableListener(templateData.rowContainer, dom.EventType.KEY_DOWN, e => { + const ev = new StandardKeyboardEvent(e); + if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { + if (this.viewModel?.editing?.id !== element.id) { + ev.preventDefault(); + ev.stopPropagation(); + this._onDidClickRequest.fire(templateData); + } + } + })); + } + + let content: IChatRendererContent[] = []; + if (!element.confirmation) { + const markdown = isChatFollowup(element.message) ? + element.message.message : + this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.sessionResource, element.message); + content = [{ content: new MarkdownString(markdown), kind: 'markdownContent' }]; + + if (this.rendererOptions.renderStyle === 'minimal' && !element.isComplete) { + templateData.value.classList.add('inline-progress'); + templateData.elementDisposables.add(toDisposable(() => templateData.value.classList.remove('inline-progress'))); + content.push({ content: new MarkdownString('', { supportHtml: true }), kind: 'markdownContent' }); + } else { + templateData.value.classList.remove('inline-progress'); + } + } + + dom.clearNode(templateData.value); + const parts: IChatContentPart[] = []; + + // Render tip above the request message (if available) + const tip = this.chatTipService.getNextTip(element.id, element.timestamp, this.contextKeyService); + if (tip) { + const tipPart = new ChatTipContentPart(tip, this.chatContentMarkdownRenderer); + templateData.value.appendChild(tipPart.domNode); + templateData.elementDisposables.add(tipPart); + } + + let inlineSlashCommandRendered = false; + content.forEach((data, contentIndex) => { + const context: IChatContentPartRenderContext = { + element, + elementIndex: index, + contentIndex: contentIndex, + content: content, + container: templateData.rowContainer, + editorPool: this._editorPool, + diffEditorPool: this._diffEditorPool, + codeBlockModelCollection: this.codeBlockModelCollection, + currentWidth: this._currentLayoutWidth, + onDidChangeVisibility: this._onDidChangeVisibility.event, + get codeBlockStartIndex() { + return parts.reduce((acc, part) => acc + (part.codeblocks?.length ?? 0), 0); + }, + get treeStartIndex() { + return parts.filter(part => part instanceof ChatTreeContentPart).length; + } + }; + const newPart = this.renderChatContentPart(data, templateData, context); + if (newPart) { + + if (this.rendererOptions.renderDetectedCommandsWithRequest + && !inlineSlashCommandRendered + && element.agentOrSlashCommandDetected && element.slashCommand + && data.kind === 'markdownContent' // TODO this is fishy but I didn't find a better way to render on the same inline as the MD request part + ) { + if (newPart.domNode) { + newPart.domNode.style.display = 'inline-flex'; + } + const cmdPart = this.instantiationService.createInstance(ChatAgentCommandContentPart, element.slashCommand, () => this._onDidClickRerunWithAgentOrCommandDetection.fire({ sessionResource: element.sessionResource, requestId: element.id })); + templateData.value.appendChild(cmdPart.domNode); + parts.push(cmdPart); + inlineSlashCommandRendered = true; + } + + if (newPart.domNode) { + templateData.value.appendChild(newPart.domNode); + } + parts.push(newPart); + } + }); + + if (templateData.renderedParts) { + dispose(templateData.renderedParts); + } + templateData.renderedParts = parts; + + if (element.variables.length) { + const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData); + if (newPart.domNode) { + // p has a :last-child rule for margin + templateData.value.appendChild(newPart.domNode); + } + templateData.elementDisposables.add(newPart); + } + } + + /** + * @returns true if progressive rendering should be considered complete- the element's data is fully rendered or the view is not visible + */ + private doNextProgressiveRender(element: IChatResponseViewModel, index: number, templateData: IChatListItemTemplate, isInRenderElement: boolean): boolean { + if (!this._isVisible) { + return true; + } + + if (element.isCanceled) { + this.traceLayout('doNextProgressiveRender', `canceled, index=${index}`); + element.renderData = undefined; + this.renderChatResponseBasic(element, index, templateData); + return true; + } + + templateData.rowContainer.classList.toggle('chat-response-loading', true); + this.traceLayout('doNextProgressiveRender', `START progressive render, index=${index}`); + const contentForThisTurn = this.getNextProgressiveRenderContent(element, templateData); + const partsToRender = this.diff(templateData.renderedParts ?? [], contentForThisTurn.content, element); + + const contentIsAlreadyRendered = partsToRender.every(part => part === null); + if (contentIsAlreadyRendered) { + if (contentForThisTurn.moreContentAvailable) { + // The content that we want to render in this turn is already rendered, but there is more content to render on the next tick + this.traceLayout('doNextProgressiveRender', 'not rendering any new content this tick, but more available'); + return false; + } else if (element.isComplete) { + // All content is rendered, and response is done, so do a normal render + this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); + element.renderData = undefined; + this.renderChatResponseBasic(element, index, templateData); + return true; + } else { + // Nothing new to render, stop rendering until next model update + this.traceLayout('doNextProgressiveRender', 'caught up with the stream- no new content to render'); + return true; + } + } + + // Do an actual progressive render + this.traceLayout('doNextProgressiveRender', `doing progressive render, ${partsToRender.length} parts to render`); + this.renderChatContentDiff(partsToRender, contentForThisTurn.content, element, index, templateData); + + return false; + } + + private renderChatContentDiff(partsToRender: ReadonlyArray, contentForThisTurn: ReadonlyArray, element: IChatResponseViewModel, elementIndex: number, templateData: IChatListItemTemplate): void { + const renderedParts = templateData.renderedParts ?? []; + templateData.renderedParts = renderedParts; + partsToRender.forEach((partToRender, contentIndex) => { + const alreadyRenderedPart = templateData.renderedParts?.[contentIndex]; + + if (!partToRender) { + // null=no change + if (!templateData.renderedPartsMounted) { + alreadyRenderedPart?.onDidRemount?.(); + } + return; + } + + // keep existing thinking part instance during streaming and update it in place + if (alreadyRenderedPart) { + if (partToRender.kind === 'thinking' && alreadyRenderedPart instanceof ChatThinkingContentPart) { + if (!Array.isArray(partToRender.value)) { + alreadyRenderedPart.updateThinking(partToRender); + } + renderedParts[contentIndex] = alreadyRenderedPart; + return; + } else if (alreadyRenderedPart instanceof ChatThinkingContentPart && this.shouldPinPart(partToRender, element)) { + // keep existing thinking part if we are pinning it (combining tool calls into it) + renderedParts[contentIndex] = alreadyRenderedPart; + return; + } + + alreadyRenderedPart.dispose(); + } + + const preceedingContentParts = renderedParts.slice(0, contentIndex); + const context: IChatContentPartRenderContext = { + element, + elementIndex: elementIndex, + content: contentForThisTurn, + contentIndex: contentIndex, + container: templateData.rowContainer, + editorPool: this._editorPool, + diffEditorPool: this._diffEditorPool, + codeBlockModelCollection: this.codeBlockModelCollection, + currentWidth: this._currentLayoutWidth, + onDidChangeVisibility: this._onDidChangeVisibility.event, + get codeBlockStartIndex() { + return preceedingContentParts.reduce((acc, part) => acc + (part.codeblocks?.length ?? 0), 0); + }, + get treeStartIndex() { + return preceedingContentParts.filter(part => part instanceof ChatTreeContentPart).length; + } + }; + + // combine tool invocations into thinking part if needed. render the tool, but do not replace the working spinner with the new part's dom node since it is already inside the thinking part. + const lastThinking = this.getLastThinkingPart(renderedParts); + if (lastThinking && (partToRender.kind === 'toolInvocation' || partToRender.kind === 'toolInvocationSerialized' || partToRender.kind === 'markdownContent' || partToRender.kind === 'textEditGroup') && this.shouldPinPart(partToRender, element)) { + const newPart = this.renderChatContentPart(partToRender, templateData, context); + if (newPart) { + renderedParts[contentIndex] = newPart; + if (alreadyRenderedPart instanceof ChatWorkingProgressContentPart && alreadyRenderedPart?.domNode) { + alreadyRenderedPart.domNode.remove(); + } + } + return; + } + + const newPart = this.renderChatContentPart(partToRender, templateData, context); + if (newPart) { + renderedParts[contentIndex] = newPart; + // Maybe the part can't be rendered in this context, but this shouldn't really happen + try { + if (alreadyRenderedPart?.domNode) { + if (newPart.domNode) { + alreadyRenderedPart.domNode.replaceWith(newPart.domNode); + } else { + alreadyRenderedPart.domNode.remove(); + } + } else if (newPart.domNode && !newPart.domNode.parentElement) { + // Only append if not already attached somewhere else (e.g. inside a thinking wrapper) + templateData.value.appendChild(newPart.domNode); + } + + } catch (err) { + this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); + } + } else { + alreadyRenderedPart?.domNode?.remove(); + } + }); + + // Delete previously rendered parts that are removed + for (let i = partsToRender.length; i < renderedParts.length; i++) { + const part = renderedParts[i]; + if (part) { + part.dispose(); + part.domNode?.remove(); + delete renderedParts[i]; + } + } + } + + /** + * Returns all content parts that should be rendered, and trimmed markdown content. We will diff this with the current rendered set. + */ + private getNextProgressiveRenderContent(element: IChatResponseViewModel, templateData: IChatListItemTemplate): { content: IChatRendererContent[]; moreContentAvailable: boolean } { + const data = this.getDataForProgressiveRender(element); + + // An unregistered setting for development- skip the word counting and smoothing, just render content as it comes in + const renderImmediately = this.configService.getValue('chat.experimental.renderMarkdownImmediately') === true; + + const renderableResponse = annotateSpecialMarkdownContent(element.response.value); + + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} at ${data.rate} words/s, counting...`); + let numNeededWords = data.numWordsToRender; + const partsToRender: IChatRendererContent[] = []; + + // Always add the references to avoid shifting the content parts when a reference is added, and having to re-diff all the content. + // The part will hide itself if the list is empty. + partsToRender.push({ kind: 'references', references: element.contentReferences }); + + let moreContentAvailable = false; + for (let i = 0; i < renderableResponse.length; i++) { + const part = renderableResponse[i]; + if (part.kind === 'markdownContent' && !renderImmediately) { + const wordCountResult = getNWords(part.content.value, numNeededWords); + this.traceLayout('getNextProgressiveRenderContent', ` Chunk ${i}: Want to render ${numNeededWords} words and found ${wordCountResult.returnedWordCount} words. Total words in chunk: ${wordCountResult.totalWordCount}`); + numNeededWords -= wordCountResult.returnedWordCount; + + if (wordCountResult.isFullString) { + partsToRender.push(part); + + // Consumed full markdown chunk- need to ensure that all following non-markdown parts are rendered + for (const nextPart of renderableResponse.slice(i + 1)) { + if (nextPart.kind !== 'markdownContent') { + i++; + partsToRender.push(nextPart); + } else { + break; + } + } + } else { + // Only taking part of this markdown part + moreContentAvailable = true; + partsToRender.push({ ...part, content: new MarkdownString(wordCountResult.value, part.content) }); + } + + if (numNeededWords <= 0) { + // Collected all words and following non-markdown parts if needed, done + if (renderableResponse.slice(i + 1).some(part => part.kind === 'markdownContent')) { + moreContentAvailable = true; + } + break; + } + } else { + partsToRender.push(part); + } + } + + const lastWordCount = element.contentUpdateTimings?.lastWordCount ?? 0; + const newRenderedWordCount = data.numWordsToRender - numNeededWords; + const bufferWords = lastWordCount - newRenderedWordCount; + this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} words. Rendering ${newRenderedWordCount} words. Buffer: ${bufferWords} words`); + if (newRenderedWordCount > 0 && newRenderedWordCount !== element.renderData?.renderedWordCount) { + // Only update lastRenderTime when we actually render new content + element.renderData = { lastRenderTime: Date.now(), renderedWordCount: newRenderedWordCount, renderedParts: partsToRender }; + } + + if (this.shouldShowWorkingProgress(element, partsToRender, templateData)) { + partsToRender.push({ kind: 'working' }); + } + + const fileChangesSummaryPart = this.getChatFileChangesSummaryPart(element); + if (fileChangesSummaryPart) { + partsToRender.push(fileChangesSummaryPart); + } + + return { content: partsToRender, moreContentAvailable }; + } + + private shouldShowFileChangesSummary(element: IChatResponseViewModel): boolean { + // Only show file changes summary for local sessions - background sessions already have their own file changes part + const isLocalSession = getChatSessionType(element.sessionResource) === localChatSessionType; + return element.isComplete && isLocalSession && this.configService.getValue('chat.checkpoints.showFileChanges'); + } + + private getDataForProgressiveRender(element: IChatResponseViewModel) { + const hasMarkdownParts = element.response.value.some(part => part.kind === 'markdownContent' && part.content.value.trim().length > 0); + if (!element.isComplete && hasMarkdownParts && (element.contentUpdateTimings ? element.contentUpdateTimings.lastWordCount : 0) === 0) { + /** + * None of the content parts in the ongoing response have been rendered yet, + * so we should render all existing parts without animation. + */ + return { + numWordsToRender: Number.MAX_SAFE_INTEGER, + rate: Number.MAX_SAFE_INTEGER + }; + } + + const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; + + const rate = this.getProgressiveRenderRate(element); + const numWordsToRender = renderData.lastRenderTime === 0 ? + 1 : + renderData.renderedWordCount + + // Additional words to render beyond what's already rendered + Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); + + return { + numWordsToRender, + rate + }; + } + + private diff(renderedParts: ReadonlyArray, contentToRender: ReadonlyArray, element: ChatTreeItem): ReadonlyArray { + const diff: (IChatRendererContent | null)[] = []; + for (let i = 0; i < contentToRender.length; i++) { + const content = contentToRender[i]; + const renderedPart = renderedParts[i]; + + if (!renderedPart || !renderedPart.hasSameContent(content, contentToRender.slice(i + 1), element)) { + diff.push(content); + } else { + // null -> no change + diff.push(null); + } + } + + return diff; + } + + private hasCodeblockUri(part: IChatRendererContent): boolean { + if (part.kind !== 'markdownContent') { + return false; + } + return hasCodeblockUriTag(part.content.value); + } + + private isCodeblockComplete(part: IChatRendererContent, element: ChatTreeItem): boolean { + if (part.kind !== 'markdownContent') { + return true; + } + return !isResponseVM(element) || element.isComplete || codeblockHasClosingBackticks(part.content.value); + } + + private shouldPinPart(part: IChatRendererContent, element?: IChatResponseViewModel): boolean { + const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); + + // thinking and working content are always pinned (they are the thinking container itself) + if (part.kind === 'thinking' || part.kind === 'working') { + return true; + } + + // should not finalize thinking + if (part.kind === 'undoStop') { + return true; + } + + if (collapsedToolsMode === CollapsedToolsDisplayMode.Off) { + return false; + } + + // is an edit related part + if (this.hasCodeblockUri(part) || part.kind === 'textEditGroup') { + return true; + } + + // Don't pin MCP tools + const isMcpTool = (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.source?.type === 'mcp'; + if (isMcpTool) { + return false; + } + + // don't pin Mermaid tools since it has rendered output + const isMermaidTool = (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.toolId.toLowerCase().includes('mermaid'); + if (isMermaidTool) { + return false; + } + + // don't pin ask questions tool invocations + const isAskQuestionsTool = (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.toolId === 'copilot_askQuestions'; + if (isAskQuestionsTool) { + return false; + } + + // Don't pin subagent tools to thinking parts - they have their own grouping + const isSubagentTool = (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && (part.subAgentInvocationId || part.toolId === RunSubagentTool.Id); + if (isSubagentTool) { + return false; + } + + // only pin terminal tools based on settings + const isTerminalTool = (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') && part.toolSpecificData?.kind === 'terminal'; + const isContributedTerminalToolInvocation = element + && (element.sessionResource.scheme !== Schemas.vscodeChatInput && element.sessionResource.scheme !== Schemas.vscodeLocalChatSession) // contributed sessions + && part.kind === 'toolInvocationSerialized' && part.toolSpecificData?.kind === 'terminal'; // contributed serialized terminal tool invocations data + if (isTerminalTool && !isContributedTerminalToolInvocation) { + // don't pin terminals with confirmation + if (part.kind === 'toolInvocation' && IChatToolInvocation.getConfirmationMessages(part)) { + return false; + } + const terminalToolsInThinking = this.configService.getValue(ChatConfiguration.TerminalToolsInThinking); + return !!terminalToolsInThinking; + } + + if (part.kind === 'toolInvocation') { + // pin when streaming since we don't know if we have confirmation yet or not + if (IChatToolInvocation.isStreaming(part)) { + return true; + } + // don't pin if waiting for confirmation or post-approval + const state = part.state.get(); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return false; + } + return !IChatToolInvocation.getConfirmationMessages(part); + } + + if (part.kind === 'toolInvocationSerialized') { + return true; + } + + return false; + } + + private getLastThinkingPart(renderedParts: ReadonlyArray | undefined): ChatThinkingContentPart | undefined { + if (!renderedParts || renderedParts.length === 0) { + return undefined; + } + + // Search backwards for the most recent active thinking part + for (let i = renderedParts.length - 1; i >= 0; i--) { + const part = renderedParts[i]; + if (part instanceof ChatThinkingContentPart && part.getIsActive()) { + return part; + } + } + + return undefined; + } + + /** + * Determines if a thinking part at the given content index is "look-ahead complete". + * A thinking part is look-ahead complete if there are subsequent parts that will NOT + * be pinned to it, meaning we know this thinking part is already done even though + * the overall response is still in progress. + */ + private isThinkingLookAheadComplete(context: IChatContentPartRenderContext, element?: IChatResponseViewModel): boolean { + // If element is already complete, no need for look-ahead + if (element?.isComplete) { + return true; + } + + // Look at all parts after the current content index + for (let i = context.contentIndex + 1; i < context.content.length; i++) { + const nextPart = context.content[i]; + // If there's any part that would NOT be pinned to the thinking part, + // then this thinking part is already complete + if (!this.shouldPinPart(nextPart, element)) { + return true; + } + } + + return false; + } + + private getSubagentPart(renderedParts: ReadonlyArray | undefined, subAgentInvocationId?: string): ChatSubagentContentPart | undefined { + if (!renderedParts || renderedParts.length === 0) { + return undefined; + } + + // Search backwards for the most recent subagent part + for (let i = renderedParts.length - 1; i >= 0; i--) { + const part = renderedParts[i]; + if (part instanceof ChatSubagentContentPart) { + // If looking for a specific ID, return the part with that ID regardless of active state + if (subAgentInvocationId && part.subAgentInvocationId === subAgentInvocationId) { + return part; + } + // If no ID specified, only return active parts + if (!subAgentInvocationId && part.getIsActive()) { + return part; + } + } + } + + return undefined; + } + + private finalizeAllSubagentParts(templateData: IChatListItemTemplate): void { + if (!templateData.renderedParts) { + return; + } + + // Finalize all active subagent parts (there can be multiple parallel subagents) + for (const part of templateData.renderedParts) { + if (part instanceof ChatSubagentContentPart && part.getIsActive()) { + part.markAsInactive(); + } + } + } + + private handleSubagentToolGrouping(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, subagentId: string, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate, codeBlockStartIndex: number): ChatSubagentContentPart { + // Finalize any active thinking part since subagent tools have their own grouping + this.finalizeCurrentThinkingPart(context, templateData); + + const lastSubagent = this.getSubagentPart(templateData.renderedParts, subagentId); + if (lastSubagent) { + // Append to existing subagent part with matching ID + // But skip the runSubagent tool itself - we only want child tools + if (toolInvocation.toolId !== RunSubagentTool.Id) { + lastSubagent.appendToolInvocation(toolInvocation, codeBlockStartIndex); + } + return lastSubagent; + } + + // Create a new subagent part - it will extract description/agentName/prompt and watch for completion + const subagentPart = this.instantiationService.createInstance( + ChatSubagentContentPart, + subagentId, + toolInvocation, + context, + this.chatContentMarkdownRenderer, + this._contentReferencesListPool, + this._toolEditorPool, + () => this._currentLayoutWidth.get(), + this._toolInvocationCodeBlockCollection, + this._announcedToolProgressKeys, + ); + // Don't append the runSubagent tool itself - its description is already shown in the title + // Only append child tools (those with subAgentInvocationId) + if (toolInvocation.toolId !== RunSubagentTool.Id) { + subagentPart.appendToolInvocation(toolInvocation, codeBlockStartIndex); + } + return subagentPart; + } + + private finalizeCurrentThinkingPart(context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): void { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + if (!lastThinking) { + return; + } + const style = this.configService.getValue('chat.agent.thinkingStyle'); + if (style === ThinkingDisplayMode.CollapsedPreview) { + lastThinking.collapseContent(); + } + lastThinking.finalizeTitleIfDefault(); + lastThinking.resetId(); + lastThinking.markAsInactive(); + } + + private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + try { + // if we get an empty thinking part, mark thinking as finished + if (content.kind === 'thinking' && (Array.isArray(content.value) ? content.value.length === 0 : content.value === '')) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + lastThinking?.resetId(); + return this.renderNoContent(other => content.kind === other.kind); + } + + const isResponseElement = isResponseVM(context.element); + const shouldPin = this.shouldPinPart(content, isResponseElement ? context.element : undefined); + + // sometimes content is rendered out of order on re-renders so instead of looking at the current chat content part's + // context and templateData, we have to look globally to find the active thinking part. + if (context.element.isComplete && !shouldPin) { + for (const templateData of this.templateDataByRequestId.values()) { + if (templateData.renderedParts) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + if (lastThinking?.getIsActive()) { + this.finalizeCurrentThinkingPart(context, templateData); + } + } + } + } + + // Check if this is subagent content + const isSubagentContent = (content.kind === 'toolInvocation' || content.kind === 'toolInvocationSerialized') + && (content.subAgentInvocationId || content.toolId === RunSubagentTool.Id); + + // Finalize all subagent parts when element is complete + // Note: We don't finalize when non-subagent content arrives because parallel subagents may still be running + if (context.element.isComplete && !isSubagentContent) { + for (const templateData of this.templateDataByRequestId.values()) { + this.finalizeAllSubagentParts(templateData); + } + } + + if (content.kind === 'treeData') { + return this.renderTreeData(content, templateData, context); + } else if (content.kind === 'multiDiffData') { + return this.renderMultiDiffData(content, templateData, context); + } else if (content.kind === 'progressMessage') { + return this.instantiationService.createInstance(ChatProgressContentPart, content, this.chatContentMarkdownRenderer, context, undefined, undefined, undefined, undefined); + } else if (content.kind === 'working') { + return this.instantiationService.createInstance(ChatWorkingProgressContentPart, content, this.chatContentMarkdownRenderer, context); + } else if (content.kind === 'progressTask' || content.kind === 'progressTaskSerialized') { + return this.renderProgressTask(content, templateData, context); + } else if (content.kind === 'command') { + return this.instantiationService.createInstance(ChatCommandButtonContentPart, content, context); + } else if (content.kind === 'textEditGroup') { + return this.renderTextEdit(context, content, templateData); + } else if (content.kind === 'confirmation') { + return this.renderConfirmation(context, content, templateData); + } else if (content.kind === 'warning') { + return this.instantiationService.createInstance(ChatErrorContentPart, ChatErrorLevel.Warning, content.content, content, this.chatContentMarkdownRenderer); + } else if (content.kind === 'markdownContent') { + return this.renderMarkdown(content, templateData, context); + } else if (content.kind === 'references') { + return this.renderContentReferencesListData(content, undefined, context, templateData); + } else if (content.kind === 'codeCitations') { + return this.renderCodeCitations(content, context, templateData); + } else if (content.kind === 'toolInvocation' || content.kind === 'toolInvocationSerialized') { + return this.renderToolInvocation(content, context, templateData); + } else if (content.kind === 'extensions') { + return this.renderExtensionsContent(content, context, templateData); + } else if (content.kind === 'pullRequest') { + return this.renderPullRequestContent(content, context, templateData); + } else if (content.kind === 'undoStop') { + return this.renderUndoStop(content); + } else if (content.kind === 'errorDetails') { + return this.renderChatErrorDetails(context, content, templateData); + } else if (content.kind === 'elicitation2' || content.kind === 'elicitationSerialized') { + return this.renderElicitation(context, content, templateData); + } else if (content.kind === 'questionCarousel') { + return this.renderQuestionCarousel(context, content, templateData); + } else if (content.kind === 'changesSummary') { + return this.renderChangesSummary(content, context, templateData); + } else if (content.kind === 'mcpServersStarting') { + return this.renderMcpServersInteractionRequired(content, context, templateData); + } else if (content.kind === 'thinking') { + return this.renderThinkingPart(content, context, templateData); + } else if (content.kind === 'workspaceEdit') { + return this.instantiationService.createInstance(ChatWorkspaceEditContentPart, content, context, this.chatContentMarkdownRenderer); + } + + return this.renderNoContent(other => content.kind === other.kind); + } catch (err) { + alert(`Chat error: ${toErrorMessage(err, false)}`); + this.logService.error('ChatListItemRenderer#renderChatContentPart: error rendering content', toErrorMessage(err, true)); + const errorPart = this.instantiationService.createInstance(ChatErrorContentPart, ChatErrorLevel.Error, new MarkdownString(localize('renderFailMsg', "Failed to render content") + `: ${toErrorMessage(err, false)}`), content, this.chatContentMarkdownRenderer); + return { + dispose: () => errorPart.dispose(), + domNode: errorPart.domNode, + hasSameContent: (other => content.kind === other.kind), + }; + } + } + + override dispose(): void { + this._announcedToolProgressKeys.clear(); + super.dispose(); + } + + + private renderChatErrorDetails(context: IChatContentPartRenderContext, content: IChatErrorDetailsPart, templateData: IChatListItemTemplate): IChatContentPart { + if (!isResponseVM(context.element)) { + return this.renderNoContent(other => content.kind === other.kind); + } + + const isLast = context.elementIndex === this.delegate.getListLength() - 1; + if (content.errorDetails.isQuotaExceeded) { + const renderedError = this.instantiationService.createInstance(ChatQuotaExceededPart, context.element, content, this.chatContentMarkdownRenderer); + 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.chatContentMarkdownRenderer, context); + return errorConfirmation; + } else { + const level = content.errorDetails.level ?? ChatErrorLevel.Error; + return this.instantiationService.createInstance(ChatErrorContentPart, level, new MarkdownString(content.errorDetails.message), content, this.chatContentMarkdownRenderer); + } + } + + private renderUndoStop(content: IChatUndoStop) { + return this.renderNoContent(other => other.kind === content.kind && other.id === content.id); + } + + private renderNoContent(equals: (other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem) => boolean): IChatContentPart { + return { + dispose: () => { }, + domNode: undefined, + hasSameContent: equals, + }; + } + + private renderTreeData(content: IChatTreeData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const data = content.treeData; + const treePart = this.instantiationService.createInstance(ChatTreeContentPart, data, this._treePool); + + if (isResponseVM(context.element)) { + const fileTreeFocusInfo = { + treeDataId: data.uri.toString(), + treeIndex: context.treeStartIndex, + focus() { + treePart.domFocus(); + } + }; + + // TODO@roblourens there's got to be a better way to navigate trees + treePart.addDisposable(treePart.onDidFocus(() => { + this.focusedFileTreesByResponseId.set(context.element.id, fileTreeFocusInfo.treeIndex); + })); + + const fileTrees = this.fileTreesByResponseId.get(context.element.id) ?? []; + fileTrees.push(fileTreeFocusInfo); + this.fileTreesByResponseId.set(context.element.id, distinct(fileTrees, (v) => v.treeDataId)); + treePart.addDisposable(toDisposable(() => this.fileTreesByResponseId.set(context.element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); + } + + return treePart; + } + + private renderMultiDiffData(content: IChatMultiDiffData | IChatMultiDiffDataSerialized, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const multiDiffPart = this.instantiationService.createInstance(ChatMultiDiffContentPart, content, context.element); + return multiDiffPart; + } + + private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCollapsibleListContentPart { + const referencesPart = this.instantiationService.createInstance(ChatUsedReferencesListContentPart, references.references, labelOverride, context, this._contentReferencesListPool, { expandedWhenEmptyResponse: checkModeOption(this.delegate.currentChatMode(), this.rendererOptions.referencesExpandedWhenEmptyResponse) }); + + return referencesPart; + } + + private renderCodeCitations(citations: IChatCodeCitations, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatCodeCitationContentPart { + const citationsPart = this.instantiationService.createInstance(ChatCodeCitationContentPart, citations, context); + return citationsPart; + } + + private handleRenderedCodeblocks(element: ChatTreeItem, part: IChatContentPart, codeBlockStartIndex: number): void { + if (!part.addDisposable || part.codeblocksPartId === undefined) { + return; + } + + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id) ?? []; + this.codeBlocksByResponseId.set(element.id, codeBlocksByResponseId); + part.addDisposable(toDisposable(() => { + const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); + if (codeBlocksByResponseId) { + // Only delete if this is my code block + part.codeblocks?.forEach((info, i) => { + const codeblock = codeBlocksByResponseId[codeBlockStartIndex + i]; + if (codeblock?.ownerMarkdownPartId === part.codeblocksPartId) { + delete codeBlocksByResponseId[codeBlockStartIndex + i]; + } + }); + } + })); + + part.codeblocks?.forEach((info, i) => { + codeBlocksByResponseId[codeBlockStartIndex + i] = info; + part.addDisposable!(thenIfNotDisposed(info.uriPromise, uri => { + if (!uri) { + return; + } + + this.codeBlocksByEditorUri.set(uri, info); + part.addDisposable!(toDisposable(() => { + const codeblock = this.codeBlocksByEditorUri.get(uri); + if (codeblock?.ownerMarkdownPartId === part.codeblocksPartId) { + this.codeBlocksByEditorUri.delete(uri); + } + })); + })); + }); + + } + + private renderToolInvocation(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart | undefined { + if (this.configService.getValue('chat.agent.thinking.collapsedTools') === CollapsedToolsDisplayMode.Off) { + this.finalizeCurrentThinkingPart(context, templateData); + } + + const codeBlockStartIndex = context.codeBlockStartIndex; + + // Factory that creates the tool invocation part with all necessary setup + let lazilyCreatedPart: ChatToolInvocationPart | undefined = undefined; + const createToolPart = (): { domNode: HTMLElement; part: ChatToolInvocationPart } => { + lazilyCreatedPart = this.instantiationService.createInstance(ChatToolInvocationPart, toolInvocation, context, this.chatContentMarkdownRenderer, this._contentReferencesListPool, this._toolEditorPool, () => this._currentLayoutWidth.get(), this._toolInvocationCodeBlockCollection, this._announcedToolProgressKeys, codeBlockStartIndex); + this.handleRenderedCodeblocks(context.element, lazilyCreatedPart, codeBlockStartIndex); + return { domNode: lazilyCreatedPart.domNode, part: lazilyCreatedPart }; + }; + + // handling for when we want to put tool invocations inside a thinking part + const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); + if (isResponseVM(context.element) && collapsedToolsMode !== CollapsedToolsDisplayMode.Off) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + + // create thinking part if it doesn't exist yet + if (!lastThinking && toolInvocation.presentation !== 'hidden' && this.shouldPinPart(toolInvocation, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always) { + const thinkingPart = this.renderThinkingPart({ + kind: 'thinking', + }, context, templateData); + + if (thinkingPart instanceof ChatThinkingContentPart) { + // Append using factory - thinking part decides whether to render lazily + thinkingPart.appendItem(createToolPart, toolInvocation.toolId, toolInvocation, templateData.value); + this.setupConfirmationTransitionWatcher(toolInvocation, thinkingPart, () => lazilyCreatedPart, createToolPart, context, templateData); + } + + return thinkingPart; + } + + if (this.shouldPinPart(toolInvocation, context.element)) { + if (lastThinking && toolInvocation.presentation !== 'hidden') { + // Append using factory - thinking part decides whether to render lazily + lastThinking.appendItem(createToolPart, toolInvocation.toolId, toolInvocation, templateData.value); + this.setupConfirmationTransitionWatcher(toolInvocation, lastThinking, () => lazilyCreatedPart, createToolPart, context, templateData); + return this.renderNoContent((other, followingContent, element) => lazilyCreatedPart ? + lazilyCreatedPart.hasSameContent(other, followingContent, element) : + toolInvocation.kind === other.kind); + } + } else { + this.finalizeCurrentThinkingPart(context, templateData); + } + } + + // Check for subagent grouping before creating tool part - subagent part handles lazy creation + const subagentId = toolInvocation.toolId === RunSubagentTool.Id ? toolInvocation.toolCallId : toolInvocation.subAgentInvocationId; + if (subagentId && isResponseVM(context.element) && toolInvocation.presentation !== 'hidden') { + return this.handleSubagentToolGrouping(toolInvocation, subagentId, context, templateData, codeBlockStartIndex); + } + + // For cases not handled above (no thinking part, no subagent, etc.), create the part now + const { part } = createToolPart(); + + return part; + } + + // watch for confirmation part transition when tool invocation is streaming + private setupConfirmationTransitionWatcher( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + thinkingPart: ChatThinkingContentPart, + getCreatedPart: () => ChatToolInvocationPart | undefined, + createToolPart: () => { domNode: HTMLElement; part: ChatToolInvocationPart }, + context: IChatContentPartRenderContext, + templateData: IChatListItemTemplate + ): void { + if (toolInvocation.kind !== 'toolInvocation') { + return; + } + + const removeConfirmationWidget = () => { + const createdPart = getCreatedPart(); + // move the created part out of thinking and into the main template + if (createdPart?.domNode) { + const wrapper = createdPart.domNode.parentElement; + if (wrapper?.classList.contains('chat-thinking-tool-wrapper')) { + wrapper.remove(); + } + templateData.value.appendChild(createdPart.domNode); + } else { + thinkingPart.removeLazyItem(toolInvocation.toolId); + const { domNode } = createToolPart(); + templateData.value.appendChild(domNode); + } + this.finalizeCurrentThinkingPart(context, templateData); + }; + + const currentState = toolInvocation.state.get(); + if (currentState.type === IChatToolInvocation.StateKind.WaitingForConfirmation || currentState.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + removeConfirmationWidget(); + return; + } + + const isWorkingState = (type: IChatToolInvocation.StateKind) => + type === IChatToolInvocation.StateKind.Streaming || type === IChatToolInvocation.StateKind.Executing; + + if (!isWorkingState(currentState.type)) { + return; + } + + let didRemoveConfirmationWidget = false; + const disposable = autorun(reader => { + const state = toolInvocation.state.read(reader); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + if (didRemoveConfirmationWidget) { + return; + } + didRemoveConfirmationWidget = true; + disposable.dispose(); + removeConfirmationWidget(); + } + }); + + thinkingPart.addDisposable(disposable); + } + + private renderExtensionsContent(extensionsContent: IChatExtensionsContent, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart | undefined { + const part = this.instantiationService.createInstance(ChatExtensionsContentPart, extensionsContent); + return part; + } + + private renderPullRequestContent(pullRequestContent: IChatPullRequestContent, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart | undefined { + const part = this.instantiationService.createInstance(ChatPullRequestContentPart, pullRequestContent); + return part; + } + + private renderProgressTask(task: IChatTask | IChatTaskSerialized, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { + if (!isResponseVM(context.element)) { + return; + } + + const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.chatContentMarkdownRenderer, context); + return taskPart; + } + + + private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { + const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); + return part; + } + + private renderElicitation(context: IChatContentPartRenderContext, elicitation: IChatElicitationRequest | IChatElicitationRequestSerialized, templateData: IChatListItemTemplate): IChatContentPart { + if (elicitation.kind === 'elicitationSerialized' ? elicitation.isHidden : elicitation.isHidden?.get()) { + return this.renderNoContent(other => elicitation.kind === other.kind); + } + + this.finalizeCurrentThinkingPart(context, templateData); + + const part = this.instantiationService.createInstance(ChatElicitationContentPart, elicitation, context); + return part; + } + + private renderQuestionCarousel(context: IChatContentPartRenderContext, carousel: IChatQuestionCarousel, templateData: IChatListItemTemplate): IChatContentPart { + this.finalizeCurrentThinkingPart(context, templateData); + + const part = this.instantiationService.createInstance(ChatQuestionCarouselPart, carousel, context, { + onSubmit: async (answers) => { + // Mark the carousel as used and store the answers + const answersRecord = answers ? Object.fromEntries(answers) : undefined; + if (answersRecord) { + carousel.data = answersRecord; + } + carousel.isUsed = true; + + // Notify the extension about the carousel answers to resolve the deferred promise + if (isResponseVM(context.element) && carousel.resolveId) { + this.chatService.notifyQuestionCarouselAnswer(context.element.requestId, carousel.resolveId, answersRecord); + } + + // Remove from pending carousels + this.removeCarouselFromTracking(context, part); + } + }); + + // If global auto-approve (yolo mode) is enabled, skip with defaults immediately + if (!carousel.isUsed && this.configService.getValue(ChatConfiguration.GlobalAutoApprove)) { + part.skip(); + } + + // Track the carousel for auto-skip when user submits a new message + if (isResponseVM(context.element) && carousel.allowSkip && !carousel.isUsed) { + let carousels = this.pendingQuestionCarousels.get(context.element.sessionResource); + if (!carousels) { + carousels = new Set(); + this.pendingQuestionCarousels.set(context.element.sessionResource, carousels); + } + carousels.add(part); + + // Clean up when the part is disposed + part.addDisposable({ dispose: () => this.removeCarouselFromTracking(context, part) }); + } + + return part; + } + + private removeCarouselFromTracking(context: IChatContentPartRenderContext, part: ChatQuestionCarouselPart): void { + if (isResponseVM(context.element)) { + const carousels = this.pendingQuestionCarousels.get(context.element.sessionResource); + if (carousels) { + carousels.delete(part); + } + } + } + + private renderChangesSummary(content: IChatChangesSummaryPart, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart { + const part = this.instantiationService.createInstance(ChatCheckpointFileChangesSummaryContentPart, content, context); + return part; + } + + private renderAttachments(variables: readonly IChatRequestVariableEntry[], contentReferences: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, { + variables, + contentReferences, + domNode: undefined + }); + } + + private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { + const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth.get()); + return textEditPart; + } + + private renderMarkdown(markdown: IChatMarkdownContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { + const element = context.element; + const isFinalAnswerPart = isResponseVM(element) && element.isComplete && context.contentIndex === context.content.length - 1; + if (!this.hasCodeblockUri(markdown) || isFinalAnswerPart) { + this.finalizeCurrentThinkingPart(context, templateData); + } + const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete || !!element.renderData); + const codeBlockStartIndex = context.codeBlockStartIndex; + const markdownPart = templateData.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.chatContentMarkdownRenderer, undefined, this._currentLayoutWidth.get(), this.codeBlockModelCollection, {}); + if (isRequestVM(element)) { + markdownPart.domNode.tabIndex = 0; + if (this.configService.getValue('chat.editRequests') === 'inline' && this.rendererOptions.editable) { + markdownPart.domNode.classList.add('clickable'); + markdownPart.addDisposable(dom.addDisposableListener(markdownPart.domNode, dom.EventType.CLICK, (e: MouseEvent) => { + if (this.viewModel?.editing?.id === element.id) { + return; + } + + // Don't handle clicks on links + const clickedElement = e.target as HTMLElement; + if (clickedElement.tagName === 'A') { + return; + } + + // Don't handle if there's a text selection in the window + const selection = dom.getWindow(templateData.rowContainer).getSelection(); + if (selection && !selection.isCollapsed && selection.toString().length > 0) { + return; + } + + // Don't handle if there's a selection in code block + const monacoEditor = dom.findParentWithClass(clickedElement, 'monaco-editor'); + if (monacoEditor) { + const editorPart = Array.from(this.editorsInUse()).find(editor => + editor.element.contains(monacoEditor)); + + if (editorPart?.editor.getSelection()?.isEmpty() === false) { + return; + } + } + + e.preventDefault(); + e.stopPropagation(); + this._onDidClickRequest.fire(templateData); + })); + markdownPart.addDisposable(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), markdownPart.domNode, localize('requestMarkdownPartTitle', "Click to Edit"), { trapFocus: true })); + } + markdownPart.addDisposable(dom.addDisposableListener(markdownPart.domNode, dom.EventType.FOCUS, () => { + this.hoverVisible(templateData.requestHover); + })); + markdownPart.addDisposable(dom.addDisposableListener(markdownPart.domNode, dom.EventType.BLUR, () => { + this.hoverHidden(templateData.requestHover); + })); + } + + this.handleRenderedCodeblocks(element, markdownPart, codeBlockStartIndex); + + const collapsedToolsMode = this.configService.getValue('chat.agent.thinking.collapsedTools'); + if (isResponseVM(context.element) && collapsedToolsMode !== CollapsedToolsDisplayMode.Off && !isFinalAnswerPart) { + + // append to thinking part when the codeblock is complete + const isComplete = this.isCodeblockComplete(markdown, context.element); + + // Check if this markdown should be routed to a subagent content part + const subAgentInvocationId = extractSubAgentInvocationIdFromText(markdown.content.value); + if (subAgentInvocationId) { + const subagentPart = this.getSubagentPart(templateData.renderedParts, subAgentInvocationId); + if (subagentPart && markdownPart?.domNode && isComplete) { + subagentPart.appendMarkdownItem( + () => ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); + return subagentPart; + } + } + + // create thinking part if it doesn't exist yet + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + if (!lastThinking && markdownPart?.domNode && this.shouldPinPart(markdown, context.element) && collapsedToolsMode === CollapsedToolsDisplayMode.Always && isComplete) { + const thinkingPart = this.renderThinkingPart({ + kind: 'thinking', + }, context, templateData); + + if (thinkingPart instanceof ChatThinkingContentPart) { + // Factory wrapping already-created markdown part + thinkingPart.appendItem( + () => ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); + } + + return thinkingPart; + } + + if (this.shouldPinPart(markdown, context.element) && isComplete) { + if (lastThinking && markdownPart?.domNode) { + // Factory wrapping already-created markdown part + lastThinking.appendItem( + () => ({ domNode: markdownPart.domNode, disposable: markdownPart }), + markdownPart.codeblocksPartId, + markdown, + templateData.value + ); + } + } else if (!this.shouldPinPart(markdown, context.element)) { + this.finalizeCurrentThinkingPart(context, templateData); + } + } + + return markdownPart; + } + + renderThinkingPart(content: IChatThinkingPart, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart { + // TODO @justschen @karthiknadig: remove this when OSWE moves off commentary channel + if (!content.id) { + content.id = Date.now().toString(); + } + + // Determine if this thinking part is already complete based on look-ahead + // (i.e., there are subsequent parts that won't be pinned to this thinking part) + const element = isResponseVM(context.element) ? context.element : undefined; + const streamingCompleted = this.isThinkingLookAheadComplete(context, element); + + // if array, we do a naive part by part rendering for now + if (Array.isArray(content.value)) { + if (content.value.length < 1) { + const lastThinking = this.getLastThinkingPart(templateData.renderedParts); + lastThinking?.finalizeTitleIfDefault(); + return this.renderNoContent(other => content.kind === other.kind); + } + let lastPart: IChatContentPart | undefined; + for (const item of content.value) { + if (item) { + const lastThinkingPart = lastPart instanceof ChatThinkingContentPart && lastPart.getIsActive() ? lastPart : undefined; + if (lastThinkingPart) { + lastThinkingPart.setupThinkingContainer({ ...content, value: item }); + } else { + const itemContent = { ...content, value: item }; + const itemPart = templateData.instantiationService.createInstance(ChatThinkingContentPart, itemContent, context, this.chatContentMarkdownRenderer, streamingCompleted); + lastPart = itemPart; + } + } + } + return lastPart ?? this.renderNoContent(other => content.kind === other.kind); + // non-array, handle case where we are currently thinking vs. starting a new thinking part + } else { + const lastActiveThinking = this.getLastThinkingPart(templateData.renderedParts); + if (lastActiveThinking) { + lastActiveThinking.setupThinkingContainer(content); + return lastActiveThinking; + } else { + const part = templateData.instantiationService.createInstance(ChatThinkingContentPart, content, context, this.chatContentMarkdownRenderer, streamingCompleted); + return part; + } + + } + } + + disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate, details?: IListElementRenderDetails): void { + this.traceLayout('disposeElement', `Disposing element, index=${index}`); + templateData.elementDisposables.clear(); + + if (templateData.currentElement && !this.viewModel?.editing) { + this.templateDataByRequestId.delete(templateData.currentElement.id); + } + + if (isRequestVM(node.element) && node.element.id === this.viewModel?.editing?.id && details?.onScroll) { + this._onDidDispose.fire(templateData); + } + + // Don't retain the toolbar context which includes chat viewmodels + if (templateData.titleToolbar) { + templateData.titleToolbar.context = undefined; + } + templateData.footerToolbar.context = undefined; + templateData.checkpointToolbar.context = undefined; + templateData.checkpointRestoreToolbar.context = undefined; + } + + private renderMcpServersInteractionRequired(content: IChatMcpServersStarting | IChatMcpServersStartingSerialized, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): IChatContentPart { + return this.instantiationService.createInstance(ChatMcpServersInteractionContentPart, content, context); + } + + disposeTemplate(templateData: IChatListItemTemplate): void { + this.clearRenderedParts(templateData); + templateData.templateDisposables.dispose(); + } + + private hoverVisible(requestHover: HTMLElement) { + requestHover.style.opacity = '1'; + } + + private hoverHidden(requestHover: HTMLElement) { + requestHover.style.opacity = '0'; + } + +} + +export class ChatListDelegate extends CachedListVirtualDelegate { + constructor( + private readonly defaultElementHeight: number, + ) { + super(); + } + + protected estimateHeight(element: ChatTreeItem): number { + // currentRenderedHeight is not load-bearing here- probably if it's ever set, then the superclass cache will have the height. + return element.currentRenderedHeight ?? this.defaultElementHeight; + } + + getTemplateId(element: ChatTreeItem): string { + return ChatListItemRenderer.ID; + } + + hasDynamicHeight(element: ChatTreeItem): boolean { + return true; + } +} + +const voteDownDetailLabels: Record = { + [ChatAgentVoteDownReason.IncorrectCode]: localize('incorrectCode', "Suggested incorrect code"), + [ChatAgentVoteDownReason.DidNotFollowInstructions]: localize('didNotFollowInstructions', "Didn't follow instructions"), + [ChatAgentVoteDownReason.MissingContext]: localize('missingContext', "Missing context"), + [ChatAgentVoteDownReason.OffensiveOrUnsafe]: localize('offensiveOrUnsafe', "Offensive or unsafe"), + [ChatAgentVoteDownReason.PoorlyWrittenOrFormatted]: localize('poorlyWrittenOrFormatted', "Poorly written or formatted"), + [ChatAgentVoteDownReason.RefusedAValidRequest]: localize('refusedAValidRequest', "Refused a valid request"), + [ChatAgentVoteDownReason.IncompleteCode]: localize('incompleteCode', "Incomplete code"), + [ChatAgentVoteDownReason.WillReportIssue]: localize('reportIssue', "Report an issue"), + [ChatAgentVoteDownReason.Other]: localize('other', "Other"), +}; + +export class ChatVoteDownButton extends DropdownMenuActionViewItem { + constructor( + action: IAction, + options: IDropdownMenuActionViewItemOptions | undefined, + @ICommandService private readonly commandService: ICommandService, + @IWorkbenchIssueService private readonly issueService: IWorkbenchIssueService, + @ILogService private readonly logService: ILogService, + @IContextMenuService contextMenuService: IContextMenuService, + ) { + super(action, + { getActions: () => this.getActions(), }, + contextMenuService, + { + ...options, + classNames: ThemeIcon.asClassNameArray(Codicon.thumbsdown), + }); + } + + getActions(): readonly IAction[] { + return [ + this.getVoteDownDetailAction(ChatAgentVoteDownReason.IncorrectCode), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.DidNotFollowInstructions), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.IncompleteCode), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.MissingContext), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.PoorlyWrittenOrFormatted), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.RefusedAValidRequest), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.OffensiveOrUnsafe), + this.getVoteDownDetailAction(ChatAgentVoteDownReason.Other), + { + id: 'reportIssue', + label: voteDownDetailLabels[ChatAgentVoteDownReason.WillReportIssue], + tooltip: '', + enabled: true, + class: undefined, + run: async (context: IChatResponseViewModel) => { + if (!isResponseVM(context)) { + this.logService.error('ChatVoteDownButton#run: invalid context'); + return; + } + + await this.commandService.executeCommand(MarkUnhelpfulActionId, context, ChatAgentVoteDownReason.WillReportIssue); + await this.issueService.openReporter({ extensionId: context.agent?.extensionId.value }); + } + } + ]; + } + + override render(container: HTMLElement): void { + super.render(container); + + this.element?.classList.toggle('checked', this.action.checked); + } + + private getVoteDownDetailAction(reason: ChatAgentVoteDownReason): IAction { + const label = voteDownDetailLabels[reason]; + return { + id: MarkUnhelpfulActionId, + label, + tooltip: '', + enabled: true, + checked: (this._context as IChatResponseViewModel).voteDownReason === reason, + class: undefined, + run: async (context: IChatResponseViewModel) => { + if (!isResponseVM(context)) { + this.logService.error('ChatVoteDownButton#getVoteDownDetailAction: invalid context'); + return; + } + + await this.commandService.executeCommand(MarkUnhelpfulActionId, context, reason); + } + }; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts new file mode 100644 index 00000000000..11d2b3e74c7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatListWidget.ts @@ -0,0 +1,830 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js'; +import { Button } from '../../../../../base/browser/ui/button/button.js'; +import { ITreeContextMenuEvent, ITreeElement, ITreeFilter } from '../../../../../base/browser/ui/tree/tree.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { FuzzyScore } from '../../../../../base/common/filters.js'; +import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ScrollEvent } from '../../../../../base/common/scrollable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { asCssVariable, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js'; +import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatFollowup, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { IChatRequestModeInfo } from '../../common/model/chatModel.js'; +import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js'; +import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions } from '../chat.js'; +import { CodeBlockPart } from './chatContentParts/codeBlockPart.js'; +import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js'; +import { ChatEditorOptions } from './chatOptions.js'; + +export interface IChatListWidgetStyles { + listForeground?: string; + listBackground?: string; +} + +export interface IChatListWidgetOptions { + /** + * Options for the list item renderer. + */ + readonly rendererOptions?: IChatListItemRendererOptions; + + /** + * Default height for list elements. + */ + readonly defaultElementHeight?: number; + + /** + * DOM node for overflow widgets (e.g., code editors). + */ + readonly overflowWidgetsDomNode?: HTMLElement; + + /** + * Optional style overrides for the list. + */ + readonly styles?: IChatListWidgetStyles; + + /** + * Callback to get the current chat mode. + */ + readonly currentChatMode?: () => ChatModeKind; + + /** + * View ID for editor options (used in ChatWidget context). + */ + readonly viewId?: string; + + /** + * Input editor background color key. + */ + readonly inputEditorBackground?: string; + + /** + * Result editor background color key. + */ + readonly resultEditorBackground?: string; + + /** + * Optional filter for the tree. + */ + readonly filter?: ITreeFilter; + + /** + * Optional code block model collection to use. + * If not provided, one will be created. + */ + readonly codeBlockModelCollection?: CodeBlockModelCollection; + + /** + * Initial view model. + */ + readonly viewModel?: IChatViewModel; + + /** + * Optional pre-created editor options. + * If provided, these will be used instead of creating new ones. + */ + readonly editorOptions?: ChatEditorOptions; + + /** + * The chat location (for rerun requests). + */ + readonly location?: ChatAgentLocation; + + /** + * Callback to get current language model ID (for rerun requests). + */ + readonly getCurrentLanguageModelId?: () => string | undefined; + + /** + * Callback to get current mode info (for rerun requests). + */ + readonly getCurrentModeInfo?: () => IChatRequestModeInfo | undefined; + + /** + * The render style for the chat widget. Affects minimum height behavior. + */ + readonly renderStyle?: 'compact' | 'minimal'; +} + +/** + * A reusable widget that encapsulates chat list/tree rendering. + * This can be used in various contexts such as the main chat widget, + * hover previews, etc. + */ +export class ChatListWidget extends Disposable { + + //#region Events + + private readonly _onDidScroll = this._register(new Emitter()); + readonly onDidScroll: Event = this._onDidScroll.event; + + private readonly _onDidChangeContentHeight = this._register(new Emitter()); + readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; + + private readonly _onDidClickFollowup = this._register(new Emitter()); + readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; + + private readonly _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private readonly _onDidChangeItemHeight = this._register(new Emitter<{ element: ChatTreeItem; height: number }>()); + /** Event fired when an item's height changes. Used for dynamic layout mode. */ + readonly onDidChangeItemHeight: Event<{ element: ChatTreeItem; height: number }> = this._onDidChangeItemHeight.event; + + /** + * Event fired when a request item is clicked. + */ + get onDidClickRequest(): Event { + return this._renderer.onDidClickRequest; + } + + /** + * Event fired when an item is re-rendered. + */ + get onDidRerender(): Event { + return this._renderer.onDidRerender; + } + + /** + * Event fired when a template is disposed. + */ + get onDidDispose(): Event { + return this._renderer.onDidDispose; + } + + /** + * Event fired when focus moves outside the editing area. + */ + get onDidFocusOutside(): Event { + return this._renderer.onDidFocusOutside; + } + + //#endregion + + //#region Private fields + + private readonly _tree: WorkbenchObjectTree; + private readonly _renderer: ChatListItemRenderer; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; + + private _viewModel: IChatViewModel | undefined; + private _visible = true; + private _lastItem: ChatTreeItem | undefined; + private _mostRecentlyFocusedItemIndex: number = -1; + private _scrollLock: boolean = true; + private _settingChangeCounter: number = 0; + private _visibleChangeCount: number = 0; + + private readonly _container: HTMLElement; + private readonly _scrollDownButton: Button; + private readonly _lastItemIdContextKey: IContextKey; + + private readonly _location: ChatAgentLocation | undefined; + private readonly _getCurrentLanguageModelId: (() => string | undefined) | undefined; + private readonly _getCurrentModeInfo: (() => IChatRequestModeInfo | undefined) | undefined; + private readonly _renderStyle: 'compact' | 'minimal' | undefined; + + //#endregion + + //#region Properties + + get domNode(): HTMLElement { + return this._container; + } + + get scrollTop(): number { + return this._tree.scrollTop; + } + + set scrollTop(value: number) { + this._tree.scrollTop = value; + } + + get scrollHeight(): number { + return this._tree.scrollHeight; + } + + get renderHeight(): number { + return this._tree.renderHeight; + } + + get contentHeight(): number { + return this._tree.contentHeight; + } + + /** + * Whether the list is scrolled to the bottom. + */ + get isScrolledToBottom(): boolean { + return this._tree.scrollTop + this._tree.renderHeight >= this._tree.scrollHeight - 2; + } + + /** + * The last item in the list. + */ + get lastItem(): ChatTreeItem | undefined { + return this._lastItem; + } + + + + //#endregion + + constructor( + container: HTMLElement, + options: IChatListWidgetOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @ILogService private readonly logService: ILogService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, + ) { + super(); + + this._viewModel = options.viewModel; + this._codeBlockModelCollection = options.codeBlockModelCollection ?? this._register(this.instantiationService.createInstance(CodeBlockModelCollection, 'chatListWidget')); + this._location = options.location; + this._getCurrentLanguageModelId = options.getCurrentLanguageModelId; + this._getCurrentModeInfo = options.getCurrentModeInfo; + this._lastItemIdContextKey = ChatContextKeys.lastItemId.bindTo(this.contextKeyService); + this._container = container; + + const scopedInstantiationService = this._register(this.instantiationService.createChild( + new ServiceCollection([IContextKeyService, this.contextKeyService]) + )); + this._renderStyle = options.renderStyle; + + // Create overflow widgets container + const overflowWidgetsContainer = options.overflowWidgetsDomNode ?? document.createElement('div'); + if (!options.overflowWidgetsDomNode) { + overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor'); + this._container.append(overflowWidgetsContainer); + this._register(toDisposable(() => overflowWidgetsContainer.remove())); + } + + // Create editor options (use provided or create new) + const editorOptions = options.editorOptions ?? this._register(scopedInstantiationService.createInstance( + ChatEditorOptions, + options.viewId, + 'foreground', + options.inputEditorBackground ?? 'chat.requestEditor.background', + options.resultEditorBackground ?? 'chat.responseEditor.background' + )); + + // Create delegate + const delegate = scopedInstantiationService.createInstance( + ChatListDelegate, + options.defaultElementHeight ?? 200 + ); + + // Create renderer delegate + const rendererDelegate: IChatRendererDelegate = { + getListLength: () => this._tree.getNode(null).visibleChildrenCount, + onDidScroll: this.onDidScroll, + container: this._container, + currentChatMode: options.currentChatMode ?? (() => ChatModeKind.Ask), + }; + + // Create renderer + this._renderer = this._register(scopedInstantiationService.createInstance( + ChatListItemRenderer, + editorOptions, + options.rendererOptions ?? {}, + rendererDelegate, + this._codeBlockModelCollection, + overflowWidgetsContainer, + this._viewModel, + )); + + // Wire up renderer events + this._register(this._renderer.onDidClickFollowup(item => { + this._onDidClickFollowup.fire(item); + })); + + this._register(this._renderer.onDidChangeItemHeight(e => { + this._updateElementHeight(e.element, e.height); + + // If the second-to-last item's height changed, update the last item's min height + const secondToLastItem = this._viewModel?.getItems().at(-2); + if (e.element.id === secondToLastItem?.id) { + this.updateLastItemMinHeight(); + } + + this._onDidChangeItemHeight.fire(e); + })); + + // Handle rerun with agent or command detection internally + this._register(this._renderer.onDidClickRerunWithAgentOrCommandDetection(e => { + const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId); + if (request) { + const sendOptions: IChatSendRequestOptions = { + noCommandDetection: true, + attempt: request.attempt + 1, + location: this._location, + userSelectedModelId: this._getCurrentLanguageModelId?.(), + modeInfo: this._getCurrentModeInfo?.(), + }; + this.chatAccessibilityService.acceptRequest(e.sessionResource); + this.chatService.resendRequest(request, sendOptions).catch(e => this.logService.error('FAILED to rerun request', e)); + } + })); + + // Create tree + const styles = options.styles ?? {}; + this._tree = this._register(scopedInstantiationService.createInstance( + WorkbenchObjectTree, + 'ChatList', + this._container, + delegate, + [this._renderer], + { + identityProvider: { getId: (e: ChatTreeItem) => e.id }, + horizontalScrolling: false, + alwaysConsumeMouseWheel: false, + supportDynamicHeights: true, + hideTwistiesOfChildlessElements: true, + accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (e: ChatTreeItem) => + isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' + }, + setRowLineHeight: false, + scrollToActiveElement: true, + filter: options.filter, + overrideStyles: { + listFocusBackground: styles.listBackground, + listInactiveFocusBackground: styles.listBackground, + listActiveSelectionBackground: styles.listBackground, + listFocusAndSelectionBackground: styles.listBackground, + listInactiveSelectionBackground: styles.listBackground, + listHoverBackground: styles.listBackground, + listBackground: styles.listBackground, + listFocusForeground: styles.listForeground, + listHoverForeground: styles.listForeground, + listInactiveFocusForeground: styles.listForeground, + listInactiveSelectionForeground: styles.listForeground, + listActiveSelectionForeground: styles.listForeground, + listFocusAndSelectionForeground: styles.listForeground, + listActiveSelectionIconForeground: undefined, + listInactiveSelectionIconForeground: undefined, + } + } + )); + + // Create scroll-down button + this._scrollDownButton = this._register(new Button(this._container, { + buttonBackground: asCssVariable(buttonSecondaryBackground), + buttonForeground: asCssVariable(buttonSecondaryForeground), + buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground), + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined, + supportIcons: true, + })); + this._scrollDownButton.element.classList.add('chat-scroll-down'); + this._scrollDownButton.label = `$(${Codicon.chevronDown.id})`; + this._scrollDownButton.element.style.display = 'none'; // Hidden by default + + this._register(this._scrollDownButton.onDidClick(() => { + this.setScrollLock(true); + this.scrollToEnd(); + })); + + // Wire up tree events + + // Handle content height changes (fires high-level event, internal scroll handling) + this._register(this._tree.onDidChangeContentHeight(() => { + this._onDidChangeContentHeight.fire(); + })); + + this._register(this._tree.onDidFocus(() => { + this._onDidFocus.fire(); + })); + + // Handle focus changes internally (update mostRecentlyFocusedItemIndex) + this._register(this._tree.onDidChangeFocus(() => { + const focused = this.getFocus(); + if (focused && focused.length > 0) { + const focusedItem = focused[0]; + const items = this.getItems(); + const idx = items.findIndex(i => i === focusedItem); + if (idx !== -1) { + this._mostRecentlyFocusedItemIndex = idx; + } + } + })); + + // Handle scroll events (fire public event and manage scroll-down button) + this._register(this._tree.onDidScroll((e) => { + this._onDidScroll.fire(e); + this.updateScrollDownButtonVisibility(); + })); + + // Handle context menu internally + this._register(this._tree.onContextMenu(e => { + this.handleContextMenu(e); + })); + + this._register(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) { + this._settingChangeCounter++; + this.refresh(); + } + })); + } + + //#region Internal event handlers + + /** + * Update scroll-down button visibility based on scroll position and scroll lock. + */ + private updateScrollDownButtonVisibility(): void { + const show = !this.isScrolledToBottom && !this._scrollLock; + this._scrollDownButton.element.style.display = show ? '' : 'none'; + } + + /** + * Handle context menu events. + */ + private handleContextMenu(e: ITreeContextMenuEvent): void { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + + const selected = e.element; + + // Check if the context menu was opened on a KaTeX element + const target = e.browserEvent.target as HTMLElement; + const isKatexElement = target.closest(`.${katexContainerClassName}`) !== null; + + const scopedContextKeyService = this.contextKeyService.createOverlay([ + [ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered], + [ChatContextKeys.isKatexMathElement.key, isKatexElement] + ]); + this.contextMenuService.showContextMenu({ + menuId: MenuId.ChatContext, + menuActionOptions: { shouldForwardArgs: true }, + contextKeyService: scopedContextKeyService, + getAnchor: () => e.anchor, + getActionsContext: () => selected, + }); + } + + //#endregion + + //#region ViewModel methods + + /** + * Set the view model for the list to render. + */ + setViewModel(viewModel: IChatViewModel | undefined): void { + this._viewModel = viewModel; + this._renderer.updateViewModel(viewModel); + } + + /** + * Refresh the list from the current view model. + * Uses internal state for diff identity calculation. + */ + refresh(): void { + if (!this._viewModel) { + this._tree.setChildren(null, []); + this._lastItem = undefined; + this._lastItemIdContextKey.set([]); + return; + } + + const items = this._viewModel.getItems(); + this._lastItem = items.at(-1); + this._lastItemIdContextKey.set(this._lastItem ? [this._lastItem.id] : []); + + const treeItems: ITreeElement[] = items.map(item => ({ + element: item, + collapsed: false, + collapsible: false, + })); + + const editing = this._viewModel.editing; + const checkpoint = this._viewModel.model?.checkpoint; + + this._withPersistedAutoScroll(() => { + this._tree.setChildren(null, treeItems, { + diffIdentityProvider: { + getId: (element) => { + return element.dataId + + // If a response is in the process of progressive rendering, we need to ensure that it will + // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. + `${isResponseVM(element) && element.renderData ? `_${this._visibleChangeCount}` : ''}` + + // Re-render once content references are loaded + (isResponseVM(element) ? `_${element.contentReferences.length}` : '') + + // Re-render if element becomes hidden due to undo/redo + `_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` + + // Re-render if we have an element currently being edited + `_${editing ? '1' : '0'}` + + // Re-render if we have an element currently being checkpointed + `_${checkpoint ? '1' : '0'}` + + // Re-render all if invoked by setting change + `_setting${this._settingChangeCounter}` + + // Rerender request if we got new content references in the response + // since this may change how we render the corresponding attachments in the request + (isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : ''); + }, + } + }); + }); + } + + /** + * Set scroll lock state. + */ + setScrollLock(value: boolean): void { + this._scrollLock = value; + this.updateScrollDownButtonVisibility(); + } + + /** + * Get scroll lock state. + */ + get scrollLock(): boolean { + return this._scrollLock; + } + + /** + * Set the visible change count (for diff identity). + */ + setVisibleChangeCount(value: number): void { + this._visibleChangeCount = value; + } + + /** + * Scroll to reveal an element if editing. + */ + scrollToCurrentItem(currentElement: IChatRequestViewModel): void { + if (!this._viewModel?.editing || !currentElement) { + return; + } + if (!this._tree.hasElement(currentElement)) { + return; + } + const relativeTop = this._tree.getRelativeTop(currentElement); + if (relativeTop === null || relativeTop < 0 || relativeTop > 1) { + this._tree.reveal(currentElement, 0); + } + } + + //#endregion + + //#region Tree methods + + /** + * Rerender the tree. + */ + rerender(): void { + this._tree.rerender(); + } + + private getItems(): ChatTreeItem[] { + const items: ChatTreeItem[] = []; + const root = this._tree.getNode(null); + for (const child of root.children) { + if (child.element) { + items.push(child.element); + } + } + return items; + } + + + /** + * Delegate scroll events from a mouse wheel event to the tree. + */ + delegateScrollFromMouseWheelEvent(event: IMouseWheelEvent): void { + this._tree.delegateScrollFromMouseWheelEvent(event); + } + + /** + * Whether the tree has a specific element. + */ + hasElement(element: ChatTreeItem): boolean { + return this._tree.hasElement(element); + } + + /** + * Update the height of an element. + */ + private _updateElementHeight(element: ChatTreeItem, height?: number): void { + if (this._tree.hasElement(element) && this._visible) { + this._withPersistedAutoScroll(() => { + this._tree.updateElementHeight(element, height); + }); + } + } + + /** + * Scroll to reveal an element. + */ + reveal(element: ChatTreeItem, relativeTop?: number): void { + this._tree.reveal(element, relativeTop); + } + + /** + * Get the focused elements. + */ + getFocus(): ChatTreeItem[] { + return this._tree.getFocus().filter((e): e is ChatTreeItem => e !== null); + } + + /** + * Set the focused elements. + */ + setFocus(elements: ChatTreeItem[]): void { + this._tree.setFocus(elements); + } + + focusItem(item: ChatTreeItem): void { + if (!this.hasElement(item)) { + return; + } + this._tree.setFocus([item]); + this._tree.domFocus(); + } + + /** + * Focus the last item in the list. Returns the index of the focused item. + * @param useMostRecentlyFocusedIndex If true, use the mostRecentlyFocusedIndex if valid + */ + focusLastItem(useMostRecentlyFocusedIndex?: boolean): number { + const items = this.getItems(); + if (items.length === 0) { + return -1; + } + + let focusIndex: number; + if (useMostRecentlyFocusedIndex && this._mostRecentlyFocusedItemIndex >= 0 && this._mostRecentlyFocusedItemIndex < items.length) { + focusIndex = this._mostRecentlyFocusedItemIndex; + } else { + focusIndex = items.length - 1; + } + + this._tree.setFocus([items[focusIndex]]); + this._tree.domFocus(); + return focusIndex; + } + + /** + * Scroll the list to reveal the last item. + */ + scrollToEnd(): void { + if (this._lastItem) { + const offset = Math.max(this._lastItem.currentRenderedHeight ?? 0, 1e6); + if (this._tree.hasElement(this._lastItem)) { + this._tree.reveal(this._lastItem, offset); + } + } + } + + private _withPersistedAutoScroll(fn: () => void): void { + const wasScrolledToBottom = this.isScrolledToBottom; + fn(); + if (wasScrolledToBottom) { + this.scrollToEnd(); + } + } + + /** + * Focus the list. + */ + focus(): void { + this._tree.domFocus(); + } + + /** + * Get the DOM focus state. + */ + isDOMFocused(): boolean { + return this._tree.isDOMFocused(); + } + + //#endregion + + //#region Renderer methods + + /** + * Get code block info for a response. + */ + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { + return this._renderer.getCodeBlockInfosForResponse(response); + } + + /** + * Get code block info by URI. + */ + getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { + return this._renderer.getCodeBlockInfoForEditor(uri); + } + + /** + * Get file tree info for a response. + */ + getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { + return this._renderer.getFileTreeInfosForResponse(response); + } + + /** + * Get the last focused file tree for a response. + */ + getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { + return this._renderer.getLastFocusedFileTreeForResponse(response); + } + + /** + * Get editors currently in use. + */ + editorsInUse(): Iterable { + return this._renderer.editorsInUse(); + } + + /** + * Get template data for a request ID. + */ + getTemplateDataForRequestId(requestId: string | undefined): IChatListItemTemplate | undefined { + if (!requestId) { + return undefined; + } + return this._renderer.getTemplateDataForRequestId(requestId); + } + + /** + * Update renderer options. + */ + updateRendererOptions(options: IChatListItemRendererOptions): void { + this._renderer.updateOptions(options); + } + + /** + * Set the visibility of the list. + */ + setVisible(visible: boolean): void { + this._visible = visible; + this._renderer.setVisible(visible); + } + + /** + * Layout the list. + */ + layout(height: number, width: number): void { + this._bodyDimension = new dom.Dimension(width ?? this._container.clientWidth, height); + this.updateLastItemMinHeight(); + this._tree.layout(height, width); + this._renderer.layout(width ?? this._container.clientWidth); + } + + private _bodyDimension: dom.Dimension | null = null; + private _previousLastItemMinHeight: number | null = null; + + private updateLastItemMinHeight(): void { + if (!this._bodyDimension) { + return; + } + + const contentHeight = this._bodyDimension.height; + if (this._renderStyle === 'compact' || this._renderStyle === 'minimal') { + this._container.style.removeProperty('--chat-current-response-min-height'); + } else { + const secondToLastItem = this._viewModel?.getItems().at(-2); + const secondToLastItemHeight = Math.min(secondToLastItem?.currentRenderedHeight ?? 150, 150); + const lastItemMinHeight = Math.max(contentHeight - (secondToLastItemHeight + 10), 0); + this._container.style.setProperty('--chat-current-response-min-height', lastItemMinHeight + 'px'); + if (lastItemMinHeight !== this._previousLastItemMinHeight) { + this._previousLastItemMinHeight = lastItemMinHeight; + const lastItem = this._viewModel?.getItems().at(-1); + if (lastItem && this._visible && this._tree.hasElement(lastItem)) { + this._updateElementHeight(lastItem, undefined); + } + } + } + } + + //#endregion + +} diff --git a/src/vs/workbench/contrib/chat/browser/chatOptions.ts b/src/vs/workbench/contrib/chat/browser/widget/chatOptions.ts similarity index 90% rename from src/vs/workbench/contrib/chat/browser/chatOptions.ts rename to src/vs/workbench/contrib/chat/browser/widget/chatOptions.ts index 054a323f3fe..2c8a3abf418 100644 --- a/src/vs/workbench/contrib/chat/browser/chatOptions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/chatOptions.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Color } from '../../../../base/common/color.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IBracketPairColorizationOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IViewDescriptorService } from '../../../common/views.js'; +import { Color } from '../../../../../base/common/color.js'; +import { Emitter } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IBracketPairColorizationOptions, IEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { IViewDescriptorService } from '../../../../common/views.js'; export interface IChatConfiguration { editor: { diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts new file mode 100644 index 00000000000..cf3a03c1ac6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts @@ -0,0 +1,2413 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chat.css'; +import './media/chatAgentHover.css'; +import './media/chatViewWelcome.css'; +import * as dom from '../../../../../base/browser/dom.js'; +import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js'; +import { disposableTimeout, timeout } from '../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { filter } from '../../../../../base/common/objects.js'; +import { autorun, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; +import { basename, extUri, isEqual } from '../../../../../base/common/resources.js'; +import { MicrotaskDelay } from '../../../../../base/common/symbols.js'; +import { isDefined } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.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 { 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 { IThemeService } from '../../../../../platform/theme/common/themeService.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; +import { EditorResourceAccessor } from '../../../../common/editor.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { checkModeOption } from '../../common/chat.js'; +import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; +import { IChatLayoutService } from '../../common/widget/chatLayoutService.js'; +import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../common/model/chatModel.js'; +import { ChatMode, IChatModeService } from '../../common/chatModes.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js'; +import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js'; +import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js'; +import { IChatSessionsService } from '../../common/chatSessionsService.js'; +import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js'; +import { IChatTodoListService } from '../../common/tools/chatTodoListService.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js'; +import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js'; +import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js'; +import { ILanguageModelToolsService, isToolSet } from '../../common/tools/languageModelToolsService.js'; +import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js'; +import { PromptsConfig } from '../../common/promptSyntax/config/config.js'; +import { IHandOff, PromptHeader, Target } from '../../common/promptSyntax/promptFileParser.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, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js'; +import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js'; +import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js'; +import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/chatInputPart.js'; +import { IChatListItemTemplate } from './chatListRenderer.js'; +import { ChatListWidget } from './chatListWidget.js'; +import { ChatEditorOptions } from './chatOptions.js'; +import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js'; +import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; + +const $ = dom.$; + +export interface IChatWidgetStyles extends IChatInputStyles { + readonly inputEditorBackground: string; + readonly resultEditorBackground: string; +} + +export interface IChatWidgetContrib extends IDisposable { + + readonly id: string; + + /** + * A piece of state which is related to the input editor of the chat widget. + * Takes in the `contrib` object that will be saved in the {@link IChatModelInputState}. + */ + getInputState?(contrib: Record): void; + + /** + * Called with the result of getInputState when navigating input history. + */ + setInputState?(contrib: Readonly>): void; +} + +interface IChatRequestInputOptions { + input: string; + attachedContext: ChatRequestVariableSet; +} + +export interface IChatWidgetLocationOptions { + location: ChatAgentLocation; + + resolveData?(): IChatLocationData | undefined; +} + +export function isQuickChat(widget: IChatWidget): boolean { + return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isQuickChat); +} + +function isInlineChat(widget: IChatWidget): boolean { + return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isInlineChat); +} + +type ChatHandoffClickEvent = { + fromAgent: string; + toAgent: string; + hasPrompt: boolean; + autoSend: boolean; +}; + +type ChatHandoffClickClassification = { + owner: 'digitarald'; + comment: 'Event fired when a user clicks on a handoff prompt in the chat suggest-next widget'; + fromAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode the user was in before clicking the handoff' }; + toAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode specified in the handoff' }; + hasPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff includes a prompt' }; + autoSend: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff automatically submits the request' }; +}; + +type ChatHandoffWidgetShownEvent = { + agent: string; + handoffCount: number; +}; + +type ChatHandoffWidgetShownClassification = { + owner: 'digitarald'; + comment: 'Event fired when the suggest-next widget is shown with handoff prompts'; + agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current agent/mode that has handoffs defined' }; + handoffCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of handoff options shown to the user' }; +}; + +const supportsAllAttachments: Required = { + supportsFileAttachments: true, + supportsToolAttachments: true, + supportsMCPAttachments: true, + supportsImageAttachments: true, + supportsSearchResultAttachments: true, + supportsInstructionAttachments: true, + supportsSourceControlAttachments: true, + supportsProblemAttachments: true, + supportsSymbolAttachments: true, + supportsTerminalAttachments: true, +}; + +const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate."); + +export class ChatWidget extends Disposable implements IChatWidget { + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = []; + + private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); + readonly onDidSubmitAgent = this._onDidSubmitAgent.event; + + private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); + readonly onDidChangeAgent = this._onDidChangeAgent.event; + + private _onDidFocus = this._register(new Emitter()); + readonly onDidFocus = this._onDidFocus.event; + + private _onDidChangeViewModel = this._register(new Emitter()); + readonly onDidChangeViewModel = this._onDidChangeViewModel.event; + + private _onDidScroll = this._register(new Emitter()); + readonly onDidScroll = this._onDidScroll.event; + + private _onDidAcceptInput = this._register(new Emitter()); + readonly onDidAcceptInput = this._onDidAcceptInput.event; + + private _onDidHide = this._register(new Emitter()); + readonly onDidHide = this._onDidHide.event; + + private _onDidShow = this._register(new Emitter()); + readonly onDidShow = this._onDidShow.event; + + private _onDidChangeParsedInput = this._register(new Emitter()); + readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; + + private readonly _onWillMaybeChangeHeight = new Emitter(); + readonly onWillMaybeChangeHeight: Event = this._onWillMaybeChangeHeight.event; + + private _onDidChangeHeight = this._register(new Emitter()); + readonly onDidChangeHeight = this._onDidChangeHeight.event; + + private readonly _onDidChangeContentHeight = new Emitter(); + readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; + + private _onDidChangeEmptyState = this._register(new Emitter()); + readonly onDidChangeEmptyState = this._onDidChangeEmptyState.event; + + contribs: ReadonlyArray = []; + + private listContainer!: HTMLElement; + private container!: HTMLElement; + + get domNode() { return this.container; } + + private listWidget!: ChatListWidget; + private readonly _codeBlockModelCollection: CodeBlockModelCollection; + + private readonly visibilityTimeoutDisposable: MutableDisposable = this._register(new MutableDisposable()); + private readonly visibilityAnimationFrameDisposable: MutableDisposable = this._register(new MutableDisposable()); + + private readonly inputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); + private readonly inlineInputPartDisposable: MutableDisposable = this._register(new MutableDisposable()); + private inputContainer!: HTMLElement; + private focusedInputDOM!: HTMLElement; + private editorOptions!: ChatEditorOptions; + + private recentlyRestoredCheckpoint: boolean = false; + + private welcomeMessageContainer!: HTMLElement; + private readonly welcomePart: MutableDisposable = this._register(new MutableDisposable()); + + private readonly chatSuggestNextWidget: ChatSuggestNextWidget; + + private bodyDimension: dom.Dimension | undefined; + private visibleChangeCount = 0; + private requestInProgress: IContextKey; + private agentInInput: IContextKey; + private currentRequest: Promise | undefined; + + private _visible = false; + get visible() { return this._visible; } + + private _instructionFilesCheckPromise: Promise | undefined; + private _instructionFilesExist: boolean | undefined; + + private _isRenderingWelcome = false; + + // Coding agent locking state + private _lockedAgent?: { + id: string; + name: string; + prefix: string; + displayName: string; + }; + private readonly _lockedToCodingAgentContextKey: IContextKey; + private readonly _agentSupportsAttachmentsContextKey: IContextKey; + private readonly _sessionIsEmptyContextKey: IContextKey; + private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments; + + // Cache for prompt file descriptions to avoid async calls during rendering + private readonly promptDescriptionsCache = new Map(); + private readonly promptUriCache = new Map(); + private _isLoadingPromptDescriptions = false; + + private readonly viewModelDisposables = this._register(new DisposableStore()); + private _viewModel: ChatViewModel | undefined; + + private set viewModel(viewModel: ChatViewModel | undefined) { + if (this._viewModel === viewModel) { + return; + } + + const previousSessionResource = this._viewModel?.sessionResource; + this.viewModelDisposables.clear(); + + this._viewModel = viewModel; + if (viewModel) { + this.viewModelDisposables.add(viewModel); + this.logService.debug('ChatWidget#setViewModel: have viewModel'); + + // If switching to a model with a request in progress, play progress sound + if (viewModel.model.requestInProgress.get()) { + this.chatAccessibilityService.acceptRequest(viewModel.sessionResource, true); + } + } else { + this.logService.debug('ChatWidget#setViewModel: no viewModel'); + } + + this.currentRequest = undefined; + this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource }); + } + + get viewModel() { + return this._viewModel; + } + + private readonly _editingSession = observableValue(this, undefined); + + private parsedChatRequest: IParsedChatRequest | undefined; + get parsedInput() { + if (this.parsedChatRequest === undefined) { + if (!this.viewModel) { + return { text: '', parts: [] }; + } + + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser) + .parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { + selectedAgent: this._lastSelectedAgent, + mode: this.input.currentModeKind, + forcedAgent: this._lockedAgent?.id ? this.chatAgentService.getAgent(this._lockedAgent.id) : undefined + }); + this._onDidChangeParsedInput.fire(); + } + + return this.parsedChatRequest; + } + + get scopedContextKeyService(): IContextKeyService { + return this.contextKeyService; + } + + private readonly _location: IChatWidgetLocationOptions; + get location() { + return this._location.location; + } + + readonly viewContext: IChatWidgetViewContext; + + get supportsChangingModes(): boolean { + return !!this.viewOptions.supportsChangingModes; + } + + 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, + @IChatService private readonly chatService: IChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService, + @ILogService private readonly logService: ILogService, + @IThemeService private readonly themeService: IThemeService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, + @IChatEditingService chatEditingService: IChatEditingService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IPromptsService private readonly promptsService: IPromptsService, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, + @IChatModeService private readonly chatModeService: IChatModeService, + @IChatLayoutService private readonly chatLayoutService: IChatLayoutService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IChatTodoListService private readonly chatTodoListService: IChatTodoListService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @ILifecycleService private readonly lifecycleService: ILifecycleService + ) { + super(); + + this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService); + this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService); + this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService); + + this.viewContext = viewContext ?? {}; + + const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel); + + if (typeof location === 'object') { + this._location = location; + } else { + this._location = { location }; + } + + ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true); + ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location); + ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this)); + this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService); + this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService); + + this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded())); + + this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + if (!currentSession) { + return; + } + const entries = currentSession.entries.read(reader); + const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified); + return decidedEntries.map(entry => entry.entryId); + })); + this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here + const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified); + return decidedEntries.length > 0; + })); + this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => { + const currentSession = this._editingSession.read(reader); + if (!currentSession) { + return false; + } + const entries = currentSession.entries.read(reader); + return entries.length > 0; + })); + this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => { + return this._editingSession.read(reader) !== null; + })); + this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => { + return this._editingSession.read(r)?.canUndo.read(r) || false; + })); + this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => { + return this._editingSession.read(r)?.canRedo.read(r) || false; + })); + this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => { + const chatModel = viewModelObs.read(r)?.model; + const editingSession = this._editingSession.read(r); + if (!editingSession || !chatModel) { + return false; + } + const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r); + return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete; + })); + + this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection, undefined)); + this.chatSuggestNextWidget = this._register(this.instantiationService.createInstance(ChatSuggestNextWidget)); + + this._register(autorun(r => { + const viewModel = viewModelObs.read(r); + const sessions = chatEditingService.editingSessionsObs.read(r); + + const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, viewModel?.sessionResource)); + this._editingSession.set(undefined, undefined); + this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc. + + if (!session) { + // none or for a different chat widget + return; + } + + const entries = session.entries.read(r); + for (const entry of entries) { + entry.state.read(r); // SIGNAL + } + + this._editingSession.set(session, undefined); + + r.store.add(session.onDidDispose(() => { + this._editingSession.set(undefined, undefined); + this.renderChatEditingSessionState(); + })); + r.store.add(this.inputEditor.onDidChangeModelContent(() => { + if (this.getInput() === '') { + this.refreshParsedInput(); + } + })); + this.renderChatEditingSessionState(); + })); + + this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { + const resource = input.resource; + if (resource.scheme !== Schemas.vscodeChatCodeBlock) { + return null; + } + + const responseId = resource.path.split('/').at(1); + if (!responseId) { + return null; + } + + const item = this.viewModel?.getItems().find(item => item.id === responseId); + if (!item) { + return null; + } + + // TODO: needs to reveal the chat view + + this.reveal(item); + + await timeout(0); // wait for list to actually render + + for (const codeBlockPart of this.listWidget.editorsInUse()) { + if (extUri.isEqual(codeBlockPart.uri, resource, true)) { + const editor = codeBlockPart.editor; + + let relativeTop = 0; + const editorDomNode = editor.getDomNode(); + if (editorDomNode) { + const row = dom.findParentWithClass(editorDomNode, 'monaco-list-row'); + if (row) { + relativeTop = dom.getTopLeftOffset(editorDomNode).top - dom.getTopLeftOffset(row).top; + } + } + + if (input.options?.selection) { + const editorSelectionTopOffset = editor.getTopForPosition(input.options.selection.startLineNumber, input.options.selection.startColumn); + relativeTop += editorSelectionTopOffset; + + editor.focus(); + editor.setSelection({ + startLineNumber: input.options.selection.startLineNumber, + startColumn: input.options.selection.startColumn, + endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber, + endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn + }); + } + + this.reveal(item, relativeTop); + + return editor; + } + } + return null; + })); + + this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext())); + + this._register(this.chatTodoListService.onDidUpdateTodos((sessionResource) => { + if (isEqual(this.viewModel?.sessionResource, sessionResource)) { + this.inputPart.renderChatTodoListWidget(sessionResource); + } + })); + } + + private _lastSelectedAgent: IChatAgentData | undefined; + set lastSelectedAgent(agent: IChatAgentData | undefined) { + this.parsedChatRequest = undefined; + this._lastSelectedAgent = agent; + this._updateAgentCapabilitiesContextKeys(agent); + this._onDidChangeParsedInput.fire(); + } + + get lastSelectedAgent(): IChatAgentData | undefined { + return this._lastSelectedAgent; + } + + private _updateAgentCapabilitiesContextKeys(agent: IChatAgentData | undefined): void { + // Check if the agent has capabilities defined directly + const capabilities = agent?.capabilities ?? (this._lockedAgent ? this.chatSessionsService.getCapabilitiesForSessionType(this._lockedAgent.id) : undefined); + this._attachmentCapabilities = capabilities ?? supportsAllAttachments; + + const supportsAttachments = Object.keys(filter(this._attachmentCapabilities, (key, value) => value === true)).length > 0; + this._agentSupportsAttachmentsContextKey.set(supportsAttachments); + } + + get supportsFileReferences(): boolean { + return !!this.viewOptions.supportsFileReferences; + } + + get attachmentCapabilities(): IChatAgentAttachmentCapabilities { + return this._attachmentCapabilities; + } + + /** + * Either the inline input (when editing) or the main input part + */ + get input(): ChatInputPart { + return this.viewModel?.editing && this.configurationService.getValue('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart; + } + + /** + * The main input part at the buttom of the chat widget. Use `input` to get the active input (main or inline editing part). + */ + get inputPart(): ChatInputPart { + return this.inputPartDisposable.value!; + } + + private get inlineInputPart(): ChatInputPart { + return this.inlineInputPartDisposable.value!; + } + + get inputEditor(): ICodeEditor { + return this.input.inputEditor; + } + + get contentHeight(): number { + return this.input.height.get() + this.listWidget.contentHeight + this.chatSuggestNextWidget.height; + } + + get attachmentModel(): ChatAttachmentModel { + return this.input.attachmentModel; + } + + render(parent: HTMLElement): void { + const viewId = isIChatViewViewContext(this.viewContext) ? this.viewContext.viewId : undefined; + this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground)); + const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false; + const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop; + const renderStyle = this.viewOptions.renderStyle; + const renderInputToolbarBelowInput = this.viewOptions.renderInputToolbarBelowInput ?? false; + + this.container = dom.append(parent, $('.interactive-session')); + this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' })); + this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput())); + + this._register(this.chatSuggestNextWidget.onDidChangeHeight(() => { + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + })); + this._register(this.chatSuggestNextWidget.onDidSelectPrompt(({ handoff, agentId }) => { + this.handleNextPromptSelection(handoff, agentId); + })); + + if (renderInputOnTop) { + this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput }); + this.listContainer = dom.append(this.container, $(`.interactive-list`)); + } else { + this.listContainer = dom.append(this.container, $(`.interactive-list`)); + dom.append(this.container, this.chatSuggestNextWidget.domNode); + this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput }); + } + + this.renderWelcomeViewContentIfNeeded(); + this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle }); + + // Update the font family and size + this._register(autorun(reader => { + const fontFamily = this.chatLayoutService.fontFamily.read(reader); + const fontSize = this.chatLayoutService.fontSize.read(reader); + + this.container.style.setProperty('--vscode-chat-font-family', fontFamily); + this.container.style.fontSize = `${fontSize}px`; + + if (this.visible) { + this.listWidget.rerender(); + } + })); + + this._register(Event.runAndSubscribe(this.editorOptions.onDidChange, () => this.onDidStyleChange())); + + // Do initial render + if (this.viewModel) { + this.onDidChangeItems(); + this.listWidget.scrollToEnd(); + } + + this.contribs = ChatWidget.CONTRIBS.map(contrib => { + try { + return this._register(this.instantiationService.createInstance(contrib, this)); + } catch (err) { + this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err)); + return undefined; + } + }).filter(isDefined); + + this._register(this.chatWidgetService.register(this)); + + const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput); + this._register(autorun(r => { + const input = parsedInput.read(r); + + const newPromptAttachments = new Map(); + const oldPromptAttachments = new Set(); + + // get all attachments, know those that are prompt-referenced + for (const attachment of this.attachmentModel.attachments) { + if (attachment.range) { + oldPromptAttachments.add(attachment.id); + } + } + + // update/insert prompt-referenced attachments + for (const part of input.parts) { + if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) { + const entry = part.toVariableEntry(); + newPromptAttachments.set(entry.id, entry); + oldPromptAttachments.delete(entry.id); + } + } + + this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values()); + })); + + if (!this.focusedInputDOM) { + this.focusedInputDOM = this.container.appendChild(dom.$('.focused-input-dom')); + } + } + + focusInput(): void { + this.input.focus(); + + // Sometimes focusing the input part is not possible, + // but we'd like to be the last focused chat widget, + // so we emit an optimistic onDidFocus event nonetheless. + this._onDidFocus.fire(); + } + + hasInputFocus(): boolean { + return this.input.hasFocus(); + } + + refreshParsedInput() { + if (!this.viewModel) { + return; + } + + const previous = this.parsedChatRequest; + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind }); + if (!previous || !IParsedChatRequest.equals(previous, this.parsedChatRequest)) { + this._onDidChangeParsedInput.fire(); + } + } + + getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined { + if (!isResponseVM(item)) { + return; + } + const items = this.viewModel?.getItems(); + if (!items) { + return; + } + const responseItems = items.filter(i => isResponseVM(i)); + const targetIndex = responseItems.indexOf(item); + if (targetIndex === undefined) { + return; + } + const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1; + if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) { + return; + } + return responseItems[indexToFocus]; + } + + async clear(): Promise { + this.logService.debug('ChatWidget#clear'); + if (this._dynamicMessageLayoutData) { + this._dynamicMessageLayoutData.enabled = true; + } + + if (this.viewModel?.editing) { + this.finishedEditing(); + } + + if (this.viewModel) { + this.viewModel.resetInputPlaceholder(); + } + if (this._lockedAgent) { + this.lockToCodingAgent(this._lockedAgent.name, this._lockedAgent.displayName, this._lockedAgent.id); + } else { + this.unlockFromCodingAgent(); + } + + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); + this.chatSuggestNextWidget.hide(); + await this.viewOptions.clear?.(); + } + + private onDidChangeItems(skipDynamicLayout?: boolean) { + if (this._visible || !this.viewModel) { + const items = this.viewModel?.getItems() ?? []; + + if (items.length > 0) { + this.updateChatViewVisibility(); + } else { + this.renderWelcomeViewContentIfNeeded(); + } + + this._onWillMaybeChangeHeight.fire(); + + // Update list widget state and refresh + this.listWidget.setVisibleChangeCount(this.visibleChangeCount); + this.listWidget.refresh(); + + if (!skipDynamicLayout && this._dynamicMessageLayoutData) { + this.layoutDynamicChatTreeItemMode(); + } + + this.renderFollowups(); + } + } + + /** + * Updates the DOM visibility of welcome view and chat list immediately + */ + private updateChatViewVisibility(): void { + if (this.viewModel) { + const numItems = this.viewModel.getItems().length; + dom.setVisibility(numItems === 0, this.welcomeMessageContainer); + dom.setVisibility(numItems !== 0, this.listContainer); + } + + // Only show welcome getting started until extension is installed + this.container.classList.toggle('chat-view-getting-started-disabled', this.chatEntitlementService.sentiment.installed); + + this._onDidChangeEmptyState.fire(); + } + + isEmpty(): boolean { + return (this.viewModel?.getItems().length ?? 0) === 0; + } + + /** + * Renders the welcome view content when needed. + */ + private renderWelcomeViewContentIfNeeded() { + if (this._isRenderingWelcome) { + return; + } + + this._isRenderingWelcome = true; + try { + if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal' || this.lifecycleService.willShutdown) { + return; + } + + const numItems = this.viewModel?.getItems().length ?? 0; + if (!numItems) { + const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind); + let additionalMessage: string | IMarkdownString | undefined; + if (this.chatEntitlementService.anonymous && !this.chatEntitlementService.sentiment.installed) { + const providers = product.defaultChatAgent.provider; + additionalMessage = new MarkdownString(localize({ key: 'settings', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3}).", providers.default.name, providers.default.name, product.defaultChatAgent.termsStatementUrl, product.defaultChatAgent.privacyStatementUrl), { isTrusted: true }); + } else { + additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage; + } + if (!additionalMessage && !this._lockedAgent) { + additionalMessage = this._getGenerateInstructionsMessage(); + } + const welcomeContent = this.getWelcomeViewContent(additionalMessage); + if (!this.welcomePart.value || this.welcomePart.value.needsRerender(welcomeContent)) { + dom.clearNode(this.welcomeMessageContainer); + + this.welcomePart.value = this.instantiationService.createInstance( + ChatViewWelcomePart, + welcomeContent, + { + location: this.location, + isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent + } + ); + dom.append(this.welcomeMessageContainer, this.welcomePart.value.element); + } + } + + this.updateChatViewVisibility(); + } finally { + this._isRenderingWelcome = false; + } + } + + private _getGenerateInstructionsMessage(): IMarkdownString { + // Start checking for instruction files immediately if not already done + if (!this._instructionFilesCheckPromise) { + this._instructionFilesCheckPromise = this._checkForAgentInstructionFiles(); + // Use VS Code's idiomatic pattern for disposal-safe promise callbacks + this._register(thenIfNotDisposed(this._instructionFilesCheckPromise, hasFiles => { + this._instructionFilesExist = hasFiles; + // Only re-render if the current view still doesn't have items and we're showing the welcome message + const hasViewModelItems = this.viewModel?.getItems().length ?? 0; + if (hasViewModelItems === 0) { + this.renderWelcomeViewContentIfNeeded(); + } + })); + } + + // If we already know the result, use it + if (this._instructionFilesExist === true) { + // Don't show generate instructions message if files exist + return new MarkdownString(''); + } else if (this._instructionFilesExist === false) { + // Show generate instructions message if no files exist + const generateInstructionsCommand = 'workbench.action.chat.generateInstructions'; + return new MarkdownString(localize( + 'chatWidget.instructions', + "[Generate Agent Instructions]({0}) to onboard AI onto your codebase.", + `command:${generateInstructionsCommand}` + ), { isTrusted: { enabledCommands: [generateInstructionsCommand] } }); + } + + // While checking, don't show the generate instructions message + return new MarkdownString(''); + } + + /** + * Checks if any agent instruction files (.github/copilot-instructions.md or AGENTS.md) exist in the workspace. + * Used to determine whether to show the "Generate Agent Instructions" hint. + * + * @returns true if instruction files exist OR if instruction features are disabled (to hide the hint) + */ + private async _checkForAgentInstructionFiles(): Promise { + try { + const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES); + const useAgentMd = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD); + if (!useCopilotInstructionsFiles && !useAgentMd) { + // If both settings are disabled, return true to hide the hint (since the features aren't enabled) + return true; + } + return ( + (await this.promptsService.listCopilotInstructionsMDs(CancellationToken.None)).length > 0 || + // Note: only checking for AGENTS.md files at the root folder, not ones in subfolders. + (await this.promptsService.listAgentMDs(CancellationToken.None, false)).length > 0 + ); + } catch (error) { + // On error, assume no instruction files exist to be safe + this.logService.warn('[ChatWidget] Error checking for instruction files:', error); + return false; + } + } + + private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent { + if (this.isLockedToCodingAgent) { + // Check for provider-specific customizations from chat sessions service + const providerIcon = this._lockedAgent ? this.chatSessionsService.getIconForSessionType(this._lockedAgent.id) : undefined; + const providerTitle = this._lockedAgent ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgent.id) : undefined; + const providerMessage = this._lockedAgent ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgent.id) : undefined; + + // Fallback to default messages if provider doesn't specify + const message = providerMessage + ? new MarkdownString(providerMessage) + : (this._lockedAgent?.prefix === '@copilot ' + ? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._lockedAgent.prefix, 'https://aka.ms/coding-agent-docs') + DISCLAIMER, { isTrusted: true }) + : new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._lockedAgent?.prefix) + DISCLAIMER)); + + return { + title: providerTitle ?? localize('codingAgentTitle', "Delegate to {0}", this._lockedAgent?.prefix), + message, + icon: providerIcon ?? Codicon.sendToRemoteAgent, + additionalMessage, + useLargeIcon: !!providerIcon, + }; + } + + let title: string; + if (this.input.currentModeKind === ChatModeKind.Ask) { + title = localize('chatDescription', "Ask about your code"); + } else if (this.input.currentModeKind === ChatModeKind.Edit) { + title = localize('editsTitle', "Edit in context"); + } else { + title = localize('agentTitle', "Build with Agent"); + } + + return { + title, + message: new MarkdownString(DISCLAIMER), + icon: Codicon.chatSparkle, + additionalMessage, + suggestedPrompts: this.getPromptFileSuggestions() + }; + } + + private getPromptFileSuggestions(): IChatSuggestedPrompts[] { + + // Use predefined suggestions for new users + if (!this.chatEntitlementService.sentiment.installed) { + const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY; + if (isEmpty) { + return [ + { + icon: Codicon.vscode, + label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"), + prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"), + }, + { + icon: Codicon.newFolder, + label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"), + prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"), + } + ]; + } else { + return [ + { + icon: Codicon.debugAlt, + label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"), + prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"), + }, + { + icon: Codicon.gear, + label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"), + prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"), + } + ]; + } + } + + // 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, resource); + if (!suggestions) { + return []; + } + + const result: IChatSuggestedPrompts[] = []; + const promptsToLoad: string[] = []; + + // First, collect all prompts that need loading (regardless of shouldInclude) + for (const [promptName] of Object.entries(suggestions)) { + const description = this.promptDescriptionsCache.get(promptName); + if (description === undefined) { + promptsToLoad.push(promptName); + } + } + + // If we have prompts to load, load them asynchronously and don't return anything yet + // But only if we're not already loading to prevent infinite loop + if (promptsToLoad.length > 0 && !this._isLoadingPromptDescriptions) { + this.loadPromptDescriptions(promptsToLoad); + return []; + } + + // Now process the suggestions with loaded descriptions + const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = []; + + for (const [promptName, condition] of Object.entries(suggestions)) { + let score = 0; + + // Handle boolean conditions + if (typeof condition === 'boolean') { + score = condition ? 1 : 0; + } + // Handle when clause conditions + else if (typeof condition === 'string') { + try { + const whenClause = ContextKeyExpr.deserialize(condition); + if (whenClause) { + // Test against all open code editors + const allEditors = this.codeEditorService.listCodeEditors(); + + if (allEditors.length > 0) { + // Count how many editors match the when clause + score = allEditors.reduce((count, editor) => { + try { + const editorContext = this.contextKeyService.getContext(editor.getDomNode()); + return count + (whenClause.evaluate(editorContext) ? 1 : 0); + } catch (error) { + // Log error for this specific editor but continue with others + this.logService.warn('Failed to evaluate when clause for editor:', error); + return count; + } + }, 0); + } else { + // Fallback to global context if no editors are open + score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0; + } + } else { + score = 0; + } + } catch (error) { + // Log the error but don't fail completely + this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error); + score = 0; + } + } + + if (score > 0) { + promptsWithScores.push({ promptName, condition, score }); + } + } + + // Sort by score (descending) and take top 5 + promptsWithScores.sort((a, b) => b.score - a.score); + const topPrompts = promptsWithScores.slice(0, 5); + + // 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 uri = this.promptUriCache.get(promptName); + const descriptionText = description?.trim() ? description : undefined; + result.push({ + icon: Codicon.run, + label: commandLabel, + description: descriptionText, + prompt: `/${promptName} `, + uri: uri + }); + } + + return result; + } + + private async loadPromptDescriptions(promptNames: string[]): Promise { + // Don't start loading if the widget is being disposed + if (this._store.isDisposed) { + return; + } + + // Set loading guard to prevent infinite loop + this._isLoadingPromptDescriptions = true; + try { + // Get all available prompt files with their metadata + const promptCommands = await this.promptsService.getPromptSlashCommands(CancellationToken.None); + + let cacheUpdated = false; + // Load descriptions only for the specified prompts + for (const promptCommand of promptCommands) { + if (promptNames.includes(promptCommand.name)) { + const description = promptCommand.description; + if (description) { + this.promptDescriptionsCache.set(promptCommand.name, description); + cacheUpdated = true; + } else { + // Set empty string to indicate we've checked this prompt + this.promptDescriptionsCache.set(promptCommand.name, ''); + cacheUpdated = true; + } + } + } + + // Fire event to trigger a re-render of the welcome view only if cache was updated + if (cacheUpdated) { + this.renderWelcomeViewContentIfNeeded(); + } + } catch (error) { + this.logService.warn('Failed to load specific prompt descriptions:', error); + } finally { + // Always clear the loading guard, even on error + this._isLoadingPromptDescriptions = false; + } + } + + private async renderChatEditingSessionState() { + if (!this.input) { + return; + } + this.input.renderChatEditingSessionState(this._editingSession.get() ?? null); + } + + private async renderFollowups(): Promise { + const lastItem = this.listWidget.lastItem; + if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) { + this.input.renderFollowups(lastItem.replyFollowups, lastItem); + } else { + this.input.renderFollowups(undefined, undefined); + } + } + + private renderChatSuggestNextWidget(): void { + if (this.lifecycleService.willShutdown) { + return; + } + + // Skip rendering in coding agent sessions + if (this.isLockedToCodingAgent) { + this.chatSuggestNextWidget.hide(); + return; + } + + const items = this.viewModel?.getItems() ?? []; + if (!items.length) { + return; + } + + const lastItem = items[items.length - 1]; + const lastResponseComplete = lastItem && isResponseVM(lastItem) && lastItem.isComplete; + if (!lastResponseComplete) { + return; + } + // Get the currently selected mode directly from the observable + // Note: We use currentModeObs instead of currentModeKind because currentModeKind returns + // the ChatModeKind enum (e.g., 'agent'), which doesn't distinguish between custom modes. + // Custom modes all have kind='agent' but different IDs. + const currentMode = this.input.currentModeObs.get(); + const handoffs = currentMode?.handOffs?.get(); + + // Only show if: mode has handoffs AND chat has content AND not quick chat + const shouldShow = currentMode && handoffs && handoffs.length > 0; + + if (shouldShow) { + // Log telemetry only when widget transitions from hidden to visible + const wasHidden = this.chatSuggestNextWidget.domNode.style.display === 'none'; + this.chatSuggestNextWidget.render(currentMode); + + if (wasHidden) { + this.telemetryService.publicLog2('chat.handoffWidgetShown', { + agent: currentMode.id, + handoffCount: handoffs.length + }); + } + } else { + this.chatSuggestNextWidget.hide(); + } + + // Trigger layout update + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + } + + private handleNextPromptSelection(handoff: IHandOff, agentId?: string): void { + // Hide the widget after selection + this.chatSuggestNextWidget.hide(); + + const promptToUse = handoff.prompt; + + // Log telemetry + const currentMode = this.input.currentModeObs.get(); + const fromAgent = currentMode?.id ?? ''; + this.telemetryService.publicLog2('chat.handoffClicked', { + fromAgent: fromAgent, + toAgent: agentId || handoff.agent || '', + hasPrompt: Boolean(promptToUse), + autoSend: Boolean(handoff.send) + }); + + // If agentId is provided (from chevron dropdown), delegate to that chat session + // Otherwise, switch to the handoff agent + if (agentId) { + // Delegate to chat session (e.g., @background or @cloud) + this.input.setValue(`@${agentId} ${promptToUse}`, false); + this.input.focus(); + // Auto-submit for delegated chat sessions + this.acceptInput().catch(e => this.logService.error('Failed to handle handoff continueOn', e)); + } else if (handoff.agent) { + // Regular handoff to specified agent + this._switchToAgentByName(handoff.agent); + // Switch to the specified model if provided + if (handoff.model) { + this.input.switchModelByQualifiedName([handoff.model]); + } + // Insert the handoff prompt into the input + this.input.setValue(promptToUse, false); + this.input.focus(); + + // Auto-submit if send flag is true + if (handoff.send) { + this.acceptInput(); + } + } + } + + async handleDelegationExitIfNeeded(sourceAgent: Pick | undefined, targetAgent: IChatAgentData | undefined): Promise { + if (!this._shouldExitAfterDelegation(sourceAgent, targetAgent)) { + return; + } + + try { + await this._handleDelegationExit(); + } catch (e) { + this.logService.error('Failed to handle delegation exit', e); + } + } + + private _shouldExitAfterDelegation(sourceAgent: Pick | undefined, targetAgent: IChatAgentData | undefined): boolean { + if (!targetAgent) { + // Undefined behavior + return false; + } + + if (!this.configurationService.getValue(ChatConfiguration.ExitAfterDelegation)) { + return false; + } + + // Never exit if the source and target are the same (that means that you're providing a follow up, etc.) + // NOTE: sourceAgent would be the chatWidget's 'lockedAgent' + if (sourceAgent && sourceAgent.id === targetAgent.id) { + return false; + } + + if (!isIChatViewViewContext(this.viewContext)) { + return false; + } + + const contribution = this.chatSessionsService.getChatSessionContribution(targetAgent.id); + if (!contribution) { + return false; + } + + if (contribution.canDelegate !== true) { + return false; + } + + return true; + } + + /** + * Handles the exit of the panel chat when a delegation to another session occurs. + * Waits for the response to complete and any pending confirmations to be resolved, + * then clears the widget unless the final message is an error. + */ + private async _handleDelegationExit(): Promise { + const viewModel = this.viewModel; + if (!viewModel) { + return; + } + + const parentSessionResource = viewModel.sessionResource; + + // Check if response is complete, not pending confirmation, and has no error + const checkIfShouldClear = (): boolean => { + const items = viewModel.getItems(); + const lastItem = items[items.length - 1]; + if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) { + const hasError = Boolean(lastItem.result?.errorDetails); + return !hasError; + } + return false; + }; + + if (checkIfShouldClear()) { + await this.clear(); + this.archiveLocalParentSession(parentSessionResource); + return; + } + + const shouldClear = await new Promise(resolve => { + const disposable = viewModel.onDidChange(() => { + const result = checkIfShouldClear(); + if (result) { + cleanup(); + resolve(true); + } + }); + const timeout = setTimeout(() => { + cleanup(); + resolve(false); + }, 30_000); // 30 second timeout + const cleanup = () => { + clearTimeout(timeout); + disposable.dispose(); + }; + }); + + if (shouldClear) { + await this.clear(); + this.archiveLocalParentSession(parentSessionResource); + } + } + + private async archiveLocalParentSession(sessionResource: URI): Promise { + if (sessionResource.scheme !== Schemas.vscodeLocalChatSession) { + return; + } + + // Implicitly keep parent session's changes as they've now been delegated to the new agent. + await this.chatService.getSession(sessionResource)?.editingSession?.accept(); + + const session = this.agentSessionsService.getSession(sessionResource); + session?.setArchived(true); + } + + setVisible(visible: boolean): void { + const wasVisible = this._visible; + this._visible = visible; + this.visibleChangeCount++; + this.listWidget.setVisible(visible); + this.input.setVisible(visible); + + if (visible) { + if (!wasVisible) { + this.visibilityTimeoutDisposable.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); + + this.visibilityAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { + this._onDidShow.fire(); + }); + } + } else if (wasVisible) { + this._onDidHide.fire(); + } + } + + private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { + // Create a dom element to hold UI from editor widgets embedded in chat messages + const overflowWidgetsContainer = document.createElement('div'); + overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor'); + listContainer.append(overflowWidgetsContainer); + + // Create chat list widget + this.listWidget = this._register(this.instantiationService.createInstance( + ChatListWidget, + listContainer, + { + rendererOptions: options, + renderStyle: this.viewOptions.renderStyle, + defaultElementHeight: this.viewOptions.defaultElementHeight ?? 200, + overflowWidgetsDomNode: overflowWidgetsContainer, + styles: { + listForeground: this.styles.listForeground, + listBackground: this.styles.listBackground, + }, + currentChatMode: () => this.input.currentModeKind, + filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions) } : undefined, + codeBlockModelCollection: this._codeBlockModelCollection, + viewModel: this.viewModel, + editorOptions: this.editorOptions, + location: this.location, + getCurrentLanguageModelId: () => this.input.currentLanguageModel, + getCurrentModeInfo: () => this.input.currentModeInfo, + } + )); + + // Wire up ChatWidget-specific list widget events + this._register(this.listWidget.onDidClickRequest(async item => { + this.clickedRequest(item); + })); + + this._register(this.listWidget.onDidRerender(item => { + if (isRequestVM(item.currentElement) && this.configurationService.getValue('chat.editRequests') !== 'input') { + if (!item.rowContainer.contains(this.inputContainer)) { + item.rowContainer.appendChild(this.inputContainer); + } + this.input.focus(); + } + })); + + this._register(this.listWidget.onDidDispose(() => { + this.focusedInputDOM.appendChild(this.inputContainer); + this.input.focus(); + })); + + this._register(this.listWidget.onDidFocusOutside(() => { + this.finishedEditing(); + })); + + this._register(this.listWidget.onDidClickFollowup(item => { + // is this used anymore? + this.acceptInput(item.message); + })); + + this._register(this.listWidget.onDidChangeContentHeight(() => { + this._onDidChangeContentHeight.fire(); + })); + + this._register(this.listWidget.onDidFocus(() => { + this._onDidFocus.fire(); + })); + this._register(this.listWidget.onDidScroll(() => { + this._onDidScroll.fire(); + })); + } + + startEditing(requestId: string): void { + const editedRequest = this.listWidget.getTemplateDataForRequestId(requestId); + if (editedRequest) { + this.clickedRequest(editedRequest); + } + } + + private clickedRequest(item: IChatListItemTemplate) { + + const currentElement = item.currentElement; + if (isRequestVM(currentElement) && !this.viewModel?.editing) { + + const requests = this.viewModel?.model.getRequests(); + if (!requests || !this.viewModel?.sessionResource) { + return; + } + + // this will only ever be true if we restored a checkpoint + if (this.viewModel?.model.checkpoint) { + this.recentlyRestoredCheckpoint = true; + } + + this.viewModel?.model.setCheckpoint(currentElement.id); + + // set contexts and request to false + const currentContext: IChatRequestVariableEntry[] = []; + const addedContextIds = new Set(); + const addToContext = (entry: IChatRequestVariableEntry) => { + if (addedContextIds.has(entry.id) || isWorkspaceVariableEntry(entry)) { + return; + } + if ((isPromptFileVariableEntry(entry) || isPromptTextVariableEntry(entry)) && entry.automaticallyAdded) { + return; + } + addedContextIds.add(entry.id); + currentContext.push(entry); + }; + for (let i = requests.length - 1; i >= 0; i -= 1) { + const request = requests[i]; + if (request.id === currentElement.id) { + request.setShouldBeBlocked(false); // unblocking just this request. + request.attachedContext?.forEach(addToContext); + currentElement.variables.forEach(addToContext); + } + } + + // set states + this.viewModel?.setEditing(currentElement); + if (item?.contextKeyService) { + ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true); + } + + const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; + this.inputPart?.setEditing(!!this.viewModel?.editing && isInput); + + if (!isInput) { + const rowContainer = item.rowContainer; + this.inputContainer = dom.$('.chat-edit-input-container'); + rowContainer.appendChild(this.inputContainer); + this.createInput(this.inputContainer); + this.input.setChatMode(this.inputPart.currentModeObs.get().id); + } else { + this.inputPart.element.classList.add('editing'); + } + + this.inputPart.toggleChatInputOverlay(!isInput); + if (currentContext.length > 0) { + this.input.attachmentModel.addContext(...currentContext); + } + + + // rerenders + this.inputPart.dnd.setDisabledOverlay(!isInput); + this.input.renderAttachedContext(); + this.input.setValue(currentElement.messageText, false); + this.onDidChangeItems(); + this.input.inputEditor.focus(); + + this._register(this.inputPart.onDidClickOverlay(() => { + if (this.viewModel?.editing && this.configurationService.getValue('chat.editRequests') !== 'input') { + this.finishedEditing(); + } + })); + + // listeners + if (!isInput) { + this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => { + this.listWidget.scrollToCurrentItem(currentElement); + })); + + this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => { + this.listWidget.scrollToCurrentItem(currentElement); + })); + } + } + + type StartRequestEvent = { editRequestType: string }; + + type StartRequestEventClassification = { + owner: 'justschen'; + comment: 'Event used to gain insights into when edits are being pressed.'; + editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' }; + }; + + this.telemetryService.publicLog2('chat.startEditingRequests', { + editRequestType: this.configurationService.getValue('chat.editRequests'), + }); + } + + finishedEditing(completedEdit?: boolean): void { + // reset states + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); + if (this.recentlyRestoredCheckpoint) { + this.recentlyRestoredCheckpoint = false; + } else { + this.viewModel?.model.setCheckpoint(undefined); + } + this.inputPart.dnd.setDisabledOverlay(false); + if (editedRequest?.contextKeyService) { + ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false); + } + + const isInput = this.configurationService.getValue('chat.editRequests') === 'input'; + + if (!isInput) { + this.inputPart.setChatMode(this.input.currentModeObs.get().id); + const currentModel = this.input.selectedLanguageModel.get(); + if (currentModel) { + this.inputPart.switchModel(currentModel.metadata); + } + + this.inputPart?.toggleChatInputOverlay(false); + try { + if (editedRequest?.rowContainer?.contains(this.inputContainer)) { + editedRequest.rowContainer.removeChild(this.inputContainer); + } else if (this.inputContainer.parentElement) { + this.inputContainer.parentElement.removeChild(this.inputContainer); + } + } catch (e) { + this.logService.error('Error occurred while finishing editing:', e); + } + this.inputContainer = dom.$('.empty-chat-state'); + + // only dispose if we know the input is not the bottom input object. + this.input.dispose(); + } + + if (isInput) { + this.inputPart.element.classList.remove('editing'); + } + this.viewModel?.setEditing(undefined); + + this.inputPart?.setEditing(!!this.viewModel?.editing && isInput); + + this.onDidChangeItems(); + + type CancelRequestEditEvent = { + editRequestType: string; + editCanceled: boolean; + }; + + type CancelRequestEventEditClassification = { + owner: 'justschen'; + editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' }; + editCanceled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether the edit was canceled.' }; + comment: 'Event used to gain insights into when edits are being canceled.'; + }; + + this.telemetryService.publicLog2('chat.editRequestsFinished', { + editRequestType: this.configurationService.getValue('chat.editRequests'), + editCanceled: !completedEdit + }); + + this.inputPart.focus(); + } + + private getWidgetViewKindTag(): string { + if (!this.viewContext) { + return 'editor'; + } else if (isIChatViewViewContext(this.viewContext)) { + return 'view'; + } else { + return 'quick'; + } + } + + private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'compact' | 'minimal'; renderInputToolbarBelowInput?: boolean }): void { + const commonConfig: IChatInputPartOptions = { + renderFollowups: options?.renderFollowups ?? true, + renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, + renderInputToolbarBelowInput: options?.renderInputToolbarBelowInput ?? false, + menus: { + executeToolbar: MenuId.ChatExecute, + telemetrySource: 'chatWidget', + ...this.viewOptions.menus + }, + editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, + enableImplicitContext: this.viewOptions.enableImplicitContext, + renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit', + supportsChangingModes: this.viewOptions.supportsChangingModes, + dndContainer: this.viewOptions.dndContainer, + widgetViewKindTag: this.getWidgetViewKindTag(), + defaultMode: this.viewOptions.defaultMode, + sessionTypePickerDelegate: this.viewOptions.sessionTypePickerDelegate, + workspacePickerDelegate: this.viewOptions.workspacePickerDelegate + }; + + if (this.viewModel?.editing) { + const editedRequest = this.listWidget.getTemplateDataForRequestId(this.viewModel?.editing?.id); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService]))); + this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart, + this.location, + commonConfig, + this.styles, + true + ); + } else { + this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart, + this.location, + commonConfig, + this.styles, + false + ); + this._register(autorun(reader => { + this.inputPart.height.read(reader); + if (!this.listWidget) { + // This is set up before the list/renderer are created + return; + } + + if (this.bodyDimension) { + this.layout(this.bodyDimension.height, this.bodyDimension.width); + } + + this._onDidChangeContentHeight.fire(); + })); + } + + this.input.render(container, '', this); + if (this.bodyDimension?.width) { + this.input.layout(this.bodyDimension.width); + } + + this._register(this.input.onDidLoadInputState(() => { + this.refreshParsedInput(); + })); + this._register(this.input.onDidFocus(() => this._onDidFocus.fire())); + this._register(this.input.onDidAcceptFollowup(e => { + if (!this.viewModel) { + return; + } + + let msg = ''; + if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind)?.id) { + const agent = this.chatAgentService.getAgent(e.followup.agentId); + if (!agent) { + return; + } + + this.lastSelectedAgent = agent; + msg = `${chatAgentLeader}${agent.name} `; + if (e.followup.subCommand) { + msg += `${chatSubcommandLeader}${e.followup.subCommand} `; + } + } else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) { + msg = `${chatSubcommandLeader}${e.followup.subCommand} `; + } + + msg += e.followup.message; + this.acceptInput(msg); + + if (!e.response) { + // Followups can be shown by the welcome message, then there is no response associated. + // At some point we probably want telemetry for these too. + return; + } + + this.chatService.notifyUserAction({ + sessionResource: this.viewModel.sessionResource, + requestId: e.response.requestId, + agentId: e.response.agent?.id, + command: e.response.slashCommand?.name, + result: e.response.result, + action: { + kind: 'followUp', + followup: e.followup + }, + }); + })); + this._register(this.inputEditor.onDidChangeModelContent(() => { + this.parsedChatRequest = undefined; + this.updateChatInputContext(); + })); + this._register(this.chatAgentService.onDidChangeAgents(() => { + this.parsedChatRequest = undefined; + // Tools agent loads -> welcome content changes + this.renderWelcomeViewContentIfNeeded(); + })); + this._register(this.input.onDidChangeCurrentChatMode(() => { + this.renderWelcomeViewContentIfNeeded(); + this.refreshParsedInput(); + this.renderFollowups(); + this.renderChatSuggestNextWidget(); + })); + + this._register(autorun(r => { + const toolSetIds = new Set(); + const toolIds = new Set(); + for (const [entry, enabled] of this.input.selectedToolsModel.entriesMap.read(r)) { + if (enabled) { + if (isToolSet(entry)) { + toolSetIds.add(entry.id); + } else { + toolIds.add(entry.id); + } + } + } + const disabledTools = this.input.attachmentModel.attachments + .filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id)) + .map(a => a.id); + + this.input.attachmentModel.updateContext(disabledTools, Iterable.empty()); + this.refreshParsedInput(); + })); + } + + private onDidStyleChange(): void { + this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? ''); + this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? ''); + this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); + } + + + setModel(model: IChatModel | undefined): void { + if (!this.container) { + throw new Error('Call render() before setModel()'); + } + + if (!model) { + if (this.viewModel?.editing) { + this.finishedEditing(); + } + this.viewModel = undefined; + this.onDidChangeItems(); + return; + } + + if (isEqual(model.sessionResource, this.viewModel?.sessionResource)) { + return; + } + + if (this.viewModel?.editing) { + this.finishedEditing(); + } + this.inputPart.clearTodoListWidget(model.sessionResource, false); + this.chatSuggestNextWidget.hide(); + + this._codeBlockModelCollection.clear(); + + this.container.setAttribute('data-session-id', model.sessionId); + this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection, undefined); + + // Pass input model reference to input part for state syncing + this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0); + this.listWidget.setViewModel(this.viewModel); + + if (this._lockedAgent) { + let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id); + if (!placeholder) { + placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgent.id); + } + this.viewModel.setInputPlaceholder(placeholder); + this.inputEditor.updateOptions({ placeholder }); + } else if (this.viewModel.inputPlaceholder) { + this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder }); + } + + const renderImmediately = this.configurationService.getValue('chat.experimental.renderMarkdownImmediately'); + const delay = renderImmediately ? MicrotaskDelay : 0; + this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, delay), (events => { + if (!this.viewModel || this._store.isDisposed) { + // See https://github.com/microsoft/vscode/issues/278969 + return; + } + + this.requestInProgress.set(this.viewModel.model.requestInProgress.get()); + + // Update the editor's placeholder text when it changes in the view model + if (events?.some(e => e?.kind === 'changePlaceholder')) { + this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder }); + } + + this.onDidChangeItems(); + if (events?.some(e => e?.kind === 'addRequest') && this.visible) { + this.listWidget.scrollToEnd(); + } + }))); + this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { + // Ensure that view state is saved here, because we will load it again when a new model is assigned + if (this.viewModel?.editing) { + this.finishedEditing(); + } + // Disposes the viewmodel and listeners + this.viewModel = undefined; + this.onDidChangeItems(); + })); + this._sessionIsEmptyContextKey.set(model.getRequests().length === 0); + + this.refreshParsedInput(); + this.viewModelDisposables.add(model.onDidChange((e) => { + if (e.kind === 'setAgent') { + this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command }); + // Update capabilities context keys when agent changes + this._updateAgentCapabilitiesContextKeys(e.agent); + } + if (e.kind === 'addRequest') { + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false); + this._sessionIsEmptyContextKey.set(false); + } + // Hide widget on request removal + if (e.kind === 'removeRequest') { + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); + this.chatSuggestNextWidget.hide(); + this._sessionIsEmptyContextKey.set((this.viewModel?.model.getRequests().length ?? 0) === 0); + } + // Show next steps widget when response completes (not when request starts) + if (e.kind === 'completedRequest') { + const lastRequest = this.viewModel?.model.getRequests().at(-1); + const wasCancelled = lastRequest?.response?.isCanceled ?? false; + if (wasCancelled) { + // Clear todo list when request is cancelled + this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); + } + // Only show if response wasn't canceled + this.renderChatSuggestNextWidget(); + + // Mark the session as read when the request completes and the widget is visible + if (this.visible && this.viewModel?.sessionResource) { + this.agentSessionsService.getSession(this.viewModel.sessionResource)?.setRead(true); + } + } + })); + + if (this.listWidget && this.visible) { + this.onDidChangeItems(); + this.listWidget.scrollToEnd(); + } + + this.updateChatInputContext(); + this.input.renderChatTodoListWidget(this.viewModel.sessionResource); + } + + getFocus(): ChatTreeItem | undefined { + return this.listWidget.getFocus()[0] ?? undefined; + } + + reveal(item: ChatTreeItem, relativeTop?: number): void { + this.listWidget.reveal(item, relativeTop); + } + + focus(item: ChatTreeItem): void { + if (!this.listWidget.hasElement(item)) { + return; + } + + this.listWidget.focusItem(item); + } + + setInputPlaceholder(placeholder: string): void { + this.viewModel?.setInputPlaceholder(placeholder); + } + + resetInputPlaceholder(): void { + this.viewModel?.resetInputPlaceholder(); + } + + setInput(value = ''): void { + this.input.setValue(value, false); + this.refreshParsedInput(); + } + + getInput(): string { + return this.input.inputEditor.getValue(); + } + + getContrib(id: string): T | undefined { + return this.contribs.find(c => c.id === id) as T | undefined; + } + + // Coding agent locking methods + lockToCodingAgent(name: string, displayName: string, agentId: string): void { + this._lockedAgent = { + id: agentId, + name, + prefix: `@${name} `, + displayName + }; + this._lockedToCodingAgentContextKey.set(true); + this.renderWelcomeViewContentIfNeeded(); + // Update capabilities for the locked agent + const agent = this.chatAgentService.getAgent(agentId); + this._updateAgentCapabilitiesContextKeys(agent); + this.listWidget?.updateRendererOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true }); + if (this.visible) { + this.listWidget?.rerender(); + } + } + + unlockFromCodingAgent(): void { + // Clear all state related to locking + this._lockedAgent = undefined; + this._lockedToCodingAgentContextKey.set(false); + this._updateAgentCapabilitiesContextKeys(undefined); + + // Explicitly update the DOM to reflect unlocked state + this.renderWelcomeViewContentIfNeeded(); + + // Reset to default placeholder + if (this.viewModel) { + this.viewModel.resetInputPlaceholder(); + } + this.inputEditor?.updateOptions({ placeholder: undefined }); + this.listWidget?.updateRendererOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask }); + if (this.visible) { + this.listWidget?.rerender(); + } + } + + get isLockedToCodingAgent(): boolean { + return !!this._lockedAgent; + } + + get lockedAgentId(): string | undefined { + return this._lockedAgent?.id; + } + + logInputHistory(): void { + this.input.logInputHistory(); + } + + async acceptInput(query?: string, options?: IChatAcceptInputOptions): Promise { + return this._acceptInput(query ? { query } : undefined, options); + } + + async rerunLastRequest(): Promise { + if (!this.viewModel) { + return; + } + + const sessionResource = this.viewModel.sessionResource; + const lastRequest = this.chatService.getSession(sessionResource)?.getRequests().at(-1); + if (!lastRequest) { + return; + } + + const options: IChatSendRequestOptions = { + attempt: lastRequest.attempt + 1, + location: this.location, + userSelectedModelId: this.input.currentLanguageModel + }; + return await this.chatService.resendRequest(lastRequest, options); + } + + private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise { + // 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) { + return; + } + + // need to resolve the slash command to get the prompt file + const slashCommand = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.name, CancellationToken.None); + if (!slashCommand) { + return; + } + const parseResult = slashCommand.parsedPromptFile; + // 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 + requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim(); + + const input = requestInput.input.trim(); + requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`; + if (input) { + // if the input is not empty, append it to the prompt + requestInput.input += `\n${input}`; + } + if (parseResult.header) { + await this._applyPromptMetadata(parseResult.header, requestInput); + } + } + + private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise { + if (this.viewModel?.model.requestInProgress.get()) { + return; + } + + if (!query && this.input.generating) { + // if the user submits the input and generation finishes quickly, just submit it for them + const generatingAutoSubmitWindow = 500; + const start = Date.now(); + await this.input.generating; + if (Date.now() - start > generatingAutoSubmitWindow) { + return; + } + } + + while (!this._viewModel && !this._store.isDisposed) { + await Event.toPromise(this.onDidChangeViewModel, this._store); + } + + if (!this.viewModel) { + return; + } + + // Check if a custom submit handler wants to handle this submission + if (this.viewOptions.submitHandler) { + const inputValue = !query ? this.getInput() : query.query; + const handled = await this.viewOptions.submitHandler(inputValue, this.input.currentModeKind); + if (handled) { + return; + } + } + + this._onDidAcceptInput.fire(); + this.listWidget.setScrollLock(this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll)); + + const editorValue = this.getInput(); + const requestInputs: IChatRequestInputOptions = { + input: !query ? editorValue : query.query, + attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionResource) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionResource), + }; + + const isUserQuery = !query; + + if (this.viewModel?.editing) { + this.finishedEditing(true); + this.viewModel.model?.setCheckpoint(undefined); + } + + // process the prompt command + await this._applyPromptFileIfSet(requestInputs); + await this._autoAttachInstructions(requestInputs); + + if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) { + const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set + const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext; + + // Collect file variables from previous requests before sending the request + const previousRequests = this.viewModel.model.getRequests(); + for (const request of previousRequests) { + for (const variable of request.variableData.variables) { + if (URI.isUri(variable.value) && variable.kind === 'file') { + const uri = variable.value; + if (!uniqueWorkingSetEntries.has(uri)) { + editingSessionAttachedContext.add(variable); + uniqueWorkingSetEntries.add(variable.value); + } + } + } + } + requestInputs.attachedContext = editingSessionAttachedContext; + + type ChatEditingWorkingSetClassification = { + owner: 'joyceerhl'; + comment: 'Information about the working set size in a chat editing request'; + originalSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that the user tried to attach in their editing request.' }; + actualSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that were actually sent in their editing request.' }; + }; + type ChatEditingWorkingSetEvent = { + originalSize: number; + actualSize: number; + }; + this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size }); + } + this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource); + if (this.currentRequest) { + // We have to wait the current request to be properly cancelled so that it has a chance to update the model with its result metadata. + // This is awkward, it's basically a limitation of the chat provider-based agent. + await Promise.race([this.currentRequest, timeout(1000)]); + } + + this.input.validateAgentMode(); + + if (this.viewModel.model.checkpoint) { + const requests = this.viewModel.model.getRequests(); + for (let i = requests.length - 1; i >= 0; i -= 1) { + const request = requests[i]; + if (request.shouldBeBlocked) { + this.chatService.removeRequest(this.viewModel.sessionResource, request.id); + } + } + } + if (this.viewModel.sessionResource) { + this.chatAccessibilityService.acceptRequest(this._viewModel!.sessionResource); + } + + const result = await this.chatService.sendRequest(this.viewModel.sessionResource, requestInputs.input, { + userSelectedModelId: this.input.currentLanguageModel, + location: this.location, + locationData: this._location.resolveData?.(), + parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind }, + attachedContext: requestInputs.attachedContext.asArray(), + noCommandDetection: options?.noCommandDetection, + ...this.getModeRequestOptions(), + modeInfo: this.input.currentModeInfo, + agentIdSilent: this._lockedAgent?.id, + }); + + if (!result) { + this.chatAccessibilityService.disposeRequest(this.viewModel.sessionResource); + return; + } + + // visibility sync before we accept input to hide the welcome view + this.updateChatViewVisibility(); + + this.input.acceptInput(options?.storeToHistory ?? isUserQuery); + this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); + this.handleDelegationExitIfNeeded(this._lockedAgent, result.agent); + this.currentRequest = result.responseCompletePromise.then(() => { + const responses = this.viewModel?.getItems().filter(isResponseVM); + const lastResponse = responses?.[responses.length - 1]; + this.chatAccessibilityService.acceptResponse(this, this.container, lastResponse, this.viewModel?.sessionResource, options?.isVoiceInput); + if (lastResponse?.result?.nextQuestion) { + const { prompt, participant, command } = lastResponse.result.nextQuestion; + const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); + if (question) { + this.input.setValue(question, false); + } + } + this.currentRequest = undefined; + }); + + return result.responseCreatedPromise; + } + + getModeRequestOptions(): Partial { + return { + modeInfo: this.input.currentModeInfo, + userSelectedTools: this.input.selectedToolsModel.userSelectedTools, + }; + } + + getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { + return this.listWidget.getCodeBlockInfosForResponse(response); + } + + getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { + return this.listWidget.getCodeBlockInfoForEditor(uri); + } + + getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { + return this.listWidget.getFileTreeInfosForResponse(response); + } + + getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { + return this.listWidget.getLastFocusedFileTreeForResponse(response); + } + + focusResponseItem(lastFocused?: boolean): void { + this.listWidget.focusLastItem(lastFocused); + } + + layout(height: number, width: number): void { + width = Math.min(width, this.viewOptions.renderStyle === 'minimal' ? width : 950); // no min width of inline chat + + this.bodyDimension = new dom.Dimension(width, height); + + if (this.viewModel?.editing) { + this.inlineInputPart?.layout(width); + } + + this.inputPart.layout(width); + + const inputHeight = this.inputPart.height.get(); + const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; + const lastElementVisible = this.listWidget.isScrolledToBottom; + const lastItem = this.listWidget.lastItem; + + const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight); + this.listWidget.layout(contentHeight, width); + + this.welcomeMessageContainer.style.height = `${contentHeight}px`; + + const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData; + if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) { + this.listWidget.scrollToEnd(); + } + this.listContainer.style.height = `${contentHeight}px`; + + this._onDidChangeHeight.fire(height); + } + + private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean }; + + // An alternative to layout, this allows you to specify the number of ChatTreeItems + // you want to show, and the max height of the container. It will then layout the + // tree to show that many items. + // TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used + setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { + this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true }; + this._register(this.listWidget.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode())); + + const mutableDisposable = this._register(new MutableDisposable()); + this._register(this.listWidget.onDidScroll((e) => { + // TODO@TylerLeonhardt this should probably just be disposed when this is disabled + // and then set up again when it is enabled again + if (!this._dynamicMessageLayoutData?.enabled) { + return; + } + mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { + if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) { + return; + } + const renderHeight = e.height; + const diff = e.scrollHeight - renderHeight - e.scrollTop; + if (diff === 0) { + return; + } + + const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); + const width = this.bodyDimension?.width ?? this.container.offsetWidth; + this.input.layout(width); + const inputPartHeight = this.input.height.get(); + const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; + const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight); + this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width); + }); + })); + } + + updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { + this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true }; + let hasChanged = false; + let height = this.bodyDimension!.height; + let width = this.bodyDimension!.width; + if (maxHeight < this.bodyDimension!.height) { + height = maxHeight; + hasChanged = true; + } + const containerWidth = this.container.offsetWidth; + if (this.bodyDimension?.width !== containerWidth) { + width = containerWidth; + hasChanged = true; + } + if (hasChanged) { + this.layout(height, width); + } + } + + get isDynamicChatTreeItemLayoutEnabled(): boolean { + return this._dynamicMessageLayoutData?.enabled ?? false; + } + + set isDynamicChatTreeItemLayoutEnabled(value: boolean) { + if (!this._dynamicMessageLayoutData) { + return; + } + this._dynamicMessageLayoutData.enabled = value; + } + + layoutDynamicChatTreeItemMode(): void { + if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) { + return; + } + + const width = this.bodyDimension?.width ?? this.container.offsetWidth; + this.input.layout(width); + const inputHeight = this.input.height.get(); + const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height; + + const totalMessages = this.viewModel.getItems(); + // grab the last N messages + const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages); + + const needsRerender = messages.some(m => m.currentRenderedHeight === undefined); + const listHeight = needsRerender + ? this._dynamicMessageLayoutData.maxHeight + : messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0); + + this.layout( + Math.min( + // we add an additional 18px in order to show that there is scrollable content + inputHeight + chatSuggestNextWidgetHeight + listHeight + (totalMessages.length > 2 ? 18 : 0), + this._dynamicMessageLayoutData.maxHeight + ), + width + ); + + if (needsRerender || !listHeight) { + this.listWidget.scrollToEnd(); + } + } + + saveState(): void { + // no-op + } + + getViewState(): IChatModelInputState | undefined { + return this.input.getCurrentInputState(); + } + + private updateChatInputContext() { + const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart); + this.agentInInput.set(!!currentAgent); + } + + private async _switchToAgentByName(agentName: string): Promise { + const currentAgent = this.input.currentModeObs.get(); + + // switch to appropriate agent if needed + if (agentName !== currentAgent.name.get()) { + // Find the mode object to get its kind + const agent = this.chatModeService.findModeByName(agentName); + if (agent) { + if (currentAgent.kind !== agent.kind) { + const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentAgent.kind, agent.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model); + if (!chatModeCheck) { + return; + } + + if (chatModeCheck.needToClearSession) { + await this.clear(); + } + } + this.input.setChatMode(agent.id); + } + } + } + + private async _applyPromptMetadata({ agent, tools, model }: PromptHeader, requestInput: IChatRequestInputOptions): Promise { + + if (tools !== undefined && !agent && this.input.currentModeKind !== ChatModeKind.Agent) { + agent = ChatMode.Agent.name.get(); + } + // switch to appropriate agent if needed + if (agent) { + this._switchToAgentByName(agent); + } + + // if not tools to enable are present, we are done + if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) { + const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools, Target.VSCode, this.input.selectedLanguageModel.get()?.metadata); + this.input.selectedToolsModel.set(enablementMap, true); + } + + if (model !== undefined) { + this.input.switchModelByQualifiedName(model); + } + } + + /** + * Adds additional instructions to the context + * - instructions that have a 'applyTo' pattern that matches the current input + * - instructions referenced in the copilot settings 'copilot-instructions' + * - instructions referenced in an already included instruction file + */ + private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise { + this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`); + const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.userSelectedTools.get() : undefined; + const enabledSubAgents = this.input.currentModeKind === ChatModeKind.Agent ? this.input.currentModeObs.get().agents?.get() : undefined; + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, this.input.currentModeObs.get(), enabledTools, enabledSubAgents); + await computer.collect(attachedContext, CancellationToken.None); + } + + delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void { + this.listWidget.delegateScrollFromMouseWheelEvent(browserEvent); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts new file mode 100644 index 00000000000..b9bb9dbadf8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts @@ -0,0 +1,257 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../base/browser/dom.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { combinedDisposable, Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js'; +import { ACTIVE_GROUP, IEditorService, type PreferredGroup } from '../../../../services/editor/common/editorService.js'; +import { IEditorGroup, IEditorGroupsService, isEditorGroup } from '../../../../services/editor/common/editorGroupsService.js'; +import { IViewsService } from '../../../../services/views/common/viewsService.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; +import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from '../chat.js'; +import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js'; +import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js'; +import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js'; + +export class ChatWidgetService extends Disposable implements IChatWidgetService { + + declare readonly _serviceBrand: undefined; + + private _widgets: IChatWidget[] = []; + private _lastFocusedWidget: IChatWidget | undefined = undefined; + + private readonly _onDidAddWidget = this._register(new Emitter()); + readonly onDidAddWidget = this._onDidAddWidget.event; + + private readonly _onDidBackgroundSession = this._register(new Emitter()); + readonly onDidBackgroundSession = this._onDidBackgroundSession.event; + + constructor( + @IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService, + @IViewsService private readonly viewsService: IViewsService, + @IQuickChatService private readonly quickChatService: IQuickChatService, + @ILayoutService private readonly layoutService: ILayoutService, + @IEditorService private readonly editorService: IEditorService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + } + + get lastFocusedWidget(): IChatWidget | undefined { + return this._lastFocusedWidget; + } + + getAllWidgets(): ReadonlyArray { + return this._widgets; + } + + getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray { + return this._widgets.filter(w => w.location === location); + } + + getWidgetByInputUri(uri: URI): IChatWidget | undefined { + return this._widgets.find(w => isEqual(w.input.inputUri, uri)); + } + + getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined { + return this._widgets.find(w => isEqual(w.viewModel?.sessionResource, sessionResource)); + } + + async revealWidget(preserveFocus?: boolean): Promise { + const last = this.lastFocusedWidget; + if (last && await this.reveal(last, preserveFocus)) { + return last; + } + + return (await this.viewsService.openView(ChatViewId, !preserveFocus))?.widget; + } + + async reveal(widget: IChatWidget, preserveFocus?: boolean): Promise { + if (widget.viewModel?.sessionResource) { + const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(widget.viewModel.sessionResource, { preserveFocus }); + if (alreadyOpenWidget) { + return true; + } + } + + if (isIChatViewViewContext(widget.viewContext)) { + const view = await this.viewsService.openView(widget.viewContext.viewId, !preserveFocus); + if (!preserveFocus) { + view?.focus(); + } + return !!view; + } + + return false; + } + + /** + * Reveal the session if already open, otherwise open it. + */ + openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget): Promise; + openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise; + async openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget | PreferredGroup, options?: IChatEditorOptions): Promise { + // Reveal if already open unless instructed otherwise + if (typeof target === 'undefined' || options?.revealIfOpened) { + const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(sessionResource, options); + if (alreadyOpenWidget) { + return alreadyOpenWidget; + } + } else { + await this.prepareSessionForMove(sessionResource, target); + } + + // Load this session in chat view + if (target === ChatViewPaneTarget) { + const chatView = await this.viewsService.openView(ChatViewId, !options?.preserveFocus); + if (chatView) { + await chatView.loadSession(sessionResource); + if (!options?.preserveFocus) { + chatView.focusInput(); + } + } + return chatView?.widget; + } + + // Open in chat editor + const pane = await this.editorService.openEditor({ + resource: sessionResource, + options: { + ...options, + revealIfOpened: options?.revealIfOpened ?? true // always try to reveal if already opened unless explicitly told not to + } + }, target); + return pane instanceof ChatEditor ? pane.widget : undefined; + } + + private async revealSessionIfAlreadyOpen(sessionResource: URI, options?: IChatEditorOptions): Promise { + // Already open in chat view? + const chatView = this.viewsService.getViewWithId(ChatViewId); + if (chatView?.widget.viewModel?.sessionResource && isEqual(chatView.widget.viewModel.sessionResource, sessionResource)) { + const view = await this.viewsService.openView(ChatViewId, !options?.preserveFocus); + if (!options?.preserveFocus) { + view?.focus(); + } + return chatView.widget; + } + + // Already open in an editor? + const existingEditor = this.findExistingChatEditorByUri(sessionResource); + if (existingEditor) { + const existingEditorWindowId = existingEditor.group.windowId; + + // focus transfer to other documents is async. If we depend on the focus + // being synchronously transferred in consuming code, this can fail, so + // wait for it to propagate + const isGroupActive = () => dom.getWindow(this.layoutService.activeContainer).vscodeWindowId === existingEditorWindowId; + + let ensureFocusTransfer: Promise | undefined; + if (!isGroupActive() && !options?.preserveFocus) { + ensureFocusTransfer = Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive))); + } + + const pane = await existingEditor.group.openEditor(existingEditor.editor, options); + await ensureFocusTransfer; + return pane instanceof ChatEditor ? pane.widget : undefined; + } + + // Already open in quick chat? + if (isEqual(sessionResource, this.quickChatService.sessionResource)) { + this.quickChatService.focus(); + return undefined; + } + + return undefined; + } + + private async prepareSessionForMove(sessionResource: URI, target: typeof ChatViewPaneTarget | PreferredGroup | undefined): Promise { + const existingWidget = this.getWidgetBySessionResource(sessionResource); + if (existingWidget) { + const existingEditor = isIChatViewViewContext(existingWidget.viewContext) ? + undefined : + this.findExistingChatEditorByUri(sessionResource); + + if (isIChatViewViewContext(existingWidget.viewContext) && target === ChatViewPaneTarget) { + return; + } + + if (!isIChatViewViewContext(existingWidget.viewContext) && target !== ChatViewPaneTarget && existingEditor && this.isSameEditorTarget(existingEditor.group.id, target)) { + return; + } + + if (existingEditor) { + // widget.clear() on an editor leaves behind an empty chat editor + await this.editorService.closeEditor({ editor: existingEditor.editor, groupId: existingEditor.group.id }, { preserveFocus: true }); + } else { + await existingWidget.clear(); + } + } + } + + private findExistingChatEditorByUri(sessionUri: URI): { editor: ChatEditorInput; group: IEditorGroup } | undefined { + for (const group of this.editorGroupsService.groups) { + for (const editor of group.editors) { + if (editor instanceof ChatEditorInput && isEqual(editor.sessionResource, sessionUri)) { + return { editor, group }; + } + } + } + return undefined; + } + + private isSameEditorTarget(currentGroupId: number, target?: PreferredGroup): boolean { + return typeof target === 'number' && target === currentGroupId || + target === ACTIVE_GROUP && this.editorGroupsService.activeGroup?.id === currentGroupId || + isEditorGroup(target) && target.id === currentGroupId; + } + + private setLastFocusedWidget(widget: IChatWidget | undefined): void { + if (widget === this._lastFocusedWidget) { + return; + } + + this._lastFocusedWidget = widget; + } + + register(newWidget: IChatWidget): IDisposable { + if (this._widgets.some(widget => widget === newWidget)) { + throw new Error('Cannot register the same widget multiple times'); + } + + this._widgets.push(newWidget); + this._onDidAddWidget.fire(newWidget); + + if (!this._lastFocusedWidget) { + this.setLastFocusedWidget(newWidget); + } + + return combinedDisposable( + newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)), + newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => { + if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) { + return; + } + + // Timeout to ensure it wasn't just moving somewhere else + void timeout(200).then(() => { + if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) { + this._onDidBackgroundSession.fire(previousSessionResource); + } + }); + }), + toDisposable(() => { + this._widgets.splice(this._widgets.indexOf(newWidget), 1); + if (this._lastFocusedWidget === newWidget) { + this.setLastFocusedWidget(undefined); + } + }) + ); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatFollowups.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatFollowups.ts new file mode 100644 index 00000000000..8358ddf2ce6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatFollowups.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { Button, IButtonStyles } from '../../../../../../base/browser/ui/button/button.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { formatChatQuestion } from '../../../common/requestParser/chatParserTypes.js'; +import { IChatFollowup } from '../../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; + +const $ = dom.$; + +export class ChatFollowups extends Disposable { + constructor( + container: HTMLElement, + followups: T[], + private readonly location: ChatAgentLocation, + private readonly options: IButtonStyles | undefined, + private readonly clickHandler: (followup: T) => void, + @IChatAgentService private readonly chatAgentService: IChatAgentService + ) { + super(); + + const followupsContainer = dom.append(container, $('.interactive-session-followups')); + followups.forEach(followup => this.renderFollowup(followupsContainer, followup)); + } + + private renderFollowup(container: HTMLElement, followup: T): void { + + if (!this.chatAgentService.getDefaultAgent(this.location)) { + // No default agent yet, which affects how followups are rendered, so can't render this yet + return; + } + + const tooltipPrefix = formatChatQuestion(this.chatAgentService, this.location, '', followup.agentId, followup.subCommand); + if (tooltipPrefix === undefined) { + return; + } + + const baseTitle = followup.kind === 'reply' ? + (followup.title || followup.message) + : followup.title; + const message = followup.kind === 'reply' ? followup.message : followup.title; + const tooltip = (tooltipPrefix + + (followup.tooltip || message)).trim(); + const button = this._register(new Button(container, { ...this.options, title: tooltip })); + if (followup.kind === 'reply') { + button.element.classList.add('interactive-followup-reply'); + } else if (followup.kind === 'command') { + button.element.classList.add('interactive-followup-command'); + } + button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", baseTitle); + button.label = new MarkdownString(baseTitle); + + this._register(button.onDidClick(() => this.clickHandler(followup))); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts new file mode 100644 index 00000000000..26e1c63ef80 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -0,0 +1,2884 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { addDisposableListener } from '../../../../../../base/browser/dom.js'; +import { DEFAULT_FONT_FAMILY } from '../../../../../../base/browser/fonts.js'; +import { IHistoryNavigationWidget } from '../../../../../../base/browser/history.js'; +import { hasModifierKeys, StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; +import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import * as aria from '../../../../../../base/browser/ui/aria/aria.js'; +import { ButtonWithIcon } from '../../../../../../base/browser/ui/button/button.js'; +import { createInstantHoverDelegate } from '../../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { equals as arraysEqual } from '../../../../../../base/common/arrays.js'; +import { DeferredPromise, RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Iterable } from '../../../../../../base/common/iterator.js'; +import { KeyCode } from '../../../../../../base/common/keyCodes.js'; +import { Lazy } from '../../../../../../base/common/lazy.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ResourceSet } from '../../../../../../base/common/map.js'; +import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { mixin } from '../../../../../../base/common/objects.js'; +import { autorun, derived, derivedOpts, IObservable, ISettableObservable, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; +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'; +import { CodeEditorWidget } from '../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { EditorLayoutInfo, EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js'; +import { IDimension } from '../../../../../../editor/common/core/2d/dimension.js'; +import { IPosition } from '../../../../../../editor/common/core/position.js'; +import { IRange, Range } from '../../../../../../editor/common/core/range.js'; +import { isLocation } 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'; +import { CopyPasteController } from '../../../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js'; +import { DropIntoEditorController } from '../../../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js'; +import { ContentHoverController } from '../../../../../../editor/contrib/hover/browser/contentHoverController.js'; +import { GlyphHoverController } from '../../../../../../editor/contrib/hover/browser/glyphHoverController.js'; +import { LinkDetector } from '../../../../../../editor/contrib/links/browser/links.js'; +import { SuggestController } from '../../../../../../editor/contrib/suggest/browser/suggestController.js'; +import { localize } from '../../../../../../nls.js'; +import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; +import { MenuWorkbenchButtonBar } from '../../../../../../platform/actions/browser/buttonbar.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { registerAndCreateHistoryNavigationContext } from '../../../../../../platform/history/browser/contextScopedHistoryWidget.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { WorkbenchList } from '../../../../../../platform/list/browser/listService.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { ObservableMemento, observableMemento } from '../../../../../../platform/observable/common/observableMemento.js'; +import { bindContextKey } from '../../../../../../platform/observable/common/platformObservableUtils.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; +import { ISharedWebContentExtractorService } from '../../../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { IWorkspaceContextService, WorkbenchState } from '../../../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchLayoutService, Position } from '../../../../../services/layout/browser/layoutService.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../../../common/views.js'; +import { ResourceLabels } from '../../../../../browser/labels.js'; +import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; +import { IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../../services/editor/common/editorService.js'; +import { AccessibilityVerbositySettingId } from '../../../../accessibility/browser/accessibilityConfiguration.js'; +import { AccessibilityCommandId } from '../../../../accessibility/common/accessibilityCommands.js'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../../../codeEditor/browser/simpleEditorOptions.js'; +import { InlineChatConfigKeys } from '../../../../inlineChat/common/inlineChat.js'; +import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemChangeRangeVariableEntry, isSCMHistoryItemChangeVariableEntry, isSCMHistoryItemVariableEntry, isStringVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; +import { IChatFollowup, IChatService, IChatSessionContext } from '../../../common/chatService/chatService.js'; +import { agentOptionId, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService, isIChatSessionFileChange2, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../../../common/constants.js'; +import { IChatEditingSession, IModifiedFileEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../../../common/languageModels.js'; +import { IChatModelInputState, IChatRequestModeInfo, IInputModel } from '../../../common/model/chatModel.js'; +import { getChatSessionType } from '../../../common/model/chatUri.js'; +import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; +import { IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { ChatHistoryNavigator } from '../../../common/widget/chatWidgetHistoryService.js'; +import { ChatSessionPrimaryPickerAction, ChatSubmitAction, IChatExecuteActionContext, OpenDelegationPickerAction, OpenModelPickerAction, OpenModePickerAction, OpenSessionTargetPickerAction, OpenWorkspacePickerAction } from '../../actions/chatExecuteActions.js'; +import { AgentSessionProviders, getAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; +import { ChatAttachmentModel } from '../../attachments/chatAttachmentModel.js'; +import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, SCMHistoryItemChangeAttachmentWidget, SCMHistoryItemChangeRangeAttachmentWidget, TerminalCommandAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from '../../attachments/chatAttachmentWidgets.js'; +import { ChatImplicitContexts } from '../../attachments/chatImplicitContext.js'; +import { ImplicitContextAttachmentWidget } from '../../attachments/implicitContextAttachment.js'; +import { IChatWidget, ISessionTypePickerDelegate, isIChatResourceViewContext, isIChatViewViewContext, IWorkspacePickerDelegate } from '../../chat.js'; +import { ChatEditingShowChangesAction, ViewAllSessionChangesAction, ViewPreviousEditsAction } from '../../chatEditing/chatEditingActions.js'; +import { resizeImage } from '../../chatImageUtils.js'; +import { ChatSessionPickerActionItem, IChatSessionPickerDelegate } from '../../chatSessions/chatSessionPickerActionItem.js'; +import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js'; +import { IChatContextService } from '../../contextContrib/chatContextService.js'; +import { IDisposableReference } from '../chatContentParts/chatCollections.js'; +import { CollapsibleListPool, IChatCollapsibleListItem } from '../chatContentParts/chatReferencesContentPart.js'; +import { ChatTodoListWidget } from '../chatContentParts/chatTodoListWidget.js'; +import { ChatDragAndDrop } from '../chatDragAndDrop.js'; +import { ChatFollowups } from './chatFollowups.js'; +import { ChatInputPartWidgetController } from './chatInputPartWidgets.js'; +import { IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +import { ChatSelectedTools } from './chatSelectedTools.js'; +import { DelegationSessionPickerActionItem } from './delegationSessionPickerActionItem.js'; +import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js'; +import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js'; +import { SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; +import { WorkspacePickerActionItem } from './workspacePickerActionItem.js'; +import { ChatContextUsageWidget } from '../../widgetHosts/viewPane/chatContextUsageWidget.js'; + +const $ = dom.$; + +const INPUT_EDITOR_MAX_HEIGHT = 250; +const CachedLanguageModelsKey = 'chat.cachedLanguageModels.v2'; + +export interface IChatInputStyles { + overlayBackground: string; + listForeground: string; + listBackground: string; +} + +export interface IChatInputPartOptions { + defaultMode?: IChatMode; + renderFollowups: boolean; + renderStyle?: 'compact'; + renderInputToolbarBelowInput: boolean; + menus: { + executeToolbar: MenuId; + telemetrySource: string; + inputSideToolbar?: MenuId; + }; + editorOverflowWidgetsDomNode?: HTMLElement; + renderWorkingSet: boolean; + enableImplicitContext?: boolean; + supportsChangingModes?: boolean; + dndContainer?: HTMLElement; + widgetViewKindTag: string; + /** + * Optional delegate for the session target picker. + * When provided, allows the input part to maintain independent state for the selected session type. + */ + sessionTypePickerDelegate?: ISessionTypePickerDelegate; + /** + * Optional delegate for the workspace picker. + * When provided, shows a workspace picker allowing users to select a target workspace + * for their chat request. This is useful for empty window contexts. + */ + workspacePickerDelegate?: IWorkspacePickerDelegate; +} + +export interface IWorkingSetEntry { + uri: URI; +} + +export const enum ChatWidgetLocation { + SidebarLeft = 'sidebarLeft', + SidebarRight = 'sidebarRight', + Panel = 'panel', + Editor = 'editor', +} + +export interface IChatWidgetLocationInfo { + readonly location: ChatWidgetLocation; + readonly isMaximized: boolean; +} + +const emptyInputState = observableMemento({ + defaultValue: undefined, + key: 'chat.untitledInputState', + toStorage: JSON.stringify, + fromStorage(value) { + const obj = JSON.parse(value) as IChatModelInputState; + if (obj.selectedModel && !obj.selectedModel.metadata.isDefaultForLocation) { + // Migrate old `isDefault` to `isDefaultForLocation` + type OldILanguageModelChatMetadata = ILanguageModelChatMetadata & { isDefault?: boolean }; + const oldIsDefault = (obj.selectedModel.metadata as OldILanguageModelChatMetadata).isDefault; + const isDefaultForLocation = { [ChatAgentLocation.Chat]: Boolean(oldIsDefault) }; + mixin(obj.selectedModel.metadata, { isDefaultForLocation: isDefaultForLocation } satisfies Partial); + delete (obj.selectedModel.metadata as OldILanguageModelChatMetadata).isDefault; + } + return obj; + }, +}); + +export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { + private static _counter = 0; + + private _workingSetCollapsed = observableValue('chatInputPart.workingSetCollapsed', true); + private readonly _chatInputTodoListWidget = this._register(new MutableDisposable()); + private readonly _chatEditingTodosDisposables = this._register(new DisposableStore()); + private _lastEditingSessionResource: URI | undefined; + + private _onDidLoadInputState: Emitter = this._register(new Emitter()); + readonly onDidLoadInputState: Event = this._onDidLoadInputState.event; + + private _onDidFocus = this._register(new Emitter()); + readonly onDidFocus: Event = this._onDidFocus.event; + + private _onDidBlur = this._register(new Emitter()); + readonly onDidBlur: Event = this._onDidBlur.event; + + private _onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>()); + readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }> = this._onDidChangeContext.event; + + private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>()); + readonly onDidAcceptFollowup: Event<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }> = this._onDidAcceptFollowup.event; + + private _onDidClickOverlay = this._register(new Emitter()); + readonly onDidClickOverlay: Event = this._onDidClickOverlay.event; + + private readonly _implicitContextWidget: MutableDisposable = this._register(new MutableDisposable()); + + private readonly _attachmentModel: ChatAttachmentModel; + private _widget?: IChatWidget; + public get attachmentModel(): ChatAttachmentModel { + return this._attachmentModel; + } + + readonly selectedToolsModel: ChatSelectedTools; + + public getAttachedContext(sessionResource: URI) { + const contextArr = new ChatRequestVariableSet(); + contextArr.add(...this.attachmentModel.attachments, ...this.chatContextService.getWorkspaceContextItems()); + return contextArr; + } + + public getAttachedAndImplicitContext(sessionResource: URI): ChatRequestVariableSet { + + const contextArr = this.getAttachedContext(sessionResource); + + if (this.implicitContext) { + const implicitChatVariables = this.implicitContext.enabledBaseEntries(this.configurationService.getValue('chat.implicitContext.suggestedContext')); + contextArr.add(...implicitChatVariables); + } + return contextArr; + } + + private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; + private _indexOfLastOpenedContext: number = -1; + + private _implicitContext: ChatImplicitContexts | undefined; + public get implicitContext(): ChatImplicitContexts | undefined { + return this._implicitContext; + } + + private _hasFileAttachmentContextKey: IContextKey; + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + private readonly _contextResourceLabels: ResourceLabels; + + private readonly inputEditorMaxHeight: number; + private inputEditorHeight: number = 0; + private container!: HTMLElement; + + private inputSideToolbarContainer?: HTMLElement; + + private followupsContainer!: HTMLElement; + private readonly followupsDisposables: DisposableStore = this._register(new DisposableStore()); + + private attachmentsContainer!: HTMLElement; + + private chatInputOverlay!: HTMLElement; + private readonly overlayClickListener: MutableDisposable = this._register(new MutableDisposable()); + + private attachedContextContainer!: HTMLElement; + private readonly attachedContextDisposables: MutableDisposable = this._register(new MutableDisposable()); + + private chatEditingSessionWidgetContainer!: HTMLElement; + private chatInputTodoListWidgetContainer!: HTMLElement; + private chatInputWidgetsContainer!: HTMLElement; + private readonly _widgetController = this._register(new MutableDisposable()); + + private contextUsageWidget?: ChatContextUsageWidget; + private contextUsageWidgetContainer!: HTMLElement; + private readonly _contextUsageDisposables = this._register(new MutableDisposable()); + + readonly height = observableValue(this, 0); + + private _inputEditor!: CodeEditorWidget; + private _inputEditorElement!: HTMLElement; + + // Reference to the input model for syncing input state + private _inputModel: IInputModel | undefined; + + // Disposables for model observation + private readonly _modelSyncDisposables = this._register(new DisposableStore()); + + // Flag to prevent circular updates between view and model + private _isSyncingToOrFromInputModel = false; + + // Debounced scheduler for syncing text changes + private readonly _syncTextDebounced: RunOnceScheduler; + + private executeToolbar!: MenuWorkbenchToolBar; + private inputActionsToolbar!: MenuWorkbenchToolBar; + + private addFilesToolbar: MenuWorkbenchToolBar | undefined; + private addFilesButton: AddFilesButton | undefined; + + get inputEditor() { + return this._inputEditor; + } + + readonly dnd: ChatDragAndDrop; + + private history: ChatHistoryNavigator; + private historyNavigationBackwardsEnablement!: IContextKey; + private historyNavigationForewardsEnablement!: IContextKey; + private inputModel: ITextModel | undefined; + private inputEditorHasText: IContextKey; + private chatCursorAtTop: IContextKey; + private inputEditorHasFocus: IContextKey; + private currentlyEditingInputKey!: IContextKey; + private chatModeKindKey: IContextKey; + private chatModeNameKey: IContextKey; + private withinEditSessionKey: IContextKey; + private filePartOfEditSessionKey: IContextKey; + private chatSessionHasOptions: IContextKey; + private chatSessionOptionsValid: IContextKey; + private agentSessionTypeKey: IContextKey; + private chatSessionHasCustomAgentTarget: IContextKey; + private modelWidget: ModelPickerActionItem | undefined; + private modeWidget: ModePickerActionItem | undefined; + private sessionTargetWidget: SessionTypePickerActionItem | undefined; + private delegationWidget: DelegationSessionPickerActionItem | undefined; + private chatSessionPickerWidgets: Map = new Map(); + private chatSessionPickerContainer: HTMLElement | undefined; + private _lastSessionPickerAction: MenuItemAction | undefined; + private readonly _waitForPersistedLanguageModel: MutableDisposable = this._register(new MutableDisposable()); + private readonly _chatSessionOptionEmitters: Map> = new Map(); + + /** + * Scoped context key service for this chat input part. + * Used to isolate option group context keys to this specific chat input instance. + */ + private _scopedContextKeyService: IContextKeyService | undefined; + + /** + * Map of option group ID to its context key. + * Keys follow the pattern `chatSessionOption.` and hold the currently selected option item ID. + */ + private readonly _optionContextKeys: Map> = new Map(); + + private _currentLanguageModel = observableValue('_currentLanguageModel', undefined); + + get currentLanguageModel() { + return this._currentLanguageModel.get()?.identifier; + } + + get selectedLanguageModel(): IObservable { + return this._currentLanguageModel; + } + + private _onDidChangeCurrentChatMode: Emitter = this._register(new Emitter()); + readonly onDidChangeCurrentChatMode: Event = this._onDidChangeCurrentChatMode.event; + + private readonly _currentModeObservable: ISettableObservable; + + public get currentModeKind(): ChatModeKind { + const mode = this._currentModeObservable.get(); + return mode.kind === ChatModeKind.Agent && !this.agentService.hasToolsAgent ? + ChatModeKind.Edit : + mode.kind; + } + + public get currentModeObs(): IObservable { + return this._currentModeObservable; + } + + public get currentModeInfo(): IChatRequestModeInfo { + 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, + modeInstructions: modeInstructions ? { + name: mode.name.get(), + content: modeInstructions.content, + toolReferences: this.toolService.toToolReferences(modeInstructions.toolReferences), + metadata: modeInstructions.metadata, + } : undefined, + modeId: modeId, + applyCodeBlockSuggestionId: undefined, + }; + } + + private cachedWidth: number | undefined; + private cachedExecuteToolbarWidth: number | undefined; + private cachedInputToolbarWidth: number | undefined; + + readonly inputUri: URI = URI.parse(`${Schemas.vscodeChatInput}:input-${ChatInputPart._counter++}`); + + private _workingSetLinesAddedSpan = new Lazy(() => dom.$('.working-set-lines-added')); + private _workingSetLinesRemovedSpan = new Lazy(() => dom.$('.working-set-lines-removed')); + + private readonly _chatEditsActionsDisposables: DisposableStore = this._register(new DisposableStore()); + private readonly _chatEditsDisposables: DisposableStore = this._register(new DisposableStore()); + private readonly _renderingChatEdits = this._register(new MutableDisposable()); + + private _chatEditsListPool: CollapsibleListPool; + private _chatEditList: IDisposableReference> | undefined; + get selectedElements(): URI[] { + const edits = []; + const editsList = this._chatEditList?.object; + const selectedElements = editsList?.getSelectedElements() ?? []; + for (const element of selectedElements) { + if (element.kind === 'reference' && URI.isUri(element.reference)) { + edits.push(element.reference); + } + } + return edits; + } + + private _attemptedWorkingSetEntriesCount: number = 0; + /** + * The number of working set entries that the user actually wanted to attach. + * This is less than or equal to {@link ChatInputPart.chatEditWorkingSetFiles}. + */ + public get attemptedWorkingSetEntriesCount() { + return this._attemptedWorkingSetEntriesCount; + } + + /** + * Gets the pending delegation target if one is set. + * This is used when the user changes the session target picker to a different provider + * but hasn't submitted yet, so the delegation will happen on submit. + */ + public get pendingDelegationTarget(): AgentSessionProviders | undefined { + return this._pendingDelegationTarget; + } + + /** + * Number consumers holding the 'generating' lock. + */ + private _generating?: { rc: number; defer: DeferredPromise }; + + private _emptyInputState: ObservableMemento; + private _chatSessionIsEmpty = false; + private _pendingDelegationTarget: AgentSessionProviders | undefined = undefined; + + constructor( + // private readonly editorOptions: ChatEditorOptions, // TODO this should be used + private readonly location: ChatAgentLocation, + private readonly options: IChatInputPartOptions, + styles: IChatInputStyles, + private readonly inline: boolean, + @IModelService private readonly modelService: IModelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILogService private readonly logService: ILogService, + @IFileService private readonly fileService: IFileService, + @IEditorService private readonly editorService: IEditorService, + @IThemeService private readonly themeService: IThemeService, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @IStorageService private readonly storageService: IStorageService, + @IChatAgentService private readonly agentService: IChatAgentService, + @ISharedWebContentExtractorService private readonly sharedWebExtracterService: ISharedWebContentExtractorService, + @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, + @IChatEntitlementService private readonly entitlementService: IChatEntitlementService, + @IChatModeService private readonly chatModeService: IChatModeService, + @ILanguageModelToolsService private readonly toolService: ILanguageModelToolsService, + @IChatService private readonly chatService: IChatService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IChatContextService private readonly chatContextService: IChatContextService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + ) { + super(); + + // Initialize debounced text sync scheduler + this._syncTextDebounced = this._register(new RunOnceScheduler(() => this._syncInputStateToModel(), 150)); + this._emptyInputState = this._register(emptyInputState(StorageScope.WORKSPACE, StorageTarget.USER, this.storageService)); + + this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event })); + this._currentModeObservable = observableValue('currentMode', this.options.defaultMode ?? ChatMode.Agent); + this._register(this.editorService.onDidActiveEditorChange(() => { + this._indexOfLastOpenedContext = -1; + this.refreshChatSessionPickers(); + })); + + // React to chat session option changes for the active session + this._register(this.chatSessionsService.onDidChangeSessionOptions(e => { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + if (sessionResource && isEqual(sessionResource, e)) { + // Options changed for our current session - refresh pickers + this.refreshChatSessionPickers(); + } + })); + + this._register(this.chatSessionsService.onDidChangeOptionGroups(chatSessionType => { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + if (sessionResource) { + const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); + const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); + if (ctx?.chatSessionType === chatSessionType || delegateSessionType === chatSessionType) { + this.refreshChatSessionPickers(); + } + } + })); + + // Listen for session type changes from the welcome page delegate + if (this.options.sessionTypePickerDelegate?.onDidChangeActiveSessionProvider) { + this._register(this.options.sessionTypePickerDelegate.onDidChangeActiveSessionProvider(async (newSessionType) => { + this.computeVisibleOptionGroups(); + this.agentSessionTypeKey.set(newSessionType); + this.updateWidgetLockStateFromSessionType(newSessionType); + this.refreshChatSessionPickers(); + })); + } + + this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); + this._register(this._attachmentModel.onDidChange(() => this._syncInputStateToModel())); + this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, this.currentModeObs, this._currentLanguageModel)); + this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, () => this._widget, this._attachmentModel, styles)); + + this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT; + + this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService); + this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService); + this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService); + this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService); + this.chatModeNameKey = ChatContextKeys.chatModeName.bindTo(contextKeyService); + this.withinEditSessionKey = ChatContextKeys.withinEditSessionDiff.bindTo(contextKeyService); + this.filePartOfEditSessionKey = ChatContextKeys.filePartOfEditSession.bindTo(contextKeyService); + this.chatSessionHasOptions = ChatContextKeys.chatSessionHasModels.bindTo(contextKeyService); + this.chatSessionOptionsValid = ChatContextKeys.chatSessionOptionsValid.bindTo(contextKeyService); + this.agentSessionTypeKey = ChatContextKeys.agentSessionType.bindTo(contextKeyService); + + // Initialize agentSessionType from delegate if available + if (this.options.sessionTypePickerDelegate?.getActiveSessionProvider) { + const initialSessionType = this.options.sessionTypePickerDelegate.getActiveSessionProvider(); + if (initialSessionType) { + this.agentSessionTypeKey.set(initialSessionType); + } + } + this.chatSessionHasCustomAgentTarget = ChatContextKeys.chatSessionHasCustomAgentTarget.bindTo(contextKeyService); + + this.history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, this.location)); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + const newOptions: IEditorOptions = {}; + if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { + newOptions.ariaLabel = this._getAriaLabel(); + } + if (e.affectsConfiguration('editor.wordSegmenterLocales')) { + newOptions.wordSegmenterLocales = this.configurationService.getValue('editor.wordSegmenterLocales'); + } + if (e.affectsConfiguration('editor.autoClosingBrackets')) { + newOptions.autoClosingBrackets = this.configurationService.getValue('editor.autoClosingBrackets'); + } + if (e.affectsConfiguration('editor.autoClosingQuotes')) { + newOptions.autoClosingQuotes = this.configurationService.getValue('editor.autoClosingQuotes'); + } + if (e.affectsConfiguration('editor.autoSurround')) { + newOptions.autoSurround = this.configurationService.getValue('editor.autoSurround'); + } + + this.inputEditor.updateOptions(newOptions); + })); + + this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar, { verticalScrollMode: ScrollbarVisibility.Visible })); + + this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService); + + this.initSelectedModel(); + + this._register(this.languageModelsService.onDidChangeLanguageModels((vendor) => { + // Remove vendor from cache since the models changed and what is stored is no longer valid + // TODO @lramos15 - The cache should be less confusing since we have the LM Service cache + the view cache interacting weirdly + this.storageService.store( + CachedLanguageModelsKey, + this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []).filter(m => !m.identifier.startsWith(vendor)), + StorageScope.APPLICATION, + StorageTarget.MACHINE + ); + + // We've changed models and the current one is no longer available. Select a new one + const selectedModel = this._currentLanguageModel ? this.getModels().find(m => m.identifier === this._currentLanguageModel.get()?.identifier) : undefined; + const selectedModelNotAvailable = this._currentLanguageModel && (!selectedModel?.metadata.isUserSelectable); + if (!this.currentLanguageModel || selectedModelNotAvailable) { + this.setCurrentLanguageModelToDefault(); + } + })); + + this._register(this.onDidChangeCurrentChatMode(() => { + this.accessibilityService.alert(this._currentModeObservable.get().label.get()); + if (this._inputEditor) { + this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); + } + this.setImplicitContextEnablement(); + })); + this._register(autorun(reader => { + const lm = this._currentLanguageModel.read(reader); + if (lm?.metadata.name) { + this.accessibilityService.alert(lm.metadata.name); + } + this._inputEditor?.updateOptions({ ariaLabel: this._getAriaLabel() }); + })); + this._register(this.chatModeService.onDidChangeChatModes(() => this.validateCurrentChatMode())); + this._register(autorun(r => { + const mode = this._currentModeObservable.read(r); + this.chatModeKindKey.set(mode.kind); + this.chatModeNameKey.set(mode.name.read(r)); + const models = mode.model?.read(r); + if (models) { + this.switchModelByQualifiedName(models); + } + })); + + this._register(autorun(r => { + const mode = this._currentModeObservable.read(r); + const modeName = mode.name.read(r); + const sessionResource = this._widget?.viewModel?.model.sessionResource; + if (sessionResource) { + const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); + if (ctx) { + this.chatSessionsService.notifySessionOptionsChange( + ctx.chatSessionResource, + [{ optionId: agentOptionId, value: mode.isBuiltin ? '' : modeName }] + ).catch(err => this.logService.error('Failed to notify extension of agent change:', err)); + } + } + })); + + // Validate the initial mode - if Agent mode is set by default but disabled by policy, switch to Ask + this.validateCurrentChatMode(); + } + + private setImplicitContextEnablement() { + if (this.implicitContext && this.configurationService.getValue('chat.implicitContext.suggestedContext')) { + this.implicitContext.setEnabled(this._currentModeObservable.get().kind !== ChatMode.Agent.kind); + } + } + + public setIsWithinEditSession(inInsideDiff: boolean, isFilePartOfEditSession: boolean) { + this.withinEditSessionKey.set(inInsideDiff); + this.filePartOfEditSessionKey.set(isFilePartOfEditSession); + } + + private getSelectedModelStorageKey(): string { + return `chat.currentLanguageModel.${this.location}`; + } + + private getSelectedModelIsDefaultStorageKey(): string { + return `chat.currentLanguageModel.${this.location}.isDefault`; + } + + private initSelectedModel() { + const persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION); + const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'copilot/gpt-4.1'); + + if (persistedSelection) { + const model = this.getModels().find(m => m.identifier === persistedSelection); + if (model) { + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || model.metadata.isDefaultForLocation[this.location]) { + this.setCurrentLanguageModel(model); + this.checkModelSupported(); + } + } else { + this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => { + const persistedModel = this.languageModelsService.lookupLanguageModel(persistedSelection); + if (persistedModel) { + this._waitForPersistedLanguageModel.clear(); + + // Only restore the model if it wasn't the default at the time of storing or it is now the default + if (!persistedAsDefault || persistedModel.isDefaultForLocation[this.location]) { + if (persistedModel.isUserSelectable) { + this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection }); + this.checkModelSupported(); + } + } + } else { + this.setCurrentLanguageModelToDefault(); + } + }); + } + } + + this._register(this._onDidChangeCurrentChatMode.event(() => { + this.checkModelSupported(); + })); + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.Edits2Enabled)) { + this.checkModelSupported(); + } + })); + } + + public setEditing(enabled: boolean) { + this.currentlyEditingInputKey?.set(enabled); + } + + public switchModel(modelMetadata: Pick) { + const models = this.getModels(); + const model = models.find(m => m.metadata.vendor === modelMetadata.vendor && m.metadata.id === modelMetadata.id && m.metadata.family === modelMetadata.family); + if (model) { + this.setCurrentLanguageModel(model); + } + } + + public switchModelByQualifiedName(qualifiedModelNames: readonly string[]): boolean { + const models = this.getModels(); + for (const qualifiedModelName of qualifiedModelNames) { + const model = models.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedModelName, m.metadata)); + if (model) { + this.setCurrentLanguageModel(model); + return true; + } + } + this.logService.warn(`[chat] Node of the models "${qualifiedModelNames.join(', ')}" not found. Use format " ()", e.g. "GPT-4o (copilot)".`); + return false; + } + + + public switchToNextModel(): void { + const models = this.getModels(); + if (models.length > 0) { + const currentIndex = models.findIndex(model => model.identifier === this._currentLanguageModel.get()?.identifier); + const nextIndex = (currentIndex + 1) % models.length; + this.setCurrentLanguageModel(models[nextIndex]); + } + } + + public openModelPicker(): void { + this.modelWidget?.show(); + } + + public openModePicker(): void { + this.modeWidget?.show(); + } + + public openSessionTargetPicker(): void { + this.sessionTargetWidget?.show(); + } + + public openDelegationPicker(): void { + this.delegationWidget?.show(); + } + + public openChatSessionPicker(): void { + // Open the first available picker widget + const firstWidget = this.chatSessionPickerWidgets?.values()?.next().value; + firstWidget?.show(); + } + + /** + * Create picker widgets for all option groups available for the current session type. + */ + private createChatSessionPickerWidgets(action: MenuItemAction): (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] { + this._lastSessionPickerAction = action; + + const result = this.computeVisibleOptionGroups(); + if (!result) { + return []; + } + + const { visibleGroupIds, optionGroups, effectiveSessionType } = result; + // Clear existing widgets + this.disposeSessionPickerWidgets(); + + const widgets: (ChatSessionPickerActionItem | SearchableOptionPickerActionItem)[] = []; + for (const optionGroup of optionGroups) { + if (!visibleGroupIds.has(optionGroup.id)) { + continue; + } + + const initialItem = this.getCurrentOptionForGroup(optionGroup.id); + const initialState = { group: optionGroup, item: initialItem }; + + // Create delegate for this option group + const itemDelegate: IChatSessionPickerDelegate = { + getCurrentOption: () => this.getCurrentOptionForGroup(optionGroup.id), + onDidChangeOption: this.getOrCreateOptionEmitter(optionGroup.id).event, + setOption: (option: IChatSessionProviderOptionItem) => { + // Update context key for this option group + this.updateOptionContextKey(optionGroup.id, option.id); + this.getOrCreateOptionEmitter(optionGroup.id).fire(option); + + // Notify session if we have one (not in welcome view before session creation) + const sessionResource = this._widget?.viewModel?.model.sessionResource; + const currentCtx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; + if (currentCtx) { + this.chatSessionsService.notifySessionOptionsChange( + currentCtx.chatSessionResource, + [{ optionId: optionGroup.id, value: option }] + ).catch(err => this.logService.error(`Failed to notify extension of ${optionGroup.id} change:`, err)); + } + + // Refresh pickers to re-evaluate visibility of other option groups + this.refreshChatSessionPickers(); + }, + getOptionGroup: () => { + const groups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); + return groups?.find(g => g.id === optionGroup.id); + }, + getSessionResource: () => { + return this._widget?.viewModel?.model.sessionResource; + } + }; + + const widget = this.instantiationService.createInstance(optionGroup.searchable ? SearchableOptionPickerActionItem : ChatSessionPickerActionItem, action, initialState, itemDelegate); + this.chatSessionPickerWidgets.set(optionGroup.id, widget); + widgets.push(widget); + } + + return widgets; + } + + /** + * Set the input model reference for syncing input state + */ + setInputModel(model: IInputModel, chatSessionIsEmpty: boolean): void { + this._inputModel = model; + this._modelSyncDisposables.clear(); + this.selectedToolsModel.resetSessionEnablementState(); + this._chatSessionIsEmpty = chatSessionIsEmpty; + + // TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed. + if (chatSessionIsEmpty) { + this._setEmptyModelState(); + } + + // Observe changes from model and sync to view + this._modelSyncDisposables.add(autorun(reader => { + let state = model.state.read(reader); + if (!state && this._chatSessionIsEmpty) { + state = this._emptyInputState.read(undefined); + } + + this._syncFromModel(state); + })); + } + + private _setEmptyModelState() { + const storageKey = this.getDefaultModeExperimentStorageKey(); + const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false); + if (!hasSetDefaultMode) { + const isAnonymous = this.entitlementService.anonymous; + this.experimentService.getTreatment('chat.defaultMode') + .then((defaultModeTreatment => { + if (isAnonymous) { + // be deterministic for anonymous users + // to support agentic flows with default + // model. + defaultModeTreatment = ChatModeKind.Agent; + } + + if (typeof defaultModeTreatment === 'string') { + this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + const defaultMode = validateChatMode(defaultModeTreatment); + if (defaultMode) { + this.logService.trace(`Applying default mode from experiment: ${defaultMode}`); + this.setChatMode(defaultMode, false); + this.checkModelSupported(); + } + } + })); + } + } + + /** + * Sync from model to view (when model state changes) + */ + private _syncFromModel(state: IChatModelInputState | undefined): void { + // Prevent circular updates + if (this._isSyncingToOrFromInputModel) { + return; + } + + try { + this._isSyncingToOrFromInputModel = true; + + // Sync mode + if (state) { + const currentMode = this._currentModeObservable.get(); + if (currentMode.id !== state.mode.id) { + this.setChatMode(state.mode.id, false); + } + } + + // Sync selected model + if (state?.selectedModel) { + const lm = this._currentLanguageModel.get(); + if (!lm || lm.identifier !== state.selectedModel.identifier) { + this.setCurrentLanguageModel(state.selectedModel); + } + } + + // Sync attachments + const currentAttachments = this._attachmentModel.attachments; + if (!state) { + this._attachmentModel.clear(); + } else if (!arraysEqual(currentAttachments, state.attachments)) { + this._attachmentModel.clearAndSetContext(...state.attachments); + } + + // Sync input text + if (this._inputEditor) { + this._inputEditor.setValue(state?.inputText || ''); + if (state?.selections.length) { + this._inputEditor.setSelections(state.selections); + } + } + + if (state) { + this._widget?.contribs.forEach(contrib => { + contrib.setInputState?.(state.contrib); + }); + } + } finally { + this._isSyncingToOrFromInputModel = false; + } + } + + /** + * Sync current input state to the input model + */ + private _syncInputStateToModel(): void { + if (this._isSyncingToOrFromInputModel) { + return; + } + + + this._isSyncingToOrFromInputModel = true; + const state = this.getCurrentInputState(); + if (this._chatSessionIsEmpty) { + this._emptyInputState.set(state, undefined); + } + this._inputModel?.setState(state); + this._isSyncingToOrFromInputModel = false; + } + + public setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) { + this._currentLanguageModel.set(model, undefined); + + if (this.cachedWidth) { + // For quick chat and editor chat, relayout because the input may need to shrink to accomodate the model name + this.layout(this.cachedWidth); + } + + // Store as global user preference (session-specific state is in the model's inputModel) + this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER); + this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefaultForLocation[this.location], StorageScope.APPLICATION, StorageTarget.USER); + + // Sync to model + this._syncInputStateToModel(); + } + + private checkModelSupported(): void { + const lm = this._currentLanguageModel.get(); + if (lm && (!this.modelSupportedForDefaultAgent(lm) || !this.modelSupportedForInlineChat(lm))) { + this.setCurrentLanguageModelToDefault(); + } + } + + /** + * By ID- prefer this method + */ + setChatMode(mode: ChatModeKind | string, storeSelection = true): void { + if (!this.options.supportsChangingModes) { + return; + } + + const mode2 = this.chatModeService.findModeById(mode) ?? + this.chatModeService.findModeById(ChatModeKind.Agent) ?? + ChatMode.Ask; + this.setChatMode2(mode2, storeSelection); + } + + private setChatMode2(mode: IChatMode, storeSelection = true): void { + if (!this.options.supportsChangingModes) { + return; + } + + this._currentModeObservable.set(mode, undefined); + this._onDidChangeCurrentChatMode.fire(); + + // Sync to model (mode is now persisted in the model's input state) + this._syncInputStateToModel(); + } + + private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean { + // Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex + if (this.currentModeKind === ChatModeKind.Agent || (this.currentModeKind === ChatModeKind.Edit && this.configurationService.getValue(ChatConfiguration.Edits2Enabled))) { + return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata); + } + + return true; + } + + private modelSupportedForInlineChat(model: ILanguageModelChatMetadataAndIdentifier): boolean { + if (this.location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) { + return true; + } + return !!model.metadata.capabilities?.toolCalling; + } + + private getModels(): ILanguageModelChatMetadataAndIdentifier[] { + const cachedModels = this.storageService.getObject(CachedLanguageModelsKey, StorageScope.APPLICATION, []); + let models = this.languageModelsService.getLanguageModelIds() + .map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! })); + if (models.length === 0 || models.some(m => m.metadata.isDefaultForLocation[this.location]) === false) { + models = cachedModels; + } else { + this.storageService.store(CachedLanguageModelsKey, models, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name)); + return models.filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry) && this.modelSupportedForInlineChat(entry)); + } + + private setCurrentLanguageModelToDefault() { + const allModels = this.getModels(); + const defaultModel = allModels.find(m => m.metadata.isDefaultForLocation[this.location]) || allModels.find(m => m.metadata.isUserSelectable); + if (defaultModel) { + this.setCurrentLanguageModel(defaultModel); + } + } + + /** + * Get the current input state for history + */ + public getCurrentInputState(): IChatModelInputState { + const mode = this._currentModeObservable.get(); + const state: IChatModelInputState = { + inputText: this._inputEditor?.getValue() ?? '', + attachments: this._attachmentModel.attachments, + mode: { + id: mode.id, + kind: mode.kind + }, + selectedModel: this._currentLanguageModel.get(), + selections: this._inputEditor?.getSelections() || [], + contrib: {}, + }; + + for (const contrib of this._widget?.contribs || Iterable.empty()) { + contrib.getInputState?.(state.contrib); + } + + return state; + } + + private _getAriaLabel(): string { + const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); + let kbLabel; + if (verbose) { + kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); + } + const mode = this._currentModeObservable.get(); + + // Include model information if available + const modelName = this._currentLanguageModel.get()?.metadata.name; + const modelInfo = modelName ? localize('chatInput.model', ", {0}. ", modelName) : ''; + + let modeLabel = ''; + if (!mode.isBuiltin) { + const mode = this.currentModeObs.get(); + modeLabel = localize('chatInput.mode.custom', "({0}), {1}", mode.label.get(), mode.description.get()); + } else { + switch (this.currentModeKind) { + case ChatModeKind.Agent: + modeLabel = localize('chatInput.mode.agent', "(Agent), edit files in your workspace."); + break; + case ChatModeKind.Edit: + modeLabel = localize('chatInput.mode.edit', "(Edit), edit files in your workspace."); + break; + case ChatModeKind.Ask: + default: + modeLabel = localize('chatInput.mode.ask', "(Ask), ask questions or type / for topics."); + break; + } + } + if (verbose) { + return kbLabel + ? localize('actions.chat.accessibiltyHelp', "Chat Input {0}{1} Press Enter to send out the request. Use {2} for Chat Accessibility Help.", modeLabel, modelInfo, kbLabel) + : localize('chatInput.accessibilityHelpNoKb', "Chat Input {0}{1} Press Enter to send out the request. Use the Chat Accessibility Help command for more information.", modeLabel, modelInfo); + } else { + return localize('chatInput.accessibilityHelp', "Chat Input {0}{1}.", modeLabel, modelInfo); + } + } + + private validateCurrentChatMode() { + const currentMode = this._currentModeObservable.get(); + const validMode = this.chatModeService.findModeById(currentMode.id); + const isAgentModeEnabled = this.configurationService.getValue(ChatConfiguration.AgentEnabled); + if (!validMode) { + this.setChatMode(isAgentModeEnabled ? ChatModeKind.Agent : ChatModeKind.Ask); + return; + } + if (currentMode.kind === ChatModeKind.Agent && !isAgentModeEnabled) { + this.setChatMode(ChatModeKind.Ask); + return; + } + } + + private getDefaultModeExperimentStorageKey(): string { + const tag = this.options.widgetViewKindTag; + return `chat.${tag}.hasSetDefaultModeByExperiment`; + } + + logInputHistory(): void { + const historyStr = this.history.values.map(entry => JSON.stringify(entry)).join('\n'); + this.logService.info(`[${this.location}] Chat input history:`, historyStr); + } + + setVisible(visible: boolean): void { + this._onDidChangeVisibility.fire(visible); + } + + /** If consumers are busy generating the chat input, returns the promise resolved when they finish */ + get generating() { + return this._generating?.defer.p; + } + + /** Disables the input submissions buttons until the disposable is disposed. */ + startGenerating(): IDisposable { + this.logService.trace('ChatWidget#startGenerating'); + if (this._generating) { + this._generating.rc++; + } else { + this._generating = { rc: 1, defer: new DeferredPromise() }; + } + + return toDisposable(() => { + this.logService.trace('ChatWidget#doneGenerating'); + if (this._generating && !--this._generating.rc) { + this._generating.defer.complete(); + this._generating = undefined; + } + }); + } + + get element(): HTMLElement { + return this.container; + } + + async showPreviousValue(): Promise { + if (this.history.isAtStart()) { + return; + } + + const state = this.getCurrentInputState(); + if (state.inputText || state.attachments.length) { + this.history.overlay(state); + } + this.navigateHistory(true); + } + + async showNextValue(): Promise { + if (this.history.isAtEnd()) { + return; + } + + const state = this.getCurrentInputState(); + if (state.inputText || state.attachments.length) { + this.history.overlay(state); + } + this.navigateHistory(false); + } + + private async navigateHistory(previous: boolean): Promise { + const historyEntry = previous ? + this.history.previous() : this.history.next(); + + let historyAttachments = historyEntry?.attachments ?? []; + + // Check for images in history to restore the value. + if (historyAttachments.length > 0) { + historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => { + if (isImageVariableEntry(attachment) && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { + const currReference = attachment.references[0].reference; + try { + const imageBinary = currReference.toString(true).startsWith('http') ? await this.sharedWebExtracterService.readImage(currReference, CancellationToken.None) : (await this.fileService.readFile(currReference)).value; + if (!imageBinary) { + return undefined; + } + const newAttachment = { ...attachment }; + newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? imageBinary.buffer : await resizeImage(imageBinary.buffer); // if pasted image, we do not need to resize. + return newAttachment; + } catch (err) { + this.logService.error('Failed to fetch and reference.', err); + return undefined; + } + } + return attachment; + }))).filter(attachment => attachment !== undefined); + } + + this._attachmentModel.clearAndSetContext(...historyAttachments); + + const inputText = historyEntry?.inputText ?? ''; + const contribData = historyEntry?.contrib ?? {}; + aria.status(inputText); + this.setValue(inputText, true); + this._widget?.contribs.forEach(contrib => { + contrib.setInputState?.(contribData); + }); + this._onDidLoadInputState.fire(); + + const model = this._inputEditor.getModel(); + if (!model) { + return; + } + + if (previous) { + // When navigating to previous history, always position cursor at the start (line 1, column 1) + // This ensures that pressing up again will continue to navigate history + this._inputEditor.setPosition({ lineNumber: 1, column: 1 }); + } else { + this._inputEditor.setPosition(getLastPosition(model)); + } + } + + setValue(value: string, transient: boolean): void { + this.inputEditor.setValue(value); + // always leave cursor at the end + const model = this.inputEditor.getModel(); + if (model) { + this.inputEditor.setPosition(getLastPosition(model)); + } + } + + focus() { + this._inputEditor.focus(); + } + + hasFocus(): boolean { + return this._inputEditor.hasWidgetFocus(); + } + + /** + * Reset the input and update history. + * @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed. + */ + async acceptInput(isUserQuery?: boolean): Promise { + if (isUserQuery) { + const userQuery = this.getCurrentInputState(); + this.history.append(this._getFilteredEntry(userQuery)); + } + + if (this._chatSessionIsEmpty) { + this._chatSessionIsEmpty = false; + this._emptyInputState.set(undefined, undefined); + } + + // Clear attached context, fire event to clear input state, and clear the input editor + this.attachmentModel.clear(); + this._onDidLoadInputState.fire(); + if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { + this._acceptInputForVoiceover(); + } else { + this._inputEditor.focus(); + this._inputEditor.setValue(''); + } + } + + validateAgentMode(): void { + if (!this.agentService.hasToolsAgent && this._currentModeObservable.get().kind === ChatModeKind.Agent) { + this.setChatMode(ChatModeKind.Edit); + } + } + + // A function that filters out specifically the `value` property of the attachment. + private _getFilteredEntry(inputState: IChatModelInputState): IChatModelInputState { + const attachmentsWithoutImageValues = inputState.attachments.map(attachment => { + if (isImageVariableEntry(attachment) && attachment.references?.length && attachment.value) { + const newAttachment = { ...attachment }; + newAttachment.value = undefined; + return newAttachment; + } + return attachment; + }); + + return { ...inputState, attachments: attachmentsWithoutImageValues }; + } + + private _acceptInputForVoiceover(): void { + const domNode = this._inputEditor.getDomNode(); + if (!domNode) { + return; + } + // Remove the input editor from the DOM temporarily to prevent VoiceOver + // from reading the cleared text (the request) to the user. + domNode.remove(); + this._inputEditor.setValue(''); + this._inputEditorElement.appendChild(domNode); + this._inputEditor.focus(); + } + + private _handleAttachedContextChange() { + this._hasFileAttachmentContextKey.set(Boolean(this._attachmentModel.attachments.find(a => a.kind === 'file'))); + this.renderAttachedContext(); + } + + private getOrCreateOptionEmitter(optionGroupId: string): Emitter { + let emitter = this._chatSessionOptionEmitters.get(optionGroupId); + if (!emitter) { + emitter = this._register(new Emitter()); + this._chatSessionOptionEmitters.set(optionGroupId, emitter); + } + return emitter; + } + + /** + * Get or create a context key for an option group. + * Context keys follow the pattern `chatSessionOption.`. + */ + private getOrCreateOptionContextKey(optionGroupId: string): IContextKey | undefined { + if (!this._scopedContextKeyService) { + return undefined; + } + let contextKey = this._optionContextKeys.get(optionGroupId); + if (!contextKey) { + const rawKey = new RawContextKey(`chatSessionOption.${optionGroupId}`, ''); + contextKey = rawKey.bindTo(this._scopedContextKeyService); + this._optionContextKeys.set(optionGroupId, contextKey); + } + return contextKey; + } + + /** + * Update the context key for an option group with the current selection. + * This enables `when` expressions on other option groups to react to changes. + */ + private updateOptionContextKey(optionGroupId: string, optionItemId: string): void { + const normalizedOptionId = optionItemId.trim(); + const contextKey = this.getOrCreateOptionContextKey(optionGroupId); + if (contextKey) { + contextKey.set(normalizedOptionId); + } + } + + /** + * Evaluate whether an option group should be visible based on its `when` expression. + * Returns true if the option group should be visible, false otherwise. + */ + private evaluateOptionGroupVisibility(optionGroup: { id: string; when?: string }): boolean { + if (!optionGroup.when) { + return true; // No condition means always visible + } + + if (!this._scopedContextKeyService) { + return true; // No context key service yet, default to visible + } + + const expr = ContextKeyExpr.deserialize(optionGroup.when); + if (!expr) { + return true; // Invalid expression defaults to visible + } + + return this._scopedContextKeyService.contextMatchesRules(expr); + } + + /** + * Computes which option groups should be visible for the current session. + * + * A picker should show if and only if: + * 1. We can determine a session type (from session context OR delegate) + * 2. That session type has option groups registered + * 3. At least one option group has items AND passes its `when` clause + * + * This method also updates the `chatSessionHasOptions` context key, which controls + * whether the picker action is shown in the toolbar via its `when` clause. + * + * @returns The result containing visible group IDs and related context, or undefined + * if there are no visible option groups + */ + private computeVisibleOptionGroups(): { + visibleGroupIds: Set; + optionGroups: IChatSessionProviderOptionGroup[]; + ctx: IChatSessionContext | undefined; + effectiveSessionType: string; + } | undefined { + const setNoOptions = () => { + this.chatSessionHasOptions.set(false); + this.chatSessionOptionsValid.set(true); + }; + + const sessionResource = this._widget?.viewModel?.model.sessionResource; + const ctx = sessionResource ? this.chatService.getChatSessionFromInternalUri(sessionResource) : undefined; + + // Check if this session type has a customAgentTarget + const customAgentTarget = ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType); + this.chatSessionHasCustomAgentTarget.set(!!customAgentTarget); + + // Handle agent option from session - set initial mode + if (customAgentTarget) { + const agentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, agentOptionId); + if (typeof agentOption !== 'undefined') { + const agentId = (typeof agentOption === 'string' ? agentOption : agentOption.id) || ChatMode.Agent.id; + const currentMode = this._currentModeObservable.get(); + const isDefaultAgent = agentId === ChatMode.Agent.id; + const needsUpdate = isDefaultAgent + ? currentMode.id !== ChatMode.Agent.id + : currentMode.label.get() !== agentId; // Extensions use Label (name) as identifier for custom agents. + + if (needsUpdate) { + this.setChatMode(agentId); + } + } + } + + // Step 1: Determine the session type + // - Panel/Editor: Use actual session's type (ctx available) + // - Welcome view: Use delegate's type (ctx may not exist yet) + const delegateSessionType = this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.(); + const effectiveSessionType = delegateSessionType ?? ctx?.chatSessionType; + + if (!effectiveSessionType) { + setNoOptions(); + return undefined; + } + + // Step 2: Get option groups for this session type + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); + if (!optionGroups || optionGroups.length === 0) { + setNoOptions(); + return undefined; + } + + // Update context keys with current option values before evaluating `when` clauses. + // This ensures interdependent `when` expressions work correctly. + if (ctx) { + for (const optionGroup of optionGroups) { + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id); + if (currentOption) { + const optionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + this.updateOptionContextKey(optionGroup.id, optionId); + } + } + } + + // Step 3: Filter to visible groups (has items AND passes `when` clause AND session has option configured) + const visibleGroupIds = new Set(); + for (const optionGroup of optionGroups) { + const hasItems = optionGroup.items.length > 0 || (optionGroup.commands || []).length > 0; + const passesWhenClause = this.evaluateOptionGroupVisibility(optionGroup); + + // Only show picker if the session has this option configured once a real session exists. + // In the welcome view (no `ctx` yet), treat groups as eligible so they can be rendered. + const sessionHasOption = !ctx || this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroup.id) !== undefined; + + if (hasItems && passesWhenClause && sessionHasOption) { + visibleGroupIds.add(optionGroup.id); + } + } + + if (visibleGroupIds.size === 0) { + setNoOptions(); + return undefined; + } + + // Validate selected options exist in their respective groups + let allOptionsValid = true; + if (ctx) { + for (const groupId of visibleGroupIds) { + const optionGroup = optionGroups.find(g => g.id === groupId); + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, groupId); + if (optionGroup && currentOption) { + const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + // TODO: @osortega @joshspicer should we add a `placeHolder` item to option groups to straighten this check? + if (!optionGroup.items.some(item => item.id === currentOptionId) && typeof currentOption === 'string') { + allOptionsValid = false; + break; + } + } + } + } + + this.chatSessionHasOptions.set(true); + this.chatSessionOptionsValid.set(allOptionsValid); + + return { visibleGroupIds, optionGroups, ctx, effectiveSessionType }; + } + + /** + * Refresh all registered option groups for the current chat session. + * Fires events for each option group with their current selection. + */ + private refreshChatSessionPickers(): void { + // Use the shared helper to compute visibility and update context keys + const result = this.computeVisibleOptionGroups(); + + if (!result) { + // No visible options - helper already updated context keys + this.hideAllSessionPickerWidgets(); + return; + } + + const { visibleGroupIds, optionGroups, ctx } = result; + + // Check if widgets need recreation (different set of visible groups) + const currentWidgetGroupIds = new Set(this.chatSessionPickerWidgets.keys()); + const needsRecreation = + currentWidgetGroupIds.size !== visibleGroupIds.size || + !Array.from(visibleGroupIds).every(id => currentWidgetGroupIds.has(id)); + + if (needsRecreation && this._lastSessionPickerAction && this.chatSessionPickerContainer) { + const widgets = this.createChatSessionPickerWidgets(this._lastSessionPickerAction); + dom.clearNode(this.chatSessionPickerContainer); + for (const widget of widgets) { + const container = dom.$('.action-item.chat-sessionPicker-item'); + widget.render(container); + this.chatSessionPickerContainer.appendChild(container); + } + } + + if (this.chatSessionPickerContainer) { + this.chatSessionPickerContainer.style.display = ''; + } + + // Fire option change events for existing widgets to sync their state + // (only if we have a session context - in welcome view, options aren't persisted yet) + if (ctx) { + for (const [optionGroupId] of this.chatSessionPickerWidgets.entries()) { + const currentOption = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); + if (currentOption) { + const optionGroup = optionGroups.find(g => g.id === optionGroupId); + if (optionGroup) { + const currentOptionId = typeof currentOption === 'string' ? currentOption : currentOption.id; + const item = optionGroup.items.find((m: IChatSessionProviderOptionItem) => m.id === currentOptionId); + // If currentOption is an object (not a string ID), it represents a complete option item and should be used directly. + // Otherwise, if it's a string ID, look up the corresponding item and use that. + if (item && typeof currentOption === 'string') { + this.getOrCreateOptionEmitter(optionGroupId).fire(item); + } else if (typeof currentOption !== 'string') { + this.getOrCreateOptionEmitter(optionGroupId).fire(currentOption); + } + + } + } + } + } + } + + private hideAllSessionPickerWidgets(): void { + if (this.chatSessionPickerContainer) { + this.chatSessionPickerContainer.style.display = 'none'; + } + } + + private disposeSessionPickerWidgets(): void { + for (const widget of this.chatSessionPickerWidgets.values()) { + widget.dispose(); + } + this.chatSessionPickerWidgets.clear(); + } + + /** + * Get the current option for a specific option group. + * Returns undefined if the session doesn't have this option configured. + */ + private getCurrentOptionForGroup(optionGroupId: string): IChatSessionProviderOptionItem | undefined { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + if (!sessionResource) { + return; + } + const ctx = this.chatService.getChatSessionFromInternalUri(sessionResource); + if (!ctx) { + return; + } + + // Only return an option if the session has it configured + if (this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId) === undefined) { + return; + } + + const effectiveSessionType = this.getEffectiveSessionType(ctx, this.options.sessionTypePickerDelegate); + const optionGroups = this.chatSessionsService.getOptionGroupsForSessionType(effectiveSessionType); + const optionGroup = optionGroups?.find(g => g.id === optionGroupId); + if (!optionGroup || optionGroup.items.length === 0) { + return; + } + + const currentOptionValue = this.chatSessionsService.getSessionOption(ctx.chatSessionResource, optionGroupId); + if (!currentOptionValue) { + const defaultItem = optionGroup.items.find(item => item.default); + return defaultItem; + } + + if (typeof currentOptionValue === 'string') { + const normalizedOptionId = currentOptionValue.trim(); + return optionGroup.items.find(m => m.id === normalizedOptionId); + } else { + return currentOptionValue as IChatSessionProviderOptionItem; + } + + } + + private getEffectiveSessionType(ctx: IChatSessionContext | undefined, delegate: ISessionTypePickerDelegate | undefined): string { + return this.options.sessionTypePickerDelegate?.getActiveSessionProvider?.() || ctx?.chatSessionType || ''; + } + + /** + * Updates the agentSessionType context key based on delegate or actual session. + */ + private updateAgentSessionTypeContextKey(): void { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + + // Determine effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const sessionType = delegateSessionType || (sessionResource ? getChatSessionType(sessionResource) : ''); + + this.agentSessionTypeKey.set(sessionType); + } + + /** + * Updates the widget lock state based on a session type. + * Local sessions unlock from coding agent mode, while remote/cloud sessions lock to coding agent mode. + */ + private updateWidgetLockStateFromSessionType(sessionType: string): void { + if (sessionType === localChatSessionType) { + this._widget?.unlockFromCodingAgent(); + return; + } + + const contribution = this.chatSessionsService.getChatSessionContribution(sessionType); + if (contribution) { + this._widget?.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type); + } else { + this._widget?.unlockFromCodingAgent(); + } + } + + /** + * Updates the widget controller based on session type. + */ + private tryUpdateWidgetController(): void { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + if (!sessionResource) { + return; + } + + // Determine effective session type: + // - If we have a delegate with a setter (e.g., welcome page), use the delegate's session type + // - Otherwise, use the actual session's type + const delegate = this.options.sessionTypePickerDelegate; + const delegateSessionType = delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider?.(); + const sessionType = delegateSessionType || this._pendingDelegationTarget || getChatSessionType(sessionResource); + const isLocalSession = sessionType === localChatSessionType; + + if (!isLocalSession) { + this._widgetController.clear(); + return; + } + + if (!this._widgetController.value) { + this._widgetController.value = this.instantiationService.createInstance(ChatInputPartWidgetController, this.chatInputWidgetsContainer); + } + } + + /** + * Updates the context usage widget based on the current model. + */ + private updateContextUsageWidget(): void { + this._contextUsageDisposables.clear(); + + const model = this._widget?.viewModel?.model; + if (!model || !this.contextUsageWidget) { + return; + } + + const store = new DisposableStore(); + this._contextUsageDisposables.value = store; + + store.add(model.onDidChange(e => { + if (e.kind === 'addRequest' || e.kind === 'completedRequest') { + this.contextUsageWidget?.update(model.lastRequest); + } + })); + + // Initial update + this.contextUsageWidget.update(model.lastRequest); + } + + render(container: HTMLElement, initialValue: string, widget: IChatWidget) { + this._widget = widget; + this.computeVisibleOptionGroups(); + + // Initialize lock state when rendering with a pre-selected session provider (e.g., welcome view restore) + const delegate = this.options.sessionTypePickerDelegate; + if (delegate?.setActiveSessionProvider && delegate?.getActiveSessionProvider) { + const initialSessionType = delegate.getActiveSessionProvider(); + if (initialSessionType) { + this.updateWidgetLockStateFromSessionType(initialSessionType); + } + } + + this._register(widget.onDidChangeViewModel(() => { + this._pendingDelegationTarget = undefined; + // Update agentSessionType when view model changes + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); + this.tryUpdateWidgetController(); + this.updateContextUsageWidget(); + })); + + let elements; + if (this.options.renderStyle === 'compact') { + elements = dom.h('.interactive-input-part', [ + dom.h('.interactive-input-and-edit-session', [ + dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'), + dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'), + dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), + dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ + dom.h('.chat-input-container@inputContainer', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + dom.h('.chat-editor-container@editorContainer'), + dom.h('.chat-input-toolbars@inputToolbars'), + ]), + ]), + dom.h('.chat-attachments-container@attachmentsContainer', [ + dom.h('.chat-attachment-toolbar@attachmentToolbar'), + dom.h('.chat-attached-context@attachedContextContainer'), + ]), + dom.h('.interactive-input-followups@followupsContainer'), + ]) + ]); + } else { + elements = dom.h('.interactive-input-part', [ + dom.h('.interactive-input-followups@followupsContainer'), + dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'), + dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'), + dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), + dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ + dom.h('.chat-input-container@inputContainer', [ + dom.h('.chat-context-usage-container@contextUsageWidgetContainer'), + dom.h('.chat-attachments-container@attachmentsContainer', [ + dom.h('.chat-attachment-toolbar@attachmentToolbar'), + dom.h('.chat-attached-context@attachedContextContainer'), + ]), + dom.h('.chat-editor-container@editorContainer'), + dom.h('.chat-input-toolbars@inputToolbars'), + ]), + ]), + ]); + } + this.container = elements.root; + this.chatInputOverlay = dom.$('.chat-input-overlay'); + container.append(this.container); + this.container.append(this.chatInputOverlay); + this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); + + // Create a scoped context key service for option group visibility expressions + // This isolates chatSessionOption.* context keys to this specific chat input instance + this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(this.container)); + + this.followupsContainer = elements.followupsContainer; + const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right + const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars + const editorContainer = elements.editorContainer; + this.attachmentsContainer = elements.attachmentsContainer; + this.attachedContextContainer = elements.attachedContextContainer; + const toolbarsContainer = elements.inputToolbars; + const attachmentToolbarContainer = elements.attachmentToolbar; + this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; + this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; + this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; + this.contextUsageWidgetContainer = elements.contextUsageWidgetContainer; + + // Context usage widget + this.contextUsageWidget = this._register(this.instantiationService.createInstance(ChatContextUsageWidget)); + this.contextUsageWidgetContainer.appendChild(this.contextUsageWidget.domNode); + + if (this.options.enableImplicitContext && !this._implicitContext) { + this._implicitContext = this._register( + this.instantiationService.createInstance(ChatImplicitContexts), + ); + this.setImplicitContextEnablement(); + + this._register(this._implicitContext.onDidChangeValue(() => { + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + this._handleAttachedContextChange(); + })); + } else if (!this.options.enableImplicitContext && this._implicitContext) { + this._implicitContext?.dispose(); + this._implicitContext = undefined; + } + + this.tryUpdateWidgetController(); + + this._register(this._attachmentModel.onDidChange((e) => { + if (e.added.length > 0) { + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + } + this._handleAttachedContextChange(); + })); + + this.renderChatEditingSessionState(null); + + this.dnd.addOverlay(this.options.dndContainer ?? container, this.options.dndContainer ?? container); + + const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); + ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true); + this.currentlyEditingInputKey = ChatContextKeys.currentlyEditingInput.bindTo(inputScopedContextKeyService); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService]))); + + const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); + this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; + this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; + + const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService); + options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode; + options.pasteAs = EditorOptions.pasteAs.defaultValue; + options.readOnly = false; + options.ariaLabel = this._getAriaLabel(); + options.fontFamily = DEFAULT_FONT_FAMILY; + options.fontSize = 13; + options.lineHeight = 20; + options.padding = this.options.renderStyle === 'compact' ? { top: 2, bottom: 2 } : { top: 8, bottom: 8 }; + options.cursorWidth = 1; + options.wrappingStrategy = 'advanced'; + options.bracketPairColorization = { enabled: false }; + // Respect user's editor settings for auto-closing and auto-surrounding behavior + options.autoClosingBrackets = this.configurationService.getValue('editor.autoClosingBrackets'); + options.autoClosingQuotes = this.configurationService.getValue('editor.autoClosingQuotes'); + options.autoSurround = this.configurationService.getValue('editor.autoSurround'); + options.suggest = { + showIcons: true, + showSnippets: false, + showWords: true, + showStatusBar: false, + insertMode: 'insert', + }; + options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' }; + options.stickyScroll = { enabled: false }; + + this._inputEditorElement = dom.append(editorContainer, $(chatInputEditorContainerSelector)); + const editorOptions = getSimpleCodeEditorWidgetOptions(); + editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID])); + this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); + + SuggestController.get(this._inputEditor)?.forceRenderingAbove(); + options.overflowWidgetsDomNode?.classList.add('hideSuggestTextIcons'); + this._inputEditorElement.classList.add('hideSuggestTextIcons'); + + // Prevent Enter key from creating new lines - but respect user's custom keybindings + // Only prevent default behavior if ChatSubmitAction is bound to Enter AND its precondition is met + this._register(this._inputEditor.onKeyDown((e) => { + if (e.keyCode === KeyCode.Enter && !hasModifierKeys(e)) { + // Check if ChatSubmitAction has a keybinding for plain Enter in the current context + // This respects user's custom keybindings that disable the submit action + for (const keybinding of this.keybindingService.lookupKeybindings(ChatSubmitAction.ID)) { + const chords = keybinding.getDispatchChords(); + const isPlainEnter = chords.length === 1 && chords[0] === '[Enter]'; + if (isPlainEnter) { + // Do NOT call stopPropagation() so the keybinding service can still process this event + e.preventDefault(); + break; + } + } + } + })); + + this._register(this._inputEditor.onDidChangeModelContent(() => { + const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); + if (currentHeight !== this.inputEditorHeight) { + this.inputEditorHeight = currentHeight; + // Directly update editor layout - ResizeObserver will notify parent about height change + if (this.cachedWidth) { + this._layout(this.cachedWidth); + } + } + + const model = this._inputEditor.getModel(); + const inputHasText = !!model && model.getValue().trim().length > 0; + this.inputEditorHasText.set(inputHasText); + + // Debounced sync to model for text changes + this._syncTextDebounced.schedule(); + })); + this._register(this._inputEditor.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this.inputEditorHeight = !this.inline ? e.contentHeight : this.inputEditorHeight; + // Directly update editor layout - ResizeObserver will notify parent about height change + if (this.cachedWidth) { + this._layout(this.cachedWidth); + } + } + })); + this._register(this._inputEditor.onDidFocusEditorText(() => { + this.inputEditorHasFocus.set(true); + this._onDidFocus.fire(); + inputContainer.classList.toggle('focused', true); + })); + this._register(this._inputEditor.onDidBlurEditorText(() => { + this.inputEditorHasFocus.set(false); + inputContainer.classList.toggle('focused', false); + + this._onDidBlur.fire(); + })); + this._register(this._inputEditor.onDidBlurEditorWidget(() => { + CopyPasteController.get(this._inputEditor)?.clearWidgets(); + DropIntoEditorController.get(this._inputEditor)?.clearWidgets(); + })); + + const hoverDelegate = this._register(createInstantHoverDelegate()); + + const { location, isMaximized } = this.getWidgetLocationInfo(widget); + + const pickerOptions: IChatInputPickerOptions = { + getOverflowAnchor: () => this.inputActionsToolbar.getElement(), + actionContext: { widget }, + onlyShowIconsForDefaultActions: observableFromEvent( + this._inputEditor.onDidLayoutChange, + (l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 650 /* This is a magical number based on testing*/ + ).recomputeInitiallyAndOnChange(this._store), + hoverPosition: { + forcePosition: true, + hoverPosition: location === ChatWidgetLocation.SidebarRight && !isMaximized ? HoverPosition.LEFT : HoverPosition.RIGHT + }, + }; + + this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); + this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus())); + this.inputActionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, this.options.renderInputToolbarBelowInput ? this.attachmentsContainer : toolbarsContainer, MenuId.ChatInput, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { shouldForwardArgs: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + hoverDelegate, + responsiveBehavior: { + enabled: true, + kind: 'last', + minItems: 1, + actionMinWidth: 40 + }, + actionViewItemProvider: (action, options) => { + if (action.id === OpenModelPickerAction.ID && action instanceof MenuItemAction) { + if (!this._currentLanguageModel) { + this.setCurrentLanguageModelToDefault(); + } + + const itemDelegate: IModelPickerDelegate = { + currentModel: this._currentLanguageModel, + setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { + this._waitForPersistedLanguageModel.clear(); + this.setCurrentLanguageModel(model); + this.renderAttachedContext(); + }, + getModels: () => this.getModels() + }; + return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, undefined, itemDelegate, pickerOptions); + } else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) { + const delegate: IModePickerDelegate = { + currentMode: this._currentModeObservable, + sessionResource: () => this._widget?.viewModel?.sessionResource, + customAgentTarget: () => { + const sessionResource = this._widget?.viewModel?.model.sessionResource; + const ctx = sessionResource && this.chatService.getChatSessionFromInternalUri(sessionResource); + return ctx && this.chatSessionsService.getCustomAgentTargetForSessionType(ctx.chatSessionType); + }, + }; + return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions); + } else if ((action.id === OpenSessionTargetPickerAction.ID || action.id === OpenDelegationPickerAction.ID) && action instanceof MenuItemAction) { + // Use provided delegate if available, otherwise create default delegate + const getActiveSessionType = () => { + const sessionResource = this._widget?.viewModel?.sessionResource; + return sessionResource ? getAgentSessionProvider(sessionResource) : undefined; + }; + const delegate: ISessionTypePickerDelegate = this.options.sessionTypePickerDelegate ?? { + getActiveSessionProvider: () => { + return getActiveSessionType(); + }, + getPendingDelegationTarget: () => { + return this._pendingDelegationTarget; + }, + setPendingDelegationTarget: (provider: AgentSessionProviders) => { + const isActive = getActiveSessionType() === provider; + this._pendingDelegationTarget = isActive ? undefined : provider; + this.updateWidgetLockStateFromSessionType(provider); + this.updateAgentSessionTypeContextKey(); + this.refreshChatSessionPickers(); + }, + }; + const isWelcomeViewMode = !!this.options.sessionTypePickerDelegate?.setActiveSessionProvider; + const Picker = (action.id === OpenSessionTargetPickerAction.ID || isWelcomeViewMode) ? SessionTypePickerActionItem : DelegationSessionPickerActionItem; + return this.sessionTargetWidget = this.instantiationService.createInstance(Picker, action, location === ChatWidgetLocation.Editor ? 'editor' : 'sidebar', delegate, pickerOptions); + } else if (action.id === OpenWorkspacePickerAction.ID && action instanceof MenuItemAction) { + if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) { + return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions); + } else { + const empty = new BaseActionViewItem(undefined, action); + if (empty.element) { + empty.element.style.display = 'none'; + } + return empty; + } + } else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) { + // Create all pickers and return a container action view item + const widgets = this.createChatSessionPickerWidgets(action); + if (widgets.length === 0) { + return undefined; + } + // Create a container to hold all picker widgets + return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets); + } + return undefined; + } + })); + this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar'); + this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext; + this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => { + // Update container reference for the pickers + const toolbarElement = this.inputActionsToolbar.getElement(); + // eslint-disable-next-line no-restricted-syntax + const container = toolbarElement.querySelector('.chat-sessionPicker-container'); + this.chatSessionPickerContainer = container as HTMLElement | undefined; + + if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) { + this.layout(this.cachedWidth); + } + })); + this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { + shouldForwardArgs: true + }, + hoverDelegate, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + })); + this.executeToolbar.getElement().classList.add('chat-execute-toolbar'); + this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext; + this._register(this.executeToolbar.onDidChangeMenuItems(() => { + if (this.cachedWidth && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) { + this.layout(this.cachedWidth); + } + })); + if (this.options.menus.inputSideToolbar) { + const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, this.options.menus.inputSideToolbar, { + telemetrySource: this.options.menus.telemetrySource, + menuOptions: { + shouldForwardArgs: true + }, + hoverDelegate + })); + this.inputSideToolbarContainer = toolbarSide.getElement(); + toolbarSide.getElement().classList.add('chat-side-toolbar'); + toolbarSide.context = { widget } satisfies IChatExecuteActionContext; + } + + let inputModel = this.modelService.getModel(this.inputUri); + if (!inputModel) { + inputModel = this.modelService.createModel('', null, this.inputUri, true); + } + + this.textModelResolverService.createModelReference(this.inputUri).then(ref => { + // make sure to hold a reference so that the model doesn't get disposed by the text model service + if (this._store.isDisposed) { + ref.dispose(); + return; + } + this._register(ref); + }); + + this.inputModel = inputModel; + this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } }); + this._inputEditor.setModel(this.inputModel); + if (initialValue) { + this.inputModel.setValue(initialValue); + const lineNumber = this.inputModel.getLineCount(); + this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) }); + } + + const onDidChangeCursorPosition = () => { + const model = this._inputEditor.getModel(); + if (!model) { + return; + } + + const position = this._inputEditor.getPosition(); + if (!position) { + return; + } + + const atTop = position.lineNumber === 1 && position.column === 1; + this.chatCursorAtTop.set(atTop); + + this.historyNavigationBackwardsEnablement.set(atTop); + this.historyNavigationForewardsEnablement.set(position.equals(getLastPosition(model))); + + // Sync cursor and selection to model + this._syncInputStateToModel(); + }; + this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition())); + onDidChangeCursorPosition(); + + this._register(this.themeService.onDidFileIconThemeChange(() => { + this.renderAttachedContext(); + })); + + this.addFilesToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, { + telemetrySource: this.options.menus.telemetrySource, + label: true, + menuOptions: { shouldForwardArgs: true, renderShortTitle: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + hoverDelegate, + actionViewItemProvider: (action, options) => { + if (action.id === 'workbench.action.chat.attachContext') { + const viewItem = this.instantiationService.createInstance(AddFilesButton, this._attachmentModel, action, options); + viewItem.setShowLabel(this._attachmentModel.size === 0 && !this._implicitContextWidget.value?.hasRenderedContexts); + this.addFilesButton = viewItem; + return this.addFilesButton; + } + return undefined; + } + })); + this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }; + this.renderAttachedContext(); + + const inputResizeObserver = this._register(new dom.DisposableResizeObserver(() => { + const newHeight = this.container.offsetHeight; + this.height.set(newHeight, undefined); + })); + this._register(inputResizeObserver.observe(this.container)); + } + + public toggleChatInputOverlay(editing: boolean): void { + this.chatInputOverlay.classList.toggle('disabled', editing); + if (editing) { + this.overlayClickListener.value = dom.addStandardDisposableListener(this.chatInputOverlay, dom.EventType.CLICK, e => { + e.preventDefault(); + e.stopPropagation(); + this._onDidClickOverlay.fire(); + }); + } else { + this.overlayClickListener.clear(); + } + } + + public renderAttachedContext() { + const container = this.attachedContextContainer; + const store = new DisposableStore(); + this.attachedContextDisposables.value = store; + + dom.clearNode(container); + + store.add(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => { + this.handleAttachmentNavigation(e); + })); + + const attachments = [...this.attachmentModel.attachments.entries()]; + const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.hasValue); + dom.setVisibility(Boolean(this.options.renderInputToolbarBelowInput || hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer); + dom.setVisibility(hasAttachments, this.attachedContextContainer); + if (!attachments.length) { + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + this._indexOfLastOpenedContext = -1; + } + + const isSuggestedEnabled = this.configurationService.getValue('chat.implicitContext.suggestedContext'); + + for (const [index, attachment] of attachments) { + const resource = URI.isUri(attachment.value) ? attachment.value : isLocation(attachment.value) ? attachment.value.uri : undefined; + const range = isLocation(attachment.value) ? attachment.value.range : undefined; + const shouldFocusClearButton = index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1) && this._indexOfLastAttachedContextDeletedWithKeyboard > -1; + + let attachmentWidget; + const options = { shouldFocusClearButton, supportsDeletion: true }; + const lm = this._currentLanguageModel.get(); + if (attachment.kind === 'tool' || attachment.kind === 'toolset') { + attachmentWidget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (resource && isNotebookOutputVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, lm, options, container, this._contextResourceLabels); + } else if (isPromptFileVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isPromptTextVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, options, container, this._contextResourceLabels); + } else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) { + attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, undefined, lm, options, container, this._contextResourceLabels); + } else if (attachment.kind === 'terminalCommand') { + attachmentWidget = this.instantiationService.createInstance(TerminalCommandAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isImageVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, lm, options, container, this._contextResourceLabels); + } else if (isElementVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isPasteVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isSCMHistoryItemVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isSCMHistoryItemChangeVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemChangeAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else if (isSCMHistoryItemChangeRangeVariableEntry(attachment)) { + attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemChangeRangeAttachmentWidget, attachment, lm, options, container, this._contextResourceLabels); + } else { + attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, lm, options, container, this._contextResourceLabels); + } + + if (shouldFocusClearButton) { + attachmentWidget.element.focus(); + } + + if (index === Math.min(this._indexOfLastOpenedContext, this.attachmentModel.size - 1)) { + attachmentWidget.element.focus(); + } + + store.add(attachmentWidget); + store.add(attachmentWidget.onDidDelete(e => { + this.handleAttachmentDeletion(e, index, attachment); + })); + + store.add(attachmentWidget.onDidOpen(e => { + this.handleAttachmentOpen(index, attachment); + })); + } + + if (isSuggestedEnabled && this.implicitContext?.hasValue) { + this._implicitContextWidget.value = this.instantiationService.createInstance(ImplicitContextAttachmentWidget, () => this._widget, (targetUri: URI | undefined, targetRange: IRange | undefined, targetHandle: number | undefined) => this.isAttachmentAlreadyAttached(targetUri, targetRange, targetHandle, attachments.map(([, a]) => a)), this.implicitContext, this._contextResourceLabels, this._attachmentModel, container); + } else { + this._implicitContextWidget.clear(); + } + + this.addFilesButton?.setShowLabel(this._attachmentModel.size === 0 && !this._implicitContextWidget.value?.hasRenderedContexts); + + this._indexOfLastOpenedContext = -1; + } + + private isAttachmentAlreadyAttached(targetUri: URI | undefined, targetRange: IRange | undefined, targetHandle: number | undefined, attachments: IChatRequestVariableEntry[]): boolean { + return attachments.some((attachment) => { + let uri: URI | undefined; + let range: IRange | undefined; + let handle: number | undefined; + + if (URI.isUri(attachment.value)) { + uri = attachment.value; + } else if (isLocation(attachment.value)) { + uri = attachment.value.uri; + range = attachment.value.range; + } else if (isStringVariableEntry(attachment)) { + uri = attachment.uri; + handle = attachment.handle; + } + + if ((handle !== undefined && targetHandle === undefined) || (handle === undefined && targetHandle !== undefined)) { + return false; + } + + if (handle !== undefined && targetHandle !== undefined && handle !== targetHandle) { + return false; + } + + if (!uri || !isEqual(uri, targetUri)) { + return false; + } + + // check if the exact range is already attached + if (targetRange) { + return range && Range.equalsRange(range, targetRange); + } + + return true; + }); + } + + 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)) { + this._indexOfLastAttachedContextDeletedWithKeyboard = index; + } + + this._attachmentModel.delete(attachment.id); + + + if (this.configurationService.getValue('chat.implicitContext.enableImplicitContext')) { + // if currently opened file is deleted, do not show implicit context + for (const implicitContext of (this._implicitContext?.values || [])) { + const implicitValue = URI.isUri(implicitContext?.value) && URI.isUri(attachment.value) && isEqual(implicitContext.value, attachment.value); + + if (implicitContext?.isFile && implicitValue) { + implicitContext.enabled = false; + } + } + } + + if (this._attachmentModel.size === 0) { + this.focus(); + } + + this._onDidChangeContext.fire({ removed: [attachment] }); + this.renderAttachedContext(); + } + + private handleAttachmentOpen(index: number, attachment: IChatRequestVariableEntry): void { + this._indexOfLastOpenedContext = index; + this._indexOfLastAttachedContextDeletedWithKeyboard = -1; + + if (this._attachmentModel.size === 0) { + this.focus(); + } + } + + private handleAttachmentNavigation(e: StandardKeyboardEvent): void { + if (!e.equals(KeyCode.LeftArrow) && !e.equals(KeyCode.RightArrow)) { + return; + } + + // eslint-disable-next-line no-restricted-syntax + const toolbar = this.addFilesToolbar?.getElement().querySelector('.action-label'); + if (!toolbar) { + return; + } + + // eslint-disable-next-line no-restricted-syntax + const attachments = Array.from(this.attachedContextContainer.querySelectorAll('.chat-attached-context-attachment')); + if (!attachments.length) { + return; + } + + attachments.unshift(toolbar); + + const activeElement = dom.getWindow(this.attachmentsContainer).document.activeElement; + const currentIndex = attachments.findIndex(attachment => attachment === activeElement); + let newIndex = currentIndex; + + if (e.equals(KeyCode.LeftArrow)) { + newIndex = currentIndex > 0 ? currentIndex - 1 : attachments.length - 1; + } else if (e.equals(KeyCode.RightArrow)) { + newIndex = currentIndex < attachments.length - 1 ? currentIndex + 1 : 0; + } + + if (newIndex !== -1) { + const nextElement = attachments[newIndex] as HTMLElement; + nextElement.focus(); + e.preventDefault(); + e.stopPropagation(); + } + } + + async renderChatTodoListWidget(chatSessionResource: URI) { + + const isTodoWidgetEnabled = this.configurationService.getValue(ChatConfiguration.TodosShowWidget) !== false; + if (!isTodoWidgetEnabled) { + return; + } + + if (!this._chatInputTodoListWidget.value) { + const widget = this._chatEditingTodosDisposables.add(this.instantiationService.createInstance(ChatTodoListWidget)); + this._chatInputTodoListWidget.value = widget; + + // Add the widget's DOM node to the dedicated todo list container + dom.clearNode(this.chatInputTodoListWidgetContainer); + dom.append(this.chatInputTodoListWidgetContainer, widget.domNode); + } + + this._chatInputTodoListWidget.value.render(chatSessionResource); + } + + clearTodoListWidget(sessionResource: URI | undefined, force: boolean): void { + this._chatInputTodoListWidget.value?.clear(sessionResource, force); + } + + setWorkingSetCollapsed(collapsed: boolean): void { + this._workingSetCollapsed.set(collapsed, undefined); + } + + renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) { + dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); + + if (chatEditingSession) { + if (!isEqual(chatEditingSession.chatSessionResource, this._lastEditingSessionResource)) { + this._workingSetCollapsed.set(true, undefined); + } + this._lastEditingSessionResource = chatEditingSession.chatSessionResource; + } + + const modifiedEntries = derivedOpts({ equalsFn: arraysEqual }, r => { + // Background chat sessions render the working set based on the session files, and not the editing session + const sessionResource = chatEditingSession?.chatSessionResource ?? this._widget?.viewModel?.model.sessionResource; + if (sessionResource && getChatSessionType(sessionResource) === AgentSessionProviders.Background) { + return []; + } + + return chatEditingSession?.entries.read(r).filter(entry => entry.state.read(r) === ModifiedFileEntryState.Modified) || []; + }); + + const editSessionEntries = derived((reader): IChatCollapsibleListItem[] => { + const seenEntries = new ResourceSet(); + const entries: IChatCollapsibleListItem[] = []; + for (const entry of modifiedEntries.read(reader)) { + if (entry.state.read(reader) !== ModifiedFileEntryState.Modified) { + continue; + } + + if (!seenEntries.has(entry.modifiedURI)) { + seenEntries.add(entry.modifiedURI); + const linesAdded = entry.linesAdded?.read(reader); + const linesRemoved = entry.linesRemoved?.read(reader); + entries.push({ + reference: entry.modifiedURI, + state: ModifiedFileEntryState.Modified, + kind: 'reference', + options: { + status: undefined, + diffMeta: { added: linesAdded ?? 0, removed: linesRemoved ?? 0 }, + isDeletion: !!entry.isDeletion, + originalUri: entry.isDeletion ? entry.originalURI : undefined, + } + }); + } + } + + entries.sort((a, b) => { + if (a.kind === 'reference' && b.kind === 'reference') { + if (a.state === b.state || a.state === undefined || b.state === undefined) { + return a.reference.toString().localeCompare(b.reference.toString()); + } + return a.state - b.state; + } + return 0; + }); + + return entries; + }); + + const sessionFileChanges = observableFromEvent( + this, + this.agentSessionsService.model.onDidChangeSessions, + () => { + const sessionResource = this._widget?.viewModel?.model?.sessionResource; + if (!sessionResource) { + return Iterable.empty(); + } + const model = this.agentSessionsService.getSession(sessionResource); + return model?.changes instanceof Array ? model.changes : Iterable.empty(); + }, + ); + + const sessionFiles = derived(reader => + sessionFileChanges.read(reader).map((entry): IChatCollapsibleListItem => ({ + reference: isIChatSessionFileChange2(entry) + ? entry.modifiedUri ?? entry.uri + : entry.modifiedUri, + state: ModifiedFileEntryState.Accepted, + kind: 'reference', + options: { + diffMeta: { added: entry.insertions, removed: entry.deletions }, + isDeletion: entry.modifiedUri === undefined, + originalUri: entry.originalUri, + status: undefined + } + })) + ); + + const shouldRender = derived(reader => + editSessionEntries.read(reader).length > 0 || sessionFiles.read(reader).length > 0); + + this._renderingChatEdits.value = autorun(reader => { + if (this.options.renderWorkingSet && shouldRender.read(reader)) { + this.renderChatEditingSessionWithEntries( + reader.store, + chatEditingSession, + editSessionEntries, + sessionFiles + ); + } else { + dom.clearNode(this.chatEditingSessionWidgetContainer); + this._chatEditsDisposables.clear(); + this._chatEditList = undefined; + } + }); + } + private renderChatEditingSessionWithEntries( + store: DisposableStore, + chatEditingSession: IChatEditingSession | null, + editSessionEntriesObs: IObservable, + sessionEntriesObs: IObservable + ) { + // Summary of number of files changed + // eslint-disable-next-line no-restricted-syntax + const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); + + // eslint-disable-next-line no-restricted-syntax + const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview')); + // eslint-disable-next-line no-restricted-syntax + const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title')); + + // Clear out the previous actions (if any) + this._chatEditsActionsDisposables.clear(); + + // Chat editing session actions + // eslint-disable-next-line no-restricted-syntax + const actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement ?? dom.append(overviewRegion, $('.chat-editing-session-actions')); + + const sessionResource = chatEditingSession?.chatSessionResource || this._widget?.viewModel?.model.sessionResource; + + const scopedContextKeyService = this._chatEditsActionsDisposables.add(this.contextKeyService.createScoped(actionsContainer)); + if (sessionResource) { + scopedContextKeyService.createKey(ChatContextKeys.agentSessionType.key, getChatSessionType(sessionResource)); + } + + this._chatEditsActionsDisposables.add(bindContextKey(ChatContextKeys.hasAgentSessionChanges, scopedContextKeyService, r => !!sessionEntriesObs.read(r)?.length)); + + const scopedInstantiationService = this._chatEditsActionsDisposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, scopedContextKeyService]))); + + // Working set + // eslint-disable-next-line no-restricted-syntax + const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); + + const button = this._chatEditsActionsDisposables.add(new ButtonWithIcon(overviewTitle, { + supportIcons: true, + secondary: true, + ariaLabel: localize('chatEditingSession.toggleWorkingSet', 'Toggle changed files.'), + })); + + const topLevelStats = derived(reader => { + const entries = editSessionEntriesObs.read(reader); + const sessionEntries = sessionEntriesObs.read(reader); + + let added = 0, removed = 0; + + if (entries.length > 0) { + for (const entry of entries) { + if (entry.kind === 'reference' && entry.options?.diffMeta) { + added += entry.options.diffMeta.added; + removed += entry.options.diffMeta.removed; + } + } + } else { + for (const entry of sessionEntries) { + if (entry.kind === 'reference' && entry.options?.diffMeta) { + added += entry.options.diffMeta.added; + removed += entry.options.diffMeta.removed; + } + } + } + + const files = entries.length > 0 ? entries.length : sessionEntries.length; + const topLevelIsSessionMenu = entries.length === 0 && sessionEntries.length > 0; + const shouldShowEditingSession = entries.length > 0 || sessionEntries.length > 0; + + return { files, added, removed, shouldShowEditingSession, topLevelIsSessionMenu }; + }); + + const topLevelIsSessionMenu = topLevelStats.map(t => t.topLevelIsSessionMenu); + + store.add(autorun(reader => { + const isSessionMenu = topLevelIsSessionMenu.read(reader); + reader.store.add(scopedInstantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, isSessionMenu ? MenuId.ChatEditingSessionChangesToolbar : MenuId.ChatEditingWidgetToolbar, { + telemetrySource: this.options.menus.telemetrySource, + small: true, + menuOptions: { + arg: sessionResource && (isSessionMenu ? sessionResource : { + $mid: MarshalledId.ChatViewContext, + sessionResource, + } satisfies IChatViewTitleActionContext), + }, + disableWhileRunning: isSessionMenu, + buttonConfigProvider: (action) => { + if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id || action.id === ViewAllSessionChangesAction.ID) { + return { showIcon: true, showLabel: false, isSecondary: true }; + } + return undefined; + } + })); + })); + + store.add(autorun(reader => { + const { files, added, removed, shouldShowEditingSession } = topLevelStats.read(reader); + + const buttonLabel = files === 1 + ? localize('chatEditingSession.oneFile', '1 file changed') + : localize('chatEditingSession.manyFiles', '{0} files changed', files); + + button.label = buttonLabel; + button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', buttonLabel, added, removed)); + + this._workingSetLinesAddedSpan.value.textContent = `+${added}`; + this._workingSetLinesRemovedSpan.value.textContent = `-${removed}`; + + dom.setVisibility(shouldShowEditingSession, this.chatEditingSessionWidgetContainer); + })); + + const countsContainer = dom.$('.working-set-line-counts'); + button.element.appendChild(countsContainer); + countsContainer.appendChild(this._workingSetLinesAddedSpan.value); + countsContainer.appendChild(this._workingSetLinesRemovedSpan.value); + + const toggleWorkingSet = () => { + this._workingSetCollapsed.set(!this._workingSetCollapsed.get(), undefined); + }; + + this._chatEditsActionsDisposables.add(button.onDidClick(toggleWorkingSet)); + this._chatEditsActionsDisposables.add(addDisposableListener(overviewRegion, 'click', e => { + if (e.defaultPrevented) { + return; + } + const target = e.target as HTMLElement; + if (target.closest('.monaco-button')) { + return; + } + toggleWorkingSet(); + })); + + this._chatEditsActionsDisposables.add(autorun(reader => { + const collapsed = this._workingSetCollapsed.read(reader); + button.icon = collapsed ? Codicon.chevronRight : Codicon.chevronDown; + workingSetContainer.classList.toggle('collapsed', collapsed); + })); + + if (!this._chatEditList) { + this._chatEditList = this._chatEditsListPool.get(); + const list = this._chatEditList.object; + this._chatEditsDisposables.add(this._chatEditList); + this._chatEditsDisposables.add(list.onDidFocus(() => { + this._onDidFocus.fire(); + })); + this._chatEditsDisposables.add(list.onDidOpen(async (e) => { + if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) { + const modifiedFileUri = e.element.reference; + const originalUri = e.element.options?.originalUri; + + if (e.element.options?.isDeletion && originalUri) { + await this.editorService.openEditor({ + resource: originalUri, // instead of modified, because modified will not exist + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + return; + } + + // If there's a originalUri, open as diff editor + if (originalUri) { + await this.editorService.openEditor({ + original: { resource: originalUri }, + modified: { resource: modifiedFileUri }, + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + return; + } + + const entry = chatEditingSession?.getEntry(modifiedFileUri); + + const pane = await this.editorService.openEditor({ + resource: modifiedFileUri, + options: e.editorOptions + }, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + + if (pane) { + entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus); + } + } + })); + this._chatEditsDisposables.add(addDisposableListener(list.getHTMLElement(), 'click', e => { + if (!this.hasFocus()) { + this._onDidFocus.fire(); + } + }, true)); + dom.append(workingSetContainer, list.getHTMLElement()); + dom.append(innerContainer, workingSetContainer); + } + + store.add(autorun(reader => { + const editEntries = editSessionEntriesObs.read(reader); + const sessionFileEntries = sessionEntriesObs.read(reader); + + // Combine edit session entries with session file changes. At the moment, we + // we can combine these two arrays since local chat sessions use edit session + // entries, while background chat sessions use session file changes. + const allEntries = editEntries.concat(sessionFileEntries); + + const maxItemsShown = 6; + const itemsShown = Math.min(allEntries.length, maxItemsShown); + const height = itemsShown * 22; + const list = this._chatEditList!.object; + list.layout(height); + list.getHTMLElement().style.height = `${height}px`; + list.splice(0, list.length, allEntries); + })); + } + + async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { + if (!this.options.renderFollowups) { + return; + } + this.followupsDisposables.clear(); + dom.clearNode(this.followupsContainer); + + if (items && items.length > 0) { + this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); + } + } + + /** + * Layout the input part with the given width. Height is intrinsic - determined by content + * and detected via ResizeObserver, which updates `inputPartHeight` for the parent to observe. + */ + layout(width: number) { + this.cachedWidth = width; + + return this._layout(width); + } + + private previousInputEditorDimension: IDimension | undefined; + private _layout(width: number, allowRecurse = true): void { + const data = this.getLayoutData(); + + const followupsWidth = width - data.inputPartHorizontalPadding; + this.followupsContainer.style.width = `${followupsWidth}px`; + + const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); + const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth; + const inputEditorHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight); + const newDimension = { width: newEditorWidth, height: inputEditorHeight }; + if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { + // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler + // to be invoked, and we have a lot of these on this editor. Only doing a layout this when the editor size has actually changed makes it much easier to follow. + this._inputEditor.layout(newDimension); + this.previousInputEditorDimension = newDimension; + } + + if (allowRecurse && initialEditorScrollWidth < 10) { + // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight + return this._layout(width, false); + } + } + + private getLayoutData() { + + // ########################################################################### + // # # + // # CHANGING THIS METHOD HAS RENDERING IMPLICATIONS FOR THE CHAT VIEW # + // # IF YOU MAKE CHANGES HERE, PLEASE TEST THE CHAT VIEW THOROUGHLY: # + // # - produce various chat responses # + // # - click the response to get a focus outline # + // # - ensure the outline is not cut off at the bottom # + // # # + // ########################################################################### + + const inputSideToolbarWidth = this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) : 0; + + const getToolbarsWidthCompact = () => { + const executeToolbarWidth = this.cachedExecuteToolbarWidth = this.executeToolbar.getItemsWidth(); + const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth(); + const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * 4; + const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * 4 : 0; + return executeToolbarWidth + executeToolbarPadding + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding); + }; + + return { + editorBorder: 2, + inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, + inputPartHorizontalPaddingInside: 12, + toolbarsWidth: this.options.renderStyle === 'compact' ? getToolbarsWidthCompact() : 0, + sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, + }; + } + + /** + * Gets the location of the chat widget and whether that location is maximized. + */ + private getWidgetLocationInfo(widget: IChatWidget): IChatWidgetLocationInfo { + // Editor context (quick chat, inline chat, etc.) + if (isIChatResourceViewContext(widget.viewContext)) { + return { location: ChatWidgetLocation.Editor, isMaximized: false }; + } + + // View context - determine actual location from view descriptor service + if (isIChatViewViewContext(widget.viewContext)) { + const viewLocation = this.viewDescriptorService.getViewLocationById(widget.viewContext.viewId); + const sideBarPosition = this.layoutService.getSideBarPosition(); + + switch (viewLocation) { + case ViewContainerLocation.Panel: + return { + location: ChatWidgetLocation.Panel, + isMaximized: this.layoutService.isPanelMaximized(), + }; + case ViewContainerLocation.AuxiliaryBar: + // AuxiliaryBar is on the opposite side of the primary sidebar + return { + location: sideBarPosition === Position.LEFT ? ChatWidgetLocation.SidebarRight : ChatWidgetLocation.SidebarLeft, + isMaximized: this.layoutService.isAuxiliaryBarMaximized(), + }; + case ViewContainerLocation.Sidebar: + default: + // Primary sidebar follows its configured position + // Note: Primary sidebar cannot be maximized, so always false + return { + location: sideBarPosition === Position.LEFT ? ChatWidgetLocation.SidebarLeft : ChatWidgetLocation.SidebarRight, + isMaximized: false, + }; + } + } + + // Fallback for unknown contexts + return { location: ChatWidgetLocation.Editor, isMaximized: false }; + } +} + + +function getLastPosition(model: ITextModel): IPosition { + return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; +} + +const chatInputEditorContainerSelector = '.interactive-input-editor'; +setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector); + +type ChatSessionPickerWidget = ChatSessionPickerActionItem | SearchableOptionPickerActionItem; + +class ChatSessionPickersContainerActionItem extends ActionViewItem { + constructor( + action: IAction, + private readonly widgets: ChatSessionPickerWidget[], + options?: IActionViewItemOptions + ) { + super(null, action, options ?? {}); + } + + override render(container: HTMLElement): void { + container.classList.add('chat-sessionPicker-container'); + for (const widget of this.widgets) { + const itemContainer = dom.$('.action-item.chat-sessionPicker-item'); + widget.render(itemContainer); + container.appendChild(itemContainer); + } + } + + override dispose(): void { + for (const widget of this.widgets) { + widget.dispose(); + } + super.dispose(); + } +} + +class AddFilesButton extends ActionViewItem { + private showLabel: boolean | undefined; + + constructor(context: unknown, action: IAction, options: IActionViewItemOptions) { + super(context, action, { + ...options, + icon: false, + label: true, + keybindingNotRenderedWithLabel: true, + }); + } + + 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); + 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/widget/input/chatInputPartWidgets.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPartWidgets.ts new file mode 100644 index 00000000000..59fe244e3af --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPartWidgets.ts @@ -0,0 +1,133 @@ +/*--------------------------------------------------------------------------------------------- + * 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 } from '../../../../../../base/common/lifecycle.js'; +import { ContextKeyExpression, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { BrandedService, IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; + +/** + * A widget that can be rendered on top of the chat input part. + */ +export interface IChatInputPartWidget extends IDisposable { + /** + * The DOM node of the widget. + */ + readonly domNode: HTMLElement; + + /** + * The current height of the widget in pixels. + */ + readonly height: number; +} + +export interface IChatInputPartWidgetDescriptor { + readonly id: string; + readonly when?: ContextKeyExpression; + readonly ctor: new (...services: Services) => IChatInputPartWidget; +} + +/** + * Registry for chat input part widgets. + * Widgets register themselves and are instantiated by the controller based on context key conditions. + */ +export const ChatInputPartWidgetsRegistry = new class { + readonly widgets: IChatInputPartWidgetDescriptor[] = []; + + register(id: string, ctor: new (...services: Services) => IChatInputPartWidget, when?: ContextKeyExpression): void { + this.widgets.push({ id, ctor: ctor as IChatInputPartWidgetDescriptor['ctor'], when }); + } + + getWidgets(): readonly IChatInputPartWidgetDescriptor[] { + return this.widgets; + } +}(); + +interface IRenderedWidget { + readonly descriptor: IChatInputPartWidgetDescriptor; + readonly widget: IChatInputPartWidget; + readonly disposables: DisposableStore; +} + +/** + * Controller that manages the rendering of widgets in the chat input part. + * Widgets are shown/hidden based on context key conditions. + */ +export class ChatInputPartWidgetController extends Disposable { + + private readonly renderedWidgets = new Map(); + + constructor( + private readonly container: HTMLElement, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.update(); + + this._register(this.contextKeyService.onDidChangeContext(e => { + const relevantKeys = new Set(); + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (descriptor.when) { + for (const key of descriptor.when.keys()) { + relevantKeys.add(key); + } + } + } + if (e.affectsSome(relevantKeys)) { + this.update(); + } + })); + } + + private update(): void { + const visibleIds = new Set(); + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (this.contextKeyService.contextMatchesRules(descriptor.when)) { + visibleIds.add(descriptor.id); + } + } + + for (const [id, rendered] of this.renderedWidgets) { + if (!visibleIds.has(id)) { + rendered.widget.domNode.remove(); + rendered.disposables.dispose(); + this.renderedWidgets.delete(id); + } + } + + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (!visibleIds.has(descriptor.id)) { + continue; + } + + if (!this.renderedWidgets.has(descriptor.id)) { + const disposables = new DisposableStore(); + const widget = this.instantiationService.createInstance(descriptor.ctor); + disposables.add(widget); + + this.renderedWidgets.set(descriptor.id, { descriptor, widget, disposables }); + this.container.appendChild(widget.domNode); + } + } + } + + get height(): number { + let total = 0; + for (const rendered of this.renderedWidgets.values()) { + total += rendered.widget.height; + } + return total; + } + + override dispose(): void { + for (const rendered of this.renderedWidgets.values()) { + rendered.widget.domNode.remove(); + rendered.disposables.dispose(); + } + this.renderedWidgets.clear(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts new file mode 100644 index 00000000000..1377aa52607 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../../../base/browser/dom.js'; +import { IHoverPositionOptions } from '../../../../../../base/browser/ui/hover/hover.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { autorun, IObservable } from '../../../../../../base/common/observable.js'; +import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { IChatExecuteActionContext } from '../../actions/chatExecuteActions.js'; + +export interface IChatInputPickerOptions { + /** + * Provides a fallback anchor element when the picker's own element + * is not available in the DOM (e.g., when inside an overflow menu). + */ + readonly getOverflowAnchor?: () => HTMLElement | undefined; + + readonly actionContext?: IChatExecuteActionContext; + + readonly onlyShowIconsForDefaultActions: IObservable; + + readonly hoverPosition?: IHoverPositionOptions; +} + +/** + * Base class for chat input picker action items (model picker, mode picker, session target picker). + * Provides common anchor resolution logic for dropdown positioning. + */ +export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdownActionViewItem { + + constructor( + action: IAction, + actionWidgetOptions: Omit, + protected readonly pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + // Inject the anchor getter into the options + const optionsWithAnchor: Omit = { + ...actionWidgetOptions, + getAnchor: () => this.getAnchorElement(), + }; + + super(action, optionsWithAnchor, actionWidgetService, keybindingService, contextKeyService, telemetryService); + + this._register(autorun(reader => { + this.pickerOptions.onlyShowIconsForDefaultActions.read(reader); + if (this.element) { + this.renderLabel(this.element); + } + })); + } + + /** + * Returns the anchor element for the dropdown. + * Falls back to the overflow anchor if this element is not in the DOM. + */ + protected getAnchorElement(): HTMLElement { + if (this.element && getActiveWindow().document.contains(this.element)) { + return this.element; + } + return this.pickerOptions.getOverflowAnchor?.() ?? this.element!; + } + + override render(container: HTMLElement): void { + super.render(container); + container.classList.add('chat-input-picker-item'); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatSelectedTools.ts similarity index 76% rename from src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts rename to src/vs/workbench/contrib/chat/browser/widget/input/chatSelectedTools.ts index 92f0d0787ca..0c15407e636 100644 --- a/src/vs/workbench/contrib/chat/browser/chatSelectedTools.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatSelectedTools.ts @@ -3,22 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { derived, IObservable, observableFromEvent, ObservableMap } from '../../../../base/common/observable.js'; -import { isObject } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ObservableMemento, observableMemento } from '../../../../platform/observable/common/observableMemento.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { UserSelectedTools } from '../common/chatAgents.js'; -import { IChatMode } from '../common/chatModes.js'; -import { ChatModeKind } from '../common/constants.js'; -import { ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, ToolSet } from '../common/languageModelToolsService.js'; -import { PromptsStorage } from '../common/promptSyntax/service/promptsService.js'; -import { PromptFileRewriter } from './promptSyntax/promptFileRewriter.js'; - - +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { derived, IObservable, ObservableMap } from '../../../../../../base/common/observable.js'; +import { isObject } from '../../../../../../base/common/types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ObservableMemento, observableMemento } from '../../../../../../platform/observable/common/observableMemento.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { IChatMode } from '../../../common/chatModes.js'; +import { ChatModeKind } from '../../../common/constants.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../../common/languageModels.js'; +import { UserSelectedTools } from '../../../common/participants/chatAgents.js'; +import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolSet, isToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { PromptFileRewriter } from '../../promptSyntax/promptFileRewriter.js'; + + +// todo@connor4312/bhavyaus: make tools key off displayName so model-specific tool +// enablement can stick between models with different underlying tool definitions type ToolEnablementStates = { readonly toolSets: ReadonlyMap; readonly tools: ReadonlyMap; @@ -40,7 +43,7 @@ namespace ToolEnablementStates { export function fromMap(map: IToolAndToolSetEnablementMap): ToolEnablementStates { const toolSets: Map = new Map(), tools: Map = new Map(); for (const [entry, enabled] of map.entries()) { - if (entry instanceof ToolSet) { + if (isToolSet(entry)) { toolSets.set(entry.id, enabled); } else { tools.set(entry.id, enabled); @@ -98,11 +101,11 @@ export class ChatSelectedTools extends Disposable { private readonly _globalState: ObservableMemento; private readonly _sessionStates = new ObservableMap(); - - private readonly _allTools: IObservable[]>; + private readonly _currentTools: IObservable; constructor( private readonly _mode: IObservable, + private readonly languageModel: IObservable, @ILanguageModelToolsService private readonly _toolsService: ILanguageModelToolsService, @IStorageService _storageService: IStorageService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -117,14 +120,17 @@ export class ChatSelectedTools extends Disposable { }); this._globalState = this._store.add(globalStateMemento(StorageScope.PROFILE, StorageTarget.MACHINE, _storageService)); - this._allTools = observableFromEvent(_toolsService.onDidChangeTools, () => Array.from(_toolsService.getTools())); + this._currentTools = languageModel.map(lm => + _toolsService.observeTools(lm?.metadata)).map((o, r) => o.read(r)); } /** * All tools and tool sets with their enabled state. + * Tools are filtered based on the current model context. */ public readonly entriesMap: IObservable = derived(r => { - const map = new Map(); + const map = new Map(); + const lm = this.languageModel.read(r)?.metadata; // look up the tools in the hierarchy: session > mode > global const currentMode = this._mode.read(r); @@ -133,18 +139,19 @@ export class ChatSelectedTools extends Disposable { const modeTools = currentMode.customTools?.read(r); if (modeTools) { const target = currentMode.target?.read(r); - currentMap = ToolEnablementStates.fromMap(this._toolsService.toToolAndToolSetEnablementMap(modeTools, target)); + currentMap = ToolEnablementStates.fromMap(this._toolsService.toToolAndToolSetEnablementMap(modeTools, target, lm)); } } if (!currentMap) { currentMap = this._globalState.read(r); } - for (const tool of this._allTools.read(r)) { + // Use getTools with contextKeyService to filter tools by current model + for (const tool of this._currentTools.read(r)) { if (tool.canBeReferencedInPrompt) { map.set(tool, currentMap.tools.get(tool.id) !== false); // if unknown, it's enabled } } - for (const toolSet of this._toolsService.toolSets.read(r)) { + for (const toolSet of this._toolsService.getToolSetsForModel(lm, r)) { const toolSetEnabled = currentMap.toolSets.get(toolSet.id) !== false; // if unknown, it's enabled map.set(toolSet, toolSetEnabled); for (const tool of toolSet.getTools(r)) { @@ -159,7 +166,7 @@ export class ChatSelectedTools extends Disposable { const result: UserSelectedTools = {}; const map = this.entriesMap.read(r); for (const [item, enabled] of map) { - if (!(item instanceof ToolSet)) { + if (!isToolSet(item)) { result[item.id] = enabled; } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts new file mode 100644 index 00000000000..03954d815de --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatStatusWidget.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../../base/common/actions.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; +import { ChatEntitlement, ChatEntitlementContextKeys, IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { CHAT_SETUP_ACTION_ID } from '../../actions/chatActions.js'; +import { ChatInputPartWidgetsRegistry, IChatInputPartWidget } from './chatInputPartWidgets.js'; +import './media/chatStatusWidget.css'; + +const $ = dom.$; + +/** + * Widget that displays a status message with an optional action button. + * Only shown for free tier users when the setting is enabled (experiment controlled via onExP tag). + */ +export class ChatStatusWidget extends Disposable implements IChatInputPartWidget { + + static readonly ID = 'chatStatusWidget'; + + readonly domNode: HTMLElement; + + private messageElement: HTMLElement | undefined; + private actionButton: Button | undefined; + + constructor( + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + ) { + super(); + + this.domNode = $('.chat-status-widget'); + this.domNode.style.display = 'none'; + this.initializeIfEnabled(); + } + + private initializeIfEnabled(): void { + const entitlement = this.chatEntitlementService.entitlement; + const isAnonymous = this.chatEntitlementService.anonymous; + + // Free tier is always enabled, anonymous is controlled by experiment via chat.statusWidget.sku + const enabledSku = this.configurationService.getValue('chat.statusWidget.sku'); + + if (isAnonymous && enabledSku === 'anonymous') { + this.createWidgetContent('anonymous'); + } else if (entitlement === ChatEntitlement.Free) { + this.createWidgetContent('free'); + } else { + return; + } + + this.domNode.style.display = ''; + } + + get height(): number { + return this.domNode.style.display === 'none' ? 0 : this.domNode.offsetHeight; + } + + private createWidgetContent(enabledSku: 'free' | 'anonymous'): void { + const contentContainer = $('.chat-status-content'); + this.messageElement = $('.chat-status-message'); + contentContainer.appendChild(this.messageElement); + + const actionContainer = $('.chat-status-action'); + this.actionButton = this._register(new Button(actionContainer, { + ...defaultButtonStyles, + supportIcons: true + })); + this.actionButton.element.classList.add('chat-status-button'); + + if (enabledSku === 'anonymous') { + const message = localize('chat.anonymousRateLimited.message', "You've reached the limit for chat messages. Try Copilot Pro for free."); + const buttonLabel = localize('chat.anonymousRateLimited.signIn', "Sign In"); + this.messageElement.textContent = message; + this.actionButton.label = buttonLabel; + this.actionButton.element.ariaLabel = localize('chat.anonymousRateLimited.signIn.ariaLabel', "{0} {1}", message, buttonLabel); + } else { + const message = localize('chat.freeQuotaExceeded.message', "You've reached the limit for chat messages."); + const buttonLabel = localize('chat.freeQuotaExceeded.upgrade', "Upgrade"); + this.messageElement.textContent = message; + this.actionButton.label = buttonLabel; + this.actionButton.element.ariaLabel = localize('chat.freeQuotaExceeded.upgrade.ariaLabel', "{0} {1}", message, buttonLabel); + } + + this._register(this.actionButton.onDidClick(async () => { + const commandId = this.chatEntitlementService.anonymous + ? CHAT_SETUP_ACTION_ID + : 'workbench.action.chat.upgradePlan'; + this.telemetryService.publicLog2('workbenchActionExecuted', { + id: commandId, + from: 'chatStatusWidget' + }); + await this.commandService.executeCommand(commandId); + })); + + this.domNode.appendChild(contentContainer); + this.domNode.appendChild(actionContainer); + } +} + +ChatInputPartWidgetsRegistry.register( + ChatStatusWidget.ID, + ChatStatusWidget, + ContextKeyExpr.and( + ChatContextKeys.chatQuotaExceeded, + ChatContextKeys.chatSessionIsEmpty, + ContextKeyExpr.or( + ChatContextKeys.Entitlement.planFree, + ChatEntitlementContextKeys.chatAnonymous + ) + ) +); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts new file mode 100644 index 00000000000..a51b59a4548 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/delegationSessionPickerActionItem.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from '../../../../../../base/common/actions.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetDropdownAction } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; +import { AgentSessionProviders, getAgentCanContinueIn, getAgentSessionProvider, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { ISessionTypeItem, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js'; + +/** + * Action view item for delegating to a remote session (Background or Cloud). + * This picker allows switching to remote execution providers when the session is not empty. + */ +export class DelegationSessionPickerActionItem extends SessionTypePickerActionItem { + protected override _run(sessionTypeItem: ISessionTypeItem): void { + if (this.delegate.setPendingDelegationTarget) { + this.delegate.setPendingDelegationTarget(sessionTypeItem.type); + } + if (this.element) { + this.renderLabel(this.element); + } + } + + protected override _getSelectedSessionType(): AgentSessionProviders | undefined { + const delegationTarget = this.delegate.getPendingDelegationTarget ? this.delegate.getPendingDelegationTarget() : undefined; + if (delegationTarget) { + return delegationTarget; + } + return this.delegate.getActiveSessionProvider(); + } + + protected override _isSessionTypeEnabled(type: AgentSessionProviders): boolean { + const allContributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === type); + + if (this.delegate.getActiveSessionProvider() !== AgentSessionProviders.Local) { + return false; // Can only delegate when active session is local + } + + if (contribution && !contribution.canDelegate && this.delegate.getActiveSessionProvider() !== type /* Allow switching back to active type */) { + return false; + } + + return this._getSelectedSessionType() !== type; // Always allow switching back to active session + } + + protected override _isVisible(type: AgentSessionProviders): boolean { + if (this.delegate.getActiveSessionProvider() === type) { + return true; // Always show active session type + } + + return getAgentCanContinueIn(type); + } + + protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) { + if (isFirstPartyAgentSessionProvider(sessionTypeItem.type)) { + return { label: localize('continueIn', "Continue In"), order: 1, showHeader: true }; + } + return { label: localize('continueInThirdParty', "Continue In (Third Party)"), order: 2, showHeader: false }; + } + + protected override _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { + const allContributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = allContributions.find(contribution => getAgentSessionProvider(contribution.type) === sessionTypeItem.type); + + return contribution?.name ? `@${contribution.name}` : undefined; + } + + protected override _getLearnMore(): IAction { + const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in'; + return { + id: 'workbench.action.chat.agentOverview.learnMoreHandOff', + label: localize('chat.learnMoreAgentHandOff', "Learn about agent handoff..."), + tooltip: learnMoreUrl, + class: undefined, + enabled: true, + run: async () => { + await this.openerService.open(URI.parse(learnMoreUrl)); + } + }; + } + + protected override _getAdditionalActions(): IActionWidgetDropdownAction[] { + return [{ + id: 'newChatSession', + class: undefined, + label: localize('chat.newChatSession', "New Chat Session"), + tooltip: '', + hover: { content: '', position: this.pickerOptions.hoverPosition }, + checked: false, + icon: Codicon.plus, + enabled: true, + category: { label: localize('chat.newChatSession.category', "New Chat Session"), order: 0, showHeader: false }, + description: this.keybindingService.lookupKeybinding(ACTION_ID_NEW_CHAT)?.getLabel() || undefined, + run: async () => { + this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.chatSessionPosition); + }, + }]; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatEditorInputContentProvider.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatEditorInputContentProvider.ts new file mode 100644 index 00000000000..391cc889e34 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatEditorInputContentProvider.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { ITextModelContentProvider, ITextModelService } from '../../../../../../../editor/common/services/resolverService.js'; + + +export class ChatInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatInput, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('chatinput'), resource); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts similarity index 86% rename from src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts rename to src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts index 3c76c5cbb7f..062f4d55cb6 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputCompletions.ts @@ -3,62 +3,62 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce } from '../../../../../base/common/arrays.js'; -import { raceTimeout } from '../../../../../base/common/async.js'; -import { decodeBase64 } from '../../../../../base/common/buffer.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { isPatternInWord } from '../../../../../base/common/filters.js'; -import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { basename } from '../../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; -import { ICodeEditor, getCodeEditor, isCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; -import { Position } from '../../../../../editor/common/core/position.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, ProviderResult, SymbolKind, SymbolKinds } from '../../../../../editor/common/languages.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; -import { localize } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { FileKind, IFileService } from '../../../../../platform/files/common/files.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { INotificationService } from '../../../../../platform/notification/common/notification.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; -import { EditorsOrder } from '../../../../common/editor.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { IHistoryService } from '../../../../services/history/common/history.js'; -import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; -import { ISearchService } from '../../../../services/search/common/search.js'; -import { McpPromptArgumentPick } from '../../../mcp/browser/mcpPromptArgumentPick.js'; -import { IMcpPrompt, IMcpPromptMessage, IMcpServer, IMcpService, McpResourceURI } from '../../../mcp/common/mcpTypes.js'; -import { searchFilesAndFolders } from '../../../search/browser/searchChatContext.js'; -import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../common/chatAgents.js'; -import { IChatEditingService } from '../../common/chatEditingService.js'; -import { getAttachableImageExtension } from '../../common/chatModel.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; -import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { IChatRequestVariableEntry } from '../../common/chatVariableEntries.js'; -import { IDynamicVariable } from '../../common/chatVariables.js'; -import { ChatAgentLocation, ChatModeKind, isSupportedChatFileScheme } from '../../common/constants.js'; -import { ToolSet } from '../../common/languageModelToolsService.js'; -import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; -import { ChatSubmitAction } from '../actions/chatExecuteActions.js'; -import { IChatWidget, IChatWidgetService } from '../chat.js'; -import { resizeImage } from '../imageUtils.js'; -import { ChatDynamicVariableModel } from './chatDynamicVariables.js'; +import { coalesce } from '../../../../../../../base/common/arrays.js'; +import { decodeBase64 } from '../../../../../../../base/common/buffer.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { StopWatch } from '../../../../../../../base/common/stopwatch.js'; +import { isPatternInWord } from '../../../../../../../base/common/filters.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { ResourceSet } from '../../../../../../../base/common/map.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import { basename } from '../../../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../../../base/common/themables.js'; +import { assertType } from '../../../../../../../base/common/types.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../../../base/common/uuid.js'; +import { ICodeEditor, getCodeEditor, isCodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; +import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { IWordAtPosition, getWordAtText } from '../../../../../../../editor/common/core/wordHelper.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemProvider, CompletionList, DocumentSymbol, Location, ProviderResult, SymbolKind, SymbolKinds } from '../../../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'; +import { IOutlineModelService } from '../../../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; +import { localize } from '../../../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../../../platform/actions/common/actions.js'; +import { CommandsRegistry } from '../../../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { FileKind, IFileService } from '../../../../../../../platform/files/common/files.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { INotificationService } from '../../../../../../../platform/notification/common/notification.js'; +import { Registry } from '../../../../../../../platform/registry/common/platform.js'; +import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../../common/contributions.js'; +import { EditorsOrder, isDiffEditorInput } from '../../../../../../common/editor.js'; +import { IEditorService } from '../../../../../../services/editor/common/editorService.js'; +import { IHistoryService } from '../../../../../../services/history/common/history.js'; +import { LifecyclePhase } from '../../../../../../services/lifecycle/common/lifecycle.js'; +import { ISearchService } from '../../../../../../services/search/common/search.js'; +import { McpPromptArgumentPick } from '../../../../../mcp/browser/mcpPromptArgumentPick.js'; +import { IMcpPrompt, IMcpPromptMessage, IMcpServer, IMcpService, McpResourceURI } from '../../../../../mcp/common/mcpTypes.js'; +import { searchFilesAndFolders } from '../../../../../search/browser/searchChatContext.js'; +import { IChatAgentData, IChatAgentNameService, IChatAgentService, getFullyQualifiedId } from '../../../../common/participants/chatAgents.js'; +import { getAttachableImageExtension } from '../../../../common/model/chatModel.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../../../common/requestParser/chatParserTypes.js'; +import { IChatSlashCommandService } from '../../../../common/participants/chatSlashCommands.js'; +import { IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; +import { IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; +import { ChatAgentLocation, ChatModeKind, isSupportedChatFileScheme } from '../../../../common/constants.js'; +import { isToolSet } from '../../../../common/tools/languageModelToolsService.js'; +import { IChatSessionsService } from '../../../../common/chatSessionsService.js'; +import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ChatSubmitAction, IChatExecuteActionContext } from '../../../actions/chatExecuteActions.js'; +import { IChatWidget, IChatWidgetService } from '../../../chat.js'; +import { resizeImage } from '../../../chatImageUtils.js'; +import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -115,7 +115,7 @@ class SlashCommandCompletions extends Disposable { range, sortText: c.sortText ?? 'a'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` } satisfies IChatExecuteActionContext] } : undefined, }; }) }; @@ -160,7 +160,7 @@ class SlashCommandCompletions extends Disposable { filterText: `${chatAgentLeader}${c.command}`, sortText: c.sortText ?? 'z'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, + command: c.executeImmediately ? { id: ChatSubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` } satisfies IChatExecuteActionContext] } : undefined, }; }) }; @@ -169,7 +169,7 @@ class SlashCommandCompletions extends Disposable { this._register(this.languageFeaturesService.completionProvider.register({ scheme: Schemas.vscodeChatInput, hasAccessToAllModels: true }, { _debugDisplayName: 'promptSlashCommands', triggerCharacters: [chatSubcommandLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget || !widget.viewModel) { return null; @@ -192,7 +192,7 @@ class SlashCommandCompletions extends Disposable { return; } - const promptCommands = await this.promptsService.findPromptSlashCommands(); + const promptCommands = await this.promptsService.getPromptSlashCommands(token); if (promptCommands.length === 0) { return null; } @@ -203,12 +203,12 @@ class SlashCommandCompletions extends Disposable { return { suggestions: promptCommands.map((c, i): CompletionItem => { - const label = `/${c.command}`; - const description = c.promptPath ? this.promptsService.getPromptLocationLabel(c.promptPath) : undefined; + const label = `/${c.name}`; + const description = c.description; return { label: { label, description }, insertText: `${label} `, - documentation: c.detail, + documentation: c.description, range, sortText: 'a'.repeat(i + 1), kind: CompletionItemKind.Text, // The icons are disabled here anyway, @@ -271,6 +271,7 @@ class AgentCompletions extends Disposable { @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatAgentService private readonly chatAgentService: IChatAgentService, @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, ) { super(); @@ -338,6 +339,11 @@ class AgentCompletions extends Disposable { const agents = this.chatAgentService.getAgents() .filter(a => a.locations.includes(widget.location)); + // Filter out chatSessions contributions for slash command completions + const chatSessionContributions = this.chatSessionsService.getAllChatSessionContributions(); + const chatSessionAgentIds = new Set(chatSessionContributions.map(contribution => contribution.type)); + const agentsForSlashCommands = agents.filter(a => !chatSessionAgentIds.has(a.id)); + // When the input is only `/`, items are sorted by sortText. // When typing, filterText is used to score and sort. // The same list is refiltered/ranked while typing. @@ -370,7 +376,7 @@ class AgentCompletions extends Disposable { return { suggestions: justAgents.concat( - coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { + coalesce(agentsForSlashCommands.flatMap(agent => agent.slashCommands.map((c, i) => { if (agent.isDefault && this.chatAgentService.getDefaultAgent(widget.location, widget.input.currentModeKind)?.id !== agent.id) { return; } @@ -430,7 +436,9 @@ class AgentCompletions extends Disposable { } const agents = this.chatAgentService.getAgents() - .filter(a => a.locations.includes(widget.location) && a.modes.includes(widget.input.currentModeKind)); + .filter(a => a.locations.includes(widget.location) && a.modes.includes(widget.input.currentModeKind)) + // Filter out chatSessions contributions for slash command completions + .filter(a => !this.chatSessionsService.getChatSessionContribution(a.id)); return { suggestions: coalesce(agents.flatMap(agent => agent.slashCommands.map((c, i) => { @@ -605,7 +613,7 @@ class StartParameterizedPromptAction extends Action2 { const widgetService = accessor.get(IChatWidgetService); const fileService = accessor.get(IFileService); - const chatWidget = widgetService.lastFocusedWidget; + const chatWidget = await widgetService.revealWidget(true); if (!chatWidget) { return; } @@ -785,7 +793,6 @@ class BuiltinDynamicCompletions extends Disposable { @ILabelService private readonly labelService: ILabelService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, - @IChatEditingService private readonly _chatEditingService: IChatEditingService, @IOutlineModelService private readonly outlineService: IOutlineModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @@ -973,39 +980,27 @@ class BuiltinDynamicCompletions extends Disposable { // HISTORY // always take the last N items for (const [i, item] of this.historyService.getHistory().entries()) { - if (!item.resource || seen.has(item.resource) || !this.instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, item.resource!.scheme))) { + const resource = isDiffEditorInput(item) ? item.modified.resource : item.resource; + if (!resource || seen.has(resource) || !this.instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, resource.scheme))) { // ignore editors without a resource continue; } if (pattern) { // use pattern if available - const basename = this.labelService.getUriBasenameLabel(item.resource).toLowerCase(); + const basename = this.labelService.getUriBasenameLabel(resource).toLowerCase(); if (!isPatternInWord(pattern, 0, pattern.length, basename, 0, basename.length)) { continue; } } - seen.add(item.resource); - const newLen = result.suggestions.push(makeCompletionItem(item.resource, FileKind.FILE, i === 0 ? localize('activeFile', 'Active file') : undefined, i === 0)); + seen.add(resource); + const newLen = result.suggestions.push(makeCompletionItem(resource, FileKind.FILE, i === 0 ? localize('activeFile', 'Active file') : undefined, i === 0)); if (newLen - len >= 5) { break; } } - // RELATED FILES - if (widget.input.currentModeKind !== ChatModeKind.Ask && widget.viewModel && widget.viewModel.model.editingSession) { - const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionResource, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; - for (const relatedFileGroup of relatedFiles) { - for (const relatedFile of relatedFileGroup.files) { - if (!seen.has(relatedFile.uri)) { - seen.add(relatedFile.uri); - result.suggestions.push(makeCompletionItem(relatedFile.uri, FileKind.FILE, relatedFile.description)); - } - } - } - } - // SEARCH // use file search when having a pattern if (pattern) { @@ -1036,6 +1031,8 @@ class BuiltinDynamicCompletions extends Disposable { } private addSymbolEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { + const timeoutMs = 100; + const stopwatch = new StopWatch(); const makeSymbolCompletionItem = (symbolItem: { name: string; location: Location; kind: SymbolKind }, pattern: string): CompletionItem => { const text = `${chatVariableLeader}sym:${symbolItem.name}`; @@ -1075,11 +1072,17 @@ class BuiltinDynamicCompletions extends Disposable { } } + let timedOut = false; + for (const symbol of symbolsToAdd) { + if (stopwatch.elapsed() > timeoutMs || token.isCancellationRequested) { + timedOut = true; + break; + } result.suggestions.push(makeSymbolCompletionItem({ ...symbol.symbol, location: { uri: symbol.uri, range: symbol.symbol.range } }, pattern ?? '')); } - result.incomplete = !!pattern; + result.incomplete = !!pattern || timedOut; } private updateCacheKey() { @@ -1207,9 +1210,10 @@ class ToolCompletions extends Disposable { } let detail: string | undefined; + let documentation: string | undefined; let name: string; - if (item instanceof ToolSet) { + if (isToolSet(item)) { detail = item.description; name = item.referenceName; @@ -1217,6 +1221,7 @@ class ToolCompletions extends Disposable { const source = item.source; detail = localize('tool_source_completion', "{0}: {1}", source.label, item.displayName); name = item.toolReferenceName ?? item.displayName; + documentation = item.userDescription ?? item.modelDescription; } if (usedNames.has(name)) { @@ -1228,9 +1233,9 @@ class ToolCompletions extends Disposable { label: withLeader, range, detail, + documentation, insertText: withLeader + ' ', kind: CompletionItemKind.Tool, - sortText: 'z', }); } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts new file mode 100644 index 00000000000..477895be331 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorContrib.ts @@ -0,0 +1,447 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { Disposable, MutableDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../../../base/common/observable.js'; +import { themeColorFromId } from '../../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { IDecorationOptions } from '../../../../../../../editor/common/editorCommon.js'; +import { TrackedRangeStickiness } from '../../../../../../../editor/common/model.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { inputPlaceholderForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; +import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../../../common/participants/chatAgents.js'; +import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../common/widget/chatColors.js'; +import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader } from '../../../../common/requestParser/chatParserTypes.js'; +import { ChatRequestParser } from '../../../../common/requestParser/chatRequestParser.js'; +import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { IChatWidget } from '../../../chat.js'; +import { ChatWidget } from '../../chatWidget.js'; +import { dynamicVariableDecorationType } from '../../../attachments/chatDynamicVariables.js'; +import { NativeEditContextRegistry } from '../../../../../../../editor/browser/controller/editContext/native/nativeEditContextRegistry.js'; +import { TextAreaEditContextRegistry } from '../../../../../../../editor/browser/controller/editContext/textArea/textAreaEditContextRegistry.js'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { ThrottledDelayer } from '../../../../../../../base/common/async.js'; + +const decorationDescription = 'chat'; +const placeholderDecorationType = 'chat-session-detail'; +const slashCommandTextDecorationType = 'chat-session-text'; +const variableTextDecorationType = 'chat-variable-text'; + +function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string { + return subcommand ? `${agent.id}__${subcommand}` : agent.id; +} + +function isWhitespaceOrPromptPart(p: IParsedChatRequestPart): boolean { + return (p instanceof ChatRequestTextPart && !p.text.trim().length) || (p instanceof ChatRequestSlashPromptPart); +} + +function exactlyOneSpaceAfterPart(parsedRequest: readonly IParsedChatRequestPart[], part: IParsedChatRequestPart): boolean { + const partIdx = parsedRequest.indexOf(part); + if (parsedRequest.length > partIdx + 2) { + return false; + } + + const nextPart = parsedRequest[partIdx + 1]; + return nextPart && nextPart instanceof ChatRequestTextPart && nextPart.text === ' '; +} + +function getRangeForPlaceholder(part: IParsedChatRequestPart) { + return { + startLineNumber: part.editorRange.startLineNumber, + endLineNumber: part.editorRange.endLineNumber, + startColumn: part.editorRange.endColumn + 1, + endColumn: 1000 + }; +} + +class InputEditorDecorations extends Disposable { + + private static readonly UPDATE_DELAY = 200; + + public readonly id = 'inputEditorDecorations'; + + private readonly previouslyUsedAgents = new Set(); + + private readonly viewModelDisposables = this._register(new MutableDisposable()); + + + private readonly updateThrottle = this._register(new ThrottledDelayer(InputEditorDecorations.UPDATE_DELAY)); + + constructor( + private readonly widget: IChatWidget, + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, + @IThemeService private readonly themeService: IThemeService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @ILabelService private readonly labelService: ILabelService, + @IPromptsService private readonly promptsService: IPromptsService, + ) { + super(); + + this.registeredDecorationTypes(); + this.triggerInputEditorDecorationsUpdate(); + this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.triggerInputEditorDecorationsUpdate())); + this._register(this.widget.onDidChangeParsedInput(() => this.triggerInputEditorDecorationsUpdate())); + this._register(this.widget.onDidChangeViewModel(() => { + this.registerViewModelListeners(); + this.previouslyUsedAgents.clear(); + this.triggerInputEditorDecorationsUpdate(); + })); + this._register(this.widget.onDidSubmitAgent((e) => { + this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name)); + })); + this._register(this.chatAgentService.onDidChangeAgents(() => this.triggerInputEditorDecorationsUpdate())); + this._register(this.promptsService.onDidChangeSlashCommands(() => this.triggerInputEditorDecorationsUpdate())); + this._register(autorun(reader => { + // Watch for changes to the current mode and its properties + const currentMode = this.widget.input.currentModeObs.read(reader); + if (currentMode) { + // Also watch the mode's description to react to any changes + currentMode.description.read(reader); + } + // Trigger decoration update when mode or its properties change + this.triggerInputEditorDecorationsUpdate(); + })); + + this.registerViewModelListeners(); + } + + private registerViewModelListeners(): void { + this.viewModelDisposables.value = this.widget.viewModel?.onDidChange(e => { + if (e?.kind === 'changePlaceholder' || e?.kind === 'initialize') { + this.triggerInputEditorDecorationsUpdate(); + } + }); + } + + private registeredDecorationTypes() { + this._register(this.codeEditorService.registerDecorationType(decorationDescription, placeholderDecorationType, {})); + this._register(this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), + borderRadius: '3px' + })); + this._register(this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), + borderRadius: '3px' + })); + this._register(this.codeEditorService.registerDecorationType(decorationDescription, dynamicVariableDecorationType, { + color: themeColorFromId(chatSlashCommandForeground), + backgroundColor: themeColorFromId(chatSlashCommandBackground), + borderRadius: '3px', + rangeBehavior: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges + })); + } + + private getPlaceholderColor(): string | undefined { + const theme = this.themeService.getColorTheme(); + const transparentForeground = theme.getColor(inputPlaceholderForeground); + return transparentForeground?.toString(); + } + + private triggerInputEditorDecorationsUpdate(): void { + // update placeholder decorations immediately, in sync + this.updateInputPlaceholderDecoration(); + + // with a delay, update the rest of the decorations + this.updateThrottle.trigger(token => this.updateAsyncInputEditorDecorations(token)); + } + + private updateInputPlaceholderDecoration(): void { + const inputValue = this.widget.inputEditor.getValue(); + + const viewModel = this.widget.viewModel; + if (!viewModel) { + this.updateAriaPlaceholder(undefined); + return; + } + + if (!inputValue) { + const mode = this.widget.input.currentModeObs.get(); + const placeholder = mode.argumentHint?.get() ?? mode.description.get() ?? ''; + const displayPlaceholder = viewModel.inputPlaceholder || placeholder; + + const decoration: IDecorationOptions[] = [ + { + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: 1, + endColumn: 1000 + }, + renderOptions: { + after: { + contentText: displayPlaceholder, + color: this.getPlaceholderColor() + } + } + } + ]; + this.updateAriaPlaceholder(displayPlaceholder || undefined); + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, decoration); + return; + } + + this.updateAriaPlaceholder(undefined); + + const parsedRequest = this.widget.parsedInput.parts; + + let placeholderDecoration: IDecorationOptions[] | undefined; + const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); + + const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); + if (onlyAgentAndWhitespace) { + // Agent reference with no other text - show the placeholder + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined)); + const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder; + if (agentPart.agent.description && exactlyOneSpaceAfterPart(parsedRequest, agentPart)) { + placeholderDecoration = [{ + range: getRangeForPlaceholder(agentPart), + renderOptions: { + after: { + contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + + const onlyAgentAndAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentAndAgentCommandAndWhitespace) { + // Agent reference and subcommand with no other text - show the placeholder + const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); + const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; + if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(parsedRequest, agentSubcommandPart)) { + placeholderDecoration = [{ + range: getRangeForPlaceholder(agentSubcommandPart), + renderOptions: { + after: { + contentText: shouldRenderFollowupPlaceholder ? agentSubcommandPart.command.followupPlaceholder : agentSubcommandPart.command.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + + const onlyAgentCommandAndWhitespace = agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentSubcommandPart); + if (onlyAgentCommandAndWhitespace) { + // Agent subcommand with no other text - show the placeholder + if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(parsedRequest, agentSubcommandPart)) { + placeholderDecoration = [{ + range: getRangeForPlaceholder(agentSubcommandPart), + renderOptions: { + after: { + contentText: agentSubcommandPart.command.description, + color: this.getPlaceholderColor(), + } + } + }]; + } + } + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); + } + + private async updateAsyncInputEditorDecorations(token: CancellationToken): Promise { + + const parsedRequest = this.widget.parsedInput.parts; + + const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); + const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); + const slashCommandPart = parsedRequest.find((p): p is ChatRequestSlashCommandPart => p instanceof ChatRequestSlashCommandPart); + const slashPromptPart = parsedRequest.find((p): p is ChatRequestSlashPromptPart => p instanceof ChatRequestSlashPromptPart); + + // first, fetch all async context + const promptSlashCommand = slashPromptPart ? await this.promptsService.resolvePromptSlashCommand(slashPromptPart.name, token) : undefined; + if (token.isCancellationRequested) { + // a new update came in while we were waiting + return; + } + + if (slashPromptPart && promptSlashCommand) { + const onlyPromptCommandAndWhitespace = slashPromptPart && parsedRequest.every(isWhitespaceOrPromptPart); + if (onlyPromptCommandAndWhitespace && exactlyOneSpaceAfterPart(parsedRequest, slashPromptPart) && promptSlashCommand) { + const description = promptSlashCommand.argumentHint ?? promptSlashCommand.description; + if (description) { + this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, [{ + range: getRangeForPlaceholder(slashPromptPart), + renderOptions: { + after: { + contentText: description, + color: this.getPlaceholderColor(), + } + } + }]); + } + } + } + + const textDecorations: IDecorationOptions[] | undefined = []; + if (agentPart) { + textDecorations.push({ range: agentPart.editorRange }); + } + if (agentSubcommandPart) { + textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); + } + + if (slashCommandPart) { + textDecorations.push({ range: slashCommandPart.editorRange }); + } + + if (slashPromptPart && promptSlashCommand) { + textDecorations.push({ range: slashPromptPart.editorRange }); + } + + this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); + + const varDecorations: IDecorationOptions[] = []; + const toolParts = parsedRequest.filter((p): p is ChatRequestToolPart => p instanceof ChatRequestToolPart || p instanceof ChatRequestToolSetPart); + for (const tool of toolParts) { + varDecorations.push({ range: tool.editorRange }); + } + + const dynamicVariableParts = parsedRequest.filter((p): p is ChatRequestDynamicVariablePart => p instanceof ChatRequestDynamicVariablePart); + + const isEditingPreviousRequest = !!this.widget.viewModel?.editing; + if (isEditingPreviousRequest) { + for (const variable of dynamicVariableParts) { + varDecorations.push({ range: variable.editorRange, hoverMessage: URI.isUri(variable.data) ? new MarkdownString(this.labelService.getUriLabel(variable.data, { relative: true })) : undefined }); + } + } + + this.widget.inputEditor.setDecorationsByType(decorationDescription, variableTextDecorationType, varDecorations); + } + + private updateAriaPlaceholder(value: string | undefined): void { + const nativeEditContext = NativeEditContextRegistry.get(this.widget.inputEditor.getId()); + if (nativeEditContext) { + const domNode = nativeEditContext.domNode.domNode; + if (value && value.trim().length) { + domNode.setAttribute('aria-placeholder', value); + } else { + domNode.removeAttribute('aria-placeholder'); + } + } else { + const textAreaEditContext = TextAreaEditContextRegistry.get(this.widget.inputEditor.getId()); + if (textAreaEditContext) { + const textArea = textAreaEditContext.textArea.domNode; + if (value && value.trim().length) { + textArea.setAttribute('aria-placeholder', value); + } else { + textArea.removeAttribute('aria-placeholder'); + } + } + } + } +} + +class InputEditorSlashCommandMode extends Disposable { + public readonly id = 'InputEditorSlashCommandMode'; + + constructor( + private readonly widget: IChatWidget + ) { + super(); + this._register(this.widget.onDidChangeAgent(e => { + if (e.slashCommand && e.slashCommand.isSticky || !e.slashCommand && e.agent.metadata.isSticky) { + this.repopulateAgentCommand(e.agent, e.slashCommand); + } + })); + this._register(this.widget.onDidSubmitAgent(e => { + this.repopulateAgentCommand(e.agent, e.slashCommand); + })); + } + + private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { + // Make sure we don't repopulate if the user already has something in the input + if (this.widget.inputEditor.getValue().trim()) { + return; + } + + let value: string | undefined; + if (slashCommand && slashCommand.isSticky) { + value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; + } else if (agent.metadata.isSticky) { + value = `${chatAgentLeader}${agent.name} `; + } + + if (value) { + this.widget.inputEditor.setValue(value); + this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); + } + } +} + +ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); + +class ChatTokenDeleter extends Disposable { + + public readonly id = 'chatTokenDeleter'; + + constructor( + private readonly widget: IChatWidget, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + const parser = this.instantiationService.createInstance(ChatRequestParser); + const inputValue = this.widget.inputEditor.getValue(); + let previousInputValue: string | undefined; + let previousSelectedAgent: IChatAgentData | undefined; + + // A simple heuristic to delete the previous token when the user presses backspace. + // The sophisticated way to do this would be to have a parse tree that can be updated incrementally. + this._register(this.widget.inputEditor.onDidChangeModelContent(e => { + if (!previousInputValue) { + previousInputValue = inputValue; + previousSelectedAgent = this.widget.lastSelectedAgent; + } + + // Don't try to handle multi-cursor edits right now + const change = e.changes[0]; + + // If this was a simple delete, try to find out whether it was inside a token + if (!change.text && this.widget.viewModel) { + const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionResource, previousInputValue, widget.location, { selectedAgent: previousSelectedAgent, mode: this.widget.input.currentModeKind }); + + // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping + const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestSlashPromptPart || p instanceof ChatRequestToolPart); + deletableTokens.forEach(token => { + const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range); + // Part of this token was deleted, or the space after it was deleted, and the deletion range doesn't go off the front of the token, for simpler math + if (deletedRangeOfToken && Range.compareRangesUsingStarts(token.editorRange, change.range) < 0) { + // Range.intersectRanges returns an empty range when the deletion happens *exactly* at a boundary. + // In that case, only treat this as a token-delete when the deleted character was a space. + if (previousInputValue && Range.isEmpty(deletedRangeOfToken)) { + const deletedText = previousInputValue.substring(change.rangeOffset, change.rangeOffset + change.rangeLength); + if (deletedText !== ' ') { + return; + } + } + + // Assume single line tokens + const length = deletedRangeOfToken.endColumn - deletedRangeOfToken.startColumn; + const rangeToDelete = new Range(token.editorRange.startLineNumber, token.editorRange.startColumn, token.editorRange.endLineNumber, token.editorRange.endColumn - length); + this.widget.inputEditor.executeEdits(this.id, [{ + range: rangeToDelete, + text: '', + }]); + this.widget.refreshParsedInput(); + } + }); + } + + previousInputValue = this.widget.inputEditor.getValue(); + previousSelectedAgent = this.widget.lastSelectedAgent; + })); + } +} +ChatWidget.CONTRIBS.push(ChatTokenDeleter); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorHover.ts similarity index 77% rename from src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts rename to src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorHover.ts index 51808206d1a..21c36b41078 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorHover.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatInputEditorHover.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IModelDecoration } from '../../../../../editor/common/model.js'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from '../../../../../editor/contrib/hover/browser/hoverTypes.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatWidgetService } from '../chat.js'; -import { ChatAgentHover, getChatAgentHoverOptions } from '../chatAgentHover.js'; +import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; +import { ICodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { IModelDecoration } from '../../../../../../../editor/common/model.js'; +import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from '../../../../../../../editor/contrib/hover/browser/hoverTypes.js'; +import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatWidgetService } from '../../../chat.js'; +import { ChatAgentHover, getChatAgentHoverOptions } from '../../chatAgentHover.js'; import { ChatEditorHoverWrapper } from './editorHoverWrapper.js'; -import { IChatAgentData } from '../../common/chatAgents.js'; -import { extractAgentAndCommand } from '../../common/chatParserTypes.js'; -import * as nls from '../../../../../nls.js'; +import { IChatAgentData } from '../../../../common/participants/chatAgents.js'; +import { extractAgentAndCommand } from '../../../../common/requestParser/chatParserTypes.js'; +import * as nls from '../../../../../../../nls.js'; export class ChatAgentHoverParticipant implements IEditorHoverParticipant { diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts similarity index 88% rename from src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts rename to src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts index 6ea16686299..65089b20200 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/chatPasteProviders.ts @@ -2,32 +2,32 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; -import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { Mimes } from '../../../../base/common/mime.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { basename, joinPath } from '../../../../base/common/resources.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { localize } from '../../../../nls.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; -import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../common/chatVariableEntries.js'; -import { IChatVariablesService, IDynamicVariable } from '../common/chatVariables.js'; -import { IChatWidgetService } from './chat.js'; -import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js'; -import { cleanupOldImages, createFileForMedia, resizeImage } from './imageUtils.js'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../../base/common/codicons.js'; +import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../../../../base/common/dataTransfer.js'; +import { HierarchicalKind } from '../../../../../../../base/common/hierarchicalKind.js'; +import { Disposable } from '../../../../../../../base/common/lifecycle.js'; +import { revive } from '../../../../../../../base/common/marshalling.js'; +import { Mimes } from '../../../../../../../base/common/mime.js'; +import { Schemas } from '../../../../../../../base/common/network.js'; +import { basename, joinPath } from '../../../../../../../base/common/resources.js'; +import { URI, UriComponents } from '../../../../../../../base/common/uri.js'; +import { IRange } from '../../../../../../../editor/common/core/range.js'; +import { DocumentPasteContext, DocumentPasteEdit, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'; +import { IModelService } from '../../../../../../../editor/common/services/model.js'; +import { localize } from '../../../../../../../nls.js'; +import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../../platform/log/common/log.js'; +import { IExtensionService, isProposedApiEnabled } from '../../../../../../services/extensions/common/extensions.js'; +import { IChatRequestPasteVariableEntry, IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; +import { IChatVariablesService, IDynamicVariable } from '../../../../common/attachments/chatVariables.js'; +import { IChatWidgetService } from '../../../chat.js'; +import { ChatDynamicVariableModel } from '../../../attachments/chatDynamicVariables.js'; +import { cleanupOldImages, createFileForMedia, resizeImage } from '../../../chatImageUtils.js'; const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/chat/browser/widget/input/editor/editorHoverWrapper.ts similarity index 83% rename from src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts rename to src/vs/workbench/contrib/chat/browser/widget/input/editor/editorHoverWrapper.ts index 01f5adf945d..2645ae304d1 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/editorHoverWrapper.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/editor/editorHoverWrapper.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import './media/editorHoverWrapper.css'; -import * as dom from '../../../../../base/browser/dom.js'; -import { IHoverAction } from '../../../../../base/browser/ui/hover/hover.js'; -import { HoverAction } from '../../../../../base/browser/ui/hover/hoverWidget.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; +import * as dom from '../../../../../../../base/browser/dom.js'; +import { IHoverAction } from '../../../../../../../base/browser/ui/hover/hover.js'; +import { HoverAction } from '../../../../../../../base/browser/ui/hover/hoverWidget.js'; +import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'; const $ = dom.$; const h = dom.h; diff --git a/src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css b/src/vs/workbench/contrib/chat/browser/widget/input/editor/media/editorHoverWrapper.css similarity index 100% rename from src/vs/workbench/contrib/chat/browser/contrib/media/editorHoverWrapper.css rename to src/vs/workbench/contrib/chat/browser/widget/input/editor/media/editorHoverWrapper.css diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/media/chatStatusWidget.css b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatStatusWidget.css new file mode 100644 index 00000000000..1dac7ac8072 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/media/chatStatusWidget.css @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget { + padding: 6px 3px 6px 3px; + box-sizing: border-box; + border: 1px solid var(--vscode-input-border, transparent); + background-color: var(--vscode-editor-background); + border-bottom: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-content { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + padding-left: 8px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-message { + font-size: 11px; + line-height: 16px; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-action { + flex-shrink: 0; + padding-right: 4px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-button { + font-size: 11px; + padding: 2px 8px; + min-width: unset; + height: 22px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container:has(.chat-status-widget:not([style*="display: none"])) + .chat-todo-list-widget-container .chat-todo-list-widget { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container:has(.chat-status-widget:not([style*="display: none"])) + .chat-todo-list-widget-container:not(:has(.chat-todo-list-widget.has-todos)) + .chat-editing-session .chat-editing-session-container { + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts new file mode 100644 index 00000000000..4def3e504b0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -0,0 +1,321 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { coalesce } from '../../../../../../base/common/arrays.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { groupBy } from '../../../../../../base/common/collections.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun, IObservable } from '../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { getFlatActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IMenuService, MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { IProductService } from '../../../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js'; +import { isOrganizationPromptFile } from '../../../common/promptSyntax/utils/promptsServiceUtils.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; +import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; +import { getOpenChatActionIdForMode } from '../../actions/chatActions.js'; +import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; + +export interface IModePickerDelegate { + readonly currentMode: IObservable; + readonly sessionResource: () => URI | undefined; + /** + * When set, the mode picker will show custom agents whose target matches this value. + * Custom agents without a target are always shown in all session types. If no agents match the target, shows a default "Agent" option. + */ + readonly customAgentTarget?: () => string | undefined; +} + +// TODO: there should be an icon contributed for built-in modes +const builtinDefaultIcon = Codicon.tasklist; + +export class ModePickerActionItem extends ChatInputPickerActionViewItem { + constructor( + action: MenuItemAction, + private readonly delegate: IModePickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IChatAgentService chatAgentService: IChatAgentService, + @IKeybindingService keybindingService: IKeybindingService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatModeService chatModeService: IChatModeService, + @IMenuService private readonly menuService: IMenuService, + @ICommandService commandService: ICommandService, + @IProductService private readonly _productService: IProductService, + @ITelemetryService telemetryService: ITelemetryService, + @IOpenerService openerService: IOpenerService + ) { + // Get custom agent target (if filtering is enabled) + const customAgentTarget = delegate.customAgentTarget?.(); + + // Category definitions + const builtInCategory = { label: localize('built-in', "Built-In"), order: 0 }; + const customCategory = { label: localize('custom', "Custom"), order: 1 }; + const policyDisabledCategory = { label: localize('managedByOrganization', "Managed by your organization"), order: 999, showHeader: true }; + + const agentModeDisabledViaPolicy = configurationService.inspect(ChatConfiguration.AgentEnabled).policyValue === false; + + const makeAction = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => { + const isDisabledViaPolicy = + mode.kind === ChatModeKind.Agent && + agentModeDisabledViaPolicy; + + const tooltip = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip; + + // Add toolbar actions for Agent modes + const toolbarActions: IAction[] = []; + if (mode.kind === ChatModeKind.Agent && !isDisabledViaPolicy) { + if (mode.uri) { + let label, icon, id; + if (mode.source?.storage === PromptsStorage.extension) { + icon = Codicon.eye; + id = `viewAgent:${mode.id}`; + label = localize('viewModeConfiguration', "View {0} agent", mode.label.get()); + } else { + icon = Codicon.edit; + id = `editAgent:${mode.id}`; + label = localize('editModeConfiguration', "Edit {0} agent", mode.label.get()); + } + + const modeResource = mode.uri; + toolbarActions.push({ + id, + label, + tooltip: label, + class: ThemeIcon.asClassName(icon), + enabled: true, + run: async () => { + openerService.open(modeResource.get()); + } + }); + } else if (!customAgentTarget) { + const label = localize('configureToolsFor', "Configure tools for {0} agent", mode.label.get()); + toolbarActions.push({ + id: `configureTools:${mode.id}`, + label, + tooltip: label, + class: ThemeIcon.asClassName(Codicon.tools), + enabled: true, + run: async () => { + // Hide the picker before opening the tools configuration + actionWidgetService.hide(); + // First switch to the mode if not already selected + if (currentMode.id !== mode.id) { + await commandService.executeCommand( + ToggleAgentModeActionId, + { modeId: mode.id, sessionResource: this.delegate.sessionResource() } satisfies IToggleChatModeArgs + ); + } + // Then open the tools picker + await commandService.executeCommand('workbench.action.chat.configureTools', pickerOptions.actionContext, { source: 'modePicker' }); + } + }); + } + } + + return { + ...action, + id: getOpenChatActionIdForMode(mode), + label: mode.label.get(), + icon: isDisabledViaPolicy ? ThemeIcon.fromId(Codicon.lock.id) : mode.icon.get(), + class: isDisabledViaPolicy ? 'disabled-by-policy' : undefined, + enabled: !isDisabledViaPolicy, + checked: !isDisabledViaPolicy && currentMode.id === mode.id, + tooltip: '', + hover: { content: tooltip, position: this.pickerOptions.hoverPosition }, + toolbarActions, + run: async () => { + if (isDisabledViaPolicy) { + return; // Block interaction if disabled by policy + } + const result = await commandService.executeCommand( + ToggleAgentModeActionId, + { modeId: mode.id, sessionResource: this.delegate.sessionResource() } satisfies IToggleChatModeArgs + ); + if (this.element) { + this.renderLabel(this.element); + } + return result; + }, + category: isDisabledViaPolicy ? policyDisabledCategory : builtInCategory + }; + }; + + const makeActionFromCustomMode = (mode: IChatMode, currentMode: IChatMode): IActionWidgetDropdownAction => { + return { + ...makeAction(mode, currentMode), + tooltip: '', + hover: { content: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip, position: this.pickerOptions.hoverPosition }, + icon: mode.icon.get() ?? (isModeConsideredBuiltIn(mode, this._productService) ? builtinDefaultIcon : undefined), + category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory + }; + }; + + const isUserDefinedCustomAgent = (mode: IChatMode): boolean => { + if (mode.isBuiltin || !mode.source) { + return false; + } + return mode.source.storage === PromptsStorage.local || mode.source.storage === PromptsStorage.user; + }; + + const actionProviderWithCustomAgentTarget: IActionWidgetDropdownActionProvider = { + getActions: () => { + const modes = chatModeService.getModes(); + const currentMode = delegate.currentMode.get(); + const filteredCustomModes = modes.custom.filter(mode => { + const target = mode.target?.get(); + return isUserDefinedCustomAgent(mode) && (!target || target === customAgentTarget); + }); + // Always include the default "Agent" option first + const checked = currentMode.id === ChatMode.Agent.id; + const defaultAction = { ...makeAction(ChatMode.Agent, ChatMode.Agent), checked }; + + // Add filtered custom modes + const customActions = filteredCustomModes.map(mode => makeActionFromCustomMode(mode, currentMode)); + return [defaultAction, ...customActions]; + } + }; + + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const modes = chatModeService.getModes(); + const currentMode = delegate.currentMode.get(); + const agentMode = modes.builtin.find(mode => mode.id === ChatMode.Agent.id); + + const shouldHideEditMode = configurationService.getValue(ChatConfiguration.EditModeHidden) && chatAgentService.hasToolsAgent && currentMode.id !== ChatMode.Edit.id; + + const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id && !(shouldHideEditMode && mode.id === ChatMode.Edit.id)); + // Filter out 'implement' mode from the dropdown - it's available for handoffs but not user-selectable + const customModes = groupBy( + modes.custom, + mode => isModeConsideredBuiltIn(mode, this._productService) ? 'builtin' : 'custom'); + + const customBuiltinModeActions = customModes.builtin?.map(mode => { + const action = makeActionFromCustomMode(mode, currentMode); + action.category = agentModeDisabledViaPolicy ? policyDisabledCategory : builtInCategory; + return action; + }) ?? []; + customBuiltinModeActions.sort((a, b) => a.label.localeCompare(b.label)); + + const customModeActions = customModes.custom?.map(mode => makeActionFromCustomMode(mode, currentMode)) ?? []; + customModeActions.sort((a, b) => a.label.localeCompare(b.label)); + + const orderedModes = coalesce([ + agentMode && makeAction(agentMode, currentMode), + ...otherBuiltinModes.map(mode => mode && makeAction(mode, currentMode)), + ...customBuiltinModeActions, + ...customModeActions + ]); + return orderedModes; + } + }; + + const modePickerActionWidgetOptions: Omit = { + actionProvider: customAgentTarget ? actionProviderWithCustomAgentTarget : actionProvider, + actionBarActionProvider: { + getActions: () => this.getModePickerActionBarActions() + }, + showItemKeybindings: true, + reporter: { name: 'ChatModePicker', includeOptions: true }, + }; + + super(action, modePickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + + // Listen to changes in the current mode and its properties + this._register(autorun(reader => { + this.delegate.currentMode.read(reader).label.read(reader); // use the reader so autorun tracks it + if (this.element) { + this.renderLabel(this.element); + } + })); + } + + private getModePickerActionBarActions(): IAction[] { + const menuActions = this.menuService.createMenu(MenuId.ChatModePicker, this.contextKeyService); + const menuContributions = getFlatActionBarActions(menuActions.getActions({ renderShortTitle: true })); + menuActions.dispose(); + + return menuContributions; + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + + const currentMode = this.delegate.currentMode.get(); + const isDefault = currentMode.id === ChatMode.Agent.id; + const state = currentMode.label.get(); + let icon = currentMode.icon.get(); + + // Every built-in mode should have an icon. // TODO: this should be provided by the mode itself + if (!icon && isModeConsideredBuiltIn(currentMode, this._productService)) { + icon = builtinDefaultIcon; + } + + const labelElements = []; + if (icon) { + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + } + if (!isDefault || !icon || !this.pickerOptions.onlyShowIconsForDefaultActions.get()) { + labelElements.push(dom.$('span.chat-input-picker-label', undefined, state)); + } + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + return null; + } +} + +/** + * Returns true if the mode is the built-in 'implement' mode from the chat extension. + * This mode is hidden from the mode picker but available for handoffs. + */ +export function isBuiltinImplementMode(mode: IChatMode, productService: IProductService): boolean { + if (mode.name.get().toLowerCase() !== 'implement') { + return false; + } + if (mode.source?.storage !== PromptsStorage.extension) { + return false; + } + const chatExtensionId = productService.defaultChatAgent?.chatExtensionId; + return !!chatExtensionId && mode.source.extensionId.value === chatExtensionId; +} + +function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductService): boolean { + if (mode.isBuiltin) { + return true; + } + // Not built-in if not from the built-in chat extension + if (mode.source?.storage !== PromptsStorage.extension) { + return false; + } + const chatExtensionId = productService.defaultChatAgent?.chatExtensionId; + if (!chatExtensionId || mode.source.extensionId.value !== chatExtensionId) { + return false; + } + // Organization-provided agents (under /github/ path) are also not considered built-in + const modeUri = mode.uri?.get(); + if (!modeUri) { + // If somehow there is no URI, but it's from the built-in chat extension, consider it built-in + return true; + } + return !isOrganizationPromptFile(modeUri, mode.source.extensionId, productService); +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts new file mode 100644 index 00000000000..4b419fca28c --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; +import { IManagedHoverContent } from '../../../../../../base/browser/ui/hover/hover.js'; +import { renderIcon, renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun, IObservable } from '../../../../../../base/common/observable.js'; +import { localize } from '../../../../../../nls.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { IProductService } from '../../../../../../platform/product/common/productService.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { TelemetryTrustedValue } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { ChatEntitlement, IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; +import { MANAGE_CHAT_COMMAND_ID } from '../../../common/constants.js'; +import { ILanguageModelChatMetadataAndIdentifier } from '../../../common/languageModels.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../../common/widget/input/modelPickerWidget.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; + +export interface IModelPickerDelegate { + readonly currentModel: IObservable; + setModel(model: ILanguageModelChatMetadataAndIdentifier): void; + getModels(): ILanguageModelChatMetadataAndIdentifier[]; +} + +type ChatModelChangeClassification = { + owner: 'lramos15'; + comment: 'Reporting when the model picker is switched'; + fromModel?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The previous chat model' }; + toModel: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The new chat model' }; +}; + +type ChatModelChangeEvent = { + fromModel: string | TelemetryTrustedValue | undefined; + toModel: string | TelemetryTrustedValue; +}; + + +function modelDelegateToWidgetActionsProvider(delegate: IModelPickerDelegate, telemetryService: ITelemetryService, pickerOptions: IChatInputPickerOptions): IActionWidgetDropdownActionProvider { + return { + getActions: () => { + const models = delegate.getModels(); + if (models.length === 0) { + // Show a fake "Auto" entry when no models are available + return [{ + id: 'auto', + enabled: true, + checked: true, + category: DEFAULT_MODEL_PICKER_CATEGORY, + class: undefined, + description: localize('chat.modelPicker.auto.detail', "Best for your request based on capacity and performance."), + tooltip: localize('chat.modelPicker.auto', "Auto"), + label: localize('chat.modelPicker.auto', "Auto"), + hover: { content: localize('chat.modelPicker.auto.description', "Automatically selects the best model for your task based on context and complexity."), position: pickerOptions.hoverPosition }, + run: () => { } + } satisfies IActionWidgetDropdownAction]; + } + return models.map(model => { + const hoverContent = model.metadata.tooltip; + return { + id: model.metadata.id, + enabled: true, + icon: model.metadata.statusIcon, + checked: model.identifier === delegate.currentModel.get()?.identifier, + category: model.metadata.modelPickerCategory || DEFAULT_MODEL_PICKER_CATEGORY, + class: undefined, + description: model.metadata.multiplier ?? model.metadata.detail, + tooltip: hoverContent ? '' : model.metadata.name, + hover: hoverContent ? { content: hoverContent, position: pickerOptions.hoverPosition } : undefined, + label: model.metadata.name, + run: () => { + const previousModel = delegate.currentModel.get(); + telemetryService.publicLog2('chat.modelChange', { + fromModel: previousModel?.metadata.vendor === 'copilot' ? new TelemetryTrustedValue(previousModel.identifier) : 'unknown', + toModel: model.metadata.vendor === 'copilot' ? new TelemetryTrustedValue(model.identifier) : 'unknown' + }); + delegate.setModel(model); + } + } satisfies IActionWidgetDropdownAction; + }); + } + }; +} + +function getModelPickerActionBarActionProvider(commandService: ICommandService, chatEntitlementService: IChatEntitlementService, productService: IProductService): IActionProvider { + + const actionProvider: IActionProvider = { + getActions: () => { + const additionalActions: IAction[] = []; + if ( + chatEntitlementService.entitlement === ChatEntitlement.Free || + chatEntitlementService.entitlement === ChatEntitlement.Pro || + chatEntitlementService.entitlement === ChatEntitlement.ProPlus || + chatEntitlementService.entitlement === ChatEntitlement.Business || + chatEntitlementService.entitlement === ChatEntitlement.Enterprise || + chatEntitlementService.isInternal + ) { + additionalActions.push({ + id: 'manageModels', + label: localize('chat.manageModels', "Manage Models..."), + enabled: true, + tooltip: localize('chat.manageModels.tooltip', "Manage Language Models"), + class: undefined, + run: () => { + commandService.executeCommand(MANAGE_CHAT_COMMAND_ID); + } + }); + } + + // Add sign-in / upgrade option if entitlement is anonymous / free / new user + const isNewOrAnonymousUser = !chatEntitlementService.sentiment.installed || + chatEntitlementService.entitlement === ChatEntitlement.Available || + chatEntitlementService.anonymous || + chatEntitlementService.entitlement === ChatEntitlement.Unknown; + if (isNewOrAnonymousUser || chatEntitlementService.entitlement === ChatEntitlement.Free) { + additionalActions.push({ + id: 'moreModels', + label: isNewOrAnonymousUser ? localize('chat.moreModels', "Add Language Models") : localize('chat.morePremiumModels', "Add Premium Models"), + enabled: true, + tooltip: isNewOrAnonymousUser ? localize('chat.moreModels.tooltip', "Add Language Models") : localize('chat.morePremiumModels.tooltip', "Add Premium Models"), + class: undefined, + run: () => { + const commandId = isNewOrAnonymousUser ? 'workbench.action.chat.triggerSetup' : 'workbench.action.chat.upgradePlan'; + commandService.executeCommand(commandId); + } + }); + } + + return additionalActions; + } + }; + return actionProvider; +} + +/** + * Action view item for selecting a language model in the chat interface. + */ +export class ModelPickerActionItem extends ChatInputPickerActionViewItem { + protected currentModel: ILanguageModelChatMetadataAndIdentifier | undefined; + + constructor( + action: IAction, + widgetOptions: Omit | undefined, + delegate: IModelPickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService, + @IKeybindingService keybindingService: IKeybindingService, + @ITelemetryService telemetryService: ITelemetryService, + @IProductService productService: IProductService, + ) { + // Modify the original action with a different label and make it show the current model + const actionWithLabel: IAction = { + ...action, + label: delegate.currentModel.get()?.metadata.name ?? localize('chat.modelPicker.auto', "Auto"), + run: () => { } + }; + + const modelPickerActionWidgetOptions: Omit = { + actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService, pickerOptions), + actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService), + reporter: { name: 'ChatModelPicker', includeOptions: true }, + }; + + super(actionWithLabel, widgetOptions ?? modelPickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + this.currentModel = delegate.currentModel.get(); + + // Listen for model changes from the delegate + this._register(autorun(t => { + const model = delegate.currentModel.read(t); + this.currentModel = model; + this.updateTooltip(); + if (this.element) { + this.renderLabel(this.element); + } + })); + } + + protected override getHoverContents(): IManagedHoverContent | undefined { + const label = `${localize('chat.modelPicker.label', "Pick Model")}${super.getHoverContents()}`; + const { statusIcon, tooltip } = this.currentModel?.metadata || {}; + return statusIcon && tooltip ? `${label} • ${tooltip}` : label; + } + + protected override setAriaLabelAttributes(element: HTMLElement): void { + super.setAriaLabelAttributes(element); + const modelName = this.currentModel?.metadata.name ?? localize('chat.modelPicker.auto', "Auto"); + element.ariaLabel = localize('chat.modelPicker.ariaLabel', "Pick Model, {0}", modelName); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + const { name, statusIcon } = this.currentModel?.metadata || {}; + const domChildren = []; + + if (statusIcon) { + const iconElement = renderIcon(statusIcon); + domChildren.push(iconElement); + } + + domChildren.push(dom.$('span.chat-input-picker-label', undefined, name ?? localize('chat.modelPicker.auto', "Auto"))); + domChildren.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...domChildren); + this.setAriaLabelAttributes(element); + return null; + } + +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts new file mode 100644 index 00000000000..69721f5bd5b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -0,0 +1,207 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName, isFirstPartyAgentSessionProvider } from '../../agentSessions/agentSessions.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +import { ISessionTypePickerDelegate } from '../../chat.js'; +import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; + +export interface ISessionTypeItem { + type: AgentSessionProviders; + label: string; + hoverDescription: string; + commandId: string; +} + +const firstPartyCategory = { label: localize('chat.sessionTarget.category.agent', "Agent Types"), order: 1 }; +const otherCategory = { label: localize('chat.sessionTarget.category.other', "Other"), order: 2 }; + +/** + * Action view item for selecting a session target in the chat interface. + * This picker allows switching between different chat session types for new/empty sessions. + */ +export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem { + private _sessionTypeItems: ISessionTypeItem[] = []; + + constructor( + action: MenuItemAction, + protected readonly chatSessionPosition: 'sidebar' | 'editor', + protected readonly delegate: ISessionTypePickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService protected readonly keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @IChatSessionsService protected readonly chatSessionsService: IChatSessionsService, + @ICommandService protected readonly commandService: ICommandService, + @IOpenerService protected readonly openerService: IOpenerService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const currentType = this._getSelectedSessionType(); + + const actions: IActionWidgetDropdownAction[] = [...this._getAdditionalActions().map(a => ({ ...action, ...a }))]; + for (const sessionTypeItem of this._sessionTypeItems) { + if (!this._isVisible(sessionTypeItem.type)) { + continue; + } + + actions.push({ + ...action, + id: sessionTypeItem.commandId, + label: sessionTypeItem.label, + checked: currentType === sessionTypeItem.type, + icon: getAgentSessionProviderIcon(sessionTypeItem.type), + enabled: this._isSessionTypeEnabled(sessionTypeItem.type), + category: this._getSessionCategory(sessionTypeItem), + description: this._getSessionDescription(sessionTypeItem), + tooltip: '', + hover: { content: sessionTypeItem.hoverDescription, position: this.pickerOptions.hoverPosition }, + run: async () => { + this._run(sessionTypeItem); + }, + }); + } + + return actions; + } + }; + + const actionBarActionProvider: IActionProvider = { + getActions: () => { + return [this._getLearnMore()]; + } + }; + + const sessionTargetPickerOptions: Omit = { + actionProvider, + actionBarActionProvider, + showItemKeybindings: true, + reporter: { name: `ChatSessionTypePicker`, includeOptions: true }, + }; + + super(action, sessionTargetPickerOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + + this._updateAgentSessionItems(); + this._register(this.chatSessionsService.onDidChangeAvailability(() => { + this._updateAgentSessionItems(); + })); + } + + protected _run(sessionTypeItem: ISessionTypeItem): void { + if (this.delegate.setActiveSessionProvider) { + // Use provided setter (for welcome view) + this.delegate.setActiveSessionProvider(sessionTypeItem.type); + } else { + // Execute command to create new session + this.commandService.executeCommand(sessionTypeItem.commandId, this.chatSessionPosition); + } + if (this.element) { + this.renderLabel(this.element); + } + } + + protected _getSelectedSessionType(): AgentSessionProviders | undefined { + return this.delegate.getActiveSessionProvider(); + } + + protected _getAdditionalActions(): IActionWidgetDropdownAction[] { + return []; + } + + protected _getLearnMore(): IAction { + const learnMoreUrl = 'https://code.visualstudio.com/docs/copilot/agents/overview'; + return { + id: 'workbench.action.chat.agentOverview.learnMore', + label: localize('chat.learnMoreAgentTypes', "Learn about agent types..."), + tooltip: learnMoreUrl, + class: undefined, + enabled: true, + run: async () => { + await this.openerService.open(URI.parse(learnMoreUrl)); + } + }; + } + + private _updateAgentSessionItems(): void { + const localSessionItem: ISessionTypeItem = { + type: AgentSessionProviders.Local, + label: getAgentSessionProviderName(AgentSessionProviders.Local), + hoverDescription: getAgentSessionProviderDescription(AgentSessionProviders.Local), + commandId: `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.Local}`, + }; + + const agentSessionItems: ISessionTypeItem[] = [localSessionItem]; + + const contributions = this.chatSessionsService.getAllChatSessionContributions(); + for (const contribution of contributions) { + const agentSessionType = getAgentSessionProvider(contribution.type); + if (!agentSessionType) { + continue; + } + + agentSessionItems.push({ + type: agentSessionType, + label: getAgentSessionProviderName(agentSessionType), + hoverDescription: getAgentSessionProviderDescription(agentSessionType), + commandId: contribution.canDelegate ? + `workbench.action.chat.openNewChatSessionInPlace.${contribution.type}` : + `workbench.action.chat.openNewChatSessionExternal.${contribution.type}`, + }); + } + this._sessionTypeItems = agentSessionItems; + } + + protected _isVisible(type: AgentSessionProviders): boolean { + return true; + } + + protected _isSessionTypeEnabled(type: AgentSessionProviders): boolean { + return true; + } + + protected _getSessionCategory(sessionTypeItem: ISessionTypeItem) { + return isFirstPartyAgentSessionProvider(sessionTypeItem.type) ? firstPartyCategory : otherCategory; + } + + protected _getSessionDescription(sessionTypeItem: ISessionTypeItem): string | undefined { + return undefined; + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + const currentType = this._getSelectedSessionType(); + + const label = getAgentSessionProviderName(currentType ?? AgentSessionProviders.Local); + const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local); + + const labelElements = []; + labelElements.push(...renderLabelWithIcons(`$(${icon.id})`)); + if (currentType !== AgentSessionProviders.Local || !this.pickerOptions.onlyShowIconsForDefaultActions.get()) { + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + } + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + + return null; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts new file mode 100644 index 00000000000..c3ac32a9250 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; + +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { basename } from '../../../../../../base/common/resources.js'; +import { localize } from '../../../../../../nls.js'; +import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js'; +import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js'; +import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js'; +import { IWorkspacePickerDelegate } from '../../chat.js'; +import { IActionProvider } from '../../../../../../base/browser/ui/dropdown/dropdown.js'; + +/** + * Action view item for selecting a target workspace in the chat interface. + * This picker allows selecting a recent workspace to run the chat request in, + * which is useful for empty window contexts. + */ +export class WorkspacePickerActionItem extends ChatInputPickerActionViewItem { + + constructor( + action: MenuItemAction, + private readonly delegate: IWorkspacePickerDelegate, + pickerOptions: IChatInputPickerOptions, + @IActionWidgetService actionWidgetService: IActionWidgetService, + @IKeybindingService keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService private readonly commandService: ICommandService, + @ITelemetryService telemetryService: ITelemetryService, + ) { + const actionProvider: IActionWidgetDropdownActionProvider = { + getActions: () => { + const currentWorkspace = this.delegate.getSelectedWorkspace(); + const workspaces = this.delegate.getWorkspaces(); + + const actions: IActionWidgetDropdownAction[] = workspaces.map(workspace => ({ + ...action, + id: `workspace.${workspace.uri.toString()}`, + label: workspace.label, + checked: currentWorkspace?.uri.toString() === workspace.uri.toString(), + icon: workspace.isFolder ? { id: 'folder' } : { id: 'file-symlink-directory' }, + enabled: true, + tooltip: workspace.uri.fsPath, + run: async () => { + this.delegate.setSelectedWorkspace(workspace); + if (this.element) { + this.renderLabel(this.element); + } + }, + })); + + // Add "Open Folder..." option + actions.push({ + ...action, + id: 'workspace.openFolder', + label: localize('openFolder', "Open Folder..."), + checked: false, + enabled: true, + tooltip: localize('openFolderTooltip', "Open Folder..."), + run: async () => { + this.commandService.executeCommand(this.delegate.openFolderCommand); + }, + }); + + return actions; + } + }; + + const actionBarActionProvider: IActionProvider = { + getActions: () => [] + }; + + const workspacePickerOptions: Omit = { + actionProvider, + actionBarActionProvider, + showItemKeybindings: false, + reporter: { name: 'ChatWorkspacePicker', includeOptions: false }, + }; + + super(action, workspacePickerOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService, telemetryService); + + this._register(this.delegate.onDidChangeSelectedWorkspace(() => { + if (this.element) { + this.renderLabel(this.element); + } + })); + + this._register(this.delegate.onDidChangeWorkspaces(() => { + // Re-render when workspaces list changes + if (this.element) { + this.renderLabel(this.element); + } + })); + } + + protected override renderLabel(element: HTMLElement): IDisposable | null { + this.setAriaLabelAttributes(element); + const currentWorkspace = this.delegate.getSelectedWorkspace(); + + const labelElements: (string | HTMLElement)[] = []; + + if (currentWorkspace) { + // Show the workspace label or folder name + const label = currentWorkspace.label || basename(currentWorkspace.uri); + labelElements.push(...renderLabelWithIcons(`$(folder)`)); + labelElements.push(dom.$('span.chat-input-picker-label', undefined, label)); + } else { + labelElements.push(...renderLabelWithIcons(`$(folder)`)); + labelElements.push(dom.$('span.chat-input-picker-label', undefined, localize('selectWorkspace', "Workspace"))); + } + + labelElements.push(...renderLabelWithIcons(`$(chevron-down)`)); + + dom.reset(element, ...labelElements); + + return null; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css similarity index 100% rename from src/vs/workbench/contrib/chat/browser/media/chat.css rename to src/vs/workbench/contrib/chat/browser/widget/media/chat.css diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatAgentHover.css similarity index 100% rename from src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css rename to src/vs/workbench/contrib/chat/browser/widget/media/chatAgentHover.css diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css new file mode 100644 index 00000000000..17c3b7dd1c2 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chatViewWelcome.css @@ -0,0 +1,236 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.pane-body.chat-view-welcome-visible { + + & > .interactive-session { + display: none; + } + + & > .chat-view-welcome { + display: flex; + } +} + +.interactive-session.chat-view-getting-started-disabled { + + /* hide most welcome pieces (except suggested actions) when we show recent sessions to make some space */ + .chat-welcome-view .chat-welcome-view-icon, + .chat-welcome-view .chat-welcome-view-title, + .chat-welcome-view .chat-welcome-view-message, + .chat-welcome-view .chat-welcome-view-disclaimer, + .chat-welcome-view .chat-welcome-view-tips { + visibility: hidden; + } +} + +/* Chat welcome container */ +.interactive-session .chat-welcome-view-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + flex: 1; + position: relative; /* Allow absolute positioning of prompts */ +} + +/* Container for ChatViewPane welcome view */ +.pane-body > .chat-view-welcome { + flex-direction: column; + justify-content: center; + overflow: hidden; + height: 100%; + display: none; +} + +div.chat-welcome-view { + display: flex; + flex-direction: column; + align-items: center; + padding: 12px; + + & > .chat-welcome-view-icon { + min-height: 48px; + } + + & > .chat-welcome-view-icon .codicon { + font-size: 40px; + } + + & > .chat-welcome-view-icon.custom-icon { + mask-size: 40px; + -webkit-mask-size: 40px; + } + + & > .chat-welcome-view-icon.large-icon { + min-width: 72px; + min-height: 72px; + } + + & > .chat-welcome-view-icon.large-icon .codicon { + font-size: 72px; + width: 72px; + height: 72px; + } + + & > .chat-welcome-view-icon.large-icon.custom-icon { + mask-size: 72px !important; + -webkit-mask-size: 72px !important; + width: 72px; + height: 72px; + } + + & > .chat-welcome-view-title { + font-size: 24px; + margin-top: 5px; + text-align: center; + line-height: normal; + padding: 0 8px; + } + + & > .chat-welcome-view-message { + position: relative; + text-align: center; + max-width: 100%; + padding: 0 20px; + margin: 0 auto; + color: var(--vscode-foreground); + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover, + a:focus { + text-decoration: underline; + outline: 1px solid var(--vscode-focusBorder); + } + + p { + margin-top: 8px; + margin-bottom: 8px; + } + } + + .monaco-button { + display: inline-block; + width: initial; + } + + & > .chat-welcome-view-tips { + max-width: 250px; + margin: 10px 5px 0px; + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover, + a:focus { + text-decoration: underline; + outline: 1px solid var(--vscode-focusBorder); + } + + .rendered-markdown { + gap: 6px; + display: flex; + align-items: start; + flex-direction: column; + } + + .rendered-markdown p { + display: flex; + gap: 6px; + margin: 6px 0 0 0; + + .codicon { + padding-top: 1px; + } + } + } + + & > .chat-welcome-view-disclaimer { + color: var(--vscode-foreground); + text-align: center; + margin: -16px auto; + max-width: 400px; + padding: 0 12px; + + a { + color: var(--vscode-textLink-foreground); + } + + a:hover, + a:focus { + text-decoration: underline; + outline: 1px solid var(--vscode-focusBorder); + } + } +} + +/* Suggested prompts section - positioned at bottom of welcome container */ +.chat-welcome-view-suggested-prompts { + position: absolute; + bottom: 0; + left: 0; + right: 0; + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + gap: 8px; + /* Extra top padding for title */ + padding: 32px 16px 8px 16px; + /* Avoids bleeding into other content since we use absolute positioning */ + background: var(--vscode-chat-list-background); + + .chat-welcome-view-suggested-prompts-title { + position: absolute; + top: 8px; + left: 16px; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--vscode-descriptionForeground); + margin: 0; + text-align: left; + } + + > .chat-welcome-view-suggested-prompt { + display: flex; + align-items: center; + gap: 6px; + height: 24px; + padding: 0 8px; + border-radius: 4px; + background-color: var(--vscode-editorWidget-background); + cursor: pointer; + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + width: fit-content; + margin: 0; + box-sizing: border-box; + flex: 0 0 auto; + + & > .chat-welcome-view-suggested-prompt-title { + font-size: 13px; + color: var(--vscode-editorWidget-foreground); + white-space: nowrap; + } + + & > .chat-welcome-view-suggested-prompt-description { + font-size: 13px; + color: var(--vscode-descriptionForeground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 0 1 auto; + min-width: 0; + } + } + + > .chat-welcome-view-suggested-prompt:hover { + background-color: var(--vscode-list-hoverBackground); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts similarity index 77% rename from src/vs/workbench/contrib/chat/browser/chatQuick.ts rename to src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts index 175fcc7cd89..fda57a33cdc 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/chatQuick.ts @@ -3,35 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as dom from '../../../../base/browser/dom.js'; -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 { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; -import { localize } from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; -import product from '../../../../platform/product/common/product.js'; -import { IQuickInputService, IQuickWidget } from '../../../../platform/quickinput/common/quickInput.js'; -import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../platform/theme/common/colorRegistry.js'; -import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; -import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; -import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { ChatModel, isCellTextEditOperationArray } from '../common/chatModel.js'; -import { ChatMode } from '../common/chatModes.js'; -import { IParsedChatRequest } from '../common/chatParserTypes.js'; -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 * as dom from '../../../../../base/browser/dom.js'; +import { Orientation, Sash } from '../../../../../base/browser/ui/sash/sash.js'; +import { disposableTimeout } from '../../../../../base/common/async.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Selection } from '../../../../../editor/common/core/selection.js'; +import { localize } from '../../../../../nls.js'; +import { MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; +import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js'; +import product from '../../../../../platform/product/common/product.js'; +import { IQuickInputService, IQuickWidget } from '../../../../../platform/quickinput/common/quickInput.js'; +import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../../platform/theme/common/colorRegistry.js'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../../common/theme.js'; +import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; +import { isCellTextEditOperationArray } from '../../common/model/chatModel.js'; +import { ChatMode } from '../../common/chatModes.js'; +import { IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js'; +import { IChatModelReference, IChatProgress, IChatService } from '../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../common/constants.js'; +import { IChatWidgetService, IQuickChatOpenOptions, IQuickChatService } from '../chat.js'; +import { ChatWidget } from '../widget/chatWidget.js'; export class QuickChatService extends Disposable implements IQuickChatService { readonly _serviceBrand: undefined; @@ -64,6 +63,10 @@ export class QuickChatService extends Disposable implements IQuickChatService { return dom.isAncestorOfActiveElement(widget); } + get sessionResource(): URI | undefined { + return this._input && this._currentChat?.sessionResource; + } + toggle(options?: IQuickChatOpenOptions): void { // If the input is already shown, hide it. This provides a toggle behavior of the quick // pick. This should not happen when there is a query. @@ -151,28 +154,32 @@ class QuickChat extends Disposable { private widget!: ChatWidget; private sash!: Sash; - private model: ChatModel | undefined; - private _currentQuery: string | undefined; + private modelRef: IChatModelReference | undefined; private readonly maintainScrollTimer: MutableDisposable = this._register(new MutableDisposable()); private _deferUpdatingDynamicLayout: boolean = false; + public get sessionResource() { + return this.modelRef?.object.sessionResource; + } + constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IChatService private readonly chatService: IChatService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IViewsService private readonly viewsService: IViewsService, + @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, @IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService, ) { super(); } - clear() { - this.model?.dispose(); - this.model = undefined; + private clear() { + this.modelRef?.dispose(); + this.modelRef = undefined; this.updateModel(); this.widget.inputEditor.setValue(''); + return Promise.resolve(); } focus(selection?: Selection): void { @@ -236,7 +243,8 @@ class QuickChat extends Disposable { renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide, telemetrySource: 'chatQuick' }, enableImplicitContext: true, - defaultMode: ChatMode.Ask + defaultMode: ChatMode.Ask, + clear: () => this.clear(), }, { listForeground: quickInputForeground, @@ -291,10 +299,6 @@ class QuickChat extends Disposable { this._deferUpdatingDynamicLayout = true; } })); - this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { - this._currentQuery = this.widget.inputEditor.getValue(); - })); - this._register(this.widget.onDidClear(() => this.clear())); this._register(this.widget.onDidChangeHeight((e) => this.sash.layout())); const width = parent.offsetWidth; this._register(this.sash.onDidStart(() => { @@ -318,12 +322,13 @@ class QuickChat extends Disposable { } async openChatView(): Promise { - const widget = await showChatView(this.viewsService, this.layoutService); - if (!widget?.viewModel || !this.model) { + const widget = await this.chatWidgetService.revealWidget(); + const model = this.modelRef?.object; + if (!widget?.viewModel || !model) { return; } - for (const request of this.model.getRequests()) { + for (const request of model.getRequests()) { if (request.response?.response.value || request.response?.result) { @@ -372,9 +377,9 @@ class QuickChat extends Disposable { } } - const value = this.widget.inputEditor.getValue(); + const value = this.widget.getViewState(); if (value) { - widget.inputEditor.setValue(value); + widget.viewModel.model.inputModel.setState(value); } widget.focusInput(); } @@ -389,11 +394,19 @@ class QuickChat extends Disposable { } private updateModel(): void { - this.model ??= this.chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None); - if (!this.model) { + this.modelRef ??= this.chatService.startSession(ChatAgentLocation.Chat, { disableBackgroundKeepAlive: true }); + const model = this.modelRef?.object; + if (!model) { throw new Error('Could not start chat session'); } - this.widget.setModel(this.model, { inputValue: this._currentQuery }); + this.modelRef.object.inputModel.setState({ inputText: '', selections: [] }); + this.widget.setModel(model); + } + + override dispose(): void { + this.modelRef?.dispose(); + this.modelRef = undefined; + super.dispose(); } } diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts new file mode 100644 index 00000000000..fffb5a27aa8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditor.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../../../base/browser/dom.js'; +import { renderIcon } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { raceCancellationError } from '../../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import * as nls from '../../../../../../nls.js'; +import { IContextKeyService, IScopedContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IEditorOptions } from '../../../../../../platform/editor/common/editor.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { editorBackground, editorForeground, inputBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; +import { EditorPane } from '../../../../../browser/parts/editor/editorPane.js'; +import { IEditorOpenContext } from '../../../../../common/editor.js'; +import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../../../common/theme.js'; +import { IEditorGroup } from '../../../../../services/editor/common/editorGroupsService.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { IChatModel, IChatModelInputState, IExportableChatData, ISerializableChatData } from '../../../common/model/chatModel.js'; +import { IChatService } from '../../../common/chatService/chatService.js'; +import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { clearChatEditor } from '../../actions/chatClear.js'; +import { ChatEditorInput } from './chatEditorInput.js'; +import { ChatWidget } from '../../widget/chatWidget.js'; + +export interface IChatEditorOptions extends IEditorOptions { + /** + * Input state of the model when the editor is opened. Currently needed since + * new sessions are not persisted but may go away with + * https://github.com/microsoft/vscode/pull/278476 as input state is stored on the model. + */ + modelInputState?: IChatModelInputState; + target?: { data: IExportableChatData | ISerializableChatData }; + title?: { + preferred?: string; + fallback?: string; + }; +} + +export class ChatEditor extends EditorPane { + private _widget!: ChatWidget; + public get widget(): ChatWidget { + return this._widget; + } + private _scopedContextKeyService!: IScopedContextKeyService; + override get scopedContextKeyService() { + return this._scopedContextKeyService; + } + + private dimension = new dom.Dimension(0, 0); + private _loadingContainer: HTMLElement | undefined; + private _editorContainer: HTMLElement | undefined; + + constructor( + group: IEditorGroup, + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + ) { + super(ChatEditorInput.EditorID, group, telemetryService, themeService, storageService); + } + + private async clear() { + if (this.input) { + return this.instantiationService.invokeFunction(clearChatEditor, this.input as ChatEditorInput); + } + } + + protected override createEditor(parent: HTMLElement): void { + this._editorContainer = parent; + // Ensure the container has position relative for the loading overlay + parent.classList.add('chat-editor-relative'); + this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + ChatContextKeys.inChatEditor.bindTo(this._scopedContextKeyService).set(true); + + this._widget = this._register( + scopedInstantiationService.createInstance( + ChatWidget, + ChatAgentLocation.Chat, + undefined, + { + autoScroll: mode => mode !== ChatModeKind.Ask, + renderFollowups: true, + supportsFileReferences: true, + clear: () => this.clear(), + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return true; + }, + referencesExpandedWhenEmptyResponse: false, + progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask, + }, + enableImplicitContext: true, + enableWorkingSet: 'explicit', + supportsChangingModes: true, + }, + { + listForeground: editorForeground, + listBackground: editorBackground, + overlayBackground: EDITOR_DRAG_AND_DROP_BACKGROUND, + inputEditorBackground: inputBackground, + resultEditorBackground: editorBackground + })); + this._register(this.widget.onDidSubmitAgent(() => { + this.group.pinEditor(this.input); + })); + this.widget.render(parent); + this.widget.setVisible(true); + } + + protected override setEditorVisible(visible: boolean): void { + super.setEditorVisible(visible); + + this.widget?.setVisible(visible); + + if (visible && this.widget) { + this.widget.layout(this.dimension.height, this.dimension.width); + } + } + + public override focus(): void { + super.focus(); + + this.widget?.focusInput(); + } + + override clearInput(): void { + this.saveState(); + this.widget.setModel(undefined); + super.clearInput(); + } + + private showLoadingInChatWidget(message: string): void { + if (!this._editorContainer) { + return; + } + + // If already showing, just update text + if (this._loadingContainer) { + // eslint-disable-next-line no-restricted-syntax + const existingText = this._loadingContainer.querySelector('.chat-loading-content span'); + if (existingText) { + existingText.textContent = message; + return; // aria-live will announce the text change + } + this.hideLoadingInChatWidget(); // unexpected structure + } + + // Mark container busy for assistive technologies + this._editorContainer.setAttribute('aria-busy', 'true'); + + this._loadingContainer = dom.append(this._editorContainer, dom.$('.chat-loading-overlay')); + // Accessibility: announce loading state politely without stealing focus + this._loadingContainer.setAttribute('role', 'status'); + this._loadingContainer.setAttribute('aria-live', 'polite'); + // Rely on live region text content instead of aria-label to avoid duplicate announcements + this._loadingContainer.tabIndex = -1; // ensure it isn't focusable + const loadingContent = dom.append(this._loadingContainer, dom.$('.chat-loading-content')); + const spinner = renderIcon(ThemeIcon.modify(Codicon.loading, 'spin')); + spinner.setAttribute('aria-hidden', 'true'); + loadingContent.appendChild(spinner); + const text = dom.append(loadingContent, dom.$('span')); + text.textContent = message; + } + + private hideLoadingInChatWidget(): void { + if (this._loadingContainer) { + this._loadingContainer.remove(); + this._loadingContainer = undefined; + } + if (this._editorContainer) { + this._editorContainer.removeAttribute('aria-busy'); + } + } + + override async setInput(input: ChatEditorInput, options: IChatEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // Show loading indicator early for non-local sessions to prevent layout shifts + let isContributedChatSession = false; + const chatSessionType = input.getSessionType(); + if (chatSessionType !== localChatSessionType) { + const loadingMessage = nls.localize('chatEditor.loadingSession', "Loading..."); + this.showLoadingInChatWidget(loadingMessage); + } + + await super.setInput(input, options, context, token); + if (token.isCancellationRequested) { + this.hideLoadingInChatWidget(); + return; + } + + if (!this.widget) { + throw new Error('ChatEditor lifecycle issue: no editor widget'); + } + + if (chatSessionType !== localChatSessionType) { + try { + await raceCancellationError(this.chatSessionsService.canResolveChatSession(input.resource), token); + const contributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = contributions.find(c => c.type === chatSessionType); + if (contribution) { + this.widget.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type); + isContributedChatSession = true; + } else { + this.widget.unlockFromCodingAgent(); + } + } catch (error) { + this.hideLoadingInChatWidget(); + throw error; + } + } else { + this.widget.unlockFromCodingAgent(); + } + + try { + const editorModel = await raceCancellationError(input.resolve(), token); + + if (!editorModel) { + throw new Error(`Failed to get model for chat editor. resource: ${input.sessionResource}`); + } + + // Hide loading state before updating model + if (chatSessionType !== localChatSessionType) { + this.hideLoadingInChatWidget(); + } + + if (options?.modelInputState) { + editorModel.model.inputModel.setState(options.modelInputState); + } + + this.updateModel(editorModel.model); + + if (isContributedChatSession && options?.title?.preferred && input.sessionResource) { + this.chatService.setChatSessionTitle(input.sessionResource, options.title.preferred); + } + } catch (error) { + this.hideLoadingInChatWidget(); + throw error; + } + } + + private updateModel(model: IChatModel): void { + this.widget.setModel(model); + } + + override layout(dimension: dom.Dimension, position?: dom.IDomPosition | undefined): void { + this.dimension = dimension; + if (this.widget) { + this.widget.layout(dimension.height, dimension.width); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts new file mode 100644 index 00000000000..708621ded2b --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/editor/chatEditorInput.ts @@ -0,0 +1,405 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; +import { truncate } from '../../../../../../base/common/strings.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import * as nls from '../../../../../../nls.js'; +import { ConfirmResult, IDialogService } from '../../../../../../platform/dialogs/common/dialogs.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { registerIcon } from '../../../../../../platform/theme/common/iconRegistry.js'; +import { EditorInputCapabilities, IEditorIdentifier, IEditorSerializer, IUntypedEditorInput, Verbosity } from '../../../../../common/editor.js'; +import { EditorInput, IEditorCloseHandler } from '../../../../../common/editor/editorInput.js'; +import { IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; +import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { ChatAgentLocation, ChatEditorTitleMaxLength } from '../../../common/constants.js'; +import { IChatEditingSession, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { IChatModel } from '../../../common/model/chatModel.js'; +import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; +import { IClearEditingSessionConfirmationOptions } from '../../actions/chatActions.js'; +import type { IChatEditorOptions } from './chatEditor.js'; + +const ChatEditorIcon = registerIcon('chat-editor-label-icon', Codicon.chatSparkle, nls.localize('chatEditorLabelIcon', 'Icon of the chat editor label.')); + +export class ChatEditorInput extends EditorInput implements IEditorCloseHandler { + static readonly TypeID: string = 'workbench.input.chatSession'; + static readonly EditorID: string = 'workbench.editor.chatSession'; + + private _sessionResource: URI | undefined; + + /** + * Get the uri of the session this editor input is associated with. + * + * This should be preferred over using `resource` directly, as it handles cases where a chat editor becomes a session + */ + public get sessionResource(): URI | undefined { return this._sessionResource; } + + private didTransferOutEditingSession = false; + private cachedIcon: ThemeIcon | URI | undefined; + + private readonly modelRef = this._register(new MutableDisposable()); + + private get model(): IChatModel | undefined { + return this.modelRef.value?.object; + } + + static getNewEditorUri(): URI { + return ChatEditorUri.getNewEditorUri(); + } + + constructor( + readonly resource: URI, + readonly options: IChatEditorOptions, + @IChatService private readonly chatService: IChatService, + @IDialogService private readonly dialogService: IDialogService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + ) { + super(); + + if (resource.scheme === Schemas.vscodeChatEditor) { + const parsed = ChatEditorUri.parse(resource); + if (!parsed || typeof parsed !== 'number') { + throw new Error('Invalid chat URI'); + } + } else if (resource.scheme === Schemas.vscodeLocalChatSession) { + const localSessionId = LocalChatSessionUri.parseLocalSessionId(resource); + if (!localSessionId) { + throw new Error('Invalid local chat session URI'); + } + this._sessionResource = resource; + } else { + this._sessionResource = resource; + } + } + + override closeHandler = this; + + showConfirm(): boolean { + return !!(this.model && shouldShowClearEditingSessionConfirmation(this.model)); + } + + transferOutEditingSession(): IChatEditingSession | undefined { + this.didTransferOutEditingSession = true; + return this.model?.editingSession; + } + + async confirm(editors: ReadonlyArray): Promise { + if (!this.model?.editingSession || this.didTransferOutEditingSession || this.getSessionType() !== localChatSessionType) { + return ConfirmResult.SAVE; + } + + const titleOverride = nls.localize('chatEditorConfirmTitle', "Close Chat Editor"); + const messageOverride = nls.localize('chat.startEditing.confirmation.pending.message.default', "Closing the chat editor will end your current edit session."); + const result = await showClearEditingSessionConfirmation(this.model, this.dialogService, { titleOverride, messageOverride }); + return result ? ConfirmResult.SAVE : ConfirmResult.CANCEL; + } + + override get editorId(): string | undefined { + return ChatEditorInput.EditorID; + } + + override get capabilities(): EditorInputCapabilities { + return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.CanDropIntoEditor; + } + + override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { + if (!(otherInput instanceof ChatEditorInput)) { + return false; + } + + return isEqual(this.sessionResource, otherInput.sessionResource); + } + + override get typeId(): string { + return ChatEditorInput.TypeID; + } + + override getName(): string { + // If we have a resolved model, use its title + if (this.model?.title) { + // Only truncate if the default title is being used (don't truncate custom titles) + return this.model.hasCustomTitle ? this.model.title : truncate(this.model.title, ChatEditorTitleMaxLength); + } + + // If we have a sessionId but no resolved model, try to get the title from persisted sessions + if (this._sessionResource) { + // First try the active session registry + const existingSession = this.chatService.getSession(this._sessionResource); + if (existingSession?.title) { + return existingSession.title; + } + + // If not in active registry, try persisted session data + const persistedTitle = this.chatService.getSessionTitle(this._sessionResource); + if (persistedTitle && persistedTitle.trim()) { // Only use non-empty persisted titles + return persistedTitle; + } + } + + // If a preferred title was provided in options, use it + if (this.options.title?.preferred) { + return this.options.title.preferred; + } + + // Fall back to default naming pattern + return this.options.title?.fallback ?? nls.localize('chatEditorName', "Chat"); + } + + override getTitle(verbosity?: Verbosity): string { + const name = this.getName(); + if (verbosity === Verbosity.LONG) { // Verbosity LONG is used for tooltips + const sessionTypeDisplayName = this.getSessionTypeDisplayName(); + if (sessionTypeDisplayName) { + return `${name} | ${sessionTypeDisplayName}`; + } + } + return name; + } + + private getSessionTypeDisplayName(): string | undefined { + const sessionType = this.getSessionType(); + if (sessionType === localChatSessionType) { + return; + } + const contributions = this.chatSessionsService.getAllChatSessionContributions(); + const contribution = contributions.find(c => c.type === sessionType); + return contribution?.displayName; + } + + override getIcon(): ThemeIcon | URI | undefined { + const resolvedIcon = this.resolveIcon(); + if (resolvedIcon) { + this.cachedIcon = resolvedIcon; + return resolvedIcon; + } + + // Fall back to default icon + return ChatEditorIcon; + } + + private resolveIcon(): ThemeIcon | URI | undefined { + // TODO@osortega,@rebornix double check: Chat Session Item icon is reserved for chat session list and deprecated for chat session status. thus here we use session type icon. We may want to show status for the Editor Title. + const sessionType = this.getSessionType(); + if (sessionType !== localChatSessionType) { + const typeIcon = this.chatSessionsService.getIconForSessionType(sessionType); + if (typeIcon) { + return typeIcon; + } + } + + return undefined; + } + + /** + * Returns chat session type from a URI, or {@linkcode localChatSessionType} if not specified or cannot be determined. + */ + public getSessionType(): string { + return getChatSessionType(this.resource); + } + + override async resolve(): Promise { + const searchParams = new URLSearchParams(this.resource.query); + const chatSessionType = searchParams.get('chatSessionType'); + const inputType = chatSessionType ?? this.resource.authority; + + if (this._sessionResource) { + this.modelRef.value = await this.chatService.loadSessionForResource(this._sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + + // For local session only, if we find no existing session, create a new one + if (!this.model && LocalChatSessionUri.parseLocalSessionId(this._sessionResource)) { + this.modelRef.value = this.chatService.startSession(ChatAgentLocation.Chat, { canUseTools: true }); + } + } else if (!this.options.target) { + this.modelRef.value = this.chatService.startSession(ChatAgentLocation.Chat, { canUseTools: !inputType }); + } else if (this.options.target.data) { + this.modelRef.value = this.chatService.loadSessionFromContent(this.options.target.data); + } + + if (!this.model || this.isDisposed()) { + return null; + } + + this._sessionResource = this.model.sessionResource; + + this._register(this.model.onDidChange((e) => { + // Invalidate icon cache when label changes + this.cachedIcon = undefined; + this._onDidChangeLabel.fire(); + })); + + // Check if icon has changed after model resolution + const newIcon = this.resolveIcon(); + if (newIcon && (!this.cachedIcon || !this.iconsEqual(this.cachedIcon, newIcon))) { + this.cachedIcon = newIcon; + } + + this._onDidChangeLabel.fire(); + + return this._register(new ChatEditorModel(this.model)); + } + + private iconsEqual(a: ThemeIcon | URI, b: ThemeIcon | URI): boolean { + if (ThemeIcon.isThemeIcon(a) && ThemeIcon.isThemeIcon(b)) { + return a.id === b.id; + } + if (a instanceof URI && b instanceof URI) { + return a.toString() === b.toString(); + } + return false; + } + +} + +export class ChatEditorModel extends Disposable { + private _isResolved = false; + + constructor( + readonly model: IChatModel + ) { super(); } + + async resolve(): Promise { + this._isResolved = true; + } + + isResolved(): boolean { + return this._isResolved; + } + + isDisposed(): boolean { + return this._store.isDisposed; + } +} + + +namespace ChatEditorUri { + + const scheme = Schemas.vscodeChatEditor; + + export function getNewEditorUri(): URI { + const handle = Math.floor(Math.random() * 1e9); + return URI.from({ scheme, path: `chat-${handle}` }); + } + + export function parse(resource: URI): number | undefined { + if (resource.scheme !== scheme) { + return undefined; + } + + const match = resource.path.match(/chat-(\d+)/); + const handleStr = match?.[1]; + if (typeof handleStr !== 'string') { + return undefined; + } + + const handle = parseInt(handleStr); + if (isNaN(handle)) { + return undefined; + } + + return handle; + } +} + +interface ISerializedChatEditorInput { + readonly options: IChatEditorOptions; + readonly resource: URI; + readonly sessionResource: URI | undefined; +} + +export class ChatEditorInputSerializer implements IEditorSerializer { + canSerialize(input: EditorInput): input is ChatEditorInput { + return input instanceof ChatEditorInput && !!input.sessionResource; + } + + serialize(input: EditorInput): string | undefined { + if (!this.canSerialize(input)) { + return undefined; + } + + const obj: ISerializedChatEditorInput = { + options: input.options, + sessionResource: input.sessionResource, + resource: input.resource, + + }; + return JSON.stringify(obj); + } + + deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined { + try { + // Old inputs have a session id for local session + const parsed: ISerializedChatEditorInput & { readonly sessionId: string | undefined } = JSON.parse(serializedEditor); + + // First if we have a modern session resource, use that + if (parsed.sessionResource) { + const sessionResource = URI.revive(parsed.sessionResource); + return instantiationService.createInstance(ChatEditorInput, sessionResource, parsed.options); + } + + // Otherwise check to see if we're a chat editor with a local session id + let resource = URI.revive(parsed.resource); + if (resource.scheme === Schemas.vscodeChatEditor && parsed.sessionId) { + resource = LocalChatSessionUri.forSession(parsed.sessionId); + } + + return instantiationService.createInstance(ChatEditorInput, resource, parsed.options); + } catch (err) { + return undefined; + } + } +} + +export async function showClearEditingSessionConfirmation(model: IChatModel, dialogService: IDialogService, options?: IClearEditingSessionConfirmationOptions): Promise { + const undecidedEdits = shouldShowClearEditingSessionConfirmation(model, options); + if (!undecidedEdits) { + return true; // safe to dispose without confirmation + } + + const defaultPhrase = nls.localize('chat.startEditing.confirmation.pending.message.default1', "Starting a new chat will end your current edit session."); + const defaultTitle = nls.localize('chat.startEditing.confirmation.title', "Start new chat?"); + const phrase = options?.messageOverride ?? defaultPhrase; + const title = options?.titleOverride ?? defaultTitle; + + const { result } = await dialogService.prompt({ + title, + message: phrase + ' ' + nls.localize('chat.startEditing.confirmation.pending.message.2', "Do you want to keep pending edits to {0} files?", undecidedEdits), + type: 'info', + cancelButton: true, + buttons: [ + { + label: nls.localize('chat.startEditing.confirmation.acceptEdits', "Keep & Continue"), + run: async () => { + await model.editingSession!.accept(); + return true; + } + }, + { + label: nls.localize('chat.startEditing.confirmation.discardEdits', "Undo & Continue"), + run: async () => { + await model.editingSession!.reject(); + return true; + } + } + ], + }); + + return Boolean(result); +} + +/** Returns the number of files in the model's modifications that need a prompt before saving */ +export function shouldShowClearEditingSessionConfirmation(model: IChatModel, options?: IClearEditingSessionConfirmationOptions): number { + if (!model.editingSession || (model.willKeepAlive && !options?.isArchiveAction)) { + return 0; // safe to dispose without confirmation + } + + const currentEdits = model.editingSession.entries.get(); + const undecidedEdits = currentEdits.filter((edit) => edit.state.get() === ModifiedFileEntryState.Modified); + return undecidedEdits.length; +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.ts new file mode 100644 index 00000000000..647486aa963 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageDetails.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. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatContextUsageDetails.css'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { IMenuService, MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { MenuWorkbenchButtonBar } from '../../../../../../platform/actions/browser/buttonbar.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; + +const $ = dom.$; + +export interface IChatContextUsagePromptTokenDetail { + category: string; + label: string; + percentageOfPrompt: number; +} + +export interface IChatContextUsageData { + promptTokens: number; + maxInputTokens: number; + percentage: number; + promptTokenDetails?: readonly IChatContextUsagePromptTokenDetail[]; +} + +/** + * Detailed widget that shows context usage breakdown. + * Displayed when the user clicks on the ChatContextUsageIcon. + */ +export class ChatContextUsageDetails extends Disposable { + + readonly domNode: HTMLElement; + + private readonly quotaItem: HTMLElement; + private readonly percentageLabel: HTMLElement; + private readonly tokenCountLabel: HTMLElement; + private readonly progressFill: HTMLElement; + private readonly tokenDetailsContainer: HTMLElement; + private readonly warningMessage: HTMLElement; + private readonly actionsSection: HTMLElement; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(); + + this.domNode = $('.chat-context-usage-details'); + this.domNode.setAttribute('tabindex', '0'); + + // Using same structure as ChatUsageWidget quota items + this.quotaItem = this.domNode.appendChild($('.quota-item')); + + // Header row with label + const quotaItemHeader = this.quotaItem.appendChild($('.quota-item-header')); + const quotaItemLabel = quotaItemHeader.appendChild($('.quota-item-label')); + quotaItemLabel.textContent = localize('contextWindow', "Context Window"); + + // Token count and percentage row (on same line) + const tokenRow = this.quotaItem.appendChild($('.token-row')); + this.tokenCountLabel = tokenRow.appendChild($('.token-count-label')); + this.percentageLabel = tokenRow.appendChild($('.quota-item-value')); + + // Progress bar - using same structure as chat usage widget + const progressBar = this.quotaItem.appendChild($('.quota-bar')); + this.progressFill = progressBar.appendChild($('.quota-bit')); + + // Token details container (for category breakdown) + this.tokenDetailsContainer = this.domNode.appendChild($('.token-details-container')); + + // Warning message (shown when usage is high) + this.warningMessage = this.domNode.appendChild($('.warning-message')); + this.warningMessage.textContent = localize('qualityWarning', "Quality may decline as limit nears."); + this.warningMessage.style.display = 'none'; + + // Actions section with header, separator, and button bar + this.actionsSection = this.domNode.appendChild($('.actions-section')); + this.actionsSection.appendChild($('.separator')); + const actionsHeader = this.actionsSection.appendChild($('.actions-header')); + actionsHeader.textContent = localize('actions', "Actions"); + const buttonBarContainer = this.actionsSection.appendChild($('.button-bar-container')); + this._register(this.instantiationService.createInstance(MenuWorkbenchButtonBar, buttonBarContainer, MenuId.ChatContextUsageActions, { + toolbarOptions: { + primaryGroup: () => true + }, + buttonConfigProvider: () => ({ isSecondary: true }) + })); + + // Listen to menu changes to show/hide actions section + const menu = this._register(this.menuService.createMenu(MenuId.ChatContextUsageActions, this.contextKeyService)); + const updateActionsVisibility = () => { + const actions = menu.getActions(); + const hasActions = actions.length > 0 && actions.some(([, items]) => items.length > 0); + this.actionsSection.style.display = hasActions ? '' : 'none'; + }; + this._register(menu.onDidChange(updateActionsVisibility)); + updateActionsVisibility(); + } + + update(data: IChatContextUsageData): void { + const { percentage, promptTokens, maxInputTokens, promptTokenDetails } = data; + + // Update token count and percentage on same line + this.tokenCountLabel.textContent = localize( + 'tokenCount', + "{0} / {1} tokens", + this.formatTokenCount(promptTokens, 1), + this.formatTokenCount(maxInputTokens, 0) + ); + this.percentageLabel.textContent = `• ${percentage.toFixed(0)}%`; + + // Update progress bar + this.progressFill.style.width = `${Math.min(100, percentage)}%`; + + // Update color classes based on usage level on the quota item + this.quotaItem.classList.remove('warning', 'error'); + if (percentage >= 90) { + this.quotaItem.classList.add('error'); + } else if (percentage >= 75) { + this.quotaItem.classList.add('warning'); + } + + // Render token details breakdown if available + this.renderTokenDetails(promptTokenDetails, percentage); + + // Show/hide warning message + this.warningMessage.style.display = percentage >= 75 ? '' : 'none'; + } + + private formatTokenCount(count: number, decimals: number): string { + if (count >= 1000000) { + return `${(count / 1000000).toFixed(decimals)}M`; + } else if (count >= 1000) { + return `${(count / 1000).toFixed(decimals)}K`; + } + return count.toString(); + } + + private renderTokenDetails(details: readonly IChatContextUsagePromptTokenDetail[] | undefined, contextWindowPercentage: number): void { + // Clear previous content + dom.clearNode(this.tokenDetailsContainer); + + if (!details || details.length === 0) { + this.tokenDetailsContainer.style.display = 'none'; + return; + } + + this.tokenDetailsContainer.style.display = ''; + + // Group details by category + const categoryMap = new Map(); + let totalPercentage = 0; + + for (const detail of details) { + const existing = categoryMap.get(detail.category) || []; + existing.push({ label: detail.label, percentageOfPrompt: detail.percentageOfPrompt }); + categoryMap.set(detail.category, existing); + totalPercentage += detail.percentageOfPrompt; + } + + // Add uncategorized if percentages don't sum to 100% + if (totalPercentage < 100) { + const uncategorizedPercentage = 100 - totalPercentage; + categoryMap.set(localize('uncategorized', "Uncategorized"), [ + { label: localize('other', "Other"), percentageOfPrompt: uncategorizedPercentage } + ]); + } + + // Render each category + for (const [category, items] of categoryMap) { + const categorySection = this.tokenDetailsContainer.appendChild($('.token-category')); + + // Category header + const categoryHeader = categorySection.appendChild($('.token-category-header')); + categoryHeader.textContent = category; + + // Category items + for (const item of items) { + const itemRow = categorySection.appendChild($('.token-detail-item')); + + const itemLabel = itemRow.appendChild($('.token-detail-label')); + itemLabel.textContent = item.label; + + // Calculate percentage relative to context window + // E.g., if context window is at 10% and item uses 10% of prompt, show 1% + const contextRelativePercentage = (item.percentageOfPrompt / 100) * contextWindowPercentage; + + const itemValue = itemRow.appendChild($('.token-detail-value')); + itemValue.textContent = `${contextRelativePercentage.toFixed(1)}%`; + } + } + } + + focus(): void { + this.domNode.focus(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts new file mode 100644 index 00000000000..05f0f2ab499 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatContextUsageWidget.ts @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatContextUsageWidget.css'; +import * as dom from '../../../../../../base/browser/dom.js'; +import { EventType, addDisposableListener } from '../../../../../../base/browser/dom.js'; +import { IDelayedHoverOptions } from '../../../../../../base/browser/ui/hover/hover.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { localize } from '../../../../../../nls.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatRequestModel, IChatResponseModel } from '../../../common/model/chatModel.js'; +import { ILanguageModelsService } from '../../../common/languageModels.js'; +import { ChatContextUsageDetails, IChatContextUsageData } from './chatContextUsageDetails.js'; +import { StandardKeyboardEvent } from '../../../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../../../base/common/keyCodes.js'; + +const $ = dom.$; + +/** + * A reusable circular progress indicator that displays a pie chart. + * The pie fills clockwise from the top based on the percentage value. + */ +export class CircularProgressIndicator { + + readonly domNode: SVGSVGElement; + + private readonly progressPie: SVGPathElement; + + private static readonly CENTER_X = 18; + private static readonly CENTER_Y = 18; + private static readonly RADIUS = 16; + + constructor() { + this.domNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + this.domNode.setAttribute('viewBox', '0 0 36 36'); + this.domNode.classList.add('circular-progress'); + + // Background circle (outline only) + const bgCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + bgCircle.setAttribute('cx', String(CircularProgressIndicator.CENTER_X)); + bgCircle.setAttribute('cy', String(CircularProgressIndicator.CENTER_Y)); + bgCircle.setAttribute('r', String(CircularProgressIndicator.RADIUS)); + bgCircle.classList.add('progress-bg'); + this.domNode.appendChild(bgCircle); + + // Progress pie (filled arc) + this.progressPie = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + this.progressPie.classList.add('progress-pie'); + this.domNode.appendChild(this.progressPie); + } + + /** + * Updates the pie chart to display the given percentage (0-100). + * @param percentage The percentage of the pie to fill (clamped to 0-100) + */ + setProgress(percentage: number): void { + const cx = CircularProgressIndicator.CENTER_X; + const cy = CircularProgressIndicator.CENTER_Y; + const r = CircularProgressIndicator.RADIUS; + + if (percentage >= 100) { + // Full circle - use a circle element's path equivalent + this.progressPie.setAttribute('d', `M ${cx} ${cy - r} A ${r} ${r} 0 1 1 ${cx - 0.001} ${cy - r} Z`); + } else if (percentage <= 0) { + // Empty - no path + this.progressPie.setAttribute('d', ''); + } else { + // Calculate the arc endpoint + const angle = (percentage / 100) * 360; + const radians = (angle - 90) * (Math.PI / 180); // Start from top (-90 degrees) + const x = cx + r * Math.cos(radians); + const y = cy + r * Math.sin(radians); + const largeArcFlag = angle > 180 ? 1 : 0; + + // Create pie slice path: move to center, line to top, arc to endpoint, close + const d = [ + `M ${cx} ${cy}`, // Move to center + `L ${cx} ${cy - r}`, // Line to top + `A ${r} ${r} 0 ${largeArcFlag} 1 ${x} ${y}`, // Arc to endpoint + 'Z' // Close path back to center + ].join(' '); + + this.progressPie.setAttribute('d', d); + } + } +} + +/** + * Widget that displays the context/token usage for the current chat session. + * Shows a circular progress icon that expands on hover/focus to show token counts, + * and on click shows the detailed context usage widget. + */ +export class ChatContextUsageWidget extends Disposable { + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility: Event = this._onDidChangeVisibility.event; + + readonly domNode: HTMLElement; + + private readonly progressIndicator: CircularProgressIndicator; + + private readonly _isVisible = observableValue(this, false); + get isVisible(): IObservable { return this._isVisible; } + + private readonly _lastRequestDisposable = this._register(new MutableDisposable()); + private readonly _hoverDisposable = this._register(new MutableDisposable()); + private readonly _contextUsageDetails = this._register(new MutableDisposable()); + + private currentData: IChatContextUsageData | undefined; + + constructor( + @IHoverService private readonly hoverService: IHoverService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + ) { + super(); + + this.domNode = $('.chat-context-usage-widget'); + this.domNode.style.display = 'none'; + this.domNode.setAttribute('tabindex', '0'); + this.domNode.setAttribute('role', 'button'); + this.domNode.setAttribute('aria-label', localize('contextUsageLabel', "Context window usage")); + + // Icon container (always visible, contains the pie chart) + const iconContainer = this.domNode.appendChild($('.icon-container')); + this.progressIndicator = new CircularProgressIndicator(); + iconContainer.appendChild(this.progressIndicator.domNode); + + // Set up hover - will be configured when data is available + this.setupHover(); + } + + private setupHover(): void { + this._hoverDisposable.clear(); + const store = new DisposableStore(); + this._hoverDisposable.value = store; + + const createDetails = (): ChatContextUsageDetails | undefined => { + if (!this._isVisible.get() || !this.currentData) { + return undefined; + } + this._contextUsageDetails.value = this.instantiationService.createInstance(ChatContextUsageDetails); + this._contextUsageDetails.value.update(this.currentData); + return this._contextUsageDetails.value; + }; + + const hoverOptions: Omit = { + appearance: { showPointer: true, compact: true }, + persistence: { hideOnHover: false }, + trapFocus: true + }; + + store.add(this.hoverService.setupDelayedHover(this.domNode, () => ({ + ...hoverOptions, + content: createDetails()?.domNode ?? '' + }))); + + const showStickyHover = () => { + const details = createDetails(); + if (details) { + this.hoverService.showInstantHover( + { ...hoverOptions, content: details.domNode, target: this.domNode, persistence: { hideOnHover: false, sticky: true } }, + true + ); + } + }; + + // Show sticky + focused hover on click + store.add(addDisposableListener(this.domNode, EventType.CLICK, e => { + e.stopPropagation(); + showStickyHover(); + })); + + // Show sticky + focused hover on keyboard activation (Space/Enter) + store.add(addDisposableListener(this.domNode, EventType.KEY_DOWN, e => { + const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) { + e.preventDefault(); + showStickyHover(); + } + })); + } + + /** + * Updates the widget with the latest request/response data. + * The model is retrieved from the request's modelId. + * @param lastRequest The last request in the session + */ + update(lastRequest: IChatRequestModel | undefined): void { + this._lastRequestDisposable.clear(); + + if (!lastRequest?.response || !lastRequest.modelId) { + this.hide(); + return; + } + + const response = lastRequest.response; + const modelId = lastRequest.modelId; + + // Update immediately if usage data is already available + this.updateFromResponse(response, modelId); + + // Subscribe to response changes to update whenever usage data changes + this._lastRequestDisposable.value = response.onDidChange(() => { + this.updateFromResponse(response, modelId); + }); + } + + private updateFromResponse(response: IChatResponseModel, modelId: string): void { + const usage = response.usage; + const modelMetadata = this.languageModelsService.lookupLanguageModel(modelId); + const maxInputTokens = modelMetadata?.maxInputTokens; + + if (!usage || !maxInputTokens || maxInputTokens <= 0) { + this.hide(); + return; + } + + const promptTokens = usage.promptTokens; + const promptTokenDetails = usage.promptTokenDetails; + const percentage = Math.min(100, (promptTokens / maxInputTokens) * 100); + + this.render(percentage, promptTokens, maxInputTokens, promptTokenDetails); + this.show(); + } + + private render(percentage: number, promptTokens: number, maxTokens: number, promptTokenDetails?: readonly { category: string; label: string; percentageOfPrompt: number }[]): void { + // Store current data for use in details popup + this.currentData = { promptTokens, maxInputTokens: maxTokens, percentage, promptTokenDetails }; + + // Update pie chart progress + this.progressIndicator.setProgress(percentage); + + // Update color based on usage level + this.domNode.classList.remove('warning', 'error'); + if (percentage >= 90) { + this.domNode.classList.add('error'); + } else if (percentage >= 75) { + this.domNode.classList.add('warning'); + } + } + + private show(): void { + if (this.domNode.style.display === 'none') { + this.domNode.style.display = ''; + this._isVisible.set(true, undefined); + this._onDidChangeVisibility.fire(); + } + } + + private hide(): void { + if (this.domNode.style.display !== 'none') { + this.domNode.style.display = 'none'; + this._isVisible.set(false, undefined); + this._onDidChangeVisibility.fire(); + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts new file mode 100644 index 00000000000..0b30d04b3fc --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -0,0 +1,1026 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatViewPane.css'; +import { $, addDisposableListener, append, EventHelper, EventType, getWindow, setVisibility } from '../../../../../../base/browser/dom.js'; +import { StandardMouseEvent } from '../../../../../../base/browser/mouseEvent.js'; +import { Button } from '../../../../../../base/browser/ui/button/button.js'; +import { Orientation, Sash } from '../../../../../../base/browser/ui/sash/sash.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { MutableDisposable, toDisposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; +import { autorun, IReader } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { defaultButtonStyles } from '../../../../../../platform/theme/browser/defaultStyles.js'; +import { editorBackground } from '../../../../../../platform/theme/common/colorRegistry.js'; +import { ChatViewTitleControl } from './chatViewTitleControl.js'; +import { IThemeService } from '../../../../../../platform/theme/common/themeService.js'; +import { IViewPaneOptions, ViewPane } from '../../../../../browser/parts/views/viewPane.js'; +import { Memento } from '../../../../../common/memento.js'; +import { SIDE_BAR_FOREGROUND } from '../../../../../common/theme.js'; +import { IViewDescriptorService, ViewContainerLocation } from '../../../../../common/views.js'; +import { ILifecycleService, StartupKind } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; +import { IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js'; +import { IChatModel, IChatModelInputState } from '../../../common/model/chatModel.js'; +import { CHAT_PROVIDER_ID } from '../../../common/participants/chatParticipantContribTypes.js'; +import { IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; +import { IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { LocalChatSessionUri, getChatSessionType } from '../../../common/model/chatUri.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js'; +import { AgentSessionsControl } from '../../agentSessions/agentSessionsControl.js'; +import { ACTION_ID_NEW_CHAT } from '../../actions/chatActions.js'; +import { ChatWidget } from '../../widget/chatWidget.js'; +import { ChatViewWelcomeController, IViewWelcomeDelegate } from '../../viewsWelcome/chatViewWelcomeController.js'; +import { IWorkbenchLayoutService, LayoutSettings, Position } from '../../../../../services/layout/browser/layoutService.js'; +import { AgentSessionsViewerOrientation, AgentSessionsViewerPosition } from '../../agentSessions/agentSessions.js'; +import { IProgressService } from '../../../../../../platform/progress/common/progress.js'; +import { ChatViewId } from '../../chat.js'; +import { IActivityService, ProgressBadge } from '../../../../../services/activity/common/activity.js'; +import { disposableTimeout } from '../../../../../../base/common/async.js'; +import { AgentSessionsFilter, AgentSessionsGrouping } from '../../agentSessions/agentSessionsFilter.js'; +import { IAgentSessionsService } from '../../agentSessions/agentSessionsService.js'; +import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; +import { IAgentSession } from '../../agentSessions/agentSessionsModel.js'; +import { IChatEntitlementService } from '../../../../../services/chat/common/chatEntitlementService.js'; + +interface IChatViewPaneState extends Partial { + sessionId?: string; + + sessionsSidebarWidth?: number; +} + +type ChatViewPaneOpenedClassification = { + owner: 'sbatten'; + comment: 'Event fired when the chat view pane is opened'; +}; + +export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { + + private readonly memento: Memento; + private readonly viewState: IChatViewPaneState; + + private viewPaneContainer: HTMLElement | undefined; + private readonly chatViewLocationContext: IContextKey; + + private lastDimensions: { height: number; width: number } | undefined; + private readonly lastDimensionsPerOrientation: Map = new Map(); + + private welcomeController: ChatViewWelcomeController | undefined; + + private restoringSession: Promise | undefined; + private readonly modelRef = this._register(new MutableDisposable()); + + private readonly activityBadge = this._register(new MutableDisposable()); + + constructor( + options: IViewPaneOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @IHoverService hoverService: IHoverService, + @IStorageService private readonly storageService: IStorageService, + @IChatService private readonly chatService: IChatService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @ILogService private readonly logService: ILogService, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ILifecycleService lifecycleService: ILifecycleService, + @IProgressService private readonly progressService: IProgressService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, + @IActivityService private readonly activityService: IActivityService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); + + // View state for the ViewPane is currently global per-provider basically, + // but some other strictly per-model state will require a separate memento. + this.memento = new Memento(`interactive-session-view-${CHAT_PROVIDER_ID}`, this.storageService); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + if ( + lifecycleService.startupKind !== StartupKind.ReloadedWindow && + this.configurationService.getValue(ChatConfiguration.RestoreLastPanelSession) === false + ) { + this.viewState.sessionId = undefined; // clear persisted session on fresh start + } + this.sessionsViewerVisible = false; // will be updated from layout code + this.sessionsViewerSidebarWidth = Math.max(ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, this.viewState.sessionsSidebarWidth ?? ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH); + + // Contextkeys + this.chatViewLocationContext = ChatContextKeys.panelLocation.bindTo(contextKeyService); + this.sessionsViewerOrientationContext = ChatContextKeys.agentSessionsViewerOrientation.bindTo(contextKeyService); + this.sessionsViewerPositionContext = ChatContextKeys.agentSessionsViewerPosition.bindTo(contextKeyService); + this.sessionsViewerVisibilityContext = ChatContextKeys.agentSessionsViewerVisible.bindTo(contextKeyService); + + this.updateContextKeys(); + + this.registerListeners(); + } + + private updateContextKeys(): void { + const { position, location } = this.getViewPositionAndLocation(); + + this.chatViewLocationContext.set(location ?? ViewContainerLocation.AuxiliaryBar); + this.sessionsViewerOrientationContext.set(this.sessionsViewerOrientation); + this.sessionsViewerPositionContext.set(position === Position.RIGHT ? AgentSessionsViewerPosition.Right : AgentSessionsViewerPosition.Left); + } + + private getViewPositionAndLocation(): { position: Position; location: ViewContainerLocation } { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + const sideBarPosition = this.layoutService.getSideBarPosition(); + const panelPosition = this.layoutService.getPanelPosition(); + + let sideSessionsOnRightPosition: boolean; + switch (viewLocation) { + case ViewContainerLocation.Sidebar: + sideSessionsOnRightPosition = sideBarPosition === Position.RIGHT; + break; + case ViewContainerLocation.Panel: + sideSessionsOnRightPosition = panelPosition !== Position.LEFT; + break; + default: + sideSessionsOnRightPosition = sideBarPosition === Position.LEFT; + break; + } + + return { + position: sideSessionsOnRightPosition ? Position.RIGHT : Position.LEFT, + location: viewLocation ?? ViewContainerLocation.AuxiliaryBar, + }; + } + + private getSessionHoverPosition() { + const viewLocation = this.viewDescriptorService.getViewLocationById(this.id); + const sideBarPosition = this.layoutService.getSideBarPosition(); + + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + return viewLocation === ViewContainerLocation.Sidebar && sideBarPosition === Position.RIGHT ? HoverPosition.LEFT : HoverPosition.RIGHT; + } + + return { + [Position.LEFT]: HoverPosition.RIGHT, + [Position.RIGHT]: HoverPosition.LEFT, + [Position.TOP]: HoverPosition.BELOW, + [Position.BOTTOM]: HoverPosition.ABOVE + }[viewLocation === ViewContainerLocation.Panel ? this.layoutService.getPanelPosition() : sideBarPosition]; + } + + private updateViewPaneClasses(fromEvent: boolean): void { + const activityBarLocationDefault = this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) === 'default'; + this.viewPaneContainer?.classList.toggle('activity-bar-location-default', activityBarLocationDefault); + this.viewPaneContainer?.classList.toggle('activity-bar-location-other', !activityBarLocationDefault); + + const { position, location } = this.getViewPositionAndLocation(); + + this.viewPaneContainer?.classList.toggle('chat-view-location-auxiliarybar', location === ViewContainerLocation.AuxiliaryBar); + this.viewPaneContainer?.classList.toggle('chat-view-location-sidebar', location === ViewContainerLocation.Sidebar); + this.viewPaneContainer?.classList.toggle('chat-view-location-panel', location === ViewContainerLocation.Panel); + + this.viewPaneContainer?.classList.toggle('chat-view-position-left', position === Position.LEFT); + this.viewPaneContainer?.classList.toggle('chat-view-position-right', position === Position.RIGHT); + + if (fromEvent) { + this.relayout(); + } + } + + private registerListeners(): void { + + // Agent changes + this._register(this.chatAgentService.onDidChangeAgents(() => this.onDidChangeAgents())); + + // Layout changes + this._register(Event.any( + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('workbench.sideBar.location')), + this.layoutService.onDidChangePanelPosition, + Event.filter(this.viewDescriptorService.onDidChangeContainerLocation, e => e.viewContainer === this.viewDescriptorService.getViewContainerByViewId(this.id)) + )(() => { + this.updateContextKeys(); + this.updateViewPaneClasses(true /* layout here */); + })); + + // Settings changes + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => { + return e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION); + })(() => this.updateViewPaneClasses(true))); + } + + private onDidChangeAgents(): void { + if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat)) { + if (!this._widget?.viewModel && !this.restoringSession) { + const sessionResource = this.getTransferredOrPersistedSessionInfo(); + this.restoringSession = + (sessionResource ? this.chatService.getOrRestoreSession(sessionResource) : Promise.resolve(undefined)).then(async modelRef => { + if (!this._widget) { + return; // renderBody has not been called yet + } + + // The widget may be hidden at this point, because welcome views were allowed. Use setVisible to + // avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome` + // so it should fire onDidChangeViewWelcomeState. + const wasVisible = this._widget.visible; + try { + this._widget.setVisible(false); + + await this.showModel(modelRef); + } finally { + this._widget.setVisible(wasVisible); + } + }); + + this.restoringSession.finally(() => this.restoringSession = undefined); + } + } + + this._onDidChangeViewWelcomeState.fire(); + } + + private getTransferredOrPersistedSessionInfo(): URI | undefined { + if (this.chatService.transferredSessionResource) { + return this.chatService.transferredSessionResource; + } + + return this.viewState.sessionId ? LocalChatSessionUri.forSession(this.viewState.sessionId) : undefined; + } + + protected override renderBody(parent: HTMLElement): void { + super.renderBody(parent); + + this.telemetryService.publicLog2<{}, ChatViewPaneOpenedClassification>('chatViewPaneOpened'); + + this.viewPaneContainer = parent; + this.viewPaneContainer.classList.add('chat-viewpane'); + this.updateViewPaneClasses(false); + + this.createControls(parent); + + this.setupContextMenu(parent); + + this.applyModel(); + } + + private createControls(parent: HTMLElement): void { + + // Sessions Control + const sessionsControl = this.createSessionsControl(parent); + + // Welcome Control (used to show chat specific extension provided welcome views via `chatViewsWelcome` contribution point) + const welcomeController = this.welcomeController = this._register(this.instantiationService.createInstance(ChatViewWelcomeController, parent, this, ChatAgentLocation.Chat)); + + // Chat Control + const chatWidget = this.createChatControl(parent); + + // Controls Listeners + this.registerControlsListeners(sessionsControl, chatWidget, welcomeController); + + // Update sessions control visibility when all controls are created + this.updateSessionsControlVisibility(); + } + + //#region Sessions Control + + private static readonly SESSIONS_SIDEBAR_MIN_WIDTH = 200; + private static readonly SESSIONS_SIDEBAR_SNAP_THRESHOLD = this.SESSIONS_SIDEBAR_MIN_WIDTH / 2; // snap to hide when dragged below half of minimum width + private static readonly SESSIONS_SIDEBAR_DEFAULT_WIDTH = 300; + private static readonly CHAT_WIDGET_DEFAULT_WIDTH = 300; + private static readonly SESSIONS_SIDEBAR_VIEW_MIN_WIDTH = this.CHAT_WIDGET_DEFAULT_WIDTH + this.SESSIONS_SIDEBAR_DEFAULT_WIDTH; + + private sessionsContainer: HTMLElement | undefined; + private sessionsTitleContainer: HTMLElement | undefined; + private sessionsTitle: HTMLElement | undefined; + private sessionsNewButtonContainer: HTMLElement | undefined; + private sessionsControlContainer: HTMLElement | undefined; + private sessionsControl: AgentSessionsControl | undefined; + private sessionsViewerVisible: boolean; + private sessionsViewerOrientation = AgentSessionsViewerOrientation.Stacked; + private sessionsViewerOrientationConfiguration: 'stacked' | 'sideBySide' = 'sideBySide'; + private sessionsViewerOrientationContext: IContextKey; + private sessionsViewerVisibilityContext: IContextKey; + private sessionsViewerPositionContext: IContextKey; + private sessionsViewerSidebarWidth: number; + private sessionsViewerSash: Sash | undefined; + private readonly sessionsViewerSashDisposables = this._register(new MutableDisposable()); + + private createSessionsControl(parent: HTMLElement): AgentSessionsControl { + const sessionsContainer = this.sessionsContainer = parent.appendChild($('.agent-sessions-container')); + + // Sessions Title + const sessionsTitleContainer = this.sessionsTitleContainer = append(sessionsContainer, $('.agent-sessions-title-container')); + const sessionsTitle = this.sessionsTitle = append(sessionsTitleContainer, $('span.agent-sessions-title')); + sessionsTitle.textContent = localize('sessions', "Sessions"); + this._register(addDisposableListener(sessionsTitle, EventType.CLICK, () => { + this.sessionsControl?.scrollToTop(); + this.sessionsControl?.focus(); + })); + + // Sessions Toolbar + const sessionsToolbarContainer = append(sessionsTitleContainer, $('.agent-sessions-toolbar')); + const sessionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, sessionsToolbarContainer, MenuId.AgentSessionsToolbar, { + menuOptions: { shouldForwardArgs: true } + })); + + // Sessions Filter + const sessionsFilter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, { + filterMenuId: MenuId.AgentSessionsViewerFilterSubMenu, + groupResults: () => this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked ? AgentSessionsGrouping.Capped : AgentSessionsGrouping.Date + })); + this._register(Event.runAndSubscribe(sessionsFilter.onDidChange, () => { + sessionsToolbarContainer.classList.toggle('filtered', !sessionsFilter.isDefault()); + })); + + // New Session Button + const newSessionButtonContainer = this.sessionsNewButtonContainer = append(sessionsContainer, $('.agent-sessions-new-button-container')); + const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true })); + newSessionButton.label = localize('newSession', "New Session"); + this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_NEW_CHAT))); + + // Sessions Control + this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); + const sessionsControl = this.sessionsControl = this._register(this.instantiationService.createInstance(AgentSessionsControl, this.sessionsControlContainer, { + source: 'chatViewPane', + filter: sessionsFilter, + overrideStyles: this.getLocationBasedColors().listOverrideStyles, + getHoverPosition: () => this.getSessionHoverPosition(), + trackActiveEditorSession: () => { + return !this._widget || this._widget.isEmpty(); // only track and reveal if chat widget is empty + }, + overrideSessionOpenOptions: openEvent => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked && !openEvent.sideBySide) { + return { ...openEvent, editorOptions: { ...openEvent.editorOptions, preserveFocus: false /* focus the chat widget when opening from stacked sessions viewer since this closes the stacked viewer */ } }; + } + return openEvent; + }, + })); + this._register(this.onDidChangeBodyVisibility(visible => sessionsControl.setVisible(visible))); + + sessionsToolbar.context = sessionsControl; + + // Deal with orientation configuration + this._register(Event.runAndSubscribe(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.ChatViewSessionsOrientation)), e => { + const newSessionsViewerOrientationConfiguration = this.configurationService.getValue<'stacked' | 'sideBySide' | unknown>(ChatConfiguration.ChatViewSessionsOrientation); + this.doUpdateConfiguredSessionsViewerOrientation(newSessionsViewerOrientationConfiguration, { updateConfiguration: false, layout: !!e }); + })); + + return sessionsControl; + } + + getSessionsViewerOrientation(): AgentSessionsViewerOrientation { + return this.sessionsViewerOrientation; + } + + updateConfiguredSessionsViewerOrientation(orientation: 'stacked' | 'sideBySide' | unknown): void { + return this.doUpdateConfiguredSessionsViewerOrientation(orientation, { updateConfiguration: true, layout: true }); + } + + private doUpdateConfiguredSessionsViewerOrientation(orientation: 'stacked' | 'sideBySide' | unknown, options: { updateConfiguration: boolean; layout: boolean }): void { + const oldSessionsViewerOrientationConfiguration = this.sessionsViewerOrientationConfiguration; + + let validatedOrientation: 'stacked' | 'sideBySide'; + if (orientation === 'stacked' || orientation === 'sideBySide') { + validatedOrientation = orientation; + } else { + validatedOrientation = 'sideBySide'; // default + } + this.sessionsViewerOrientationConfiguration = validatedOrientation; + + if (oldSessionsViewerOrientationConfiguration === this.sessionsViewerOrientationConfiguration) { + return; // no change from our existing config + } + + if (options.updateConfiguration) { + this.configurationService.updateValue(ChatConfiguration.ChatViewSessionsOrientation, validatedOrientation); + } + + if (options.layout) { + this.relayout(); + } + } + + private updateSessionsControlVisibility(): { changed: boolean; visible: boolean } { + if (!this.sessionsContainer || !this.viewPaneContainer) { + return { changed: false, visible: false }; + } + + let newSessionsContainerVisible: boolean; + if (!this.configurationService.getValue(ChatConfiguration.ChatViewSessionsEnabled)) { + newSessionsContainerVisible = false; // disabled in settings + } else { + + // Sessions control: stacked + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + newSessionsContainerVisible = + !!this.chatEntitlementService.sentiment.installed && // chat is installed (otherwise make room for terms and welcome) + (!this._widget || (this._widget.isEmpty() && !!this._widget.viewModel)) && // chat widget empty (but not when model is loading) + !this.welcomeController?.isShowingWelcome.get(); // welcome not showing + } + + // Sessions control: sidebar + else { + newSessionsContainerVisible = + !this.welcomeController?.isShowingWelcome.get() && // welcome not showing + !!this.lastDimensions && this.lastDimensions.width >= ChatViewPane.SESSIONS_SIDEBAR_VIEW_MIN_WIDTH; // has sessions or is showing all sessions + } + } + + this.viewPaneContainer.classList.toggle('has-sessions-control', newSessionsContainerVisible); + + const sessionsContainerVisible = this.sessionsContainer.style.display !== 'none'; + setVisibility(newSessionsContainerVisible, this.sessionsContainer); + this.sessionsViewerVisible = newSessionsContainerVisible; + this.sessionsViewerVisibilityContext.set(newSessionsContainerVisible); + + return { + changed: sessionsContainerVisible !== newSessionsContainerVisible, + visible: newSessionsContainerVisible + }; + } + + getFocusedSessions(): IAgentSession[] { + return this.sessionsControl?.getFocus() ?? []; + } + + //#endregion + + //#region Chat Control + + private static readonly MIN_CHAT_WIDGET_HEIGHT = 116; + + private _widget!: ChatWidget; + get widget(): ChatWidget { return this._widget; } + + private titleControl: ChatViewTitleControl | undefined; + + private createChatControl(parent: HTMLElement): ChatWidget { + const chatControlsContainer = append(parent, $('.chat-controls-container')); + + const locationBasedColors = this.getLocationBasedColors(); + + const editorOverflowWidgetsDomNode = this.layoutService.getContainer(getWindow(chatControlsContainer)).appendChild($('.chat-editor-overflow.monaco-editor')); + this._register(toDisposable(() => editorOverflowWidgetsDomNode.remove())); + + // Chat Title + this.createChatTitleControl(chatControlsContainer); + + // Chat Widget + const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); + this._widget = this._register(scopedInstantiationService.createInstance( + ChatWidget, + ChatAgentLocation.Chat, + { viewId: this.id }, + { + autoScroll: mode => mode !== ChatModeKind.Ask, + renderFollowups: true, + supportsFileReferences: true, + clear: () => this.clear(), + rendererOptions: { + renderTextEditsAsSummary: (uri) => { + return true; + }, + referencesExpandedWhenEmptyResponse: false, + progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask, + }, + editorOverflowWidgetsDomNode, + enableImplicitContext: true, + enableWorkingSet: 'explicit', + supportsChangingModes: true, + dndContainer: parent, + }, + { + listForeground: SIDE_BAR_FOREGROUND, + listBackground: locationBasedColors.background, + overlayBackground: locationBasedColors.overlayBackground, + inputEditorBackground: locationBasedColors.background, + resultEditorBackground: editorBackground, + })); + this._widget.render(chatControlsContainer); + + const updateWidgetVisibility = (reader?: IReader) => this._widget.setVisible(this.isBodyVisible() && !this.welcomeController?.isShowingWelcome.read(reader)); + this._register(this.onDidChangeBodyVisibility(() => updateWidgetVisibility())); + this._register(autorun(reader => updateWidgetVisibility(reader))); + + return this._widget; + } + + private createChatTitleControl(parent: HTMLElement): void { + this.titleControl = this._register(this.instantiationService.createInstance(ChatViewTitleControl, + parent, + { + focusChat: () => this._widget.focusInput() + } + )); + + this._register(this.titleControl.onDidChangeHeight(() => { + this.relayout(); + })); + } + + //#endregion + + private registerControlsListeners(sessionsControl: AgentSessionsControl, chatWidget: ChatWidget, welcomeController: ChatViewWelcomeController): void { + + // Sessions control visibility is impacted by multiple things: + // - chat widget being in empty state or showing a chat + // - extensions provided welcome view showing or not + // - configuration setting + this._register(Event.any( + chatWidget.onDidChangeEmptyState, + Event.fromObservable(welcomeController.isShowingWelcome), + Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.ChatViewSessionsEnabled)) + )(() => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + sessionsControl.clearFocus(); // improve visual appearance when switching visibility by clearing focus + } + const { changed: visibilityChanged } = this.updateSessionsControlVisibility(); + if (visibilityChanged) { + this.relayout(); + } + })); + + // Track the active chat model and reveal it in the sessions control if side-by-side + this._register(chatWidget.onDidChangeViewModel(() => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + return; // only reveal in side-by-side mode + } + + const sessionResource = chatWidget.viewModel?.sessionResource; + if (sessionResource) { + const revealed = sessionsControl.reveal(sessionResource); + if (!revealed) { + // Session doesn't exist in the list yet (e.g., new untitled session), + // clear the selection so the list doesn't show stale selection + sessionsControl.clearFocus(); + } + } + })); + + // When sessions change (e.g., after first message in a new session) + // reveal it unless the user is interacting with the list already + this._register(this.agentSessionsService.model.onDidChangeSessions(() => { + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + return; // only reveal in side-by-side mode + } + + if (sessionsControl.hasFocusOrSelection()) { + return; // do not reveal if user is interacting with sessions control + } + + const sessionResource = chatWidget.viewModel?.sessionResource; + if (sessionResource) { + sessionsControl.reveal(sessionResource); + } + })); + + // When showing sessions stacked, adjust the height of the sessions list to make room for chat input + this._register(autorun(reader => { + chatWidget.inputPart.height.read(reader); + if (this.sessionsViewerVisible && this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + this.relayout(); + } + })); + + // Show progress badge when the current session is in progress + const progressBadgeDisposables = this._register(new MutableDisposable()); + const updateProgressBadge = () => { + progressBadgeDisposables.value = new DisposableStore(); + + if (!this.configurationService.getValue(ChatConfiguration.ChatViewProgressBadgeEnabled)) { + this.activityBadge.clear(); + return; + } + + const model = chatWidget.viewModel?.model; + if (model) { + progressBadgeDisposables.value.add(autorun(reader => { + if (model.requestInProgress.read(reader)) { + this.activityBadge.value = this.activityService.showViewActivity(this.id, { + badge: new ProgressBadge(() => localize('sessionInProgress', "Agent Session in Progress")) + }); + } else { + this.activityBadge.clear(); + } + })); + } else { + this.activityBadge.clear(); + } + }; + this._register(chatWidget.onDidChangeViewModel(() => updateProgressBadge())); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.ChatViewProgressBadgeEnabled))(() => updateProgressBadge())); + updateProgressBadge(); + } + + private setupContextMenu(parent: HTMLElement): void { + this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => { + EventHelper.stop(e, true); + + this.contextMenuService.showContextMenu({ + menuId: MenuId.ChatWelcomeContext, + contextKeyService: this.contextKeyService, + getAnchor: () => new StandardMouseEvent(getWindow(parent), e) + }); + })); + } + + //#region Model Management + + private async applyModel(): Promise { + const sessionResource = this.getTransferredOrPersistedSessionInfo(); + const modelRef = sessionResource ? await this.chatService.getOrRestoreSession(sessionResource) : undefined; + await this.showModel(modelRef); + } + + private async showModel(modelRef?: IChatModelReference | undefined, startNewSession = true): Promise { + const oldModelResource = this.modelRef.value?.object.sessionResource; + this.modelRef.value = undefined; + + let ref: IChatModelReference | undefined; + if (startNewSession) { + ref = modelRef ?? (this.chatService.transferredSessionResource + ? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionResource) + : this.chatService.startSession(ChatAgentLocation.Chat)); + if (!ref) { + throw new Error('Could not start chat session'); + } + } + + this.modelRef.value = ref; + const model = ref?.object; + + if (model) { + await this.updateWidgetLockState(model.sessionResource); // Update widget lock state based on session type + + this.viewState.sessionId = model.sessionId; // remember as model to restore in view state + } + + this._widget.setModel(model); + + // Update title control + this.titleControl?.update(model); + + // Update the toolbar context with new sessionId + this.updateActions(); + + // Mark the old model as read when closing + if (oldModelResource) { + this.agentSessionsService.model.getSession(oldModelResource)?.setRead(true); + } + + return model; + } + + private async updateWidgetLockState(sessionResource: URI): Promise { + const sessionType = getChatSessionType(sessionResource); + if (sessionType === localChatSessionType) { + this._widget.unlockFromCodingAgent(); + return; + } + + let canResolve = false; + try { + canResolve = await this.chatSessionsService.canResolveChatSession(sessionResource); + } catch (error) { + this.logService.warn(`Failed to resolve chat session '${sessionResource.toString()}' for locking`, error); + } + + if (!canResolve) { + this._widget.unlockFromCodingAgent(); + return; + } + + const contribution = this.chatSessionsService.getChatSessionContribution(sessionType); + if (contribution) { + this._widget.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type); + } else { + this._widget.unlockFromCodingAgent(); + } + } + + private async clear(): Promise { + + // Grab the widget's latest view state because it will be loaded back into the widget + this.updateViewState(); + await this.showModel(undefined); + + // Update the toolbar context with new sessionId + this.updateActions(); + } + + async loadSession(sessionResource: URI): Promise { + return this.progressService.withProgress({ location: ChatViewId, delay: 200 }, async () => { + let queue: Promise = Promise.resolve(); + + // A delay here to avoid blinking because only Cloud sessions are slow, most others are fast + const clearWidget = disposableTimeout(() => { + // clear current model without starting a new one + queue = this.showModel(undefined, false).then(() => { }); + }, 100); + + const sessionType = getChatSessionType(sessionResource); + if (sessionType !== localChatSessionType) { + await this.chatSessionsService.canResolveChatSession(sessionResource); + } + + const newModelRef = await this.chatService.loadSessionForResource(sessionResource, ChatAgentLocation.Chat, CancellationToken.None); + clearWidget.dispose(); + await queue; + + return this.showModel(newModelRef); + }); + } + + //#endregion + + override focus(): void { + super.focus(); + + this.focusInput(); + } + + focusInput(): void { + this._widget.focusInput(); + } + + focusSessions(): boolean { + if (this.sessionsContainer?.style.display === 'none') { + return false; // not visible + } + + this.sessionsControl?.focus(); + + return true; + } + + //#region Layout + + private layoutingBody = false; + + private relayout(): void { + if (this.lastDimensions) { + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + } + } + + protected override layoutBody(height: number, width: number): void { + if (this.layoutingBody) { + return; // prevent re-entrancy + } + + this.layoutingBody = true; + try { + this.doLayoutBody(height, width); + } finally { + this.layoutingBody = false; + } + } + + private doLayoutBody(height: number, width: number): void { + super.layoutBody(height, width); + + this.lastDimensions = { height, width }; + + let remainingHeight = height; + let remainingWidth = width; + + // Sessions Control + const { heightReduction, widthReduction } = this.layoutSessionsControl(remainingHeight, remainingWidth); + remainingHeight -= heightReduction; + remainingWidth -= widthReduction; + + // Title Control + remainingHeight -= this.titleControl?.getHeight() ?? 0; + + // Chat Widget + this._widget.layout(remainingHeight, remainingWidth); + + // Remember last dimensions per orientation + this.lastDimensionsPerOrientation.set(this.sessionsViewerOrientation, { height, width }); + } + + private layoutSessionsControl(height: number, width: number): { heightReduction: number; widthReduction: number } { + let heightReduction = 0; + let widthReduction = 0; + + if (!this.sessionsContainer || !this.sessionsControlContainer || !this.sessionsControl || !this.viewPaneContainer || !this.sessionsTitleContainer || !this.sessionsTitle) { + return { heightReduction, widthReduction }; + } + + const oldSessionsViewerOrientation = this.sessionsViewerOrientation; + let newSessionsViewerOrientation: AgentSessionsViewerOrientation; + switch (this.sessionsViewerOrientationConfiguration) { + // Stacked + case 'stacked': + newSessionsViewerOrientation = AgentSessionsViewerOrientation.Stacked; + break; + // Update orientation based on available width + default: + newSessionsViewerOrientation = width >= ChatViewPane.SESSIONS_SIDEBAR_VIEW_MIN_WIDTH ? AgentSessionsViewerOrientation.SideBySide : AgentSessionsViewerOrientation.Stacked; + } + + this.sessionsViewerOrientation = newSessionsViewerOrientation; + + if (newSessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + this.viewPaneContainer.classList.toggle('sessions-control-orientation-sidebyside', true); + this.viewPaneContainer.classList.toggle('sessions-control-orientation-stacked', false); + this.sessionsViewerOrientationContext.set(AgentSessionsViewerOrientation.SideBySide); + } else { + this.viewPaneContainer.classList.toggle('sessions-control-orientation-sidebyside', false); + this.viewPaneContainer.classList.toggle('sessions-control-orientation-stacked', true); + this.sessionsViewerOrientationContext.set(AgentSessionsViewerOrientation.Stacked); + } + + if (oldSessionsViewerOrientation !== this.sessionsViewerOrientation) { + const updatePromise = this.sessionsControl.update(); // Changing orientation has an impact to grouping, so we need to update + + // Switching to side-by-side, reveal the current session after elements have loaded + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + updatePromise.then(() => { + const sessionResource = this._widget?.viewModel?.sessionResource; + if (sessionResource) { + this.sessionsControl?.reveal(sessionResource); + } + }); + } + } + + // Ensure visibility is in sync before we layout + const { visible: sessionsContainerVisible } = this.updateSessionsControlVisibility(); + + // Handle Sash (only visible in side-by-side) + if (!sessionsContainerVisible || this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + this.sessionsViewerSashDisposables.clear(); + this.sessionsViewerSash = undefined; + } else if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + if (!this.sessionsViewerSashDisposables.value && this.viewPaneContainer) { + this.createSessionsViewerSash(this.viewPaneContainer, height, width); + } + } + + if (!sessionsContainerVisible) { + return { heightReduction: 0, widthReduction: 0 }; + } + + let availableSessionsHeight = height - this.sessionsTitleContainer.offsetHeight; + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.Stacked) { + availableSessionsHeight -= Math.max(ChatViewPane.MIN_CHAT_WIDGET_HEIGHT, this._widget?.input?.height.get() ?? 0); + } else { + availableSessionsHeight -= this.sessionsNewButtonContainer?.offsetHeight ?? 0; + } + + // Show as sidebar + if (this.sessionsViewerOrientation === AgentSessionsViewerOrientation.SideBySide) { + const sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(width); + + this.sessionsControlContainer.style.height = `${availableSessionsHeight}px`; + this.sessionsControlContainer.style.width = `${sessionsViewerSidebarWidth}px`; + this.sessionsControl.layout(availableSessionsHeight, sessionsViewerSidebarWidth); + this.sessionsViewerSash?.layout(); + + heightReduction = 0; // side by side to chat widget + widthReduction = this.sessionsContainer.offsetWidth; + } + + // Show stacked + else { + const sessionsHeight = availableSessionsHeight - 1 /* border bottom */; + + this.sessionsControlContainer.style.height = `${sessionsHeight}px`; + this.sessionsControlContainer.style.width = ``; + this.sessionsControl.layout(sessionsHeight, width); + + heightReduction = this.sessionsContainer.offsetHeight; + widthReduction = 0; // stacked on top of the chat widget + } + + return { heightReduction, widthReduction }; + } + + private computeEffectiveSideBySideSessionsSidebarWidth(width: number, sessionsViewerSidebarWidth = this.sessionsViewerSidebarWidth): number { + return Math.max( + ChatViewPane.SESSIONS_SIDEBAR_MIN_WIDTH, // never smaller than min width for side by side sessions + Math.min( + sessionsViewerSidebarWidth, + width - ChatViewPane.CHAT_WIDGET_DEFAULT_WIDTH // never so wide that chat widget is smaller than default width + ) + ); + } + + getLastDimensions(orientation: AgentSessionsViewerOrientation): { height: number; width: number } | undefined { + return this.lastDimensionsPerOrientation.get(orientation); + } + + private createSessionsViewerSash(container: HTMLElement, height: number, width: number): void { + const disposables = this.sessionsViewerSashDisposables.value = new DisposableStore(); + + const sash = this.sessionsViewerSash = disposables.add(new Sash(container, { + getVerticalSashLeft: () => { + const sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(this.lastDimensions?.width ?? width); + const { position } = this.getViewPositionAndLocation(); + if (position === Position.RIGHT) { + return (this.lastDimensions?.width ?? width) - sessionsViewerSidebarWidth; + } + + return sessionsViewerSidebarWidth; + } + }, { orientation: Orientation.VERTICAL })); + + let sashStartWidth: number | undefined; + disposables.add(sash.onDidStart(() => sashStartWidth = this.sessionsViewerSidebarWidth)); + disposables.add(sash.onDidEnd(() => sashStartWidth = undefined)); + + disposables.add(sash.onDidChange(e => { + if (sashStartWidth === undefined || !this.lastDimensions) { + return; + } + + const { position } = this.getViewPositionAndLocation(); + const delta = e.currentX - e.startX; + const newWidth = position === Position.RIGHT ? sashStartWidth - delta : sashStartWidth + delta; + + if (newWidth < ChatViewPane.SESSIONS_SIDEBAR_SNAP_THRESHOLD) { + this.updateConfiguredSessionsViewerOrientation('stacked'); // snap to stacked when sized small enough + return; + } + + this.sessionsViewerSidebarWidth = this.computeEffectiveSideBySideSessionsSidebarWidth(this.lastDimensions.width, newWidth); + this.viewState.sessionsSidebarWidth = this.sessionsViewerSidebarWidth; + + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + })); + + disposables.add(sash.onDidReset(() => { + this.sessionsViewerSidebarWidth = ChatViewPane.SESSIONS_SIDEBAR_DEFAULT_WIDTH; + this.viewState.sessionsSidebarWidth = this.sessionsViewerSidebarWidth; + + this.relayout(); + })); + } + + //#endregion + + override saveState(): void { + + // Don't do saveState when no widget, or no viewModel in which case + // the state has not yet been restored - in that case the default + // state would overwrite the real state + if (this._widget?.viewModel) { + this._widget.saveState(); + + this.updateViewState(); + this.memento.saveMemento(); + } + + super.saveState(); + } + + private updateViewState(viewState?: IChatModelInputState): void { + const newViewState = viewState ?? this._widget.getViewState(); + if (newViewState) { + for (const [key, value] of Object.entries(newViewState)) { + (this.viewState as Record)[key] = value; // Assign all props to the memento so they get saved + } + } + } + + override shouldShowWelcome(): boolean { + const noPersistedSessions = !this.chatService.hasSessions(); + const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(ChatAgentLocation.Chat)); + const hasDefaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat) !== undefined; // only false when Hide AI Features has run and unregistered the setup agents + const shouldShow = !hasCoreAgent && (!hasDefaultAgent || !this._widget?.viewModel && noPersistedSessions); + + this.logService.trace(`ChatViewPane#shouldShowWelcome() = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`); + + return !!shouldShow; + } + + override getActionsContext(): IChatViewTitleActionContext | undefined { + return this._widget?.viewModel ? { + sessionResource: this._widget.viewModel.sessionResource, + $mid: MarshalledId.ChatViewContext + } : undefined; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts new file mode 100644 index 00000000000..8cd698568b8 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewTitleControl.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatViewTitleControl.css'; +import { addDisposableListener, EventType, h } from '../../../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../../../base/browser/markdownRenderer.js'; +import { Gesture, EventType as TouchEventType } from '../../../../../../base/browser/touch.js'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { Disposable, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { MarshalledId } from '../../../../../../base/common/marshallingIds.js'; +import { localize } from '../../../../../../nls.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../../platform/actions/browser/toolbar.js'; +import { Action2, MenuId, registerAction2 } from '../../../../../../platform/actions/common/actions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IChatViewTitleActionContext } from '../../../common/actions/chatActions.js'; +import { IChatModel } from '../../../common/model/chatModel.js'; +import { ActionViewItem, IActionViewItemOptions } from '../../../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { IAction } from '../../../../../../base/common/actions.js'; +import { AgentSessionsPicker } from '../../agentSessions/agentSessionsPicker.js'; + +export interface IChatViewTitleDelegate { + focusChat(): void; +} + +export class ChatViewTitleControl extends Disposable { + + private static readonly DEFAULT_TITLE = localize('chat', "Chat"); + private static readonly PICK_AGENT_SESSION_ACTION_ID = 'workbench.action.chat.pickAgentSession'; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + readonly onDidChangeHeight = this._onDidChangeHeight.event; + + private title: string | undefined = undefined; + + private titleContainer: HTMLElement | undefined; + private titleLabel = this._register(new MutableDisposable()); + + private model: IChatModel | undefined; + private modelDisposables = this._register(new MutableDisposable()); + + private navigationToolbar?: MenuWorkbenchToolBar; + private actionsToolbar?: MenuWorkbenchToolBar; + + private lastKnownHeight = 0; + + constructor( + private readonly container: HTMLElement, + private readonly delegate: IChatViewTitleDelegate, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.render(this.container); + + this.registerActions(); + } + + private registerActions(): void { + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: ChatViewTitleControl.PICK_AGENT_SESSION_ACTION_ID, + title: localize('chat.pickAgentSession', "Pick Agent Session"), + f1: false, + menu: [{ + id: MenuId.ChatViewSessionTitleNavigationToolbar, + group: 'navigation', + order: 2 + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + + const agentSessionsPicker = instantiationService.createInstance(AgentSessionsPicker); + await agentSessionsPicker.pickAgentSession(); + } + })); + } + + private render(parent: HTMLElement): void { + const elements = h('div.chat-view-title-container', [ + h('div.chat-view-title-navigation-toolbar@navigationToolbar'), + h('div.chat-view-title-actions-toolbar@actionsToolbar'), + ]); + + // Toolbar on the left + this.navigationToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, elements.navigationToolbar, MenuId.ChatViewSessionTitleNavigationToolbar, { + actionViewItemProvider: (action: IAction) => { + if (action.id === ChatViewTitleControl.PICK_AGENT_SESSION_ACTION_ID) { + this.titleLabel.value = new ChatViewTitleLabel(action); + this.titleLabel.value.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE); + + return this.titleLabel.value; + } + + return undefined; + }, + hiddenItemStrategy: HiddenItemStrategy.NoHide, + menuOptions: { shouldForwardArgs: true } + })); + + // Actions toolbar on the right + this.actionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, elements.actionsToolbar, MenuId.ChatViewSessionTitleToolbar, { + menuOptions: { shouldForwardArgs: true }, + hiddenItemStrategy: HiddenItemStrategy.NoHide + })); + + // Title controls + this.titleContainer = elements.root; + this._register(Gesture.addTarget(this.titleContainer)); + for (const eventType of [TouchEventType.Tap, EventType.CLICK]) { + this._register(addDisposableListener(this.titleContainer, eventType, () => { + this.delegate.focusChat(); + })); + } + + parent.appendChild(this.titleContainer); + } + + update(model: IChatModel | undefined): void { + this.model = model; + + this.modelDisposables.value = model?.onDidChange(e => { + if (e.kind === 'setCustomTitle' || e.kind === 'addRequest') { + this.doUpdate(); + } + }); + + this.doUpdate(); + } + + private doUpdate(): void { + const markdownTitle = new MarkdownString(this.model?.title ?? ''); + this.title = renderAsPlaintext(markdownTitle); + + this.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE); + + const context = this.model && { + $mid: MarshalledId.ChatViewContext, + sessionResource: this.model.sessionResource + } satisfies IChatViewTitleActionContext; + + if (this.navigationToolbar) { + this.navigationToolbar.context = context; + } + + if (this.actionsToolbar) { + this.actionsToolbar.context = context; + } + } + + private updateTitle(title: string): void { + if (!this.titleContainer) { + return; + } + + this.titleContainer.classList.toggle('visible', this.shouldRender()); + this.titleLabel.value?.updateTitle(title); + + const currentHeight = this.getHeight(); + if (currentHeight !== this.lastKnownHeight) { + this.lastKnownHeight = currentHeight; + + this._onDidChangeHeight.fire(); + } + } + + private shouldRender(): boolean { + return !!this.model?.title; // we need a chat showing and not being empty + } + + getHeight(): number { + if (!this.titleContainer || this.titleContainer.style.display === 'none') { + return 0; + } + + return this.titleContainer.offsetHeight; + } +} + +class ChatViewTitleLabel extends ActionViewItem { + + private title: string | undefined; + + private titleLabel: HTMLSpanElement | undefined = undefined; + + constructor(action: IAction, options?: IActionViewItemOptions) { + super(null, action, { ...options, icon: false, label: true }); + } + + override render(container: HTMLElement): void { + super.render(container); + + container.classList.add('chat-view-title-action-item'); + this.label?.classList.add('chat-view-title-label-container'); + + this.titleLabel = this.label?.appendChild(h('span.chat-view-title-label').root); + + this.updateLabel(); + } + + updateTitle(title: string): void { + this.title = title; + + this.updateLabel(); + } + + protected override updateLabel(): void { + if (!this.titleLabel) { + return; + } + + if (this.title) { + this.titleLabel.textContent = this.title; + } else { + this.titleLabel.textContent = ''; + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css new file mode 100644 index 00000000000..41b7a7ad9d1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageDetails.css @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Remove the outline and change border color on focus instead */ +.workbench-hover-container.locked:has(.chat-context-usage-details) .monaco-hover.workbench-hover { + outline: none; +} +.workbench-hover-container:focus-within.locked:has(.chat-context-usage-details) .monaco-hover.workbench-hover { + outline: none; + border-color: var(--vscode-focusBorder); +} + +.chat-context-usage-details { + display: flex; + flex-direction: column; + padding: 4px 0; + min-width: 200px; +} + +.chat-context-usage-details:focus { + outline: none; +} + +/* Using same structure as ChatUsageWidget quota items */ +.chat-context-usage-details .quota-item { + margin-bottom: 4px; +} + +.chat-context-usage-details .quota-item-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 2px; +} + +.chat-context-usage-details .quota-item-label { + color: var(--vscode-foreground); +} + +.chat-context-usage-details .quota-item-value { + color: var(--vscode-descriptionForeground); +} + +.chat-context-usage-details .token-row { + display: flex; + align-items: center; + gap: 4px; + margin-bottom: 2px; +} + +.chat-context-usage-details .token-count-label { + font-size: 12px; + color: var(--vscode-descriptionForeground); +} + +/* Progress bar - matching chat usage implementation */ +.chat-context-usage-details .quota-item .quota-bar { + width: 100%; + height: 4px; + background-color: var(--vscode-gauge-background); + border-radius: 4px; + border: 1px solid var(--vscode-gauge-border); + margin: 2px 0; +} + +.chat-context-usage-details .quota-item .quota-bar .quota-bit { + height: 100%; + background-color: var(--vscode-gauge-foreground); + border-radius: 4px; + transition: width 0.3s ease; +} + +.chat-context-usage-details .quota-item.warning .quota-bar { + background-color: var(--vscode-gauge-warningBackground); +} + +.chat-context-usage-details .quota-item.warning .quota-bar .quota-bit { + background-color: var(--vscode-gauge-warningForeground); +} + +.chat-context-usage-details .quota-item.error .quota-bar { + background-color: var(--vscode-gauge-errorBackground); +} + +.chat-context-usage-details .quota-item.error .quota-bar .quota-bit { + background-color: var(--vscode-gauge-errorForeground); +} + +.chat-context-usage-details .warning-message { + font-size: 12px; + color: var(--vscode-descriptionForeground); + margin-bottom: 4px; +} + +/* Token details breakdown */ +.chat-context-usage-details .token-details-container { + margin-top: 4px; +} + +.chat-context-usage-details .token-category { + margin-bottom: 4px; +} + +.chat-context-usage-details .token-category-header { + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 2px; +} + +.chat-context-usage-details .token-detail-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 8px; +} + +.chat-context-usage-details .token-detail-label { + color: var(--vscode-foreground); +} + +.chat-context-usage-details .token-detail-value { + color: var(--vscode-descriptionForeground); +} + +.chat-context-usage-details .actions-section .separator { + border-top: 1px solid var(--vscode-editorHoverWidget-border); + margin: 4px 0; +} + +.chat-context-usage-details .actions-section .actions-header { + font-weight: 600; + color: var(--vscode-foreground); + margin-bottom: 4px; +} + +.chat-context-usage-details .actions-section .button-bar-container { + display: flex; + flex-direction: column; + gap: 4px; +} + +.chat-context-usage-details .actions-section .button-bar-container .monaco-button-bar { + flex-direction: column; + gap: 4px; +} + +.chat-context-usage-details .actions-section .button-bar-container .monaco-button { + width: 100%; + justify-content: center; +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css new file mode 100644 index 00000000000..9906a0b8171 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatContextUsageWidget.css @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-context-usage-widget { + display: flex; + align-items: center; + justify-content: center; + height: 22px; + flex-shrink: 0; + cursor: pointer; + padding: 3px; + border-radius: 5px; + box-sizing: border-box; +} + +.chat-context-usage-widget .icon-container { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.chat-context-usage-widget:hover { + background-color: var(--vscode-toolbar-hoverBackground); + outline: 1px dashed var(--vscode-toolbar-hoverOutline); + outline-offset: -1px; +} + +.chat-context-usage-widget:focus { + outline: 1px solid var(--vscode-focusBorder); + outline-offset: -1px; +} + +.chat-context-usage-widget:active { + background-color: var(--vscode-toolbar-activeBackground); +} + +.chat-context-usage-widget .circular-progress { + width: 100%; + height: 100%; + pointer-events: none; +} + +.chat-context-usage-widget .progress-bg { + fill: transparent; + stroke: var(--vscode-icon-foreground); + stroke-width: 2; + opacity: 0.4; +} + +.chat-context-usage-widget .progress-pie { + fill: var(--vscode-icon-foreground); + opacity: 0.8; + pointer-events: none; +} + +.chat-context-usage-widget.warning .progress-pie { + fill: var(--vscode-editorWarning-foreground); +} + +.chat-context-usage-widget.error .progress-pie { + fill: var(--vscode-editorError-foreground); +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css new file mode 100644 index 00000000000..2a6c5e1ed94 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* Overall styles */ +.chat-viewpane { + display: flex; + flex-direction: column; + + .chat-controls-container { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + min-height: 0; + min-width: 0; + + .interactive-session { + + /* needed so that the chat input does not overflow and input grows over content above */ + width: 100%; + min-height: 0; + min-width: 0; + } + } +} + +/* Sessions control: either sidebar or stacked */ +.chat-viewpane.has-sessions-control .agent-sessions-container { + display: flex; + flex-direction: column; + + .agent-sessions-title-container { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.5px; + + .agent-sessions-title { + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .agent-sessions-toolbar { + + .action-item { + /* align with the title actions*/ + margin-right: 4px; + } + + &.filtered .action-label.codicon.codicon-filter { + /* indicate when sessions filter is enabled */ + border-color: var(--vscode-inputOption-activeBorder); + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); + } + } +} + +/* Sessions control: stacked */ +.chat-viewpane.has-sessions-control.sessions-control-orientation-stacked { + + .agent-sessions-container { + border-bottom: 1px solid var(--vscode-panel-border); + } + + .agent-sessions-new-button-container { + /* hide new session button when stacked */ + display: none; + } +} + +/* Sessions control: side by side */ +.chat-viewpane.has-sessions-control.sessions-control-orientation-sidebyside { + + &.chat-view-position-left { + flex-direction: row; + + .agent-sessions-container { + border-right: 1px solid var(--vscode-panel-border); + } + } + + &.chat-view-position-right { + flex-direction: row-reverse; + + .agent-sessions-container { + border-left: 1px solid var(--vscode-panel-border); + } + } + + .agent-sessions-new-button-container { + padding: 8px 12px; + } +} + +/* Sessions control: panel location */ +.chat-viewpane.has-sessions-control.chat-view-location-panel { + + .agent-sessions-new-button-container { + /* hide new session button in the panel considering less vertical space */ + display: none !important; + } +} + +/* + * Padding rules for agent sessions elements based on: + * - orientation (stacked vs sidebyside) + * - view position (left vs right) + * - activity bar location (default vs other for auxiliarybar) + */ +.chat-viewpane.has-sessions-control { + + /* Base padding: left-aligned content */ + .agent-sessions-title-container { + padding: 0 8px 0 20px; + } + + .agent-session-section { + padding: 0 12px 0 20px; + } + + /* Right position: symmetric padding */ + &.sessions-control-orientation-sidebyside.chat-view-position-right { + + .agent-sessions-title-container { + padding: 0 8px; + } + + .agent-session-section { + padding: 0 12px 0 8px; + } + } + + /* Auxiliarybar with non-default activity bar: tighter title padding */ + &.activity-bar-location-other.chat-view-location-auxiliarybar { + + .agent-sessions-title-container { + padding-right: 4px; + } + + /* Right position needs adjusted left padding too */ + &.sessions-control-orientation-sidebyside.chat-view-position-right { + + .agent-sessions-title-container, + .agent-session-section { + padding-left: 8px; + } + } + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css new file mode 100644 index 00000000000..9434f08cef3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewTitleControl.css @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-viewpane { + + .chat-view-title-container { + display: none; + align-items: center; + cursor: pointer; + + .chat-view-title-navigation-toolbar { + overflow: hidden; + + .chat-view-title-action-item { + flex: 1 1 auto; + min-width: 0; + + .chat-view-title-label-container { + display: flex; + gap: 4px; + } + } + } + + .chat-view-title-label { + text-transform: uppercase; + font-size: 11px; + font-weight: 700; + line-height: 16px; + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; + } + + .chat-view-title-actions-toolbar { + margin-left: auto; + padding-left: 4px; + flex-shrink: 0; + } + } + + .chat-view-title-container.visible { + display: flex; + } +} + +/* + * Below is a very complicated set of CSS rules that try to align the + * chat title to the surrounding elements depending on: + * - the activity bar position + * - the chat view container (sidebar, panel, auxiliarybar) + * - the container orientation (left, right) + * - the visibility of side by side + */ +.chat-viewpane { + + /* Default padding for all view locations */ + &.chat-view-location-sidebar, + &.chat-view-location-panel, + &.chat-view-location-auxiliarybar { + .chat-view-title-container { + padding: 0 12px 0 16px; + } + } + + /* Auxiliarybar with non-default activity bar position */ + &.activity-bar-location-other.chat-view-location-auxiliarybar { + .chat-view-title-container { + padding: 0 8px 0 16px; + } + } + + /* Side-by-side sessions: left position (any activity bar) */ + &.has-sessions-control.sessions-control-orientation-sidebyside.chat-view-position-left { + .chat-view-title-container { + padding: 0 8px; + } + } + + /* Side-by-side sessions: right position (default activity bar only) */ + &.activity-bar-location-default.has-sessions-control.sessions-control-orientation-sidebyside.chat-view-position-right { + .chat-view-title-container { + padding: 0 8px 0 16px; + } + } +} diff --git a/src/vs/workbench/contrib/chat/chatCodeOrganization.md b/src/vs/workbench/contrib/chat/chatCodeOrganization.md new file mode 100644 index 00000000000..bccc1226465 --- /dev/null +++ b/src/vs/workbench/contrib/chat/chatCodeOrganization.md @@ -0,0 +1,25 @@ +# workbench/contrib/chat Code Organization + +This contrib is, as of the end of 2025, the largest workbench contrib in VS Code by a substantial margin. Let's try to keep it organized! Here's a rough description of some of the key folders. + +## Key Folders + +### `browser/` + +- `accessibility/` - Screen reader support and accessible views. +- `actions/` - All chat action registrations. +- `attachments/` - Context attachment model, pickers, context widgets. +- `chatContentParts/` - Rendering components for different response content types (markdown, code blocks, tool output, etc.). +- `chatEditing/` - The edit session model, edit diff UI, edit snapshots. +- `chatSetup/` - Placeholder registrations before the chat extentension is set up. Running the chat auth/install flow. +- `contextContrib/` - The contribution point for chat context providers - note the difference from `attachments/`. +- `widget/` - The core files related to rendering parts of the ChatWidget, including the list, the input, the model/agent pickers, and other main UI parts. Must have direct references from ChatWidget itself. +- `widgetHosts/` - Hosts that embed chat widgets in other places (view pane, editor, quick chat). + +### `common/` + +- `chatService/` - IChatService interface, implementation, and related code. +- `model/` - Chat data model, view model, and session storage. +- `participants/` - Chat participant management (sometimes called "agents" in code). +- `tools/` - Language model tools infrastructure and services. + - `builtinTools/` - Implementations of some built-in tools. diff --git a/src/vs/workbench/contrib/chat/common/chatActions.ts b/src/vs/workbench/contrib/chat/common/actions/chatActions.ts similarity index 84% rename from src/vs/workbench/contrib/chat/common/chatActions.ts rename to src/vs/workbench/contrib/chat/common/actions/chatActions.ts index b311e98d6f2..d65670d076b 100644 --- a/src/vs/workbench/contrib/chat/common/chatActions.ts +++ b/src/vs/workbench/contrib/chat/common/actions/chatActions.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarshalledId } from '../../../../base/common/marshallingIds.js'; -import { URI } from '../../../../base/common/uri.js'; +import { MarshalledId } from '../../../../../base/common/marshallingIds.js'; +import { URI } from '../../../../../base/common/uri.js'; export interface IChatViewTitleActionContext { readonly $mid: MarshalledId.ChatViewContext; diff --git a/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts new file mode 100644 index 00000000000..90858649e66 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/actions/chatContextKeys.ts @@ -0,0 +1,130 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../../nls.js'; +import { ContextKeyExpr, RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IsWebContext } from '../../../../../platform/contextkey/common/contextkeys.js'; +import { RemoteNameContext } from '../../../../common/contextkeys.js'; +import { ViewContainerLocation } from '../../../../common/views.js'; +import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; +import { ChatAgentLocation, ChatModeKind } from '../constants.js'; + +export namespace ChatContextKeys { + export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); + export const responseDetectedAgentCommand = new RawContextKey('chatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('chatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); + export const responseSupportsIssueReporting = new RawContextKey('chatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('chatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); + export const responseIsFiltered = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); + export const responseHasError = new RawContextKey('chatSessionResponseError', false, { type: 'boolean', description: localize('chatResponseErrored', "True when the chat response resulted in an error.") }); + export const requestInProgress = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); + export const currentlyEditing = new RawContextKey('chatSessionCurrentlyEditing', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditing', "True when the current request is being edited.") }); + export const currentlyEditingInput = new RawContextKey('chatSessionCurrentlyEditingInput', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditingInput', "True when the current request input at the bottom is being edited.") }); + + export const isResponse = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); + export const isRequest = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); + export const itemId = new RawContextKey('chatItemId', '', { type: 'string', description: localize('chatItemId', "The id of the chat item.") }); + export const lastItemId = new RawContextKey('chatLastItemId', [], { type: 'string', description: localize('chatLastItemId', "The id of the last chat item.") }); + + export const editApplied = new RawContextKey('chatEditApplied', false, { type: 'boolean', description: localize('chatEditApplied', "True when the chat text edits have been applied.") }); + + export const inputHasText = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); + export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); + export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); + export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); + export const inChatEditor = new RawContextKey('inChatEditor', false, { type: 'boolean', description: localize('inChatEditor', "Whether focus is in a chat editor.") }); + export const inChatTerminalToolOutput = new RawContextKey('inChatTerminalToolOutput', false, { type: 'boolean', description: localize('inChatTerminalToolOutput', "True when focus is in the chat terminal output region.") }); + export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); + export const chatModeName = new RawContextKey('chatModeName', '', { type: 'string', description: localize('chatModeName', "The name of the current chat mode (e.g. 'Plan' for custom modes).") }); + + export const supported = ContextKeyExpr.or(IsWebContext.negate(), RemoteNameContext.notEqualsTo(''), ContextKeyExpr.has('config.chat.experimental.serverlessWebEnabled')); + export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); + + /** + * 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.") }); + /** + * True when the chat session has a customAgentTarget defined in its contribution, + * which means the mode picker should be shown with filtered custom agents. + */ + export const chatSessionHasCustomAgentTarget = new RawContextKey('chatSessionHasCustomAgentTarget', false, { type: 'boolean', description: localize('chatSessionHasCustomAgentTarget', "True when the chat session has a customAgentTarget defined to filter modes.") }); + export const agentSupportsAttachments = new RawContextKey('agentSupportsAttachments', false, { type: 'boolean', description: localize('agentSupportsAttachments', "True when the chat agent supports attachments.") }); + 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.") }); + export const chatEditingCanUndo = new RawContextKey('chatEditingCanUndo', false, { type: 'boolean', description: localize('chatEditingCanUndo', "True when it is possible to undo an interaction in the editing panel.") }); + export const chatEditingCanRedo = new RawContextKey('chatEditingCanRedo', false, { type: 'boolean', description: localize('chatEditingCanRedo', "True when it is possible to redo an interaction in the editing panel.") }); + export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); + export const chatSessionHasModels = new RawContextKey('chatSessionHasModels', false, { type: 'boolean', description: localize('chatSessionHasModels', "True when the chat is in a contributed chat session that has available 'models' to display.") }); + export const chatSessionOptionsValid = new RawContextKey('chatSessionOptionsValid', true, { type: 'boolean', description: localize('chatSessionOptionsValid', "True when all selected session options exist in their respective option group items.") }); + export const extensionInvalid = new RawContextKey('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") }); + export const inputCursorAtTop = new RawContextKey('chatCursorAtTop', false); + export const inputHasAgent = new RawContextKey('chatInputHasAgent', false); + export const location = new RawContextKey('chatLocation', undefined); + export const inQuickChat = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); + export const inAgentSessionsWelcome = new RawContextKey('inAgentSessionsWelcome', false, { type: 'boolean', description: localize('inAgentSessionsWelcome', "True when the chat input is within the agent sessions welcome page.") }); + export const chatSessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('chatSessionType', "The type of the current chat session.") }); + export const hasFileAttachments = new RawContextKey('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") }); + export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); + + export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); + export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); + export const hasCanDelegateProviders = new RawContextKey('chatHasCanDelegateProviders', false, { type: 'boolean', description: localize('chatHasCanDelegateProviders', "True when there are chat session providers with delegation support available.") }); + export const enableRemoteCodingAgentPromptFileOverlay = new RawContextKey('enableRemoteCodingAgentPromptFileOverlay', false, localize('enableRemoteCodingAgentPromptFileOverlay', "Whether the remote coding agent prompt file overlay feature is enabled")); + /** Used by the extension to skip the quit confirmation when #new wants to open a new folder */ + export const skipChatRequestInProgressMessage = new RawContextKey('chatSkipRequestInProgressMessage', false, { type: 'boolean', description: localize('chatSkipRequestInProgressMessage', "True when the chat request in progress message should be skipped.") }); + + // Re-exported from chat entitlement service + export const Setup = ChatEntitlementContextKeys.Setup; + export const Entitlement = ChatEntitlementContextKeys.Entitlement; + export const chatQuotaExceeded = ChatEntitlementContextKeys.chatQuotaExceeded; + export const completionsQuotaExceeded = ChatEntitlementContextKeys.completionsQuotaExceeded; + + export const Editing = { + hasToolConfirmation: new RawContextKey('chatHasToolConfirmation', false, { type: 'boolean', description: localize('chatEditingHasToolConfirmation', "True when a tool confirmation is present.") }), + hasElicitationRequest: new RawContextKey('chatHasElicitationRequest', false, { type: 'boolean', description: localize('chatEditingHasElicitationRequest', "True when a chat elicitation request is pending.") }), + }; + + export const Tools = { + toolsCount: new RawContextKey('toolsCount', 0, { type: 'number', description: localize('toolsCount', "The count of tools available in the chat.") }) + }; + + export const Modes = { + hasCustomChatModes: new RawContextKey('chatHasCustomAgents', false, { type: 'boolean', description: localize('chatHasAgents', "True when the chat has custom agents available.") }), + agentModeDisabledByPolicy: new RawContextKey('chatAgentModeDisabledByPolicy', false, { type: 'boolean', description: localize('chatAgentModeDisabledByPolicy', "True when agent mode is disabled by organization policy.") }), + }; + + export const panelLocation = new RawContextKey('chatPanelLocation', undefined, { type: 'number', description: localize('chatPanelLocation', "The location of the chat panel.") }); + + export const agentSessionsViewerFocused = new RawContextKey('agentSessionsViewerFocused', true, { type: 'boolean', description: localize('agentSessionsViewerFocused', "If the agent sessions view in the chat view is focused.") }); + export const agentSessionsViewerOrientation = new RawContextKey('agentSessionsViewerOrientation', undefined, { type: 'number', description: localize('agentSessionsViewerOrientation', "Orientation of the agent sessions view in the chat view.") }); + export const agentSessionsViewerPosition = new RawContextKey('agentSessionsViewerPosition', undefined, { type: 'number', description: localize('agentSessionsViewerPosition', "Position of the agent sessions view in the chat view.") }); + export const agentSessionsViewerVisible = new RawContextKey('agentSessionsViewerVisible', undefined, { type: 'boolean', description: localize('agentSessionsViewerVisible', "Visibility of the agent sessions view in the chat view.") }); + export const agentSessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('agentSessionType', "The type of the current agent session item.") }); + export const agentSessionSection = new RawContextKey('agentSessionSection', '', { type: 'string', description: localize('agentSessionSection', "The section of the current agent session section item.") }); + export const isArchivedAgentSession = new RawContextKey('agentSessionIsArchived', false, { type: 'boolean', description: localize('agentSessionIsArchived', "True when the agent session item is archived.") }); + export const isReadAgentSession = new RawContextKey('agentSessionIsRead', false, { type: 'boolean', description: localize('agentSessionIsRead', "True when the agent session item is read.") }); + export const hasMultipleAgentSessionsSelected = new RawContextKey('agentSessionHasMultipleSelected', false, { type: 'boolean', description: localize('agentSessionHasMultipleSelected', "True when multiple agent sessions are selected.") }); + export const hasAgentSessionChanges = new RawContextKey('agentSessionHasChanges', false, { type: 'boolean', description: localize('agentSessionHasChanges', "True when the current agent session item has changes.") }); + + export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); +} + +export namespace ChatContextKeyExprs { + + export const inEditingMode = ContextKeyExpr.or( + 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/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts deleted file mode 100644 index c7b98c6fa66..00000000000 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ /dev/null @@ -1,147 +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 { findLastIdx } from '../../../../base/common/arraysFind.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { basename } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js'; -import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js'; - -export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI - -export function annotateSpecialMarkdownContent(response: Iterable): IChatProgressRenderableResponseContent[] { - let refIdPool = 0; - - const result: IChatProgressRenderableResponseContent[] = []; - for (const item of response) { - const previousItemIndex = findLastIdx(result, p => p.kind !== 'textEditGroup' && p.kind !== 'undoStop'); - const previousItem = result[previousItemIndex]; - if (item.kind === 'inlineReference') { - let label: string | undefined = item.name; - if (!label) { - if (URI.isUri(item.inlineReference)) { - label = basename(item.inlineReference); - } else if ('name' in item.inlineReference) { - label = item.inlineReference.name; - } else { - label = basename(item.inlineReference.uri); - } - } - - const refId = refIdPool++; - const printUri = URI.parse(contentRefUrl).with({ path: String(refId) }); - const markdownText = `[${label}](${printUri.toString()})`; - - const annotationMetadata = { [refId]: item }; - - if (previousItem?.kind === 'markdownContent') { - const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); - result[previousItemIndex] = { ...previousItem, content: merged, inlineReferences: { ...annotationMetadata, ...(previousItem.inlineReferences || {}) } }; - } else { - result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' }); - } - } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) { - const merged = appendMarkdownString(previousItem.content, item.content); - result[previousItemIndex] = { ...previousItem, content: merged }; - } else if (item.kind === 'markdownVuln') { - const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); - const markdownText = `${item.content.value}`; - if (previousItem?.kind === 'markdownContent') { - // Since this is inside a codeblock, it needs to be merged into the previous markdown content. - const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); - result[previousItemIndex] = { ...previousItem, content: merged }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } else if (item.kind === 'codeblockUri') { - if (previousItem?.kind === 'markdownContent') { - const isEditText = item.isEdit ? ` isEdit` : ''; - const markdownText = `${item.uri.toString()}`; - const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); - // delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it - result.splice(previousItemIndex, 1); - result.push({ ...previousItem, content: merged }); - } - } else { - result.push(item); - } - } - - return result; -} - -export interface IMarkdownVulnerability { - readonly title: string; - readonly description: string; - readonly range: IRange; -} - -export function annotateVulnerabilitiesInText(response: ReadonlyArray): readonly IChatMarkdownContent[] { - const result: IChatMarkdownContent[] = []; - for (const item of response) { - const previousItem = result[result.length - 1]; - if (item.kind === 'markdownContent') { - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push(item); - } - } else if (item.kind === 'markdownVuln') { - const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); - const markdownText = `${item.content.value}`; - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } - } - - return result; -} - -export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; textWithoutResult: string } | undefined { - const match = /(.*?)<\/vscode_codeblock_uri>/ms.exec(text); - if (match) { - const [all, isEdit, uriString] = match; - if (uriString) { - const result = URI.parse(uriString); - const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length); - return { uri: result, textWithoutResult, isEdit: !!isEdit }; - } - } - return undefined; -} - -export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { - const vulnerabilities: IMarkdownVulnerability[] = []; - let newText = text; - let match: RegExpExecArray | null; - while ((match = /(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) { - const [full, details, content] = match; - const start = match.index; - const textBefore = newText.substring(0, start); - const linesBefore = textBefore.split('\n').length - 1; - const linesInside = content.split('\n').length - 1; - - const previousNewlineIdx = textBefore.lastIndexOf('\n'); - const startColumn = start - (previousNewlineIdx + 1) + 1; - const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n'); - const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1; - - try { - const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details)); - vulnDetails.forEach(({ title, description }) => vulnerabilities.push({ - title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } - })); - } catch (err) { - // Something went wrong with encoding this text, just ignore it - } - newText = newText.substring(0, start) + content + newText.substring(start + full.length); - } - - return { newText, vulnerabilities }; -} diff --git a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts similarity index 79% rename from src/vs/workbench/contrib/chat/common/chatVariableEntries.ts rename to src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts index 85f9e1d6e16..0426c7fba40 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariableEntries.ts +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariableEntries.ts @@ -3,19 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Codicon } from '../../../../base/common/codicons.js'; -import { basename } from '../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IOffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; -import { isLocation, Location, SymbolKind } from '../../../../editor/common/languages.js'; -import { localize } from '../../../../nls.js'; -import { MarkerSeverity, IMarker } from '../../../../platform/markers/common/markers.js'; -import { ISCMHistoryItem } from '../../scm/common/history.js'; -import { IChatContentReference } from './chatService.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { basename } from '../../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; +import { IOffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { isLocation, Location, SymbolKind } from '../../../../../editor/common/languages.js'; +import { localize } from '../../../../../nls.js'; +import { MarkerSeverity, IMarker } from '../../../../../platform/markers/common/markers.js'; +import { ISCMHistoryItem } from '../../../scm/common/history.js'; +import { IChatContentReference } from '../chatService/chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; -import { IToolData, ToolSet } from './languageModelToolsService.js'; +import { IToolData, IToolSet } from '../tools/languageModelToolsService.js'; +import { decodeBase64, encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js'; +import { Mutable } from '../../../../../base/common/types.js'; interface IBaseChatRequestVariableEntry { @@ -38,6 +41,7 @@ interface IBaseChatRequestVariableEntry { export interface IGenericChatRequestVariableEntry extends IBaseChatRequestVariableEntry { kind: 'generic'; + tooltip?: IMarkdownString; } export interface IChatRequestDirectoryEntry extends IBaseChatRequestVariableEntry { @@ -66,11 +70,17 @@ export interface IChatRequestToolSetEntry extends IBaseChatRequestVariableEntry export type ChatRequestToolReferenceEntry = IChatRequestToolEntry | IChatRequestToolSetEntry; export interface StringChatContextValue { - value: string; + value?: string; name: string; modelDescription?: string; icon: ThemeIcon; uri: URI; + tooltip?: IMarkdownString; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; } export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVariableEntry { @@ -84,12 +94,25 @@ export interface IChatRequestImplicitVariableEntry extends IBaseChatRequestVaria export interface IChatRequestStringVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'string'; - readonly value: string; + readonly value: string | undefined; readonly modelDescription?: string; readonly icon: ThemeIcon; readonly uri: URI; + readonly tooltip?: IMarkdownString; + /** + * Command ID to execute when this context item is clicked. + */ + readonly commandId?: string; + readonly handle: number; +} + +export interface IChatRequestWorkspaceVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'workspace'; + readonly value: string; + readonly modelDescription?: string; } + export interface IChatRequestPasteVariableEntry extends IBaseChatRequestVariableEntry { readonly kind: 'paste'; readonly code: string; @@ -254,13 +277,20 @@ export interface ITerminalVariableEntry extends IBaseChatRequestVariableEntry { readonly exitCode?: number; } +export interface IDebugVariableEntry extends IBaseChatRequestVariableEntry { + readonly kind: 'debugVariable'; + readonly value: string; + readonly expression: string; + readonly type?: string; +} + export type IChatRequestVariableEntry = IGenericChatRequestVariableEntry | IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | ISymbolVariableEntry | ICommandResultVariableEntry | IDiagnosticVariableEntry | IImageVariableEntry | IChatRequestToolEntry | IChatRequestToolSetEntry | IChatRequestDirectoryEntry | IChatRequestFileEntry | INotebookOutputVariableEntry | IElementVariableEntry | IPromptFileVariableEntry | IPromptTextVariableEntry | ISCMHistoryItemVariableEntry | ISCMHistoryItemChangeVariableEntry | ISCMHistoryItemChangeRangeVariableEntry | ITerminalVariableEntry - | IChatRequestStringVariableEntry; + | IChatRequestStringVariableEntry | IChatRequestWorkspaceVariableEntry | IDebugVariableEntry; export namespace IChatRequestVariableEntry { @@ -274,8 +304,44 @@ export namespace IChatRequestVariableEntry { ? entry.value.uri : undefined; } -} + export function toExport(v: IChatRequestVariableEntry): IChatRequestVariableEntry { + if (v.value instanceof Uint8Array) { + // 'dup' here is needed otherwise TS complains about the narrowed `value` in a spread operation + const dup: Mutable = { ...v }; + dup.value = { $base64: encodeBase64(VSBuffer.wrap(v.value)) }; + return dup; + } + + return v; + } + + export function fromExport(v: IChatRequestVariableEntry): IChatRequestVariableEntry { + // Old variables format + // eslint-disable-next-line local/code-no-in-operator + if (v && 'values' in v && Array.isArray(v.values)) { + return { + kind: 'generic', + id: v.id ?? '', + name: v.name, + value: v.values[0]?.value, + range: v.range, + modelDescription: v.modelDescription, + references: v.references + }; + } else { + // eslint-disable-next-line local/code-no-in-operator + if (v.value && typeof v.value === 'object' && '$base64' in v.value && typeof v.value.$base64 === 'string') { + // 'dup' here is needed otherwise TS complains about the narrowed `value` in a spread operation + const dup: Mutable = { ...v }; + dup.value = decodeBase64(v.value.$base64).buffer; + return dup; + } + + return v; + } + } +} export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; @@ -289,10 +355,18 @@ export function isTerminalVariableEntry(obj: IChatRequestVariableEntry): obj is return obj.kind === 'terminalCommand'; } +export function isDebugVariableEntry(obj: IChatRequestVariableEntry): obj is IDebugVariableEntry { + return obj.kind === 'debugVariable'; +} + export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestPasteVariableEntry { return obj.kind === 'paste'; } +export function isWorkspaceVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestWorkspaceVariableEntry { + return obj.kind === 'workspace'; +} + export function isImageVariableEntry(obj: IChatRequestVariableEntry): obj is IImageVariableEntry { return obj.kind === 'image'; } @@ -346,7 +420,7 @@ export function isStringImplicitContextValue(value: unknown): value is StringCha return ( typeof asStringImplicitContextValue === 'object' && asStringImplicitContextValue !== null && - typeof asStringImplicitContextValue.value === 'string' && + (typeof asStringImplicitContextValue.value === 'string' || typeof asStringImplicitContextValue.value === 'undefined') && typeof asStringImplicitContextValue.name === 'string' && ThemeIcon.isThemeIcon(asStringImplicitContextValue.icon) && URI.isUri(asStringImplicitContextValue.uri) @@ -417,7 +491,7 @@ export function toToolVariableEntry(entry: IToolData, range?: IOffsetRange): ICh }; } -export function toToolSetVariableEntry(entry: ToolSet, range?: IOffsetRange): IChatRequestToolSetEntry { +export function toToolSetVariableEntry(entry: IToolSet, range?: IOffsetRange): IChatRequestToolSetEntry { return { kind: 'toolset', id: entry.id, diff --git a/src/vs/workbench/contrib/chat/common/attachments/chatVariables.ts b/src/vs/workbench/contrib/chat/common/attachments/chatVariables.ts new file mode 100644 index 00000000000..40cee25c13f --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/attachments/chatVariables.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; +import { Location } from '../../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatModel } from '../model/chatModel.js'; +import { IChatContentReference, IChatProgressMessage } from '../chatService/chatService.js'; +import { IDiagnosticVariableEntryFilterData, StringChatContextValue } from './chatVariableEntries.js'; +import { IToolAndToolSetEnablementMap } from '../tools/languageModelToolsService.js'; + +export interface IChatVariableData { + id: string; + name: string; + icon?: ThemeIcon; + fullName?: string; + description: string; + modelDescription?: string; + canTakeArgument?: boolean; +} + +export interface IChatRequestProblemsVariable { + id: 'vscode.problems'; + filter: IDiagnosticVariableEntryFilterData; +} + +export const isIChatRequestProblemsVariable = (obj: unknown): obj is IChatRequestProblemsVariable => + typeof obj === 'object' && obj !== null && 'id' in obj && (obj as IChatRequestProblemsVariable).id === 'vscode.problems'; + +export type IChatRequestVariableValue = string | URI | Location | Uint8Array | IChatRequestProblemsVariable | StringChatContextValue | unknown; + +export type IChatVariableResolverProgress = + | IChatContentReference + | IChatProgressMessage; + +export interface IChatVariableResolver { + (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; +} + +export const IChatVariablesService = createDecorator('IChatVariablesService'); + +export interface IChatVariablesService { + _serviceBrand: undefined; + getDynamicVariables(sessionResource: URI): ReadonlyArray; + getSelectedToolAndToolSets(sessionResource: URI): IToolAndToolSetEnablementMap; +} + +export interface IDynamicVariable { + range: IRange; + id: string; + fullName?: string; + icon?: ThemeIcon; + modelDescription?: string; + isFile?: boolean; + isDirectory?: boolean; + data: IChatRequestVariableValue; +} diff --git a/src/vs/workbench/contrib/chat/common/chat.ts b/src/vs/workbench/contrib/chat/common/chat.ts index 6c3b229e70a..4fc05e23b91 100644 --- a/src/vs/workbench/contrib/chat/common/chat.ts +++ b/src/vs/workbench/contrib/chat/common/chat.ts @@ -3,7 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { IChatTerminalToolInvocationData, ILegacyChatTerminalToolInvocationData } from './chatService.js'; +import { ResourceSet } from '../../../../base/common/map.js'; +import { chatEditingSessionIsReady } from './editing/chatEditingService.js'; +import { IChatModel } from './model/chatModel.js'; +import { isLegacyChatTerminalToolInvocationData, type IChatSessionStats, type IChatTerminalToolInvocationData, type ILegacyChatTerminalToolInvocationData } from './chatService/chatService.js'; import { ChatModeKind } from './constants.js'; export function checkModeOption(mode: ChatModeKind, option: boolean | ((mode: ChatModeKind) => boolean) | undefined): boolean | undefined { @@ -21,7 +24,7 @@ export function checkModeOption(mode: ChatModeKind, option: boolean | ((mode: Ch * we don't break existing chats */ export function migrateLegacyTerminalToolSpecificData(data: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData): IChatTerminalToolInvocationData { - if ('command' in data) { + if (isLegacyChatTerminalToolInvocationData(data)) { data = { kind: 'terminal', commandLine: { @@ -34,3 +37,29 @@ export function migrateLegacyTerminalToolSpecificData(data: IChatTerminalToolInv } return data; } + +export async function awaitStatsForSession(model: IChatModel): Promise { + if (!model.editingSession) { + return undefined; + } + + await chatEditingSessionIsReady(model.editingSession); + await Promise.all(model.editingSession.entries.get().map(entry => entry.getDiffInfo?.())); + + const diffs = model.editingSession.entries.get(); + const reduceResult = diffs.reduce((acc, diff) => { + acc.fileUris.add(diff.originalURI); + acc.added += diff.linesAdded?.get() ?? 0; + acc.removed += diff.linesRemoved?.get() ?? 0; + return acc; + }, { fileUris: new ResourceSet(), added: 0, removed: 0 }); + + if (reduceResult.fileUris.size > 0 && (reduceResult.added > 0 || reduceResult.removed > 0)) { + return { + fileCount: reduceResult.fileUris.size, + added: reduceResult.added, + removed: reduceResult.removed + }; + } + return undefined; +} diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts deleted file mode 100644 index f51121a4bf6..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.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 { localize } from '../../../../nls.js'; -import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IsWebContext } from '../../../../platform/contextkey/common/contextkeys.js'; -import { RemoteNameContext } from '../../../common/contextkeys.js'; -import { ViewContainerLocation } from '../../../common/views.js'; -import { ChatEntitlementContextKeys } from '../../../services/chat/common/chatEntitlementService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js'; - -export namespace ChatContextKeys { - export const responseVote = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); - export const responseDetectedAgentCommand = new RawContextKey('chatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('chatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); - export const responseSupportsIssueReporting = new RawContextKey('chatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('chatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); - export const responseIsFiltered = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); - export const responseHasError = new RawContextKey('chatSessionResponseError', false, { type: 'boolean', description: localize('chatResponseErrored', "True when the chat response resulted in an error.") }); - export const requestInProgress = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); - export const currentlyEditing = new RawContextKey('chatSessionCurrentlyEditing', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditing', "True when the current request is being edited.") }); - export const currentlyEditingInput = new RawContextKey('chatSessionCurrentlyEditingInput', false, { type: 'boolean', description: localize('interactiveSessionCurrentlyEditingInput', "True when the current request input at the bottom is being edited.") }); - - export const isResponse = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); - export const isRequest = new RawContextKey('chatRequest', false, { type: 'boolean', description: localize('chatRequest', "The chat item is a request") }); - export const itemId = new RawContextKey('chatItemId', '', { type: 'string', description: localize('chatItemId', "The id of the chat item.") }); - export const lastItemId = new RawContextKey('chatLastItemId', [], { type: 'string', description: localize('chatLastItemId', "The id of the last chat item.") }); - - export const editApplied = new RawContextKey('chatEditApplied', false, { type: 'boolean', description: localize('chatEditApplied', "True when the chat text edits have been applied.") }); - - export const inputHasText = new RawContextKey('chatInputHasText', false, { type: 'boolean', description: localize('interactiveInputHasText', "True when the chat input has text.") }); - export const inputHasFocus = new RawContextKey('chatInputHasFocus', false, { type: 'boolean', description: localize('interactiveInputHasFocus', "True when the chat input has focus.") }); - export const inChatInput = new RawContextKey('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") }); - export const inChatSession = new RawContextKey('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") }); - export const inChatEditor = new RawContextKey('inChatEditor', false, { type: 'boolean', description: localize('inChatEditor', "Whether focus is in a chat editor.") }); - export const hasPromptFile = new RawContextKey('chatPromptFileAttached', false, { type: 'boolean', description: localize('chatPromptFileAttachedContextDescription', "True when the chat has a prompt file attached.") }); - export const chatModeKind = new RawContextKey('chatAgentKind', ChatModeKind.Ask, { type: 'string', description: localize('agentKind', "The 'kind' of the current agent.") }); - export const chatToolCount = new RawContextKey('chatToolCount', 0, { type: 'number', description: localize('chatToolCount', "The number of tools available in the current agent.") }); - export const chatToolGroupingThreshold = new RawContextKey('chat.toolGroupingThreshold', 0, { type: 'number', description: localize('chatToolGroupingThreshold', "The number of tools at which we start doing virtual grouping.") }); - - export const supported = ContextKeyExpr.or(IsWebContext.negate(), RemoteNameContext.notEqualsTo(''), ContextKeyExpr.has('config.chat.experimental.serverlessWebEnabled')); - export const enabled = new RawContextKey('chatIsEnabled', false, { type: 'boolean', description: localize('chatIsEnabled', "True when chat is enabled because a default chat participant is activated with an implementation.") }); - - /** - * 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 agentSupportsAttachments = new RawContextKey('agentSupportsAttachments', false, { type: 'boolean', description: localize('agentSupportsAttachments', "True when the chat agent supports attachments.") }); - 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.") }); - export const chatEditingCanUndo = new RawContextKey('chatEditingCanUndo', false, { type: 'boolean', description: localize('chatEditingCanUndo', "True when it is possible to undo an interaction in the editing panel.") }); - export const chatEditingCanRedo = new RawContextKey('chatEditingCanRedo', false, { type: 'boolean', description: localize('chatEditingCanRedo', "True when it is possible to redo an interaction in the editing panel.") }); - export const languageModelsAreUserSelectable = new RawContextKey('chatModelsAreUserSelectable', false, { type: 'boolean', description: localize('chatModelsAreUserSelectable', "True when the chat model can be selected manually by the user.") }); - export const chatSessionHasModels = new RawContextKey('chatSessionHasModels', false, { type: 'boolean', description: localize('chatSessionHasModels', "True when the chat is in a contributed chat session that has available 'models' to display.") }); - export const extensionInvalid = new RawContextKey('chatExtensionInvalid', false, { type: 'boolean', description: localize('chatExtensionInvalid', "True when the installed chat extension is invalid and needs to be updated.") }); - export const inputCursorAtTop = new RawContextKey('chatCursorAtTop', false); - export const inputHasAgent = new RawContextKey('chatInputHasAgent', false); - export const location = new RawContextKey('chatLocation', undefined); - export const inQuickChat = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); - export const hasFileAttachments = new RawContextKey('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") }); - - export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); - export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available")); - export const hasCloudButtonV2 = ContextKeyExpr.has('config.chat.useCloudButtonV2'); - export const enableRemoteCodingAgentPromptFileOverlay = new RawContextKey('enableRemoteCodingAgentPromptFileOverlay', false, localize('enableRemoteCodingAgentPromptFileOverlay', "Whether the remote coding agent prompt file overlay feature is enabled")); - /** Used by the extension to skip the quit confirmation when #new wants to open a new folder */ - export const skipChatRequestInProgressMessage = new RawContextKey('chatSkipRequestInProgressMessage', false, { type: 'boolean', description: localize('chatSkipRequestInProgressMessage', "True when the chat request in progress message should be skipped.") }); - - // Re-exported from chat entitlement service - export const Setup = ChatEntitlementContextKeys.Setup; - export const Entitlement = ChatEntitlementContextKeys.Entitlement; - export const chatQuotaExceeded = ChatEntitlementContextKeys.chatQuotaExceeded; - export const completionsQuotaExceeded = ChatEntitlementContextKeys.completionsQuotaExceeded; - - export const Editing = { - hasToolConfirmation: new RawContextKey('chatHasToolConfirmation', false, { type: 'boolean', description: localize('chatEditingHasToolConfirmation', "True when a tool confirmation is present.") }), - }; - - export const Tools = { - toolsCount: new RawContextKey('toolsCount', 0, { type: 'number', description: localize('toolsCount', "The count of tools available in the chat.") }) - }; - - export const Modes = { - hasCustomChatModes: new RawContextKey('chatHasCustomAgents', false, { type: 'boolean', description: localize('chatHasAgents', "True when the chat has custom agents available.") }), - }; - - export const panelLocation = new RawContextKey('chatPanelLocation', undefined, { type: 'number', description: localize('chatPanelLocation', "The location of the chat panel.") }); - - export const inEmptyStateWithHistoryEnabled = new RawContextKey('chatInEmptyStateWithHistoryEnabled', false, { type: 'boolean', description: localize('chatInEmptyStateWithHistoryEnabled', "True when chat empty state history is enabled AND chat is in empty state.") }); - - export const sessionType = new RawContextKey('chatSessionType', '', { type: 'string', description: localize('chatSessionType', "The type of the current chat session item.") }); - export const isHistoryItem = new RawContextKey('chatIsHistoryItem', false, { type: 'boolean', description: localize('chatIsHistoryItem', "True when the chat session item is from history.") }); - export const isActiveSession = new RawContextKey('chatIsActiveSession', false, { type: 'boolean', description: localize('chatIsActiveSession', "True when the chat session is currently active (not deletable).") }); - export const isKatexMathElement = new RawContextKey('chatIsKatexMathElement', false, { type: 'boolean', description: localize('chatIsKatexMathElement', "True when focusing a KaTeX math element.") }); -} - -export namespace ChatContextKeyExprs { - export const inEditingMode = ContextKeyExpr.or( - 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 - ); - - export const agentViewWhen = ContextKeyExpr.and( - ChatEntitlementContextKeys.Setup.hidden.negate(), - ChatEntitlementContextKeys.Setup.disabled.negate(), - ContextKeyExpr.equals(`config.${ChatConfiguration.AgentSessionsViewLocation}`, 'view')); -} diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts deleted file mode 100644 index 8d1de310635..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ /dev/null @@ -1,358 +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 { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader } from '../../../../base/common/observable.js'; -import { hasKey } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; -import { Location, TextEdit } from '../../../../editor/common/languages.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; -import { localize } from '../../../../nls.js'; -import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditorPane } from '../../../common/editor.js'; -import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { IChatAgentResult } from './chatAgents.js'; -import { ChatModel, IChatResponseModel } from './chatModel.js'; -import { IChatProgress } from './chatService.js'; - -export const IChatEditingService = createDecorator('chatEditingService'); - -export interface IChatEditingService { - - _serviceBrand: undefined; - - startOrContinueGlobalEditingSession(chatModel: ChatModel): Promise; - - getEditingSession(chatSessionResource: URI): IChatEditingSession | undefined; - - /** - * All editing sessions, sorted by recency, e.g the last created session comes first. - */ - readonly editingSessionsObs: IObservable; - - /** - * Creates a new short lived editing session - */ - createEditingSession(chatModel: ChatModel): Promise; - - /** - * Creates an editing session with state transferred from the provided session. - */ - transferEditingSession(chatModel: ChatModel, session: IChatEditingSession): Promise; - - //#region related files - - hasRelatedFilesProviders(): boolean; - registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; - getRelatedFiles(chatSessionResource: URI, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; - - //#endregion -} - -export interface IChatRequestDraft { - readonly prompt: string; - readonly files: readonly URI[]; -} - -export interface IChatRelatedFileProviderMetadata { - readonly description: string; -} - -export interface IChatRelatedFile { - readonly uri: URI; - readonly description: string; -} - -export interface IChatRelatedFilesProvider { - readonly description: string; - provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise; -} - -export interface WorkingSetDisplayMetadata { - state: ModifiedFileEntryState; - description?: string; -} - -export interface IStreamingEdits { - pushText(edits: TextEdit[], isLastEdits: boolean): void; - pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void; - pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void; - /** Marks edits as done, idempotent */ - complete(): void; -} - -export interface IModifiedEntryTelemetryInfo { - readonly agentId: string | undefined; - readonly command: string | undefined; - readonly sessionId: string; - readonly requestId: string; - readonly result: IChatAgentResult | undefined; - readonly modelId: string | undefined; - readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; - readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined; - readonly feature: 'sideBarChat' | 'inlineChat' | undefined; -} - -export interface ISnapshotEntry { - readonly resource: URI; - readonly languageId: string; - readonly snapshotUri: URI; - readonly original: string; - readonly current: string; - readonly state: ModifiedFileEntryState; - telemetryInfo: IModifiedEntryTelemetryInfo; -} - -export interface IChatEditingSession extends IDisposable { - readonly isGlobalEditingSession: boolean; - /** @deprecated */ - readonly chatSessionId: string; - readonly chatSessionResource: URI; - readonly onDidDispose: Event; - readonly state: IObservable; - readonly entries: IObservable; - show(previousChanges?: boolean): Promise; - accept(...uris: URI[]): Promise; - reject(...uris: URI[]): Promise; - getEntry(uri: URI): IModifiedFileEntry | undefined; - readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined; - - restoreSnapshot(requestId: string, stopId: string | undefined): Promise; - - /** - * Marks all edits to the given resources as agent edits until - * {@link stopExternalEdits} is called with the same ID. This is used for - * agents that make changes on-disk rather than streaming edits through the - * chat session. - */ - startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[]): Promise; - stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise; - - /** - * Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop. - * @param uri File in the workspace - */ - getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined; - - getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise; - getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise; - - /** - * Will lead to this object getting disposed - */ - stop(clearState?: boolean): Promise; - - /** - * Starts making edits to the resource. - * @param resource URI that's being edited - * @param responseModel The response model making the edits - * @param inUndoStop The undo stop the edits will be grouped in - */ - startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits; - - /** - * Gets the document diff of a change made to a URI between one undo stop and - * the next one. - * @returns The observable or undefined if there is no diff between the stops. - */ - getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable | undefined; - - /** - * Gets the document diff of a change made to a URI between one request to another one. - * @returns The observable or undefined if there is no diff between the requests. - */ - getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable; - - readonly canUndo: IObservable; - readonly canRedo: IObservable; - undoInteraction(): Promise; - redoInteraction(): Promise; -} - -export interface IEditSessionEntryDiff { - /** LHS and RHS of a diff editor, if opened: */ - originalURI: URI; - modifiedURI: URI; - - /** Diff state information: */ - quitEarly: boolean; - identical: boolean; - - /** True if nothing else will be added to this diff. */ - isFinal: boolean; - - /** Added data (e.g. line numbers) to show in the UI */ - added: number; - /** Removed data (e.g. line numbers) to show in the UI */ - removed: number; -} - -export const enum ModifiedFileEntryState { - Modified, - Accepted, - Rejected, -} - -/** - * Represents a part of a change - */ -export interface IModifiedFileEntryChangeHunk { - accept(): Promise; - reject(): Promise; -} - -export interface IModifiedFileEntryEditorIntegration extends IDisposable { - - /** - * The index of a change - */ - currentIndex: IObservable; - - /** - * Reveal the first (`true`) or last (`false`) change - */ - reveal(firstOrLast: boolean, preserveFocus?: boolean): void; - - /** - * Go to next change and increate `currentIndex` - * @param wrap When at the last, start over again or not - * @returns If it went next - */ - next(wrap: boolean): boolean; - - /** - * @see `next` - */ - previous(wrap: boolean): boolean; - - /** - * Enable the accessible diff viewer for this editor - */ - enableAccessibleDiffView(): void; - - /** - * Accept the change given or the nearest - * @param change An opaque change object - */ - acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; - - /** - * @see `acceptNearestChange` - */ - rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; - - /** - * Toggle between diff-editor and normal editor - * @param change An opaque change object - * @param show Optional boolean to control if the diff should show - */ - toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise; -} - -export interface IModifiedFileEntry { - readonly entryId: string; - readonly originalURI: URI; - readonly modifiedURI: URI; - - readonly lastModifyingRequestId: string; - - readonly state: IObservable; - readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>; - readonly lastModifyingResponse: IObservable; - readonly rewriteRatio: IObservable; - - readonly waitsForLastEdits: IObservable; - - accept(): Promise; - reject(): Promise; - - reviewMode: IObservable; - autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>; - enableReviewModeUntilSettled(): void; - - /** - * Number of changes for this file - */ - readonly changesCount: IObservable; - - /** - * Diff information for this entry - */ - readonly diffInfo?: IObservable; - - /** - * Number of lines added in this entry. - */ - readonly linesAdded?: IObservable; - - /** - * Number of lines removed in this entry - */ - readonly linesRemoved?: IObservable; - - getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration; - hasModificationAt(location: Location): boolean; -} - -export interface IChatEditingSessionStream { - textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void; - notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void; -} - -export const enum ChatEditingSessionState { - Initial = 0, - StreamingEdits = 1, - Idle = 2, - Disposed = 3 -} - -export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; - -export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); -export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); -export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); -export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); -export const inChatEditingSessionContextKey = new RawContextKey('inChatEditingSession', undefined); -export const hasUndecidedChatEditingResourceContextKey = new RawContextKey('hasUndecidedChatEditingResource', false); -export const hasAppliedChatEditsContextKey = new RawContextKey('hasAppliedChatEdits', false); -export const applyingChatEditsFailedContextKey = new RawContextKey('applyingChatEditsFailed', false); - -export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit'; -export const defaultChatEditingMaxFileLimit = 10; - -export const enum ChatEditKind { - Created, - Modified, -} - -export interface IChatEditingActionContext { - // The chat session that this editing session is associated with - sessionResource: URI; -} - -export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext { - return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true }); -} - -export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI { - return URI.from({ - scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, - authority: session.chatSessionId, - query: showPreviousChanges ? 'previous' : undefined, - }); -} - -export function parseChatMultiDiffUri(uri: URI): { chatSessionId: string; showPreviousChanges: boolean } { - const chatSessionId = uri.authority; - const showPreviousChanges = uri.query === 'previous'; - - return { chatSessionId, showPreviousChanges }; -} diff --git a/src/vs/workbench/contrib/chat/common/chatLayoutService.ts b/src/vs/workbench/contrib/chat/common/chatLayoutService.ts deleted file mode 100644 index 62c5bff443c..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatLayoutService.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 { IObservable } from '../../../../base/common/observable.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - -export const IChatLayoutService = createDecorator('chatLayoutService'); - -export interface IChatLayoutService { - readonly _serviceBrand: undefined; - - readonly fontFamily: IObservable; - readonly fontSize: IObservable; -} diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts deleted file mode 100644 index 7567b60dc28..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ /dev/null @@ -1,2045 +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 { asArray } from '../../../../base/common/arrays.js'; -import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { equals } from '../../../../base/common/objects.js'; -import { IObservable, ObservablePromise, autorunSelfDisposable, observableFromEvent, observableSignalFromEvent } from '../../../../base/common/observable.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; -import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; -import { localize } from '../../../../nls.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; -import { migrateLegacyTerminalToolSpecificData } from './chat.js'; -import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from './chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; -import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMultiDiffData, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSessionContext, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; -import { LocalChatSessionUri } from './chatUri.js'; -import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry } from './chatVariableEntries.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; - - -export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record = { - png: 'image/png', - jpg: 'image/jpeg', - jpeg: 'image/jpeg', - gif: 'image/gif', - webp: 'image/webp', -}; - -export function getAttachableImageExtension(mimeType: string): string | undefined { - return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0]; -} - -export interface IChatRequestVariableData { - variables: IChatRequestVariableEntry[]; -} - -export interface IChatRequestModel { - readonly id: string; - readonly timestamp: number; - readonly username: string; - readonly modeInfo?: IChatRequestModeInfo; - readonly avatarIconUri?: URI; - readonly session: IChatModel; - readonly message: IParsedChatRequest; - readonly attempt: number; - readonly variableData: IChatRequestVariableData; - readonly confirmation?: string; - readonly locationData?: IChatLocationData; - readonly attachedContext?: IChatRequestVariableEntry[]; - readonly isCompleteAddedRequest: boolean; - readonly response?: IChatResponseModel; - readonly editedFileEvents?: IChatAgentEditedFileEvent[]; - shouldBeRemovedOnSend: IChatRequestDisablement | undefined; - shouldBeBlocked: boolean; - readonly modelId?: string; - readonly userSelectedTools?: UserSelectedTools; -} - -export interface ICodeBlockInfo { - readonly suggestionId: EditSuggestionId; -} - -export interface IChatTextEditGroupState { - sha1: string; - applied: number; -} - -export interface IChatTextEditGroup { - uri: URI; - edits: TextEdit[][]; - state?: IChatTextEditGroupState; - kind: 'textEditGroup'; - done: boolean | undefined; - isExternalEdit?: boolean; -} - -export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation { - const candidate = value as ICellTextEditOperation; - return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri); -} - -export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] { - return value.some(isCellTextEditOperation); -} - -export interface ICellTextEditOperation { - edit: TextEdit; - uri: URI; -} - -export interface IChatNotebookEditGroup { - uri: URI; - edits: (ICellTextEditOperation[] | ICellEditOperation[])[]; - state?: IChatTextEditGroupState; - kind: 'notebookEditGroup'; - done: boolean | undefined; - isExternalEdit?: boolean; -} - -/** - * Progress kinds that are included in the history of a response. - * Excludes "internal" types that are included in history. - */ -export type IChatProgressHistoryResponseContent = - | IChatMarkdownContent - | IChatAgentMarkdownContentWithVulnerability - | IChatResponseCodeblockUriPart - | IChatTreeData - | IChatMultiDiffData - | IChatContentInlineReference - | IChatProgressMessage - | IChatCommandButton - | IChatWarningMessage - | IChatTask - | IChatTaskSerialized - | IChatTextEditGroup - | IChatNotebookEditGroup - | IChatConfirmation - | IChatExtensionsContent - | IChatThinkingPart - | IChatPullRequestContent; - -/** - * "Normal" progress kinds that are rendered as parts of the stream of content. - */ -export type IChatProgressResponseContent = - | IChatProgressHistoryResponseContent - | IChatToolInvocation - | IChatToolInvocationSerialized - | IChatUndoStop - | IChatPrepareToolInvocationPart - | IChatElicitationRequest - | IChatClearToPreviousToolInvocation - | IChatMcpServersStarting; - -const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']); -function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent { - return !nonHistoryKinds.has(content.kind); -} - -export function toChatHistoryContent(content: ReadonlyArray): IChatProgressHistoryResponseContent[] { - return content.filter(isChatProgressHistoryResponseContent); -} - -export type IChatProgressRenderableResponseContent = Exclude; - -export interface IResponse { - readonly value: ReadonlyArray; - getMarkdown(): string; - toString(): string; -} - -export interface IChatResponseModel { - readonly onDidChange: Event; - readonly id: string; - readonly requestId: string; - readonly request: IChatRequestModel | undefined; - readonly username: string; - readonly avatarIcon?: ThemeIcon | URI; - readonly session: IChatModel; - readonly agent?: IChatAgentData; - readonly usedContext: IChatUsedContext | undefined; - readonly contentReferences: ReadonlyArray; - readonly codeCitations: ReadonlyArray; - readonly progressMessages: ReadonlyArray; - readonly slashCommand?: IChatAgentCommand; - readonly agentOrSlashCommandDetected: boolean; - /** View of the response shown to the user, may have parts omitted from undo stops. */ - readonly response: IResponse; - /** Entire response from the model. */ - readonly entireResponse: IResponse; - readonly isComplete: boolean; - readonly isCanceled: boolean; - readonly isPendingConfirmation: IObservable; - readonly isInProgress: IObservable; - readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined; - shouldBeBlocked: boolean; - readonly isCompleteAddedRequest: boolean; - /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ - readonly isStale: boolean; - readonly vote: ChatAgentVoteDirection | undefined; - readonly voteDownReason: ChatAgentVoteDownReason | undefined; - readonly followups?: IChatFollowup[] | undefined; - readonly result?: IChatAgentResult; - readonly codeBlockInfos: ICodeBlockInfo[] | undefined; - - initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void; - addUndoStop(undoStop: IChatUndoStop): void; - setVote(vote: ChatAgentVoteDirection): void; - setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void; - setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean; - /** - * Adopts any partially-undo {@link response} as the {@link entireResponse}. - * Only valid when {@link isComplete}. This is needed because otherwise an - * undone and then diverged state would start showing old data because the - * undo stops would no longer exist in the model. - */ - finalizeUndoState(): void; -} - -export type ChatResponseModelChangeReason = - | { reason: 'other' } - | { reason: 'undoStop'; id: string }; - -const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; - -export interface IChatRequestModeInfo { - kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' - isBuiltin: boolean; - modeInstructions: IChatRequestModeInstructions | undefined; - modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; - applyCodeBlockSuggestionId: EditSuggestionId | undefined; -} - -export interface IChatRequestModeInstructions { - readonly name: string; - readonly content: string; - readonly toolReferences: readonly ChatRequestToolReferenceEntry[]; - readonly metadata?: Record; -} - -export interface IChatRequestModelParameters { - session: ChatModel; - message: IParsedChatRequest; - variableData: IChatRequestVariableData; - timestamp: number; - attempt?: number; - modeInfo?: IChatRequestModeInfo; - confirmation?: string; - locationData?: IChatLocationData; - attachedContext?: IChatRequestVariableEntry[]; - isCompleteAddedRequest?: boolean; - modelId?: string; - restoredId?: string; - editedFileEvents?: IChatAgentEditedFileEvent[]; - userSelectedTools?: UserSelectedTools; -} - -export class ChatRequestModel implements IChatRequestModel { - public readonly id: string; - public response: ChatResponseModel | undefined; - public shouldBeRemovedOnSend: IChatRequestDisablement | undefined; - public readonly timestamp: number; - public readonly message: IParsedChatRequest; - public readonly isCompleteAddedRequest: boolean; - public readonly modelId?: string; - public readonly modeInfo?: IChatRequestModeInfo; - public readonly userSelectedTools?: UserSelectedTools; - - public shouldBeBlocked: boolean = false; - - private _session: ChatModel; - private readonly _attempt: number; - private _variableData: IChatRequestVariableData; - private readonly _confirmation?: string; - private readonly _locationData?: IChatLocationData; - private readonly _attachedContext?: IChatRequestVariableEntry[]; - private readonly _editedFileEvents?: IChatAgentEditedFileEvent[]; - - public get session(): ChatModel { - return this._session; - } - - public get username(): string { - return this.session.requesterUsername; - } - - public get avatarIconUri(): URI | undefined { - return this.session.requesterAvatarIconUri; - } - - public get attempt(): number { - return this._attempt; - } - - public get variableData(): IChatRequestVariableData { - return this._variableData; - } - - public set variableData(v: IChatRequestVariableData) { - this._variableData = v; - } - - public get confirmation(): string | undefined { - return this._confirmation; - } - - public get locationData(): IChatLocationData | undefined { - return this._locationData; - } - - public get attachedContext(): IChatRequestVariableEntry[] | undefined { - return this._attachedContext; - } - - public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined { - return this._editedFileEvents; - } - - constructor(params: IChatRequestModelParameters) { - this._session = params.session; - this.message = params.message; - this._variableData = params.variableData; - this.timestamp = params.timestamp; - this._attempt = params.attempt ?? 0; - this.modeInfo = params.modeInfo; - this._confirmation = params.confirmation; - this._locationData = params.locationData; - this._attachedContext = params.attachedContext; - this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; - this.modelId = params.modelId; - this.id = params.restoredId ?? 'request_' + generateUuid(); - this._editedFileEvents = params.editedFileEvents; - this.userSelectedTools = params.userSelectedTools; - } - - adoptTo(session: ChatModel) { - this._session = session; - } -} - -class AbstractResponse implements IResponse { - protected _responseParts: IChatProgressResponseContent[]; - - /** - * A stringified representation of response data which might be presented to a screenreader or used when copying a response. - */ - protected _responseRepr = ''; - - /** - * Just the markdown content of the response, used for determining the rendering rate of markdown - */ - protected _markdownContent = ''; - - get value(): IChatProgressResponseContent[] { - return this._responseParts; - } - - constructor(value: IChatProgressResponseContent[]) { - this._responseParts = value; - this._updateRepr(); - } - - toString(): string { - return this._responseRepr; - } - - /** - * _Just_ the content of markdown parts in the response - */ - getMarkdown(): string { - return this._markdownContent; - } - - protected _updateRepr() { - this._responseRepr = this.partsToRepr(this._responseParts); - - this._markdownContent = this._responseParts.map(part => { - if (part.kind === 'inlineReference') { - return this.inlineRefToRepr(part); - } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { - return part.content.value; - } else { - return ''; - } - }) - .filter(s => s.length > 0) - .join(''); - } - - private partsToRepr(parts: readonly IChatProgressResponseContent[]): string { - const blocks: string[] = []; - let currentBlockSegments: string[] = []; - let hasEditGroupsAfterLastClear = false; - - for (const part of parts) { - let segment: { text: string; isBlock?: boolean } | undefined; - switch (part.kind) { - case 'clearToPreviousToolInvocation': - currentBlockSegments = []; - blocks.length = 0; - hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing - continue; - case 'treeData': - case 'progressMessage': - case 'codeblockUri': - case 'extensions': - case 'pullRequest': - case 'undoStop': - case 'prepareToolInvocation': - case 'elicitation': - case 'thinking': - case 'multiDiffData': - case 'mcpServersStarting': - case 'mcpServersInteractionRequired' as string: // obsolete part, ignore - // Ignore - continue; - case 'toolInvocation': - case 'toolInvocationSerialized': - // Include tool invocations in the copy text - segment = this.getToolInvocationText(part); - break; - case 'inlineReference': - segment = { text: this.inlineRefToRepr(part) }; - break; - case 'command': - segment = { text: part.command.title, isBlock: true }; - break; - case 'textEditGroup': - case 'notebookEditGroup': - // Mark that we have edit groups after the last clear - hasEditGroupsAfterLastClear = true; - // Skip individual edit groups to avoid duplication - continue; - case 'confirmation': - if (part.message instanceof MarkdownString) { - segment = { text: `${part.title}\n${part.message.value}`, isBlock: true }; - break; - } - segment = { text: `${part.title}\n${part.message}`, isBlock: true }; - break; - default: - segment = { text: part.content.value }; - break; - } - - if (segment.isBlock) { - if (currentBlockSegments.length) { - blocks.push(currentBlockSegments.join('')); - currentBlockSegments = []; - } - blocks.push(segment.text); - } else { - currentBlockSegments.push(segment.text); - } - } - - if (currentBlockSegments.length) { - blocks.push(currentBlockSegments.join('')); - } - - // Add consolidated edit summary at the end if there were any edit groups after the last clear - if (hasEditGroupsAfterLastClear) { - blocks.push(localize('editsSummary', "Made changes.")); - } - - return blocks.join('\n\n'); - } - - private inlineRefToRepr(part: IChatContentInlineReference) { - if ('uri' in part.inlineReference) { - return this.uriToRepr(part.inlineReference.uri); - } - - return 'name' in part.inlineReference - ? '`' + part.inlineReference.name + '`' - : this.uriToRepr(part.inlineReference); - } - - private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } { - // Extract the message and input details - let message = ''; - let input = ''; - - if (toolInvocation.pastTenseMessage) { - message = typeof toolInvocation.pastTenseMessage === 'string' - ? toolInvocation.pastTenseMessage - : toolInvocation.pastTenseMessage.value; - } else { - message = typeof toolInvocation.invocationMessage === 'string' - ? toolInvocation.invocationMessage - : toolInvocation.invocationMessage.value; - } - - // Handle different types of tool invocations - if (toolInvocation.toolSpecificData) { - if (toolInvocation.toolSpecificData.kind === 'terminal') { - message = 'Ran terminal command'; - const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData); - input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; - } - } - - // Format the tool invocation text - let text = message; - if (input) { - text += `: ${input}`; - } - - // For completed tool invocations, also include the result details if available - if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isComplete(toolInvocation))) { - const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); - if (resultDetails && 'input' in resultDetails) { - const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || IChatToolInvocation.isComplete(toolInvocation) ? 'Completed' : 'Errored'; - text += `\n${resultPrefix} with input: ${resultDetails.input}`; - } - } - - return { text, isBlock: true }; - } - - private uriToRepr(uri: URI): string { - if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) { - return uri.toString(false); - } - - return basename(uri); - } -} - -/** A view of a subset of a response */ -class ResponseView extends AbstractResponse { - constructor( - _response: IResponse, - public readonly undoStop: string, - ) { - let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop); - // Undo stops are inserted before `codeblockUri`'s, which are preceeded by a - // markdownContent containing the opening code fence. Adjust the index - // backwards to avoid a buggy response if it looked like this happened. - if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') { - idx--; - } - - super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx)); - } -} - -export class Response extends AbstractResponse implements IDisposable { - private _onDidChangeValue = new Emitter(); - public get onDidChangeValue() { - return this._onDidChangeValue.event; - } - - private _citations: IChatCodeCitation[] = []; - - - constructor(value: IMarkdownString | ReadonlyArray) { - super(asArray(value).map((v) => ( - 'kind' in v ? v : - isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : - { kind: 'treeData', treeData: v } - ))); - } - - dispose(): void { - this._onDidChangeValue.dispose(); - } - - - clear(): void { - this._responseParts = []; - this._updateRepr(true); - } - - clearToPreviousToolInvocation(message?: string): void { - // look through the response parts and find the last tool invocation, then slice the response parts to that point - let lastToolInvocationIndex = -1; - for (let i = this._responseParts.length - 1; i >= 0; i--) { - const part = this._responseParts[i]; - if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { - lastToolInvocationIndex = i; - break; - } - } - if (lastToolInvocationIndex !== -1) { - this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1); - } else { - this._responseParts = []; - } - if (message) { - this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) }); - } - this._updateRepr(true); - } - - updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void { - if (progress.kind === 'clearToPreviousToolInvocation') { - if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) { - this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt.")); - } else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) { - this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt.")); - } else { - this.clearToPreviousToolInvocation(); - } - return; - } else if (progress.kind === 'markdownContent') { - - // last response which is NOT a text edit group because we do want to support heterogenous streaming but not have - // the MD be chopped up by text edit groups (and likely other non-renderable parts) - const lastResponsePart = this._responseParts - .filter(p => p.kind !== 'textEditGroup') - .at(-1); - - if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) { - // The last part can't be merged with- not markdown, or markdown with different permissions - this._responseParts.push(progress); - } else { - // Don't modify the current object, since it's being diffed by the renderer - const idx = this._responseParts.indexOf(lastResponsePart); - this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) }; - } - this._updateRepr(quiet); - } else if (progress.kind === 'thinking') { - - // tries to split thinking chunks if it is an array. only while certain models give us array chunks. - const lastResponsePart = this._responseParts - .filter(p => p.kind !== 'textEditGroup') - .at(-1); - - const lastText = lastResponsePart && lastResponsePart.kind === 'thinking' - ? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || '')) - : ''; - const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || ''); - const isEmpty = (s: string) => s.trim().length === 0; - - // Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking - if (!lastResponsePart - || lastResponsePart.kind !== 'thinking' - || isEmpty(currText) - || isEmpty(lastText) - || !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) { - this._responseParts.push(progress); - } else { - const idx = this._responseParts.indexOf(lastResponsePart); - this._responseParts[idx] = { - ...lastResponsePart, - value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value - }; - } - this._updateRepr(quiet); - } else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') { - // If the progress.uri is a cell Uri, its possible its part of the inline chat. - // Old approach of notebook inline chat would not start and end with notebook Uri, so we need to check for old approach. - const useOldApproachForInlineNotebook = progress.uri.scheme === Schemas.vscodeNotebookCell && !this._responseParts.find(part => part.kind === 'notebookEditGroup'); - // merge edits for the same file no matter when they come in - const notebookUri = useOldApproachForInlineNotebook ? undefined : CellUri.parse(progress.uri)?.notebook; - const uri = notebookUri ?? progress.uri; - let found = false; - const groupKind = progress.kind === 'textEdit' && !notebookUri ? 'textEditGroup' : 'notebookEditGroup'; - const edits: any = groupKind === 'textEditGroup' ? progress.edits : progress.edits.map(edit => TextEdit.isTextEdit(edit) ? { uri: progress.uri, edit } : edit); - const isExternalEdit = progress.isExternalEdit; - for (let i = 0; !found && i < this._responseParts.length; i++) { - const candidate = this._responseParts[i]; - if (candidate.kind === groupKind && !candidate.done && isEqual(candidate.uri, uri)) { - candidate.edits.push(edits); - candidate.done = progress.done; - found = true; - } - } - if (!found) { - this._responseParts.push({ - kind: groupKind, - uri, - edits: groupKind === 'textEditGroup' ? [edits] : edits, - done: progress.done, - isExternalEdit, - }); - } - this._updateRepr(quiet); - } else if (progress.kind === 'progressTask') { - // Add a new resolving part - const responsePosition = this._responseParts.push(progress) - 1; - this._updateRepr(quiet); - - const disp = progress.onDidAddProgress(() => { - this._updateRepr(false); - }); - - progress.task?.().then((content) => { - // Stop listening for progress updates once the task settles - disp.dispose(); - - // Replace the resolving part's content with the resolved response - if (typeof content === 'string') { - (this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content); - } - this._updateRepr(false); - }); - - } else if (progress.kind === 'toolInvocation') { - autorunSelfDisposable(reader => { - progress.state.read(reader); // update repr when state changes - this._updateRepr(false); - - if (IChatToolInvocation.isComplete(progress, reader)) { - reader.dispose(); - } - }); - this._responseParts.push(progress); - this._updateRepr(quiet); - } else { - this._responseParts.push(progress); - this._updateRepr(quiet); - } - } - - public addCitation(citation: IChatCodeCitation) { - this._citations.push(citation); - this._updateRepr(); - } - - protected override _updateRepr(quiet?: boolean) { - super._updateRepr(); - if (!this._onDidChangeValue) { - return; // called from parent constructor - } - - this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : ''; - - if (!quiet) { - this._onDidChangeValue.fire(); - } - } -} - -export interface IChatResponseModelParameters { - responseContent: IMarkdownString | ReadonlyArray; - session: ChatModel; - agent?: IChatAgentData; - slashCommand?: IChatAgentCommand; - requestId: string; - isComplete?: boolean; - isCanceled?: boolean; - vote?: ChatAgentVoteDirection; - voteDownReason?: ChatAgentVoteDownReason; - result?: IChatAgentResult; - followups?: ReadonlyArray; - isCompleteAddedRequest?: boolean; - shouldBeRemovedOnSend?: IChatRequestDisablement; - shouldBeBlocked?: boolean; - restoredId?: string; - /** - * undefined means it will be set later. - */ - codeBlockInfos: ICodeBlockInfo[] | undefined; -} - -export class ChatResponseModel extends Disposable implements IChatResponseModel { - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - public readonly id: string; - public readonly requestId: string; - private _session: ChatModel; - private _agent: IChatAgentData | undefined; - private _slashCommand: IChatAgentCommand | undefined; - private _isComplete: boolean; - private _isCanceled: boolean; - private _vote?: ChatAgentVoteDirection; - private _voteDownReason?: ChatAgentVoteDownReason; - private _result?: IChatAgentResult; - private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined; - public readonly isCompleteAddedRequest: boolean; - private _shouldBeBlocked: boolean = false; - - public get shouldBeBlocked() { - return this._shouldBeBlocked; - } - - public get request(): IChatRequestModel | undefined { - return this.session.getRequests().find(r => r.id === this.requestId); - } - - public get session() { - return this._session; - } - - public get shouldBeRemovedOnSend() { - return this._shouldBeRemovedOnSend; - } - - public get isComplete(): boolean { - return this._isComplete; - } - - public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) { - this._shouldBeRemovedOnSend = disablement; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - public get isCanceled(): boolean { - return this._isCanceled; - } - - public get vote(): ChatAgentVoteDirection | undefined { - return this._vote; - } - - public get voteDownReason(): ChatAgentVoteDownReason | undefined { - return this._voteDownReason; - } - - public get followups(): IChatFollowup[] | undefined { - return this._followups; - } - - private _response: Response; - private _finalizedResponse?: IResponse; - public get entireResponse(): IResponse { - return this._finalizedResponse || this._response; - } - - public get result(): IChatAgentResult | undefined { - return this._result; - } - - public get username(): string { - return this.session.responderUsername; - } - - public get avatarIcon(): ThemeIcon | URI | undefined { - return this.session.responderAvatarIcon; - } - - private _followups?: IChatFollowup[]; - - public get agent(): IChatAgentData | undefined { - return this._agent; - } - - public get slashCommand(): IChatAgentCommand | undefined { - return this._slashCommand; - } - - private _agentOrSlashCommandDetected: boolean | undefined; - public get agentOrSlashCommandDetected(): boolean { - return this._agentOrSlashCommandDetected ?? false; - } - - private _usedContext: IChatUsedContext | undefined; - public get usedContext(): IChatUsedContext | undefined { - return this._usedContext; - } - - private readonly _contentReferences: IChatContentReference[] = []; - public get contentReferences(): ReadonlyArray { - return Array.from(this._contentReferences); - } - - private readonly _codeCitations: IChatCodeCitation[] = []; - public get codeCitations(): ReadonlyArray { - return this._codeCitations; - } - - private readonly _progressMessages: IChatProgressMessage[] = []; - public get progressMessages(): ReadonlyArray { - return this._progressMessages; - } - - private _isStale: boolean = false; - public get isStale(): boolean { - return this._isStale; - } - - - readonly isPendingConfirmation: IObservable; - - readonly isInProgress: IObservable; - - private _responseView?: ResponseView; - public get response(): IResponse { - const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop; - if (!undoStop) { - return this._finalizedResponse || this._response; - } - - if (this._responseView?.undoStop !== undoStop) { - this._responseView = new ResponseView(this._response, undoStop); - } - - return this._responseView; - } - - private _codeBlockInfos: ICodeBlockInfo[] | undefined; - public get codeBlockInfos(): ICodeBlockInfo[] | undefined { - return this._codeBlockInfos; - } - - constructor(params: IChatResponseModelParameters) { - super(); - - this._session = params.session; - this._agent = params.agent; - this._slashCommand = params.slashCommand; - this.requestId = params.requestId; - this._isComplete = params.isComplete ?? false; - this._isCanceled = params.isCanceled ?? false; - this._vote = params.vote; - this._voteDownReason = params.voteDownReason; - this._result = params.result; - this._followups = params.followups ? [...params.followups] : undefined; - this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; - this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend; - this._shouldBeBlocked = params.shouldBeBlocked ?? false; - - // If we are creating a response with some existing content, consider it stale - this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0); - - this._response = this._register(new Response(params.responseContent)); - this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined; - - const signal = observableSignalFromEvent(this, this.onDidChange); - - this.isPendingConfirmation = signal.map((_value, r) => { - - signal.read(r); - - return this._response.value.some(part => - part.kind === 'toolInvocation' && part.state.read(r).type === IChatToolInvocation.StateKind.WaitingForConfirmation - || part.kind === 'confirmation' && part.isUsed === false - ); - }); - - this.isInProgress = signal.map((_value, r) => { - - signal.read(r); - - return !this.isPendingConfirmation.read(r) - && !this.shouldBeRemovedOnSend - && !this._isComplete; - }); - - this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason))); - this.id = params.restoredId ?? 'response_' + generateUuid(); - - this._register(this._session.onDidChange((e) => { - if (e.kind === 'setCheckpoint') { - const isDisabled = e.disabledResponseIds.has(this.id); - const didChange = this._shouldBeBlocked === isDisabled; - this._shouldBeBlocked = isDisabled; - if (didChange) { - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - } - })); - } - - initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void { - if (this._codeBlockInfos) { - throw new BugIndicatingError('Code block infos have already been initialized'); - } - this._codeBlockInfos = [...codeBlockInfo]; - } - - /** - * Apply a progress update to the actual response content. - */ - updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) { - this._response.updateContent(responsePart, quiet); - } - - /** - * Adds an undo stop at the current position in the stream. - */ - addUndoStop(undoStop: IChatUndoStop) { - this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id }); - this._response.updateContent(undoStop, true); - } - - /** - * Apply one of the progress updates that are not part of the actual response content. - */ - applyReference(progress: IChatUsedContext | IChatContentReference) { - if (progress.kind === 'usedContext') { - this._usedContext = progress; - } else if (progress.kind === 'reference') { - this._contentReferences.push(progress); - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - } - - applyCodeCitation(progress: IChatCodeCitation) { - this._codeCitations.push(progress); - this._response.addCitation(progress); - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { - this._agent = agent; - this._slashCommand = slashCommand; - this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - setResult(result: IChatAgentResult): void { - this._result = result; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - complete(): void { - if (this._result?.errorDetails?.responseIsRedacted) { - this._response.clear(); - } - - this._isComplete = true; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - cancel(): void { - this._isComplete = true; - this._isCanceled = true; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - setFollowups(followups: IChatFollowup[] | undefined): void { - this._followups = followups; - this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row - } - - setVote(vote: ChatAgentVoteDirection): void { - this._vote = vote; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void { - this._voteDownReason = reason; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean { - if (!this.response.value.includes(edit)) { - return false; - } - if (!edit.state) { - return false; - } - edit.state.applied = editCount; // must not be edit.edits.length - this._onDidChange.fire(defaultChatResponseModelChangeReason); - return true; - } - - adoptTo(session: ChatModel) { - this._session = session; - this._onDidChange.fire(defaultChatResponseModelChangeReason); - } - - - finalizeUndoState(): void { - this._finalizedResponse = this.response; - this._responseView = undefined; - this._shouldBeRemovedOnSend = undefined; - } - -} - - -export interface IChatRequestDisablement { - requestId: string; - afterUndoStop?: string; -} - -export interface IChatModel extends IDisposable { - readonly onDidDispose: Event; - readonly onDidChange: Event; - /** @deprecated Use {@link sessionResource} instead */ - readonly sessionId: string; - readonly sessionResource: URI; - readonly initialLocation: ChatAgentLocation; - readonly title: string; - readonly hasCustomTitle: boolean; - readonly requestInProgress: boolean; - readonly requestInProgressObs: IObservable; - readonly inputPlaceholder?: string; - readonly editingSessionObs?: ObservablePromise | undefined; - readonly editingSession?: IChatEditingSession | undefined; - /** - * Sets requests as 'disabled', removing them from the UI. If a request ID - * is given without undo stops, it's removed entirely. If an undo stop - * is given, all content after that stop is removed. - */ - setDisabledRequests(requestIds: IChatRequestDisablement[]): void; - getRequests(): IChatRequestModel[]; - setCheckpoint(requestId: string | undefined): void; - readonly checkpoint: IChatRequestModel | undefined; - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools): IChatRequestModel; - acceptResponseProgress(request: IChatRequestModel, progress: IChatProgress, quiet?: boolean): void; - setResponse(request: IChatRequestModel, result: IChatAgentResult): void; - completeResponse(request: IChatRequestModel): void; - setCustomTitle(title: string): void; - toExport(): IExportableChatData; - toJSON(): ISerializableChatData; - readonly contributedChatSession: IChatSessionContext | undefined; - setContributedChatSession(session: IChatSessionContext | undefined): void; -} - -export interface ISerializableChatsData { - [sessionId: string]: ISerializableChatData; -} - -export type ISerializableChatAgentData = UriDto; - -export interface ISerializableChatRequestData { - requestId: string; - message: string | IParsedChatRequest; // string => old format - /** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */ - variableData: IChatRequestVariableData; - response: ReadonlyArray | undefined; - - /**Old, persisted name for shouldBeRemovedOnSend */ - isHidden?: boolean; - shouldBeRemovedOnSend?: IChatRequestDisablement; - responseId?: string; - agent?: ISerializableChatAgentData; - workingSet?: UriComponents[]; - slashCommand?: IChatAgentCommand; - // responseErrorDetails: IChatResponseErrorDetails | undefined; - result?: IChatAgentResult; // Optional for backcompat - followups: ReadonlyArray | undefined; - isCanceled: boolean | undefined; - vote: ChatAgentVoteDirection | undefined; - voteDownReason?: ChatAgentVoteDownReason; - /** For backward compat: should be optional */ - usedContext?: IChatUsedContext; - contentReferences?: ReadonlyArray; - codeCitations?: ReadonlyArray; - timestamp?: number; - confirmation?: string; - editedFileEvents?: IChatAgentEditedFileEvent[]; - modelId?: string; - - responseMarkdownInfo: ISerializableMarkdownInfo[] | undefined; -} - -export interface ISerializableMarkdownInfo { - readonly suggestionId: EditSuggestionId; -} - -export interface IExportableChatData { - initialLocation: ChatAgentLocation | undefined; - requests: ISerializableChatRequestData[]; - requesterUsername: string; - responderUsername: string; - requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat -} - -/* - NOTE: every time the serialized data format is updated, we need to create a new interface, because we may need to handle any old data format when parsing. -*/ - -export interface ISerializableChatData1 extends IExportableChatData { - sessionId: string; - creationDate: number; - isImported: boolean; - - /** Indicates that this session was created in this window. Is cleared after the chat has been written to storage once. Needed to sync chat creations/deletions between empty windows. */ - isNew?: boolean; -} - -export interface ISerializableChatData2 extends ISerializableChatData1 { - version: 2; - lastMessageDate: number; - computedTitle: string | undefined; -} - -export interface ISerializableChatData3 extends Omit { - version: 3; - customTitle: string | undefined; -} - -/** -* Chat data that has been parsed and normalized to the current format. -*/ -export type ISerializableChatData = ISerializableChatData3; - -/** - * Chat data that has been loaded but not normalized, and could be any format - */ -export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3; - -/** - * Normalize chat data from storage to the current format. - * TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too. - */ -export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData { - normalizeOldFields(raw); - - if (!('version' in raw)) { - return { - version: 3, - ...raw, - lastMessageDate: raw.creationDate, - customTitle: undefined, - }; - } - - if (raw.version === 2) { - return { - ...raw, - version: 3, - customTitle: raw.computedTitle - }; - } - - return raw; -} - -function normalizeOldFields(raw: ISerializableChatDataIn): void { - // Fill in fields that very old chat data may be missing - if (!raw.sessionId) { - raw.sessionId = generateUuid(); - } - - if (!raw.creationDate) { - raw.creationDate = getLastYearDate(); - } - - if ('version' in raw && (raw.version === 2 || raw.version === 3)) { - if (!raw.lastMessageDate) { - // A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing. - raw.lastMessageDate = getLastYearDate(); - } - } - - // eslint-disable-next-line local/code-no-any-casts - if ((raw.initialLocation as any) === 'editing-session') { - raw.initialLocation = ChatAgentLocation.Chat; - } -} - -function getLastYearDate(): number { - const lastYearDate = new Date(); - lastYearDate.setFullYear(lastYearDate.getFullYear() - 1); - return lastYearDate.getTime(); -} - -export function isExportableSessionData(obj: unknown): obj is IExportableChatData { - const data = obj as IExportableChatData; - return typeof data === 'object' && - typeof data.requesterUsername === 'string'; -} - -export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { - const data = obj as ISerializableChatData; - return isExportableSessionData(obj) && - typeof data.creationDate === 'number' && - typeof data.sessionId === 'string' && - obj.requests.every((request: ISerializableChatRequestData) => - !request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext) - ); -} - -export type IChatChangeEvent = - | IChatInitEvent - | IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent - | IChatAddResponseEvent - | IChatSetAgentEvent - | IChatMoveEvent - | IChatSetHiddenEvent - | IChatCompletedRequestEvent - | IChatSetCheckpointEvent - | IChatSetCustomTitleEvent - ; - -export interface IChatAddRequestEvent { - kind: 'addRequest'; - request: IChatRequestModel; -} - -export interface IChatSetCheckpointEvent { - kind: 'setCheckpoint'; - disabledRequestIds: Set; - disabledResponseIds: Set; -} - -export interface IChatChangedRequestEvent { - kind: 'changedRequest'; - request: IChatRequestModel; -} - -export interface IChatCompletedRequestEvent { - kind: 'completedRequest'; - request: IChatRequestModel; -} - -export interface IChatAddResponseEvent { - kind: 'addResponse'; - response: IChatResponseModel; -} - -export const enum ChatRequestRemovalReason { - /** - * "Normal" remove - */ - Removal, - - /** - * Removed because the request will be resent - */ - Resend, - - /** - * Remove because the request is moving to another model - */ - Adoption -} - -export interface IChatRemoveRequestEvent { - kind: 'removeRequest'; - requestId: string; - responseId?: string; - reason: ChatRequestRemovalReason; -} - -export interface IChatSetHiddenEvent { - kind: 'setHidden'; - hiddenRequestIds: readonly IChatRequestDisablement[]; -} - -export interface IChatMoveEvent { - kind: 'move'; - target: URI; - range: IRange; -} - -export interface IChatSetAgentEvent { - kind: 'setAgent'; - agent: IChatAgentData; - command?: IChatAgentCommand; -} - -export interface IChatSetCustomTitleEvent { - kind: 'setCustomTitle'; - title: string; -} - -export interface IChatInitEvent { - kind: 'initialize'; -} - -export class ChatModel extends Disposable implements IChatModel { - static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string { - const firstRequestMessage = requests.at(0)?.message ?? ''; - const message = typeof firstRequestMessage === 'string' ? - firstRequestMessage : - firstRequestMessage.text; - return message.split('\n')[0].substring(0, 200); - } - - private readonly _onDidDispose = this._register(new Emitter()); - readonly onDidDispose = this._onDidDispose.event; - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private _requests: ChatRequestModel[]; - - private _contributedChatSession: IChatSessionContext | undefined; - public get contributedChatSession(): IChatSessionContext | undefined { - return this._contributedChatSession; - } - public setContributedChatSession(session: IChatSessionContext | undefined) { - this._contributedChatSession = session; - } - - // TODO to be clear, this is not the same as the id from the session object, which belongs to the provider. - // It's easier to be able to identify this model before its async initialization is complete - private readonly _sessionId: string; - /** @deprecated Use {@link sessionResource} instead */ - get sessionId(): string { - return this._sessionId; - } - - private readonly _sessionResource: URI; - get sessionResource(): URI { - return this._sessionResource; - } - - get requestInProgress(): boolean { - return this.requestInProgressObs.get(); - } - - readonly requestInProgressObs: IObservable; - - - get hasRequests(): boolean { - return this._requests.length > 0; - } - - get lastRequest(): ChatRequestModel | undefined { - return this._requests.at(-1); - } - - private _creationDate: number; - get creationDate(): number { - return this._creationDate; - } - - private _lastMessageDate: number; - get lastMessageDate(): number { - return this._lastMessageDate; - } - - private get _defaultAgent() { - return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask); - } - - private readonly _initialRequesterUsername: string | undefined; - get requesterUsername(): string { - return this._defaultAgent?.metadata.requester?.name ?? - this._initialRequesterUsername ?? ''; - } - - private readonly _initialResponderUsername: string | undefined; - get responderUsername(): string { - return this._defaultAgent?.fullName ?? - this._initialResponderUsername ?? ''; - } - - private readonly _initialRequesterAvatarIconUri: URI | undefined; - get requesterAvatarIconUri(): URI | undefined { - return this._defaultAgent?.metadata.requester?.icon ?? - this._initialRequesterAvatarIconUri; - } - - private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; - get responderAvatarIcon(): ThemeIcon | URI | undefined { - return this._defaultAgent?.metadata.themeIcon ?? - this._initialResponderAvatarIconUri; - } - - private _isImported = false; - get isImported(): boolean { - return this._isImported; - } - - private _customTitle: string | undefined; - get customTitle(): string | undefined { - return this._customTitle; - } - - get title(): string { - return this._customTitle || ChatModel.getDefaultTitle(this._requests); - } - - get hasCustomTitle(): boolean { - return this._customTitle !== undefined; - } - - private _editingSession: ObservablePromise | undefined; - get editingSessionObs(): ObservablePromise | undefined { - return this._editingSession; - } - - get editingSession(): IChatEditingSession | undefined { - return this._editingSession?.promiseResult.get()?.data; - } - - private readonly _initialLocation: ChatAgentLocation; - get initialLocation(): ChatAgentLocation { - return this._initialLocation; - } - - private readonly _canUseTools: boolean = true; - get canUseTools(): boolean { - return this._canUseTools; - } - - constructor( - initialData: ISerializableChatData | IExportableChatData | undefined, - initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; resource?: URI }, - @ILogService private readonly logService: ILogService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, - @IChatEditingService private readonly chatEditingService: IChatEditingService, - ) { - super(); - - const isValid = isSerializableSessionData(initialData); - if (initialData && !isValid) { - this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`); - } - - this._isImported = (!!initialData && !isValid) || (initialData?.isImported ?? false); - this._sessionId = (isValid && initialData.sessionId) || generateUuid(); - this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId); - - this._requests = initialData ? this._deserialize(initialData) : []; - this._creationDate = (isValid && initialData.creationDate) || Date.now(); - this._lastMessageDate = (isValid && initialData.lastMessageDate) || this._creationDate; - this._customTitle = isValid ? initialData.customTitle : undefined; - - this._initialRequesterUsername = initialData?.requesterUsername; - this._initialResponderUsername = initialData?.responderUsername; - this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; - - this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation; - this._canUseTools = initialModelProps.canUseTools; - - const lastResponse = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1)?.response); - - this.requestInProgressObs = lastResponse.map((response, r) => { - return response?.isInProgress.read(r) ?? false; - }); - } - - startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void { - const editingSessionPromise = transferFromSession - ? this.chatEditingService.transferEditingSession(this, transferFromSession) - : isGlobalEditingSession ? - this.chatEditingService.startOrContinueGlobalEditingSession(this) : - this.chatEditingService.createEditingSession(this); - this._editingSession = new ObservablePromise(editingSessionPromise); - this._editingSession.promise.then(editingSession => { - this._store.isDisposed ? editingSession.dispose() : this._register(editingSession); - }); - } - - private currentEditedFileEvents = new ResourceMap(); - notifyEditingAction(action: IChatEditingSessionAction): void { - const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep : - action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo : - action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null; - if (state === null) { - return; - } - - if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) { - this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri }); - } - } - - private _deserialize(obj: IExportableChatData): ChatRequestModel[] { - const requests = obj.requests; - if (!Array.isArray(requests)) { - this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`); - return []; - } - - try { - return requests.map((raw: ISerializableChatRequestData) => { - const parsedRequest = - typeof raw.message === 'string' - ? this.getParsedRequestFromString(raw.message) - : reviveParsedChatRequest(raw.message); - - // Old messages don't have variableData, or have it in the wrong (non-array) shape - const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel({ - session: this, - message: parsedRequest, - variableData, - timestamp: raw.timestamp ?? -1, - restoredId: raw.requestId, - confirmation: raw.confirmation, - editedFileEvents: raw.editedFileEvents, - modelId: raw.modelId, - }); - request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; - // eslint-disable-next-line local/code-no-any-casts - if (raw.response || raw.result || (raw as any).responseErrorDetails) { - const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - reviveSerializedAgent(raw.agent) : undefined; - - // Port entries from old format - const result = 'responseErrorDetails' in raw ? - // eslint-disable-next-line local/code-no-dangerous-type-assertions - { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; - request.response = new ChatResponseModel({ - responseContent: raw.response ?? [new MarkdownString(raw.response)], - session: this, - agent, - slashCommand: raw.slashCommand, - requestId: request.id, - isComplete: true, - isCanceled: raw.isCanceled, - vote: raw.vote, - voteDownReason: raw.voteDownReason, - result, - followups: raw.followups, - restoredId: raw.responseId, - shouldBeBlocked: request.shouldBeBlocked, - codeBlockInfos: raw.responseMarkdownInfo?.map(info => ({ suggestionId: info.suggestionId })), - }); - request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; - if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? - request.response.applyReference(revive(raw.usedContext)); - } - - raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r))); - raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c))); - } - return request; - }); - } catch (error) { - this.logService.error('Failed to parse chat data', error); - return []; - } - } - - private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData { - const variableData = raw && Array.isArray(raw.variables) - ? raw : - { variables: [] }; - - variableData.variables = variableData.variables.map((v): IChatRequestVariableEntry => { - // Old variables format - if (v && 'values' in v && Array.isArray(v.values)) { - return { - kind: 'generic', - id: v.id ?? '', - name: v.name, - value: v.values[0]?.value, - range: v.range, - modelDescription: v.modelDescription, - references: v.references - }; - } else { - return v; - } - }); - - return variableData; - } - - private getParsedRequestFromString(message: string): IParsedChatRequest { - // TODO These offsets won't be used, but chat replies need to go through the parser as well - const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; - return { - text: message, - parts - }; - } - - - - getRequests(): ChatRequestModel[] { - return this._requests; - } - - resetCheckpoint(): void { - for (const request of this._requests) { - request.shouldBeBlocked = false; - } - } - - setCheckpoint(requestId: string | undefined) { - let checkpoint: ChatRequestModel | undefined; - let checkpointIndex = -1; - if (requestId !== undefined) { - this._requests.forEach((request, index) => { - if (request.id === requestId) { - checkpointIndex = index; - checkpoint = request; - request.shouldBeBlocked = true; - } - }); - - if (!checkpoint) { - return; // Invalid request ID - } - } - - const disabledRequestIds = new Set(); - const disabledResponseIds = new Set(); - for (let i = this._requests.length - 1; i >= 0; i -= 1) { - const request = this._requests[i]; - if (this._checkpoint && !checkpoint) { - request.shouldBeBlocked = false; - } else if (checkpoint && i >= checkpointIndex) { - request.shouldBeBlocked = true; - disabledRequestIds.add(request.id); - if (request.response) { - disabledResponseIds.add(request.response.id); - } - } else if (checkpoint && i < checkpointIndex) { - request.shouldBeBlocked = false; - } - } - - this._checkpoint = checkpoint; - this._onDidChange.fire({ - kind: 'setCheckpoint', - disabledRequestIds, - disabledResponseIds - }); - } - - private _checkpoint: ChatRequestModel | undefined = undefined; - public get checkpoint() { - return this._checkpoint; - } - - setDisabledRequests(requestIds: IChatRequestDisablement[]) { - this._requests.forEach((request) => { - const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id); - request.shouldBeRemovedOnSend = shouldBeRemovedOnSend; - if (request.response) { - request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend; - } - }); - - this._onDidChange.fire({ - kind: 'setHidden', - hiddenRequestIds: requestIds, - }); - } - - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools): ChatRequestModel { - const editedFileEvents = [...this.currentEditedFileEvents.values()]; - this.currentEditedFileEvents.clear(); - const request = new ChatRequestModel({ - session: this, - message, - variableData, - timestamp: Date.now(), - attempt, - modeInfo, - confirmation, - locationData, - attachedContext: attachments, - isCompleteAddedRequest, - modelId, - editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined, - userSelectedTools, - }); - request.response = new ChatResponseModel({ - responseContent: [], - session: this, - agent: chatAgent, - slashCommand, - requestId: request.id, - isCompleteAddedRequest, - codeBlockInfos: undefined, - }); - - this._requests.push(request); - this._lastMessageDate = Date.now(); - this._onDidChange.fire({ kind: 'addRequest', request }); - return request; - } - - public setCustomTitle(title: string): void { - this._customTitle = title; - this._onDidChange.fire({ kind: 'setCustomTitle', title }); - } - - updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { - request.variableData = variableData; - this._onDidChange.fire({ kind: 'changedRequest', request }); - } - - adoptRequest(request: ChatRequestModel): void { - // this doesn't use `removeRequest` because it must not dispose the request object - const oldOwner = request.session; - const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id); - - if (index === -1) { - return; - } - - oldOwner._requests.splice(index, 1); - - request.adoptTo(this); - request.response?.adoptTo(this); - this._requests.push(request); - - oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption }); - this._onDidChange.fire({ kind: 'addRequest', request }); - } - - acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { - if (!request.response) { - request.response = new ChatResponseModel({ - responseContent: [], - session: this, - requestId: request.id, - codeBlockInfos: undefined, - }); - } - - if (request.response.isComplete) { - throw new Error('acceptResponseProgress: Adding progress to a completed response'); - } - - - if (progress.kind === 'usedContext' || progress.kind === 'reference') { - request.response.applyReference(progress); - } else if (progress.kind === 'codeCitation') { - request.response.applyCodeCitation(progress); - } else if (progress.kind === 'move') { - this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range }); - } else if (progress.kind === 'codeblockUri' && progress.isEdit) { - request.response.addUndoStop({ id: generateUuid(), kind: 'undoStop' }); - request.response.updateContent(progress, quiet); - } else if (progress.kind === 'progressTaskResult') { - // Should have been handled upstream, not sent to model - this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); - } else { - request.response.updateContent(progress, quiet); - } - } - - removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void { - const index = this._requests.findIndex(request => request.id === id); - const request = this._requests[index]; - - if (index !== -1) { - this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason }); - this._requests.splice(index, 1); - request.response?.dispose(); - } - } - - cancelRequest(request: ChatRequestModel): void { - if (request.response) { - request.response.cancel(); - } - } - - setResponse(request: ChatRequestModel, result: IChatAgentResult): void { - if (!request.response) { - request.response = new ChatResponseModel({ - responseContent: [], - session: this, - requestId: request.id, - codeBlockInfos: undefined, - }); - } - - request.response.setResult(result); - } - - completeResponse(request: ChatRequestModel): void { - if (!request.response) { - throw new Error('Call setResponse before completeResponse'); - } - - request.response.complete(); - this._onDidChange.fire({ kind: 'completedRequest', request }); - } - - setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void { - if (!request.response) { - // Maybe something went wrong? - return; - } - - request.response.setFollowups(followups); - } - - setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void { - request.response = response; - this._onDidChange.fire({ kind: 'addResponse', response }); - } - - toExport(): IExportableChatData { - return { - requesterUsername: this.requesterUsername, - requesterAvatarIconUri: this.requesterAvatarIconUri, - responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIcon, - initialLocation: this.initialLocation, - requests: this._requests.map((r): ISerializableChatRequestData => { - const message = { - ...r.message, - parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p) - }; - const agent = r.response?.agent; - const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() : - agent ? { ...agent } : undefined; - return { - requestId: r.id, - message, - variableData: r.variableData, - response: r.response ? - r.response.entireResponse.value.map(item => { - // Keeping the shape of the persisted data the same for back compat - if (item.kind === 'treeData') { - return item.treeData; - } else if (item.kind === 'markdownContent') { - return item.content; - } else if (item.kind === 'thinking') { - return { - kind: 'thinking', - value: item.value, - id: item.id, - metadata: item.metadata - }; - } else if (item.kind === 'confirmation') { - return { ...item, isLive: false }; - } else { - // eslint-disable-next-line local/code-no-any-casts - return item as any; // TODO - } - }) - : undefined, - responseId: r.response?.id, - shouldBeRemovedOnSend: r.shouldBeRemovedOnSend, - result: r.response?.result, - responseMarkdownInfo: r.response?.codeBlockInfos?.map(info => ({ suggestionId: info.suggestionId })), - followups: r.response?.followups, - isCanceled: r.response?.isCanceled, - vote: r.response?.vote, - voteDownReason: r.response?.voteDownReason, - agent: agentJson, - slashCommand: r.response?.slashCommand, - usedContext: r.response?.usedContext, - contentReferences: r.response?.contentReferences, - codeCitations: r.response?.codeCitations, - timestamp: r.timestamp, - confirmation: r.confirmation, - editedFileEvents: r.editedFileEvents, - modelId: r.modelId, - }; - }), - }; - } - - toJSON(): ISerializableChatData { - return { - version: 3, - ...this.toExport(), - sessionId: this.sessionId, - creationDate: this._creationDate, - isImported: this._isImported, - lastMessageDate: this._lastMessageDate, - customTitle: this._customTitle - }; - } - - override dispose() { - this._requests.forEach(r => r.response?.dispose()); - this._onDidDispose.fire(); - - super.dispose(); - } -} - -export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData { - return { - variables: variableData.variables.map(v => ({ - ...v, - range: v.range && { - start: v.range.start - diff, - endExclusive: v.range.endExclusive - diff - } - })) - }; -} - -export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean { - if (md1.baseUri && md2.baseUri) { - const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme - && md1.baseUri.authority === md2.baseUri.authority - && md1.baseUri.path === md2.baseUri.path - && md1.baseUri.query === md2.baseUri.query - && md1.baseUri.fragment === md2.baseUri.fragment; - if (!baseUriEquals) { - return false; - } - } else if (md1.baseUri || md2.baseUri) { - return false; - } - - return equals(md1.isTrusted, md2.isTrusted) && - md1.supportHtml === md2.supportHtml && - md1.supportThemeIcons === md2.supportThemeIcons; -} - -export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { - const appendedValue = typeof md2 === 'string' ? md2 : md2.value; - return { - value: md1.value + appendedValue, - isTrusted: md1.isTrusted, - supportThemeIcons: md1.supportThemeIcons, - supportHtml: md1.supportHtml, - baseUri: md1.baseUri - }; -} - -export function getCodeCitationsMessage(citations: ReadonlyArray): string { - if (citations.length === 0) { - return ''; - } - - const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set()); - const label = licenseTypes.size === 1 ? - localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) : - localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size); - return label; -} - -export enum ChatRequestEditedFileEventKind { - Keep = 1, - Undo = 2, - UserModification = 3, -} - -export interface IChatAgentEditedFileEvent { - readonly uri: URI; - readonly eventKind: ChatRequestEditedFileEventKind; -} - -/** URI for a resource embedded in a chat request/response */ -export namespace ChatResponseResource { - export const scheme = 'vscode-chat-response-resource'; - - export function createUri(sessionId: string, toolCallId: string, index: number, basename?: string): URI { - return URI.from({ - scheme: ChatResponseResource.scheme, - authority: sessionId, - path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''), - }); - } - - export function parseUri(uri: URI): undefined | { sessionId: string; toolCallId: string; index: number } { - if (uri.scheme !== ChatResponseResource.scheme) { - return undefined; - } - - const parts = uri.path.split('/'); - if (parts.length < 5) { - return undefined; - } - - const [, kind, toolCallId, index] = parts; - if (kind !== 'tool') { - return undefined; - } - - return { - sessionId: uri.authority, - 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 1f0b22bf715..bc51edb1440 100644 --- a/src/vs/workbench/contrib/chat/common/chatModes.ts +++ b/src/vs/workbench/contrib/chat/common/chatModes.ts @@ -10,16 +10,20 @@ import { constObservable, IObservable, ISettableObservable, observableValue, tra import { URI } from '../../../../base/common/uri.js'; import { IOffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IChatAgentService } from './chatAgents.js'; -import { ChatContextKeys } from './chatContextKeys.js'; -import { ChatModeKind } from './constants.js'; +import { IChatAgentService } from './participants/chatAgents.js'; +import { ChatContextKeys } from './actions/chatContextKeys.js'; +import { ChatConfiguration, ChatModeKind } from './constants.js'; import { IHandOff } from './promptSyntax/promptFileParser.js'; -import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, ICustomAgentVisibility, IPromptsService, isCustomAgentVisibility, PromptsStorage } from './promptSyntax/service/promptsService.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { isString } from '../../../../base/common/types.js'; export const IChatModeService = createDecorator('chatModeService'); export interface IChatModeService { @@ -38,6 +42,7 @@ export class ChatModeService extends Disposable implements IChatModeService { private static readonly CUSTOM_MODES_STORAGE_KEY = 'chat.customModes'; private readonly hasCustomModes: IContextKey; + private readonly agentModeDisabledByPolicy: IContextKey; private readonly _customModeInstances = new Map(); private readonly _onDidChangeChatModes = new Emitter(); @@ -48,11 +53,16 @@ export class ChatModeService extends Disposable implements IChatModeService { @IChatAgentService private readonly chatAgentService: IChatAgentService, @IContextKeyService contextKeyService: IContextKeyService, @ILogService private readonly logService: ILogService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.hasCustomModes = ChatContextKeys.Modes.hasCustomChatModes.bindTo(contextKeyService); + this.agentModeDisabledByPolicy = ChatContextKeys.Modes.agentModeDisabledByPolicy.bindTo(contextKeyService); + + // Initialize the policy context key + this.updateAgentModePolicyContextKey(); // Load cached modes from storage first this.loadCachedModes(); @@ -63,6 +73,14 @@ export class ChatModeService extends Disposable implements IChatModeService { })); this._register(this.storageService.onWillSaveState(() => this.saveCachedModes())); + // Listen for configuration changes that affect agent mode policy + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ChatConfiguration.AgentEnabled)) { + this.updateAgentModePolicyContextKey(); + this._onDidChangeChatModes.fire(); + } + })); + // Ideally we can get rid of the setting to disable agent mode? let didHaveToolsAgent = this.chatAgentService.hasToolsAgent; this._register(this.chatAgentService.onDidChangeAgents(() => { @@ -84,7 +102,7 @@ export class ChatModeService extends Disposable implements IChatModeService { } } - private deserializeCachedModes(cachedCustomModes: any): void { + private deserializeCachedModes(cachedCustomModes: unknown): void { if (!Array.isArray(cachedCustomModes)) { this.logService.error('Invalid cached custom modes data: expected array'); return; @@ -99,11 +117,13 @@ export class ChatModeService extends Disposable implements IChatModeService { name: cachedMode.name, description: cachedMode.description, tools: cachedMode.customTools, - model: cachedMode.model, + model: isString(cachedMode.model) ? [cachedMode.model] : cachedMode.model, argumentHint: cachedMode.argumentHint, agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] }, handOffs: cachedMode.handOffs, target: cachedMode.target, + visibility: cachedMode.visibility ?? { userInvokable: true, agentInvokable: cachedMode.infer !== false }, + agents: cachedMode.agents, source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local } }; const instance = new CustomChatMode(customChatMode); @@ -134,6 +154,10 @@ export class ChatModeService extends Disposable implements IChatModeService { const seenUris = new Set(); for (const customMode of customModes) { + if (!customMode.visibility.userInvokable) { + continue; + } + const uriString = customMode.uri.toString(); seenUris.add(uriString); @@ -186,7 +210,11 @@ export class ChatModeService extends Disposable implements IChatModeService { ChatMode.Ask, ]; - if (this.chatAgentService.hasToolsAgent) { + // Include Agent mode if: + // - It's enabled (hasToolsAgent is true), OR + // - It's disabled by policy (so we can show it with a lock icon) + // But hide it if the user manually disabled it via settings + if (this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy()) { builtinModes.unshift(ChatMode.Agent); } builtinModes.push(ChatMode.Edit); @@ -194,7 +222,16 @@ export class ChatModeService extends Disposable implements IChatModeService { } private getCustomModes(): IChatMode[] { - return this.chatAgentService.hasToolsAgent ? Array.from(this._customModeInstances.values()) : []; + // Show custom modes when agent mode is enabled OR when disabled by policy (to show them in the policy-managed group) + return this.chatAgentService.hasToolsAgent || this.isAgentModeDisabledByPolicy() ? Array.from(this._customModeInstances.values()) : []; + } + + private updateAgentModePolicyContextKey(): void { + this.agentModeDisabledByPolicy.set(this.isAgentModeDisabledByPolicy()); + } + + private isAgentModeDisabledByPolicy(): boolean { + return this.configurationService.inspect(ChatConfiguration.AgentEnabled).policyValue === false; } } @@ -204,7 +241,7 @@ export interface IChatModeData { readonly description?: string; readonly kind: ChatModeKind; readonly customTools?: readonly string[]; - readonly model?: string; + readonly model?: readonly string[] | string; readonly argumentHint?: string; readonly modeInstructions?: IChatModeInstructions; readonly body?: string; /* deprecated */ @@ -212,23 +249,29 @@ export interface IChatModeData { readonly uri?: URI; readonly source?: IChatModeSourceData; readonly target?: string; + readonly visibility?: ICustomAgentVisibility; + readonly agents?: readonly string[]; + readonly infer?: boolean; // deprecated, only available in old cached data } export interface IChatMode { readonly id: string; readonly name: IObservable; readonly label: IObservable; + readonly icon: IObservable; readonly description: IObservable; readonly isBuiltin: boolean; readonly kind: ChatModeKind; readonly customTools?: IObservable; readonly handOffs?: IObservable; - readonly model?: IObservable; + readonly model?: IObservable; readonly argumentHint?: IObservable; readonly modeInstructions?: IObservable; readonly uri?: IObservable; readonly source?: IAgentSource; readonly target?: IObservable; + readonly visibility?: IObservable; + readonly agents?: IObservable; } export interface IVariableReference { @@ -254,12 +297,14 @@ function isCachedChatModeData(data: unknown): data is IChatModeData { (mode.description === undefined || typeof mode.description === 'string') && (mode.customTools === undefined || Array.isArray(mode.customTools)) && (mode.modeInstructions === undefined || (typeof mode.modeInstructions === 'object' && mode.modeInstructions !== null)) && - (mode.model === undefined || typeof mode.model === 'string') && + (mode.model === undefined || typeof mode.model === 'string' || Array.isArray(mode.model)) && (mode.argumentHint === undefined || typeof mode.argumentHint === 'string') && (mode.handOffs === undefined || Array.isArray(mode.handOffs)) && (mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) && (mode.source === undefined || isChatModeSourceData(mode.source)) && - (mode.target === undefined || typeof mode.target === 'string'); + (mode.target === undefined || typeof mode.target === 'string') && + (mode.visibility === undefined || isCustomAgentVisibility(mode.visibility)) && + (mode.agents === undefined || Array.isArray(mode.agents)); } export class CustomChatMode implements IChatMode { @@ -268,10 +313,12 @@ export class CustomChatMode implements IChatMode { private readonly _customToolsObservable: ISettableObservable; private readonly _modeInstructions: ISettableObservable; private readonly _uriObservable: ISettableObservable; - private readonly _modelObservable: ISettableObservable; + private readonly _modelObservable: ISettableObservable; private readonly _argumentHintObservable: ISettableObservable; private readonly _handoffsObservable: ISettableObservable; private readonly _targetObservable: ISettableObservable; + private readonly _visibilityObservable: ISettableObservable; + private readonly _agentsObservable: ISettableObservable; private _source: IAgentSource; public readonly id: string; @@ -284,6 +331,10 @@ export class CustomChatMode implements IChatMode { return this._descriptionObservable; } + get icon(): IObservable { + return constObservable(undefined); + } + public get isBuiltin(): boolean { return isBuiltinChatMode(this); } @@ -292,7 +343,7 @@ export class CustomChatMode implements IChatMode { return this._customToolsObservable; } - get model(): IObservable { + get model(): IObservable { return this._modelObservable; } @@ -324,6 +375,14 @@ export class CustomChatMode implements IChatMode { return this._targetObservable; } + get visibility(): IObservable { + return this._visibilityObservable; + } + + get agents(): IObservable { + return this._agentsObservable; + } + public readonly kind = ChatModeKind.Agent; constructor( @@ -337,6 +396,8 @@ export class CustomChatMode implements IChatMode { this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint); this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs); this._targetObservable = observableValue('target', customChatMode.target); + this._visibilityObservable = observableValue('visibility', customChatMode.visibility); + this._agentsObservable = observableValue('agents', customChatMode.agents); this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions); this._uriObservable = observableValue('uri', customChatMode.uri); this._source = customChatMode.source; @@ -354,6 +415,8 @@ export class CustomChatMode implements IChatMode { this._argumentHintObservable.set(newData.argumentHint, tx); this._handoffsObservable.set(newData.handOffs, tx); this._targetObservable.set(newData.target, tx); + this._visibilityObservable.set(newData.visibility, tx); + this._agentsObservable.set(newData.agents, tx); this._modeInstructions.set(newData.agentInstructions, tx); this._uriObservable.set(newData.uri, tx); this._source = newData.source; @@ -373,13 +436,15 @@ export class CustomChatMode implements IChatMode { uri: this.uri.get(), handOffs: this.handOffs.get(), source: serializeChatModeSource(this._source), - target: this.target.get() + target: this.target.get(), + visibility: this.visibility.get(), + agents: this.agents.get() }; } } type IChatModeSourceData = - | { readonly storage: PromptsStorage.extension; readonly extensionId: string } + | { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: ExtensionAgentSourceType } | { readonly storage: PromptsStorage.local | PromptsStorage.user }; function isChatModeSourceData(value: unknown): value is IChatModeSourceData { @@ -398,7 +463,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou return undefined; } if (source.storage === PromptsStorage.extension) { - return { storage: PromptsStorage.extension, extensionId: source.extensionId.value }; + return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type }; } return { storage: source.storage }; } @@ -408,7 +473,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour return undefined; } if (data.storage === PromptsStorage.extension) { - return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) }; + return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type: data.type ?? ExtensionAgentSourceType.contribution }; } return { storage: data.storage }; } @@ -417,15 +482,18 @@ export class BuiltinChatMode implements IChatMode { public readonly name: IObservable; public readonly label: IObservable; public readonly description: IObservable; + public readonly icon: IObservable; constructor( public readonly kind: ChatModeKind, label: string, - description: string + description: string, + icon: ThemeIcon, ) { this.name = constObservable(kind); this.label = constObservable(label); this.description = observableValue('description', description); + this.icon = constObservable(icon); } public get isBuiltin(): boolean { @@ -455,9 +523,9 @@ export class BuiltinChatMode implements IChatMode { } export namespace ChatMode { - export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code")); - export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code")); - export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next")); + export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question); + export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit); + export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent); } export function isBuiltinChatMode(mode: IChatMode): boolean { diff --git a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts deleted file mode 100644 index 8e11708d781..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatProgressTypes/chatToolInvocation.ts +++ /dev/null @@ -1,134 +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 { encodeBase64 } from '../../../../../base/common/buffer.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; -import { localize } from '../../../../../nls.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 { - public readonly kind: 'toolInvocation' = 'toolInvocation'; - - public readonly invocationMessage: string | IMarkdownString; - public readonly originMessage: string | IMarkdownString | undefined; - public pastTenseMessage: string | IMarkdownString | undefined; - public confirmationMessages: IToolConfirmationMessages | undefined; - public readonly presentation: IPreparedToolInvocation['presentation']; - public readonly toolId: string; - public readonly source: ToolDataSource; - public readonly fromSubAgent: boolean | undefined; - public readonly parameters: unknown; - - public readonly toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; - - private readonly _progress = observableValue<{ message?: string | IMarkdownString; progress: number | undefined }>(this, { progress: 0 }); - private readonly _state: ISettableObservable; - - public get state(): IObservable { - return this._state; - } - - - constructor(preparedInvocation: IPreparedToolInvocation | undefined, toolData: IToolData, public readonly toolCallId: string, fromSubAgent: boolean | undefined, parameters: unknown) { - const defaultMessage = localize('toolInvocationMessage', "Using {0}", `"${toolData.displayName}"`); - const invocationMessage = preparedInvocation?.invocationMessage ?? defaultMessage; - this.invocationMessage = invocationMessage; - this.pastTenseMessage = preparedInvocation?.pastTenseMessage; - this.originMessage = preparedInvocation?.originMessage; - this.confirmationMessages = preparedInvocation?.confirmationMessages; - this.presentation = preparedInvocation?.presentation; - this.toolSpecificData = preparedInvocation?.toolSpecificData; - this.toolId = toolData.id; - this.source = toolData.source; - this.fromSubAgent = fromSubAgent; - this.parameters = parameters; - - if (!this.confirmationMessages?.title) { - this._state = observableValue(this, { type: IChatToolInvocation.StateKind.Executing, confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, progress: this._progress }); - } else { - this._state = observableValue(this, { - type: IChatToolInvocation.StateKind.WaitingForConfirmation, - confirm: reason => { - if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { - this._state.set({ type: IChatToolInvocation.StateKind.Cancelled, reason: reason.type }, undefined); - } else { - this._state.set({ type: IChatToolInvocation.StateKind.Executing, confirmed: reason, progress: this._progress }, undefined); - } - } - }); - } - } - - private _setCompleted(result: IToolResult | undefined, postConfirmed?: ConfirmedReason | undefined) { - if (postConfirmed && (postConfirmed.type === ToolConfirmKind.Denied || postConfirmed.type === ToolConfirmKind.Skipped)) { - this._state.set({ type: IChatToolInvocation.StateKind.Cancelled, reason: postConfirmed.type }, undefined); - return; - } - - this._state.set({ - type: IChatToolInvocation.StateKind.Completed, - confirmed: IChatToolInvocation.executionConfirmedOrDenied(this) || { type: ToolConfirmKind.ConfirmationNotNeeded }, - resultDetails: result?.toolResultDetails, - postConfirmed, - contentForModel: result?.content || [], - }, undefined); - } - - public didExecuteTool(result: IToolResult | undefined, final?: boolean): IChatToolInvocation.State { - if (result?.toolResultMessage) { - this.pastTenseMessage = result.toolResultMessage; - } else if (this._progress.get().message) { - this.pastTenseMessage = this._progress.get().message; - } - - if (this.confirmationMessages?.confirmResults && !result?.toolResultError && result?.confirmResults !== false && !final) { - this._state.set({ - type: IChatToolInvocation.StateKind.WaitingForPostApproval, - confirmed: IChatToolInvocation.executionConfirmedOrDenied(this) || { type: ToolConfirmKind.ConfirmationNotNeeded }, - resultDetails: result?.toolResultDetails, - contentForModel: result?.content || [], - confirm: reason => this._setCompleted(result, reason), - }, undefined); - } else { - this._setCompleted(result); - } - - return this._state.get(); - } - - public acceptProgress(step: IToolProgressStep) { - const prev = this._progress.get(); - this._progress.set({ - progress: step.progress || prev.progress || 0, - message: step.message, - }, undefined); - } - - public toJSON(): IChatToolInvocationSerialized { - // persist the serialized call as 'skipped' if we were waiting for postapproval - const waitingForPostApproval = this.state.get().type === IChatToolInvocation.StateKind.WaitingForPostApproval; - const details = waitingForPostApproval ? undefined : IChatToolInvocation.resultDetails(this); - - return { - kind: 'toolInvocationSerialized', - presentation: this.presentation, - invocationMessage: this.invocationMessage, - pastTenseMessage: this.pastTenseMessage, - originMessage: this.originMessage, - isConfirmed: waitingForPostApproval ? { type: ToolConfirmKind.Skipped } : IChatToolInvocation.executionConfirmedOrDenied(this), - isComplete: true, - source: this.source, - resultDetails: isToolResultOutputDetails(details) - ? { output: { type: 'data', mimeType: details.output.mimeType, base64Data: encodeBase64(details.output.value) } } - : details, - toolSpecificData: this.toolSpecificData, - toolCallId: this.toolCallId, - toolId: this.toolId, - fromSubAgent: this.fromSubAgent, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts deleted file mode 100644 index a5d2c6bcc08..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ /dev/null @@ -1,957 +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 { IAction } from '../../../../base/common/actions.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunSelfDisposable, IObservable, IReader } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ISelection } from '../../../../editor/common/core/selection.js'; -import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; -import { FileType } from '../../../../platform/files/common/files.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAutostartResult } from '../../mcp/common/mcpTypes.js'; -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'; -import { IChatRequestVariableEntry } from './chatVariableEntries.js'; -import { IChatRequestVariableValue } from './chatVariables.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; -import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from './languageModelToolsService.js'; - -export interface IChatRequest { - message: string; - variables: Record; -} - -export enum ChatErrorLevel { - Info = 0, - Warning = 1, - Error = 2 -} - -export interface IChatResponseErrorDetailsConfirmationButton { - data: any; - label: string; - isSecondary?: boolean; -} - -export interface IChatResponseErrorDetails { - message: string; - responseIsIncomplete?: boolean; - responseIsFiltered?: boolean; - responseIsRedacted?: boolean; - isQuotaExceeded?: boolean; - isRateLimited?: boolean; - level?: ChatErrorLevel; - confirmationButtons?: IChatResponseErrorDetailsConfirmationButton[]; - code?: string; -} - -export interface IChatResponseProgressFileTreeData { - label: string; - uri: URI; - type?: FileType; - children?: IChatResponseProgressFileTreeData[]; -} - -export type IDocumentContext = { - uri: URI; - version: number; - ranges: IRange[]; -}; - -export function isIDocumentContext(obj: unknown): obj is IDocumentContext { - return ( - !!obj && - typeof obj === 'object' && - 'uri' in obj && obj.uri instanceof URI && - 'version' in obj && typeof obj.version === 'number' && - 'ranges' in obj && Array.isArray(obj.ranges) && obj.ranges.every(Range.isIRange) - ); -} - -export interface IChatUsedContext { - documents: IDocumentContext[]; - kind: 'usedContext'; -} - -export function isIUsedContext(obj: unknown): obj is IChatUsedContext { - return ( - !!obj && - typeof obj === 'object' && - 'documents' in obj && - Array.isArray(obj.documents) && - obj.documents.every(isIDocumentContext) - ); -} - -export interface IChatContentVariableReference { - variableName: string; - value?: URI | Location; -} - -export enum ChatResponseReferencePartStatusKind { - Complete = 1, - Partial = 2, - Omitted = 3 -} - -export enum ChatResponseClearToPreviousToolInvocationReason { - NoReason = 0, - FilteredContentRetry = 1, - CopyrightContentRetry = 2, -} - -export interface IChatContentReference { - reference: URI | Location | IChatContentVariableReference | string; - iconPath?: ThemeIcon | { light: URI; dark?: URI }; - options?: { - status?: { description: string; kind: ChatResponseReferencePartStatusKind }; - diffMeta?: { added: number; removed: number }; - }; - kind: 'reference'; -} - -export interface IChatChangesSummary { - readonly reference: URI; - readonly sessionId: string; - readonly requestId: string; - readonly kind: 'changesSummary'; -} - -export interface IChatCodeCitation { - value: URI; - license: string; - snippet: string; - kind: 'codeCitation'; -} - -export interface IChatContentInlineReference { - resolveId?: string; - inlineReference: URI | Location | IWorkspaceSymbol; - name?: string; - kind: 'inlineReference'; -} - -export interface IChatMarkdownContent { - kind: 'markdownContent'; - content: IMarkdownString; - inlineReferences?: Record; - fromSubagent?: boolean; -} - -export interface IChatTreeData { - treeData: IChatResponseProgressFileTreeData; - kind: 'treeData'; -} -export interface IChatMultiDiffData { - multiDiffData: { - title: string; - resources: Array<{ - originalUri?: URI; - modifiedUri?: URI; - goToFileUri?: URI; - added?: number; - removed?: number; - }>; - }; - kind: 'multiDiffData'; - readOnly?: boolean; -} - -export interface IChatProgressMessage { - content: IMarkdownString; - kind: 'progressMessage'; -} - -export interface IChatTask extends IChatTaskDto { - deferred: DeferredPromise; - progress: (IChatWarningMessage | IChatContentReference)[]; - readonly onDidAddProgress: Event; - add(progress: IChatWarningMessage | IChatContentReference): void; - - complete: (result: string | void) => void; - task: () => Promise; - isSettled: () => boolean; -} - -export interface IChatUndoStop { - kind: 'undoStop'; - id: string; -} - -export interface IChatExternalEditsDto { - kind: 'externalEdits'; - start: boolean; /** true=start, false=stop */ - resources: UriComponents[]; -} - -export interface IChatTaskDto { - content: IMarkdownString; - kind: 'progressTask'; -} - -export interface IChatTaskSerialized { - content: IMarkdownString; - progress: (IChatWarningMessage | IChatContentReference)[]; - kind: 'progressTaskSerialized'; -} - -export interface IChatTaskResult { - content: IMarkdownString | void; - kind: 'progressTaskResult'; -} - -export interface IChatWarningMessage { - content: IMarkdownString; - kind: 'warning'; -} - -export interface IChatAgentVulnerabilityDetails { - title: string; - description: string; -} - -export interface IChatResponseCodeblockUriPart { - kind: 'codeblockUri'; - uri: URI; - isEdit?: boolean; -} - -export interface IChatAgentMarkdownContentWithVulnerability { - content: IMarkdownString; - vulnerabilities: IChatAgentVulnerabilityDetails[]; - kind: 'markdownVuln'; -} - -export interface IChatCommandButton { - command: Command; - kind: 'command'; -} - -export interface IChatMoveMessage { - uri: URI; - range: IRange; - kind: 'move'; -} - -export interface IChatTextEdit { - uri: URI; - edits: TextEdit[]; - kind: 'textEdit'; - done?: boolean; - isExternalEdit?: boolean; -} - -export interface IChatClearToPreviousToolInvocation { - kind: 'clearToPreviousToolInvocation'; - reason: ChatResponseClearToPreviousToolInvocationReason; -} - -export interface IChatNotebookEdit { - uri: URI; - edits: ICellEditOperation[]; - kind: 'notebookEdit'; - done?: boolean; - isExternalEdit?: boolean; -} - -export interface IChatConfirmation { - title: string; - message: string | IMarkdownString; - data: any; - /** Indicates whether this came from a current chat session (true/undefined) or a restored historic session (false) */ - isLive?: boolean; - buttons?: string[]; - isUsed?: boolean; - kind: 'confirmation'; -} - -export interface IChatElicitationRequest { - kind: 'elicitation'; - title: string | IMarkdownString; - message: string | IMarkdownString; - acceptButtonLabel: string; - rejectButtonLabel: string | undefined; - subtitle?: string | IMarkdownString; - source?: ToolDataSource; - state: 'pending' | 'accepted' | 'rejected'; - acceptedResult?: Record; - moreActions?: IAction[]; - accept(value: IAction | true): Promise; - reject?: () => Promise; - isHidden?: IObservable; - hide?(): void; -} - -export interface IChatThinkingPart { - kind: 'thinking'; - value?: string | string[]; - id?: string; - metadata?: { readonly [key: string]: any }; -} - -export interface IChatTerminalToolInvocationData { - kind: 'terminal'; - commandLine: { - original: string; - userEdited?: string; - toolEdited?: string; - }; - /** Message for model recommending the use of an alternative tool */ - alternativeRecommendation?: string; - language: string; - terminalToolSessionId?: string; - /** The predefined command ID that will be used for this terminal command */ - terminalCommandId?: string; - autoApproveInfo?: IMarkdownString; -} - -/** - * @deprecated This is the old API shape, we should support this for a while before removing it so - * we don't break existing chats - */ -export interface ILegacyChatTerminalToolInvocationData { - kind: 'terminal'; - command: string; - language: string; -} - -export interface IChatToolInputInvocationData { - kind: 'input'; - rawInput: any; -} - -export const enum ToolConfirmKind { - Denied, - ConfirmationNotNeeded, - Setting, - LmServicePerTool, - UserAction, - Skipped -} - -export type ConfirmedReason = - | { type: ToolConfirmKind.Denied } - | { type: ToolConfirmKind.ConfirmationNotNeeded } - | { type: ToolConfirmKind.Setting; id: string } - | { type: ToolConfirmKind.LmServicePerTool; scope: 'session' | 'workspace' | 'profile' } - | { type: ToolConfirmKind.UserAction } - | { type: ToolConfirmKind.Skipped }; - -export interface IChatToolInvocation { - readonly presentation: IPreparedToolInvocation['presentation']; - readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent; - readonly confirmationMessages?: IToolConfirmationMessages; - readonly originMessage: string | IMarkdownString | undefined; - readonly invocationMessage: string | IMarkdownString; - readonly pastTenseMessage: string | IMarkdownString | undefined; - readonly source: ToolDataSource; - readonly toolId: string; - readonly toolCallId: string; - readonly parameters: unknown; - readonly fromSubAgent?: boolean; - readonly state: IObservable; - - kind: 'toolInvocation'; -} - -export namespace IChatToolInvocation { - export const enum StateKind { - WaitingForConfirmation, - Executing, - WaitingForPostApproval, - Completed, - Cancelled, - } - - interface IChatToolInvocationStateBase { - type: StateKind; - } - - interface IChatToolInvocationWaitingForConfirmationState extends IChatToolInvocationStateBase { - type: StateKind.WaitingForConfirmation; - confirm(reason: ConfirmedReason): void; - } - - interface IChatToolInvocationPostConfirmState { - confirmed: ConfirmedReason; - } - - interface IChatToolInvocationExecutingState extends IChatToolInvocationStateBase, IChatToolInvocationPostConfirmState { - type: StateKind.Executing; - progress: IObservable<{ message?: string | IMarkdownString; progress: number | undefined }>; - } - - interface IChatToolInvocationPostExecuteState extends IChatToolInvocationPostConfirmState { - resultDetails: IToolResult['toolResultDetails']; - } - - interface IChatToolWaitingForPostApprovalState extends IChatToolInvocationStateBase, IChatToolInvocationPostExecuteState { - type: StateKind.WaitingForPostApproval; - confirm(reason: ConfirmedReason): void; - contentForModel: IToolResult['content']; - } - - interface IChatToolInvocationCompleteState extends IChatToolInvocationStateBase, IChatToolInvocationPostExecuteState { - type: StateKind.Completed; - postConfirmed: ConfirmedReason | undefined; - contentForModel: IToolResult['content']; - } - - interface IChatToolInvocationCancelledState extends IChatToolInvocationStateBase { - type: StateKind.Cancelled; - reason: ToolConfirmKind.Denied | ToolConfirmKind.Skipped; - } - - export type State = - | IChatToolInvocationWaitingForConfirmationState - | IChatToolInvocationExecutingState - | IChatToolWaitingForPostApprovalState - | IChatToolInvocationCompleteState - | IChatToolInvocationCancelledState; - - export function executionConfirmedOrDenied(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): ConfirmedReason | undefined { - if (invocation.kind === 'toolInvocationSerialized') { - if (invocation.isConfirmed === undefined || typeof invocation.isConfirmed === 'boolean') { - return { type: invocation.isConfirmed ? ToolConfirmKind.UserAction : ToolConfirmKind.Denied }; - } - return invocation.isConfirmed; - } - - const state = invocation.state.read(reader); - if (state.type === StateKind.WaitingForConfirmation) { - return undefined; // don't know yet - } - if (state.type === StateKind.Cancelled) { - return { type: state.reason }; - } - - return state.confirmed; - } - - export function awaitConfirmation(invocation: IChatToolInvocation, token?: CancellationToken): Promise { - const reason = executionConfirmedOrDenied(invocation); - if (reason) { - return Promise.resolve(reason); - } - - const store = new DisposableStore(); - return new Promise(resolve => { - if (token) { - store.add(token.onCancellationRequested(() => { - resolve({ type: ToolConfirmKind.Denied }); - })); - } - - store.add(autorun(reader => { - const reason = executionConfirmedOrDenied(invocation, reader); - if (reason) { - store.dispose(); - resolve(reason); - } - })); - }).finally(() => { - store.dispose(); - }); - } - - function postApprovalConfirmedOrDenied(invocation: IChatToolInvocation, reader?: IReader): ConfirmedReason | undefined { - const state = invocation.state.read(reader); - if (state.type === StateKind.Completed) { - return state.postConfirmed || { type: ToolConfirmKind.ConfirmationNotNeeded }; - } - if (state.type === StateKind.Cancelled) { - return { type: state.reason }; - } - - return undefined; - } - - export function confirmWith(invocation: IChatToolInvocation | undefined, reason: ConfirmedReason) { - const state = invocation?.state.get(); - if (state?.type === StateKind.WaitingForConfirmation || state?.type === StateKind.WaitingForPostApproval) { - state.confirm(reason); - return true; - } - return false; - } - - export function awaitPostConfirmation(invocation: IChatToolInvocation, token?: CancellationToken): Promise { - const reason = postApprovalConfirmedOrDenied(invocation); - if (reason) { - return Promise.resolve(reason); - } - - const store = new DisposableStore(); - return new Promise(resolve => { - if (token) { - store.add(token.onCancellationRequested(() => { - resolve({ type: ToolConfirmKind.Denied }); - })); - } - - store.add(autorun(reader => { - const reason = postApprovalConfirmedOrDenied(invocation, reader); - if (reason) { - store.dispose(); - resolve(reason); - } - })); - }).finally(() => { - store.dispose(); - }); - } - - export function resultDetails(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader) { - if (invocation.kind === 'toolInvocationSerialized') { - return invocation.resultDetails; - } - - const state = invocation.state.read(reader); - if (state.type === StateKind.Completed || state.type === StateKind.WaitingForPostApproval) { - return state.resultDetails; - } - - return undefined; - } - - export function isComplete(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): boolean { - if ('isComplete' in invocation) { // serialized - return true; // always cancelled or complete - } - - const state = invocation.state.read(reader); - return state.type === StateKind.Completed || state.type === StateKind.Cancelled; - } -} - - -export interface IToolResultOutputDetailsSerialized { - output: { - type: 'data'; - mimeType: string; - base64Data: string; - }; -} - -/** - * This is a IChatToolInvocation that has been serialized, like after window reload, so it is no longer an active tool invocation. - */ -export interface IChatToolInvocationSerialized { - presentation: IPreparedToolInvocation['presentation']; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent; - invocationMessage: string | IMarkdownString; - originMessage: string | IMarkdownString | undefined; - pastTenseMessage: string | IMarkdownString | undefined; - resultDetails?: Array | IToolResultInputOutputDetails | IToolResultOutputDetailsSerialized; - /** boolean used by pre-1.104 versions */ - isConfirmed: ConfirmedReason | boolean | undefined; - isComplete: boolean; - toolCallId: string; - toolId: string; - source: ToolDataSource; - readonly fromSubAgent?: boolean; - kind: 'toolInvocationSerialized'; -} - -export interface IChatExtensionsContent { - extensions: string[]; - kind: 'extensions'; -} - -export interface IChatPullRequestContent { - uri: URI; - title: string; - description: string; - author: string; - linkTag: string; - kind: 'pullRequest'; -} - -export interface IChatTodoListContent { - kind: 'todoList'; - sessionId: string; - todoList: Array<{ - id: string; - title: string; - description: string; - status: 'not-started' | 'in-progress' | 'completed'; - }>; -} - -export interface IChatMcpServersStarting { - readonly kind: 'mcpServersStarting'; - readonly state?: IObservable; // not hydrated when serialized - didStartServerIds?: string[]; -} - -export class ChatMcpServersStarting implements IChatMcpServersStarting { - public readonly kind = 'mcpServersStarting'; - - public didStartServerIds?: string[] = []; - - public get isEmpty() { - const s = this.state.get(); - return !s.working && s.serversRequiringInteraction.length === 0; - } - - constructor(public readonly state: IObservable) { } - - wait() { - return new Promise(resolve => { - autorunSelfDisposable(reader => { - const s = this.state.read(reader); - if (!s.working) { - reader.dispose(); - resolve(s); - } - }); - }); - } - - toJSON(): IChatMcpServersStarting { - return { kind: 'mcpServersStarting', didStartServerIds: this.didStartServerIds }; - } -} - -export interface IChatPrepareToolInvocationPart { - readonly kind: 'prepareToolInvocation'; - readonly toolName: string; -} - -export type IChatProgress = - | IChatMarkdownContent - | IChatAgentMarkdownContentWithVulnerability - | IChatTreeData - | IChatMultiDiffData - | IChatUsedContext - | IChatContentReference - | IChatContentInlineReference - | IChatCodeCitation - | IChatProgressMessage - | IChatTask - | IChatTaskResult - | IChatCommandButton - | IChatWarningMessage - | IChatTextEdit - | IChatNotebookEdit - | IChatMoveMessage - | IChatResponseCodeblockUriPart - | IChatConfirmation - | IChatClearToPreviousToolInvocation - | IChatToolInvocation - | IChatToolInvocationSerialized - | IChatExtensionsContent - | IChatPullRequestContent - | IChatUndoStop - | IChatPrepareToolInvocationPart - | IChatThinkingPart - | IChatTaskSerialized - | IChatElicitationRequest - | IChatMcpServersStarting; - -export interface IChatFollowup { - kind: 'reply'; - message: string; - agentId: string; - subCommand?: string; - title?: string; - tooltip?: string; -} - -export enum ChatAgentVoteDirection { - Down = 0, - Up = 1 -} - -export enum ChatAgentVoteDownReason { - IncorrectCode = 'incorrectCode', - DidNotFollowInstructions = 'didNotFollowInstructions', - IncompleteCode = 'incompleteCode', - MissingContext = 'missingContext', - PoorlyWrittenOrFormatted = 'poorlyWrittenOrFormatted', - RefusedAValidRequest = 'refusedAValidRequest', - OffensiveOrUnsafe = 'offensiveOrUnsafe', - Other = 'other', - WillReportIssue = 'willReportIssue' -} - -export interface IChatVoteAction { - kind: 'vote'; - direction: ChatAgentVoteDirection; - reason: ChatAgentVoteDownReason | undefined; -} - -export enum ChatCopyKind { - // Keyboard shortcut or context menu - Action = 1, - Toolbar = 2 -} - -export interface IChatCopyAction { - kind: 'copy'; - codeBlockIndex: number; - copyKind: ChatCopyKind; - copiedCharacters: number; - totalCharacters: number; - copiedText: string; - totalLines: number; - copiedLines: number; - modelId: string; - languageId?: string; -} - -export interface IChatInsertAction { - kind: 'insert'; - codeBlockIndex: number; - totalCharacters: number; - totalLines: number; - languageId?: string; - modelId: string; - newFile?: boolean; -} - -export interface IChatApplyAction { - kind: 'apply'; - codeBlockIndex: number; - totalCharacters: number; - totalLines: number; - languageId?: string; - modelId: string; - newFile?: boolean; - codeMapper?: string; - editsProposed: boolean; -} - - -export interface IChatTerminalAction { - kind: 'runInTerminal'; - codeBlockIndex: number; - languageId?: string; -} - -export interface IChatCommandAction { - kind: 'command'; - commandButton: IChatCommandButton; -} - -export interface IChatFollowupAction { - kind: 'followUp'; - followup: IChatFollowup; -} - -export interface IChatBugReportAction { - kind: 'bug'; -} - -export interface IChatInlineChatCodeAction { - kind: 'inlineChat'; - action: 'accepted' | 'discarded'; -} - - -export interface IChatEditingSessionAction { - kind: 'chatEditingSessionAction'; - uri: URI; - hasRemainingEdits: boolean; - outcome: 'accepted' | 'rejected' | 'userModified'; -} - -export interface IChatEditingHunkAction { - kind: 'chatEditingHunkAction'; - uri: URI; - lineCount: number; - linesAdded: number; - linesRemoved: number; - outcome: 'accepted' | 'rejected'; - hasRemainingEdits: boolean; - modeId?: string; - modelId?: string; - languageId?: string; -} - -export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction | IChatEditingHunkAction; - -export interface IChatUserActionEvent { - action: ChatUserAction; - agentId: string | undefined; - command: string | undefined; - sessionResource: URI; - requestId: string; - result: IChatAgentResult | undefined; - modelId?: string | undefined; - modeId?: string | undefined; -} - -export interface IChatDynamicRequest { - /** - * The message that will be displayed in the UI - */ - message: string; - - /** - * Any extra metadata/context that will go to the provider. - */ - metadata?: any; -} - -export interface IChatCompleteResponse { - message: string | ReadonlyArray; - result?: IChatAgentResult; - followups?: IChatFollowup[]; -} - -export interface IChatDetail { - sessionResource: URI; - title: string; - lastMessageDate: number; - isActive: boolean; -} - -export interface IChatProviderInfo { - id: string; -} - -export interface IChatTransferredSessionData { - sessionId: string; - inputValue: string; - location: ChatAgentLocation; - mode: ChatModeKind; -} - -export interface IChatSendRequestResponseState { - responseCreatedPromise: Promise; - responseCompletePromise: Promise; -} - -export interface IChatSendRequestData extends IChatSendRequestResponseState { - agent: IChatAgentData; - slashCommand?: IChatAgentCommand; -} - -export interface IChatEditorLocationData { - type: ChatAgentLocation.EditorInline; - document: URI; - selection: ISelection; - wholeRange: IRange; - close: () => void; - delegateSessionResource: URI | undefined; -} - -export interface IChatNotebookLocationData { - type: ChatAgentLocation.Notebook; - sessionInputUri: URI; -} - -export interface IChatTerminalLocationData { - type: ChatAgentLocation.Terminal; - // TBD -} - -export type IChatLocationData = IChatEditorLocationData | IChatNotebookLocationData | IChatTerminalLocationData; - -export interface IChatSendRequestOptions { - modeInfo?: IChatRequestModeInfo; - userSelectedModelId?: string; - userSelectedTools?: IObservable; - location?: ChatAgentLocation; - locationData?: IChatLocationData; - parserContext?: IChatParserContext; - attempt?: number; - noCommandDetection?: boolean; - acceptedConfirmationData?: any[]; - rejectedConfirmationData?: any[]; - attachedContext?: IChatRequestVariableEntry[]; - - /** The target agent ID can be specified with this property instead of using @ in 'message' */ - agentId?: string; - /** agentId, but will not add a @ name to the request */ - agentIdSilent?: string; - slashCommand?: string; - - /** - * The label of the confirmation action that was selected. - */ - confirmation?: string; - - /** - * Summary data for chat sessions context - */ - chatSummary?: { - prompt?: string; - history?: string; - }; -} - -export const IChatService = createDecorator('IChatService'); - -export interface IChatService { - _serviceBrand: undefined; - transferredSessionData: IChatTransferredSessionData | undefined; - - readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>; - - isEnabled(location: ChatAgentLocation): boolean; - hasSessions(): boolean; - startSession(location: ChatAgentLocation, token: CancellationToken, isGlobalEditingSession?: boolean, options?: { canUseTools?: boolean }): ChatModel; - getSession(sessionResource: URI): IChatModel | undefined; - getSessionByLegacyId(sessionId: string): IChatModel | undefined; - getOrRestoreSession(sessionResource: URI): Promise; - getPersistedSessionTitle(sessionResource: URI): string | undefined; - isPersistedSessionEmpty(sessionResource: URI): boolean; - loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModel | undefined; - loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; - readonly editingSessions: IChatEditingSession[]; - getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined; - - /** - * Returns whether the request was accepted.` - */ - sendRequest(sessionResource: URI, message: string, options?: IChatSendRequestOptions): Promise; - - resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; - adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; - removeRequest(sessionResource: URI, requestId: string): Promise; - cancelCurrentRequestForSession(sessionResource: URI): void; - clearSession(sessionResource: URI): Promise; - addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void; - setChatSessionTitle(sessionResource: URI, title: string): void; - getLocalSessionHistory(): Promise; - clearAllHistoryEntries(): Promise; - removeHistoryEntry(sessionResource: URI): Promise; - getChatStorageFolder(): URI; - logChatIndex(): void; - - readonly onDidPerformUserAction: Event; - notifyUserAction(event: IChatUserActionEvent): void; - readonly onDidDisposeSession: Event<{ readonly sessionResource: URI; readonly reason: 'cleared' }>; - - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; - - activateDefaultAgent(location: ChatAgentLocation): Promise; - - readonly edits2Enabled: boolean; - - readonly requestInProgressObs: IObservable; -} - -export interface IChatSessionContext { - readonly chatSessionType: string; - readonly chatSessionResource: URI; - readonly isUntitled: boolean; -} - -export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts new file mode 100644 index 00000000000..ed2b28d0265 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatService/chatService.ts @@ -0,0 +1,1306 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IAction } from '../../../../../base/common/actions.js'; +import { DeferredPromise } from '../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { DisposableStore, IReference } from '../../../../../base/common/lifecycle.js'; +import { autorun, autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { hasKey } from '../../../../../base/common/types.js'; +import { URI, UriComponents } from '../../../../../base/common/uri.js'; +import { IRange, Range } from '../../../../../editor/common/core/range.js'; +import { ISelection } from '../../../../../editor/common/core/selection.js'; +import { Command, Location, TextEdit } from '../../../../../editor/common/languages.js'; +import { FileType } from '../../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IAutostartResult } from '../../../mcp/common/mcpTypes.js'; +import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { IWorkspaceSymbol } from '../../../search/common/search.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentResult, UserSelectedTools } from '../participants/chatAgents.js'; +import { IChatEditingSession } from '../editing/chatEditingService.js'; +import { IChatModel, IChatRequestModeInfo, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../model/chatModel.js'; +import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { IChatParserContext } from '../requestParser/chatRequestParser.js'; +import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; +import { IChatRequestVariableValue } from '../attachments/chatVariables.js'; +import { ChatAgentLocation } from '../constants.js'; +import { IPreparedToolInvocation, IToolConfirmationMessages, IToolResult, IToolResultInputOutputDetails, ToolDataSource } from '../tools/languageModelToolsService.js'; + +export interface IChatRequest { + message: string; + variables: Record; +} + +export enum ChatErrorLevel { + Info = 0, + Warning = 1, + Error = 2 +} + +export interface IChatResponseErrorDetailsConfirmationButton { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + label: string; + isSecondary?: boolean; +} + +export interface IChatResponseErrorDetails { + message: string; + responseIsIncomplete?: boolean; + responseIsFiltered?: boolean; + responseIsRedacted?: boolean; + isQuotaExceeded?: boolean; + isRateLimited?: boolean; + level?: ChatErrorLevel; + confirmationButtons?: IChatResponseErrorDetailsConfirmationButton[]; + code?: string; +} + +export interface IChatResponseProgressFileTreeData { + label: string; + uri: URI; + type?: FileType; + children?: IChatResponseProgressFileTreeData[]; +} + +export type IDocumentContext = { + uri: URI; + version: number; + ranges: IRange[]; +}; + +export function isIDocumentContext(obj: unknown): obj is IDocumentContext { + return ( + !!obj && + typeof obj === 'object' && + 'uri' in obj && obj.uri instanceof URI && + 'version' in obj && typeof obj.version === 'number' && + 'ranges' in obj && Array.isArray(obj.ranges) && obj.ranges.every(Range.isIRange) + ); +} + +export interface IChatUsedContext { + documents: IDocumentContext[]; + kind: 'usedContext'; +} + +export function isIUsedContext(obj: unknown): obj is IChatUsedContext { + return ( + !!obj && + typeof obj === 'object' && + 'documents' in obj && + Array.isArray(obj.documents) && + obj.documents.every(isIDocumentContext) + ); +} + +export interface IChatContentVariableReference { + variableName: string; + value?: URI | Location; +} + +export function isChatContentVariableReference(obj: unknown): obj is IChatContentVariableReference { + return !!obj && + typeof obj === 'object' && + typeof (obj as IChatContentVariableReference).variableName === 'string'; +} + +export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 +} + +export enum ChatResponseClearToPreviousToolInvocationReason { + NoReason = 0, + FilteredContentRetry = 1, + CopyrightContentRetry = 2, +} + +export interface IChatContentReference { + reference: URI | Location | IChatContentVariableReference | string; + iconPath?: ThemeIcon | { light: URI; dark?: URI }; + options?: { + status?: { description: string; kind: ChatResponseReferencePartStatusKind }; + diffMeta?: { added: number; removed: number }; + originalUri?: URI; + isDeletion?: boolean; + }; + kind: 'reference'; +} + +export interface IChatCodeCitation { + value: URI; + license: string; + snippet: string; + kind: 'codeCitation'; +} + +export interface IChatUsagePromptTokenDetail { + category: string; + label: string; + percentageOfPrompt: number; +} + +export interface IChatUsage { + promptTokens: number; + completionTokens: number; + promptTokenDetails?: readonly IChatUsagePromptTokenDetail[]; + kind: 'usage'; +} + +export interface IChatContentInlineReference { + resolveId?: string; + inlineReference: URI | Location | IWorkspaceSymbol; + name?: string; + kind: 'inlineReference'; +} + +export interface IChatMarkdownContent { + kind: 'markdownContent'; + content: IMarkdownString; + inlineReferences?: Record; +} + +export interface IChatTreeData { + treeData: IChatResponseProgressFileTreeData; + kind: 'treeData'; +} +export interface IMultiDiffResource { + originalUri?: URI; + modifiedUri?: URI; + goToFileUri?: URI; + added?: number; + removed?: number; +} + +export interface IChatMultiDiffInnerData { + title: string; + resources: IMultiDiffResource[]; +} + +export interface IChatMultiDiffData { + multiDiffData: IChatMultiDiffInnerData | IObservable; + kind: 'multiDiffData'; + collapsed?: boolean; + readOnly?: boolean; + toJSON(): IChatMultiDiffDataSerialized; +} + +export interface IChatMultiDiffDataSerialized { + multiDiffData: IChatMultiDiffInnerData; + kind: 'multiDiffData'; + collapsed?: boolean; + readOnly?: boolean; +} + +export class ChatMultiDiffData implements IChatMultiDiffData { + public readonly kind = 'multiDiffData'; + public readonly collapsed?: boolean | undefined; + public readonly readOnly?: boolean | undefined; + public readonly multiDiffData: IChatMultiDiffData['multiDiffData']; + + constructor(opts: { + multiDiffData: IChatMultiDiffInnerData | IObservable; + collapsed?: boolean; + readOnly?: boolean; + }) { + this.readOnly = opts.readOnly; + this.collapsed = opts.collapsed; + this.multiDiffData = opts.multiDiffData; + } + + toJSON(): IChatMultiDiffDataSerialized { + return { + kind: this.kind, + multiDiffData: hasKey(this.multiDiffData, { title: true }) ? this.multiDiffData : this.multiDiffData.get(), + collapsed: this.collapsed, + readOnly: this.readOnly, + }; + } +} + +export interface IChatProgressMessage { + content: IMarkdownString; + kind: 'progressMessage'; +} + +export interface IChatTask extends IChatTaskDto { + deferred: DeferredPromise; + progress: (IChatWarningMessage | IChatContentReference)[]; + readonly onDidAddProgress: Event; + add(progress: IChatWarningMessage | IChatContentReference): void; + + complete: (result: string | void) => void; + task: () => Promise; + isSettled: () => boolean; + toJSON(): IChatTaskSerialized; +} + +export interface IChatUndoStop { + kind: 'undoStop'; + id: string; +} + +export interface IChatExternalEditsDto { + kind: 'externalEdits'; + undoStopId: string; + start: boolean; /** true=start, false=stop */ + resources: UriComponents[]; +} + +export interface IChatTaskDto { + content: IMarkdownString; + kind: 'progressTask'; +} + +export interface IChatTaskSerialized { + content: IMarkdownString; + progress: (IChatWarningMessage | IChatContentReference)[]; + kind: 'progressTaskSerialized'; +} + +export interface IChatTaskResult { + content: IMarkdownString | void; + kind: 'progressTaskResult'; +} + +export interface IChatWarningMessage { + content: IMarkdownString; + kind: 'warning'; +} + +export interface IChatAgentVulnerabilityDetails { + title: string; + description: string; +} + +export interface IChatResponseCodeblockUriPart { + kind: 'codeblockUri'; + uri: URI; + isEdit?: boolean; + undoStopId?: string; + subAgentInvocationId?: string; +} + +export interface IChatAgentMarkdownContentWithVulnerability { + content: IMarkdownString; + vulnerabilities: IChatAgentVulnerabilityDetails[]; + kind: 'markdownVuln'; +} + +export interface IChatCommandButton { + command: Command; + kind: 'command'; + additionalCommands?: Command[]; // rendered as secondary buttons +} + +export interface IChatMoveMessage { + uri: URI; + range: IRange; + kind: 'move'; +} + +export interface IChatTextEdit { + uri: URI; + edits: TextEdit[]; + kind: 'textEdit'; + done?: boolean; + isExternalEdit?: boolean; +} + +export interface IChatClearToPreviousToolInvocation { + kind: 'clearToPreviousToolInvocation'; + reason: ChatResponseClearToPreviousToolInvocationReason; +} + +export interface IChatNotebookEdit { + uri: URI; + edits: ICellEditOperation[]; + kind: 'notebookEdit'; + done?: boolean; + isExternalEdit?: boolean; +} + +export interface IChatWorkspaceFileEdit { + oldResource?: URI; + newResource?: URI; +} + +export interface IChatWorkspaceEdit { + kind: 'workspaceEdit'; + edits: IChatWorkspaceFileEdit[]; +} + +export interface IChatConfirmation { + title: string; + message: string | IMarkdownString; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; + buttons?: string[]; + isUsed?: boolean; + kind: 'confirmation'; +} + +/** + * Represents an individual question in a question carousel. + */ +export interface IChatQuestion { + id: string; + type: 'text' | 'singleSelect' | 'multiSelect'; + title: string; + message?: string | IMarkdownString; + options?: { id: string; label: string; value: unknown }[]; + defaultValue?: string | string[]; + allowFreeformInput?: boolean; +} + +/** + * A carousel for presenting multiple questions inline in the chat response. + * Users can navigate between questions and submit their answers. + */ +export interface IChatQuestionCarousel { + questions: IChatQuestion[]; + allowSkip: boolean; + /** Unique identifier for resolving the carousel answers back to the extension */ + resolveId?: string; + /** Storage for collected answers when user submits */ + data?: Record; + /** Whether the carousel has been submitted/skipped */ + isUsed?: boolean; + kind: 'questionCarousel'; +} + +export const enum ElicitationState { + Pending = 'pending', + Accepted = 'accepted', + Rejected = 'rejected', +} + +export interface IChatElicitationRequest { + kind: 'elicitation2'; // '2' because initially serialized data used the same kind + title: string | IMarkdownString; + message: string | IMarkdownString; + acceptButtonLabel: string; + rejectButtonLabel: string | undefined; + subtitle?: string | IMarkdownString; + source?: ToolDataSource; + state: IObservable; + acceptedResult?: Record; + moreActions?: IAction[]; + accept(value: IAction | true): Promise; + reject?: () => Promise; + isHidden?: IObservable; + hide?(): void; + toJSON(): IChatElicitationRequestSerialized; +} + +export interface IChatElicitationRequestSerialized { + kind: 'elicitationSerialized'; + title: string | IMarkdownString; + message: string | IMarkdownString; + subtitle: string | IMarkdownString | undefined; + source: ToolDataSource | undefined; + state: ElicitationState.Accepted | ElicitationState.Rejected; + isHidden: boolean; + acceptedResult?: Record; +} + +export interface IChatThinkingPart { + kind: 'thinking'; + value?: string | string[]; + id?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: { readonly [key: string]: any }; + generatedTitle?: string; +} + +export interface IChatTerminalToolInvocationData { + kind: 'terminal'; + commandLine: { + original: string; + userEdited?: string; + toolEdited?: string; + // command to show in the chat UI (potentially different from what is actually run in the terminal) + forDisplay?: string; + }; + /** The working directory URI for the terminal */ + cwd?: UriComponents; + /** + * Pre-computed confirmation display data (localization must happen at source). + * Contains the command line to show in confirmation (potentially without cd prefix) + * and the formatted cwd label if a cd prefix was extracted. + */ + confirmation?: { + /** The command line to display in the confirmation editor */ + commandLine: string; + /** The formatted cwd label to show in title (if cd was extracted) */ + cwdLabel?: string; + /** The cd prefix to prepend back when user edits */ + cdPrefix?: string; + }; + /** + * Overrides to apply to the presentation of the tool call only, but not actually change the + * command that gets run. For example, python -c "print('hello')" can be presented as just + * the Python code with Python syntax highlighting. + */ + presentationOverrides?: { + /** The command line to display in the UI */ + commandLine: string; + /** The language for syntax highlighting */ + language?: string; + }; + /** Message for model recommending the use of an alternative tool */ + alternativeRecommendation?: string; + language: string; + terminalToolSessionId?: string; + /** The predefined command ID that will be used for this terminal command */ + terminalCommandId?: string; + /** Whether the terminal command was started as a background execution */ + isBackground?: boolean; + /** Serialized URI for the command that was executed in the terminal */ + terminalCommandUri?: UriComponents; + /** Serialized output of the executed command */ + terminalCommandOutput?: { + text: string; + truncated?: boolean; + lineCount?: number; + }; + /** Stored theme colors at execution time to style detached output */ + terminalTheme?: { + background?: string; + foreground?: string; + }; + /** Stored command state to restore decorations after reload */ + terminalCommandState?: { + exitCode?: number; + timestamp?: number; + duration?: number; + }; + /** Whether the user chose to continue in background for this tool invocation */ + didContinueInBackground?: boolean; + autoApproveInfo?: IMarkdownString; +} + +/** + * @deprecated This is the old API shape, we should support this for a while before removing it so + * we don't break existing chats + */ +export interface ILegacyChatTerminalToolInvocationData { + kind: 'terminal'; + command: string; + language: string; +} + +export function isLegacyChatTerminalToolInvocationData(data: unknown): data is ILegacyChatTerminalToolInvocationData { + return !!data && typeof data === 'object' && 'command' in data; +} + +export interface IChatToolInputInvocationData { + kind: 'input'; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rawInput: any; + /** Optional MCP App UI metadata for rendering during and after tool execution */ + mcpAppData?: { + /** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */ + resourceUri: string; + /** Reference to the server definition for reconnection */ + serverDefinitionId: string; + /** Reference to the collection containing the server */ + collectionId: string; + }; +} + +export const enum ToolConfirmKind { + Denied, + ConfirmationNotNeeded, + Setting, + LmServicePerTool, + UserAction, + Skipped +} + +export type ConfirmedReason = + | { type: ToolConfirmKind.Denied } + | { type: ToolConfirmKind.ConfirmationNotNeeded; reason?: string | IMarkdownString } + | { type: ToolConfirmKind.Setting; id: string } + | { type: ToolConfirmKind.LmServicePerTool; scope: 'session' | 'workspace' | 'profile' } + | { type: ToolConfirmKind.UserAction } + | { type: ToolConfirmKind.Skipped }; + +export interface IChatToolInvocation { + readonly presentation: IPreparedToolInvocation['presentation']; + readonly toolSpecificData?: IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; + readonly originMessage: string | IMarkdownString | undefined; + readonly invocationMessage: string | IMarkdownString; + readonly pastTenseMessage: string | IMarkdownString | undefined; + readonly source: ToolDataSource; + readonly toolId: string; + readonly toolCallId: string; + readonly subAgentInvocationId?: string; + readonly state: IObservable; + generatedTitle?: string; + + kind: 'toolInvocation'; + + toJSON(): IChatToolInvocationSerialized; +} + +export namespace IChatToolInvocation { + export const enum StateKind { + /** Tool call is streaming partial input from the LM */ + Streaming, + WaitingForConfirmation, + Executing, + WaitingForPostApproval, + Completed, + Cancelled, + } + + interface IChatToolInvocationStateBase { + type: StateKind; + } + + export interface IChatToolInvocationStreamingState extends IChatToolInvocationStateBase { + type: StateKind.Streaming; + /** Observable partial input from the LM stream */ + readonly partialInput: IObservable; + /** Custom invocation message from handleToolStream */ + readonly streamingMessage: IObservable; + } + + /** Properties available after streaming is complete */ + interface IChatToolInvocationPostStreamState { + readonly parameters: unknown; + readonly confirmationMessages?: IToolConfirmationMessages; + } + + interface IChatToolInvocationWaitingForConfirmationState extends IChatToolInvocationStateBase, IChatToolInvocationPostStreamState { + type: StateKind.WaitingForConfirmation; + confirm(reason: ConfirmedReason): void; + } + + interface IChatToolInvocationPostConfirmState extends IChatToolInvocationPostStreamState { + confirmed: ConfirmedReason; + } + + interface IChatToolInvocationExecutingState extends IChatToolInvocationStateBase, IChatToolInvocationPostConfirmState { + type: StateKind.Executing; + progress: IObservable<{ message?: string | IMarkdownString; progress: number | undefined }>; + } + + interface IChatToolInvocationPostExecuteState extends IChatToolInvocationPostConfirmState { + resultDetails: IToolResult['toolResultDetails']; + } + + interface IChatToolWaitingForPostApprovalState extends IChatToolInvocationStateBase, IChatToolInvocationPostExecuteState { + type: StateKind.WaitingForPostApproval; + confirm(reason: ConfirmedReason): void; + contentForModel: IToolResult['content']; + } + + interface IChatToolInvocationCompleteState extends IChatToolInvocationStateBase, IChatToolInvocationPostExecuteState { + type: StateKind.Completed; + postConfirmed: ConfirmedReason | undefined; + contentForModel: IToolResult['content']; + } + + interface IChatToolInvocationCancelledState extends IChatToolInvocationStateBase, IChatToolInvocationPostStreamState { + type: StateKind.Cancelled; + reason: ToolConfirmKind.Denied | ToolConfirmKind.Skipped; + } + + export type State = + | IChatToolInvocationStreamingState + | IChatToolInvocationWaitingForConfirmationState + | IChatToolInvocationExecutingState + | IChatToolWaitingForPostApprovalState + | IChatToolInvocationCompleteState + | IChatToolInvocationCancelledState; + + export function executionConfirmedOrDenied(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): ConfirmedReason | undefined { + if (invocation.kind === 'toolInvocationSerialized') { + if (invocation.isConfirmed === undefined || typeof invocation.isConfirmed === 'boolean') { + return { type: invocation.isConfirmed ? ToolConfirmKind.UserAction : ToolConfirmKind.Denied }; + } + return invocation.isConfirmed; + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Streaming || state.type === StateKind.WaitingForConfirmation) { + return undefined; // don't know yet + } + if (state.type === StateKind.Cancelled) { + return { type: state.reason }; + } + + return state.confirmed; + } + + export function awaitConfirmation(invocation: IChatToolInvocation, token?: CancellationToken): Promise { + const reason = executionConfirmedOrDenied(invocation); + if (reason) { + return Promise.resolve(reason); + } + + const store = new DisposableStore(); + return new Promise(resolve => { + if (token) { + store.add(token.onCancellationRequested(() => { + resolve({ type: ToolConfirmKind.Denied }); + })); + } + + store.add(autorun(reader => { + const reason = executionConfirmedOrDenied(invocation, reader); + if (reason) { + store.dispose(); + resolve(reason); + } + })); + }).finally(() => { + store.dispose(); + }); + } + + function postApprovalConfirmedOrDenied(invocation: IChatToolInvocation, reader?: IReader): ConfirmedReason | undefined { + const state = invocation.state.read(reader); + if (state.type === StateKind.Completed) { + return state.postConfirmed || { type: ToolConfirmKind.ConfirmationNotNeeded }; + } + if (state.type === StateKind.Cancelled) { + return { type: state.reason }; + } + + return undefined; + } + + export function confirmWith(invocation: IChatToolInvocation | undefined, reason: ConfirmedReason) { + const state = invocation?.state.get(); + if (state?.type === StateKind.WaitingForConfirmation || state?.type === StateKind.WaitingForPostApproval) { + state.confirm(reason); + return true; + } + return false; + } + + export function awaitPostConfirmation(invocation: IChatToolInvocation, token?: CancellationToken): Promise { + const reason = postApprovalConfirmedOrDenied(invocation); + if (reason) { + return Promise.resolve(reason); + } + + const store = new DisposableStore(); + return new Promise(resolve => { + if (token) { + store.add(token.onCancellationRequested(() => { + resolve({ type: ToolConfirmKind.Denied }); + })); + } + + store.add(autorun(reader => { + const reason = postApprovalConfirmedOrDenied(invocation, reader); + if (reason) { + store.dispose(); + resolve(reason); + } + })); + }).finally(() => { + store.dispose(); + }); + } + + export function resultDetails(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader) { + if (invocation.kind === 'toolInvocationSerialized') { + return invocation.resultDetails; + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Completed || state.type === StateKind.WaitingForPostApproval) { + return state.resultDetails; + } + + return undefined; + } + + export function isComplete(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): boolean { + if (invocation.kind === 'toolInvocationSerialized') { + return true; // always cancelled or complete + } + + const state = invocation.state.read(reader); + return state.type === StateKind.Completed || state.type === StateKind.Cancelled; + } + + export function isStreaming(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): boolean { + if (invocation.kind === 'toolInvocationSerialized') { + return false; + } + + const state = invocation.state.read(reader); + return state.type === StateKind.Streaming; + } + + /** + * Get parameters from invocation. Returns undefined during streaming state. + */ + export function getParameters(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): unknown | undefined { + if (invocation.kind === 'toolInvocationSerialized') { + return undefined; // serialized invocations don't store parameters + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Streaming) { + return undefined; + } + + return state.parameters; + } + + /** + * Get confirmation messages from invocation. Returns undefined during streaming state. + */ + export function getConfirmationMessages(invocation: IChatToolInvocation | IChatToolInvocationSerialized, reader?: IReader): IToolConfirmationMessages | undefined { + if (invocation.kind === 'toolInvocationSerialized') { + return undefined; // serialized invocations don't store confirmation messages + } + + const state = invocation.state.read(reader); + if (state.type === StateKind.Streaming) { + return undefined; + } + + return state.confirmationMessages; + } +} + + +export interface IToolResultOutputDetailsSerialized { + output: { + type: 'data'; + mimeType: string; + base64Data: string; + }; +} + +/** + * This is a IChatToolInvocation that has been serialized, like after window reload, so it is no longer an active tool invocation. + */ +export interface IChatToolInvocationSerialized { + presentation: IPreparedToolInvocation['presentation']; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData; + invocationMessage: string | IMarkdownString; + originMessage: string | IMarkdownString | undefined; + pastTenseMessage: string | IMarkdownString | undefined; + resultDetails?: Array | IToolResultInputOutputDetails | IToolResultOutputDetailsSerialized; + /** boolean used by pre-1.104 versions */ + isConfirmed: ConfirmedReason | boolean | undefined; + isComplete: boolean; + toolCallId: string; + toolId: string; + source: ToolDataSource | undefined; // undefined on pre-1.104 versions + readonly subAgentInvocationId?: string; + generatedTitle?: string; + kind: 'toolInvocationSerialized'; +} + +export interface IChatExtensionsContent { + extensions: string[]; + kind: 'extensions'; +} + +export interface IChatPullRequestContent { + uri: URI; + title: string; + description: string; + author: string; + linkTag: string; + kind: 'pullRequest'; +} + +export interface IChatSubagentToolInvocationData { + kind: 'subagent'; + description?: string; + agentName?: string; + prompt?: string; + result?: string; +} + +export interface IChatTodoListContent { + kind: 'todoList'; + todoList: Array<{ + id: string; + title: string; + status: 'not-started' | 'in-progress' | 'completed'; + }>; +} + +export interface IChatMcpServersStarting { + readonly kind: 'mcpServersStarting'; + readonly state?: IObservable; // not hydrated when serialized + didStartServerIds?: string[]; + toJSON(): IChatMcpServersStartingSerialized; +} + +export interface IChatMcpServersStartingSerialized { + readonly kind: 'mcpServersStarting'; + readonly state?: undefined; + didStartServerIds?: string[]; +} + +export class ChatMcpServersStarting implements IChatMcpServersStarting { + public readonly kind = 'mcpServersStarting'; + + public didStartServerIds?: string[] = []; + + public get isEmpty() { + const s = this.state.get(); + return !s.working && s.serversRequiringInteraction.length === 0; + } + + constructor(public readonly state: IObservable) { } + + wait() { + return new Promise(resolve => { + autorunSelfDisposable(reader => { + const s = this.state.read(reader); + if (!s.working) { + reader.dispose(); + resolve(s); + } + }); + }); + } + + toJSON(): IChatMcpServersStartingSerialized { + return { kind: 'mcpServersStarting', didStartServerIds: this.didStartServerIds }; + } +} + +export type IChatProgress = + | IChatMarkdownContent + | IChatAgentMarkdownContentWithVulnerability + | IChatTreeData + | IChatMultiDiffData + | IChatMultiDiffDataSerialized + | IChatUsedContext + | IChatContentReference + | IChatContentInlineReference + | IChatCodeCitation + | IChatProgressMessage + | IChatTask + | IChatTaskResult + | IChatCommandButton + | IChatWarningMessage + | IChatTextEdit + | IChatNotebookEdit + | IChatWorkspaceEdit + | IChatMoveMessage + | IChatResponseCodeblockUriPart + | IChatConfirmation + | IChatQuestionCarousel + | IChatClearToPreviousToolInvocation + | IChatToolInvocation + | IChatToolInvocationSerialized + | IChatExtensionsContent + | IChatPullRequestContent + | IChatUndoStop + | IChatThinkingPart + | IChatTaskSerialized + | IChatElicitationRequest + | IChatElicitationRequestSerialized + | IChatMcpServersStarting + | IChatMcpServersStartingSerialized; + +export interface IChatFollowup { + kind: 'reply'; + message: string; + agentId: string; + subCommand?: string; + title?: string; + tooltip?: string; +} + +export function isChatFollowup(obj: unknown): obj is IChatFollowup { + return ( + !!obj && + (obj as IChatFollowup).kind === 'reply' && + typeof (obj as IChatFollowup).message === 'string' && + typeof (obj as IChatFollowup).agentId === 'string' + ); +} + +export enum ChatAgentVoteDirection { + Down = 0, + Up = 1 +} + +export enum ChatAgentVoteDownReason { + IncorrectCode = 'incorrectCode', + DidNotFollowInstructions = 'didNotFollowInstructions', + IncompleteCode = 'incompleteCode', + MissingContext = 'missingContext', + PoorlyWrittenOrFormatted = 'poorlyWrittenOrFormatted', + RefusedAValidRequest = 'refusedAValidRequest', + OffensiveOrUnsafe = 'offensiveOrUnsafe', + Other = 'other', + WillReportIssue = 'willReportIssue' +} + +export interface IChatVoteAction { + kind: 'vote'; + direction: ChatAgentVoteDirection; + reason: ChatAgentVoteDownReason | undefined; +} + +export enum ChatCopyKind { + // Keyboard shortcut or context menu + Action = 1, + Toolbar = 2 +} + +export interface IChatCopyAction { + kind: 'copy'; + codeBlockIndex: number; + copyKind: ChatCopyKind; + copiedCharacters: number; + totalCharacters: number; + copiedText: string; + totalLines: number; + copiedLines: number; + modelId: string; + languageId?: string; +} + +export interface IChatInsertAction { + kind: 'insert'; + codeBlockIndex: number; + totalCharacters: number; + totalLines: number; + languageId?: string; + modelId: string; + newFile?: boolean; +} + +export interface IChatApplyAction { + kind: 'apply'; + codeBlockIndex: number; + totalCharacters: number; + totalLines: number; + languageId?: string; + modelId: string; + newFile?: boolean; + codeMapper?: string; + editsProposed: boolean; +} + + +export interface IChatTerminalAction { + kind: 'runInTerminal'; + codeBlockIndex: number; + languageId?: string; +} + +export interface IChatCommandAction { + kind: 'command'; + commandButton: IChatCommandButton; +} + +export interface IChatFollowupAction { + kind: 'followUp'; + followup: IChatFollowup; +} + +export interface IChatBugReportAction { + kind: 'bug'; +} + +export interface IChatInlineChatCodeAction { + kind: 'inlineChat'; + action: 'accepted' | 'discarded'; +} + + +export interface IChatEditingSessionAction { + kind: 'chatEditingSessionAction'; + uri: URI; + hasRemainingEdits: boolean; + outcome: 'accepted' | 'rejected' | 'userModified'; +} + +export interface IChatEditingHunkAction { + kind: 'chatEditingHunkAction'; + uri: URI; + lineCount: number; + linesAdded: number; + linesRemoved: number; + outcome: 'accepted' | 'rejected'; + hasRemainingEdits: boolean; + modeId?: string; + modelId?: string; + languageId?: string; +} + +export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction | IChatEditingHunkAction; + +export interface IChatUserActionEvent { + action: ChatUserAction; + agentId: string | undefined; + command: string | undefined; + sessionResource: URI; + requestId: string; + result: IChatAgentResult | undefined; + modelId?: string | undefined; + modeId?: string | undefined; +} + +export interface IChatDynamicRequest { + /** + * The message that will be displayed in the UI + */ + message: string; + + /** + * Any extra metadata/context that will go to the provider. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: any; +} + +export interface IChatCompleteResponse { + message: string | ReadonlyArray; + result?: IChatAgentResult; + followups?: IChatFollowup[]; +} + +export interface IChatSessionStats { + fileCount: number; + added: number; + removed: number; +} + +export type IChatSessionTiming = { + /** + * Timestamp when the session was created in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + created: number; + + /** + * Timestamp when the most recent request started in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if no requests have been made yet. + */ + lastRequestStarted: number | undefined; + + /** + * Timestamp when the most recent request completed in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * Should be undefined if the most recent request is still in progress or if no requests have been made yet. + */ + lastRequestEnded: number | undefined; +}; + +interface ILegacyChatSessionTiming { + startTime: number; + endTime?: number; +} + +export function convertLegacyChatSessionTiming(timing: IChatSessionTiming | ILegacyChatSessionTiming): IChatSessionTiming { + if (hasKey(timing, { created: true })) { + return timing; + } + return { + created: timing.startTime, + lastRequestStarted: timing.startTime, + lastRequestEnded: timing.endTime, + }; +} + +export const enum ResponseModelState { + Pending, + Complete, + Cancelled, + Failed, + NeedsInput, +} + +export interface IChatDetail { + sessionResource: URI; + title: string; + lastMessageDate: number; + // Also support old timing format for backwards compatibility with persisted data + timing: IChatSessionTiming | ILegacyChatSessionTiming; + isActive: boolean; + stats?: IChatSessionStats; + lastResponseState: ResponseModelState; +} + +export interface IChatProviderInfo { + id: string; +} + +export interface IChatSendRequestResponseState { + responseCreatedPromise: Promise; + responseCompletePromise: Promise; +} + +export interface IChatSendRequestData extends IChatSendRequestResponseState { + agent: IChatAgentData; + slashCommand?: IChatAgentCommand; +} + +export interface IChatEditorLocationData { + type: ChatAgentLocation.EditorInline; + id: string; + document: URI; + selection: ISelection; + wholeRange: IRange; +} + +export interface IChatNotebookLocationData { + type: ChatAgentLocation.Notebook; + sessionInputUri: URI; +} + +export interface IChatTerminalLocationData { + type: ChatAgentLocation.Terminal; + // TBD +} + +export type IChatLocationData = IChatEditorLocationData | IChatNotebookLocationData | IChatTerminalLocationData; + +export interface IChatSendRequestOptions { + modeInfo?: IChatRequestModeInfo; + userSelectedModelId?: string; + userSelectedTools?: IObservable; + location?: ChatAgentLocation; + locationData?: IChatLocationData; + parserContext?: IChatParserContext; + attempt?: number; + noCommandDetection?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + acceptedConfirmationData?: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rejectedConfirmationData?: any[]; + attachedContext?: IChatRequestVariableEntry[]; + + /** The target agent ID can be specified with this property instead of using @ in 'message' */ + agentId?: string; + /** agentId, but will not add a @ name to the request */ + agentIdSilent?: string; + slashCommand?: string; + + /** + * The label of the confirmation action that was selected. + */ + confirmation?: string; + +} + +export type IChatModelReference = IReference; + +export const IChatService = createDecorator('IChatService'); + +export interface IChatService { + _serviceBrand: undefined; + transferredSessionResource: URI | undefined; + + readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }>; + + readonly onDidCreateModel: Event; + + /** + * An observable containing all live chat models. + */ + readonly chatModels: IObservable>; + + isEnabled(location: ChatAgentLocation): boolean; + hasSessions(): boolean; + startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference; + + /** + * Get an active session without holding a reference to it. + */ + getSession(sessionResource: URI): IChatModel | undefined; + + /** + * Acquire a reference to an active session. + */ + getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined; + + getOrRestoreSession(sessionResource: URI): Promise; + getSessionTitle(sessionResource: URI): string | undefined; + loadSessionFromContent(data: IExportableChatData | ISerializableChatData | URI): IChatModelReference | undefined; + loadSessionForResource(resource: URI, location: ChatAgentLocation, token: CancellationToken): Promise; + readonly editingSessions: IChatEditingSession[]; + getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined; + + /** + * Returns whether the request was accepted.` + */ + sendRequest(sessionResource: URI, message: string, options?: IChatSendRequestOptions): Promise; + + /** + * Sets a custom title for a chat model. + */ + setTitle(sessionResource: URI, title: string): void; + appendProgress(request: IChatRequestModel, progress: IChatProgress): void; + resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; + adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise; + removeRequest(sessionResource: URI, requestId: string): Promise; + cancelCurrentRequestForSession(sessionResource: URI): void; + addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void; + setChatSessionTitle(sessionResource: URI, title: string): void; + getLocalSessionHistory(): Promise; + clearAllHistoryEntries(): Promise; + removeHistoryEntry(sessionResource: URI): Promise; + getChatStorageFolder(): URI; + logChatIndex(): void; + getLiveSessionItems(): Promise; + getHistorySessionItems(): Promise; + getMetadataForSession(sessionResource: URI): Promise; + + readonly onDidPerformUserAction: Event; + notifyUserAction(event: IChatUserActionEvent): void; + + readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: Record | undefined }>; + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void; + + readonly onDidDisposeSession: Event<{ readonly sessionResource: URI[]; readonly reason: 'cleared' }>; + + transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise; + + activateDefaultAgent(location: ChatAgentLocation): Promise; + + readonly edits2Enabled: boolean; + + readonly requestInProgressObs: IObservable; + + /** + * For tests only! + */ + setSaveModelsEnabled(enabled: boolean): void; + + /** + * For tests only! + */ + waitForModelDisposals(): Promise; +} + +export interface IChatSessionContext { + readonly chatSessionType: string; + readonly chatSessionResource: URI; + readonly isUntitled: boolean; +} + +export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; + +export interface IChatSessionStartOptions { + canUseTools?: boolean; + disableBackgroundKeepAlive?: boolean; +} diff --git a/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts new file mode 100644 index 00000000000..fb9131a929f --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceImpl.ts @@ -0,0 +1,1279 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DeferredPromise } from '../../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { BugIndicatingError, ErrorNoTelemetry } from '../../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { Disposable, DisposableResourceMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { revive } from '../../../../../base/common/marshalling.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { autorun, derived, IObservable } from '../../../../../base/common/observable.js'; +import { isEqual } from '../../../../../base/common/resources.js'; +import { StopWatch } from '../../../../../base/common/stopwatch.js'; +import { isDefined } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { Progress } from '../../../../../platform/progress/common/progress.js'; +import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; +import { InlineChatConfigKeys } from '../../../inlineChat/common/inlineChat.js'; +import { IMcpService } from '../../../mcp/common/mcpTypes.js'; +import { awaitStatsForSession } from '../chat.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from '../participants/chatAgents.js'; +import { chatEditingSessionIsReady } from '../editing/chatEditingService.js'; +import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from '../model/chatModel.js'; +import { ChatModelStore, IStartSessionProps } from '../model/chatModelStore.js'; +import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { ChatRequestParser } from '../requestParser/chatRequestParser.js'; +import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatModelReference, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent, ResponseModelState } from './chatService.js'; +import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; +import { IChatSessionsService } from '../chatSessionsService.js'; +import { ChatSessionStore, IChatSessionEntryMetadata } from '../model/chatSessionStore.js'; +import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; +import { IChatTransferService } from '../model/chatTransferService.js'; +import { LocalChatSessionUri } from '../model/chatUri.js'; +import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; +import { ChatMessageRole, IChatMessage } from '../languageModels.js'; +import { ILanguageModelToolsService } from '../tools/languageModelToolsService.js'; +import { ChatSessionOperationLog } from '../model/chatSessionOperationLog.js'; + +const serializedChatKey = 'interactive.sessions'; + +class CancellableRequest implements IDisposable { + constructor( + public readonly cancellationTokenSource: CancellationTokenSource, + public requestId: string | undefined, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService + ) { } + + dispose() { + this.cancellationTokenSource.dispose(); + } + + cancel() { + if (this.requestId) { + this.toolsService.cancelToolCallsForRequest(this.requestId); + } + + this.cancellationTokenSource.cancel(); + } +} + +export class ChatService extends Disposable implements IChatService { + declare _serviceBrand: undefined; + + private readonly _sessionModels: ChatModelStore; + private readonly _pendingRequests = this._register(new DisposableResourceMap()); + private _saveModelsEnabled = true; + + private _transferredSessionResource: URI | undefined; + public get transferredSessionResource(): URI | undefined { + return this._transferredSessionResource; + } + + private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>()); + public readonly onDidSubmitRequest = this._onDidSubmitRequest.event; + + public get onDidCreateModel() { return this._sessionModels.onDidCreateModel; } + + private readonly _onDidPerformUserAction = this._register(new Emitter()); + public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; + + private readonly _onDidReceiveQuestionCarouselAnswer = this._register(new Emitter<{ requestId: string; resolveId: string; answers: Record | undefined }>()); + public readonly onDidReceiveQuestionCarouselAnswer = this._onDidReceiveQuestionCarouselAnswer.event; + + private readonly _onDidDisposeSession = this._register(new Emitter<{ readonly sessionResource: URI[]; reason: 'cleared' }>()); + public readonly onDidDisposeSession = this._onDidDisposeSession.event; + + private readonly _sessionFollowupCancelTokens = this._register(new DisposableResourceMap()); + private readonly _chatServiceTelemetry: ChatServiceTelemetry; + private readonly _chatSessionStore: ChatSessionStore; + + readonly requestInProgressObs: IObservable; + + readonly chatModels: IObservable>; + + /** + * For test use only + */ + setSaveModelsEnabled(enabled: boolean): void { + this._saveModelsEnabled = enabled; + } + + /** + * For test use only + */ + waitForModelDisposals(): Promise { + return this._sessionModels.waitForModelDisposals(); + } + + public get edits2Enabled(): boolean { + return this.configurationService.getValue(ChatConfiguration.Edits2Enabled); + } + + private get isEmptyWindow(): boolean { + const workspace = this.workspaceContextService.getWorkspace(); + return !workspace.configuration && workspace.folders.length === 0; + } + + constructor( + @IStorageService private readonly storageService: IStorageService, + @ILogService private readonly logService: ILogService, + @IExtensionService private readonly extensionService: IExtensionService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IChatTransferService private readonly chatTransferService: IChatTransferService, + @IChatSessionsService private readonly chatSessionService: IChatSessionsService, + @IMcpService private readonly mcpService: IMcpService, + ) { + super(); + + this._sessionModels = this._register(instantiationService.createInstance(ChatModelStore, { + createModel: (props: IStartSessionProps) => this._startSession(props), + willDisposeModel: async (model: ChatModel) => { + const localSessionId = LocalChatSessionUri.parseLocalSessionId(model.sessionResource); + if (localSessionId && this.shouldStoreSession(model)) { + // Always preserve sessions that have custom titles, even if empty + if (model.getRequests().length === 0 && !model.customTitle) { + await this._chatSessionStore.deleteSession(localSessionId); + } else if (this._saveModelsEnabled) { + await this._chatSessionStore.storeSessions([model]); + } + } else if (!localSessionId && model.getRequests().length > 0) { + await this._chatSessionStore.storeSessionsMetadataOnly([model]); + } + } + })); + this._register(this._sessionModels.onDidDisposeModel(model => { + this._onDidDisposeSession.fire({ sessionResource: [model.sessionResource], reason: 'cleared' }); + })); + + this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); + this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore)); + this._chatSessionStore.migrateDataIfNeeded(() => this.migrateData()); + + const transferredData = this._chatSessionStore.getTransferredSessionData(); + if (transferredData) { + this.trace('constructor', `Transferred session ${transferredData}`); + this._transferredSessionResource = transferredData; + } + + this.reviveSessionsWithEdits(); + + this._register(storageService.onWillSaveState(() => this.saveState())); + + this.chatModels = derived(this, reader => [...this._sessionModels.observable.read(reader).values()]); + + this.requestInProgressObs = derived(reader => { + const models = this._sessionModels.observable.read(reader).values(); + return Iterable.some(models, model => model.requestInProgress.read(reader)); + }); + } + + public get editingSessions() { + return [...this._sessionModels.values()].map(v => v.editingSession).filter(isDefined); + } + + isEnabled(location: ChatAgentLocation): boolean { + return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; + } + + private migrateData(): ISerializableChatsData | undefined { + const sessionData = this.storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); + if (sessionData) { + const persistedSessions = this.deserializeChats(sessionData); + const countsForLog = Object.keys(persistedSessions).length; + if (countsForLog > 0) { + this.info('migrateData', `Restored ${countsForLog} persisted sessions`); + } + + return persistedSessions; + } + + return; + } + + private saveState(): void { + if (!this._saveModelsEnabled) { + return; + } + + const liveLocalChats = Array.from(this._sessionModels.values()) + .filter(session => this.shouldStoreSession(session)); + + this._chatSessionStore.storeSessions(liveLocalChats); + + const liveNonLocalChats = Array.from(this._sessionModels.values()) + .filter(session => !LocalChatSessionUri.parseLocalSessionId(session.sessionResource)); + this._chatSessionStore.storeSessionsMetadataOnly(liveNonLocalChats); + } + + /** + * Only persist local sessions from chat that are not imported. + */ + private shouldStoreSession(session: ChatModel): boolean { + if (!LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) { + return false; + } + return session.initialLocation === ChatAgentLocation.Chat && !session.isImported; + } + + notifyUserAction(action: IChatUserActionEvent): void { + this._chatServiceTelemetry.notifyUserAction(action); + this._onDidPerformUserAction.fire(action); + if (action.action.kind === 'chatEditingSessionAction') { + const model = this._sessionModels.get(action.sessionResource); + if (model) { + model.notifyEditingAction(action.action); + } + } + } + + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { + this._onDidReceiveQuestionCarouselAnswer.fire({ requestId, resolveId, answers }); + } + + async setChatSessionTitle(sessionResource: URI, title: string): Promise { + const model = this._sessionModels.get(sessionResource); + if (model) { + model.setCustomTitle(title); + } + + // Update the title in the file storage + const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (localSessionId) { + await this._chatSessionStore.setSessionTitle(localSessionId, title); + // Trigger immediate save to ensure consistency + this.saveState(); + } + } + + private trace(method: string, message?: string): void { + if (message) { + this.logService.trace(`ChatService#${method}: ${message}`); + } else { + this.logService.trace(`ChatService#${method}`); + } + } + + private info(method: string, message?: string): void { + if (message) { + this.logService.info(`ChatService#${method}: ${message}`); + } else { + this.logService.info(`ChatService#${method}`); + } + } + + private error(method: string, message: string): void { + this.logService.error(`ChatService#${method} ${message}`); + } + + private deserializeChats(sessionData: string): ISerializableChatsData { + try { + const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data + if (!Array.isArray(arrayOfSessions)) { + throw new Error('Expected array'); + } + + const sessions = arrayOfSessions.reduce((acc, session) => { + // Revive serialized markdown strings in response data + for (const request of session.requests) { + if (Array.isArray(request.response)) { + request.response = request.response.map((response) => { + if (typeof response === 'string') { + return new MarkdownString(response); + } + return response; + }); + } else if (typeof request.response === 'string') { + request.response = [new MarkdownString(request.response)]; + } + } + + acc[session.sessionId] = normalizeSerializableChatData(session); + return acc; + }, {}); + return sessions; + } catch (err) { + this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`); + return {}; + } + } + + /** + * todo@connor4312 This will be cleaned up with the globalization of edits. + */ + private async reviveSessionsWithEdits(): Promise { + const idx = await this._chatSessionStore.getIndex(); + await Promise.all(Object.values(idx).map(async session => { + if (!session.hasPendingEdits) { + return; + } + + const sessionResource = LocalChatSessionUri.forSession(session.sessionId); + const sessionRef = await this.getOrRestoreSession(sessionResource); + if (sessionRef?.object.editingSession) { + await chatEditingSessionIsReady(sessionRef.object.editingSession); + // the session will hold a self-reference as long as there are modified files + sessionRef.dispose(); + } + })); + } + + /** + * Returns an array of chat details for all persisted chat sessions that have at least one request. + * Chat sessions that have already been loaded into the chat view are excluded from the result. + * Imported chat sessions are also excluded from the result. + * TODO this is only used by the old "show chats" command which can be removed when the pre-agents view + * options are removed. + */ + async getLocalSessionHistory(): Promise { + const liveSessionItems = await this.getLiveSessionItems(); + const historySessionItems = await this.getHistorySessionItems(); + + return [...liveSessionItems, ...historySessionItems]; + } + + /** + * Returns an array of chat details for all local live chat sessions. + */ + async getLiveSessionItems(): Promise { + return await Promise.all(Array.from(this._sessionModels.values()) + .filter(session => this.shouldBeInHistory(session)) + .map(async (session): Promise => { + const title = session.title || localize('newChat', "New Chat"); + return { + sessionResource: session.sessionResource, + title, + lastMessageDate: session.lastMessageDate, + timing: session.timing, + isActive: true, + stats: await awaitStatsForSession(session), + lastResponseState: session.lastRequest?.response?.state ?? ResponseModelState.Pending, + }; + })); + } + + /** + * Returns an array of chat details for all local chat sessions in history (not currently loaded). + */ + async getHistorySessionItems(): Promise { + const index = await this._chatSessionStore.getIndex(); + return Object.values(index) + .filter(entry => !entry.isExternal) + .filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && entry.initialLocation === ChatAgentLocation.Chat && !entry.isEmpty) + .map((entry): IChatDetail => { + const sessionResource = LocalChatSessionUri.forSession(entry.sessionId); + return ({ + ...entry, + sessionResource, + isActive: this._sessionModels.has(sessionResource), + }); + }); + } + + async getMetadataForSession(sessionResource: URI): Promise { + const index = await this._chatSessionStore.getIndex(); + const metadata: IChatSessionEntryMetadata | undefined = index[sessionResource.toString()]; + if (metadata) { + return { + ...metadata, + sessionResource, + isActive: this._sessionModels.has(sessionResource), + }; + } + + return undefined; + } + + private shouldBeInHistory(entry: ChatModel): boolean { + return !entry.isImported && !!LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation === ChatAgentLocation.Chat; + } + + async removeHistoryEntry(sessionResource: URI): Promise { + await this._chatSessionStore.deleteSession(this.toLocalSessionId(sessionResource)); + this._onDidDisposeSession.fire({ sessionResource: [sessionResource], reason: 'cleared' }); + } + + async clearAllHistoryEntries(): Promise { + await this._chatSessionStore.clearAllSessions(); + } + + startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { + this.trace('startSession'); + const sessionId = generateUuid(); + const sessionResource = LocalChatSessionUri.forSession(sessionId); + return this._sessionModels.acquireOrCreate({ + initialData: undefined, + location, + sessionResource, + sessionId, + canUseTools: options?.canUseTools ?? true, + disableBackgroundKeepAlive: options?.disableBackgroundKeepAlive + }); + } + + private _startSession(props: IStartSessionProps): ChatModel { + const { initialData, location, sessionResource, sessionId, canUseTools, transferEditingSession, disableBackgroundKeepAlive, inputState } = props; + const model = this.instantiationService.createInstance(ChatModel, initialData, { initialLocation: location, canUseTools, resource: sessionResource, sessionId, disableBackgroundKeepAlive, inputState }); + if (location === ChatAgentLocation.Chat) { + model.startEditingSession(true, transferEditingSession); + } + + this.initializeSession(model); + return model; + } + + private initializeSession(model: ChatModel): void { + this.trace('initializeSession', `Initialize session ${model.sessionResource}`); + + // Activate the default extension provided agent but do not wait + // for it to be ready so that the session can be used immediately + // without having to wait for the agent to be ready. + this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e)); + } + + async activateDefaultAgent(location: ChatAgentLocation): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Chat); + if (!defaultAgentData) { + throw new ErrorNoTelemetry('No default agent contributed'); + } + + // Await activation of the extension provided agent + // Using `activateById` as workaround for the issue + // https://github.com/microsoft/vscode/issues/250590 + if (!defaultAgentData.isCore) { + await this.extensionService.activateById(defaultAgentData.extensionId, { + activationEvent: `onChatParticipant:${defaultAgentData.id}`, + extensionId: defaultAgentData.extensionId, + startup: false + }); + } + + const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); + if (!defaultAgent) { + throw new ErrorNoTelemetry('No default agent registered'); + } + } + + getSession(sessionResource: URI): IChatModel | undefined { + return this._sessionModels.get(sessionResource); + } + + getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined { + return this._sessionModels.acquireExisting(sessionResource); + } + + async getOrRestoreSession(sessionResource: URI): Promise { + this.trace('getOrRestoreSession', `${sessionResource}`); + const existingRef = this._sessionModels.acquireExisting(sessionResource); + if (existingRef) { + return existingRef; + } + + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + throw new Error(`Cannot restore non-local session ${sessionResource}`); + } + + let sessionData: ISerializedChatDataReference | undefined; + if (isEqual(this.transferredSessionResource, sessionResource)) { + this._transferredSessionResource = undefined; + sessionData = await this._chatSessionStore.readTransferredSession(sessionResource); + } else { + sessionData = await this._chatSessionStore.readSession(sessionId); + } + + if (!sessionData) { + return undefined; + } + + const sessionRef = this._sessionModels.acquireOrCreate({ + initialData: sessionData, + location: sessionData.value.initialLocation ?? ChatAgentLocation.Chat, + sessionResource, + sessionId, + canUseTools: true, + }); + + return sessionRef; + } + + // There are some cases where this returns a real string. What happens if it doesn't? + // This had titles restored from the index, so just return titles from index instead, sync. + getSessionTitle(sessionResource: URI): string | undefined { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + return undefined; + } + + return this._sessionModels.get(sessionResource)?.title ?? + this._chatSessionStore.getMetadataForSessionSync(sessionResource)?.title; + } + + loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModelReference | undefined { + const sessionId = (data as ISerializableChatData).sessionId ?? generateUuid(); + const sessionResource = LocalChatSessionUri.forSession(sessionId); + return this._sessionModels.acquireOrCreate({ + initialData: { value: data, serializer: new ChatSessionOperationLog() }, + location: data.initialLocation ?? ChatAgentLocation.Chat, + sessionResource, + sessionId, + canUseTools: true, + }); + } + + async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { + // TODO: Move this into a new ChatModelService + + if (chatSessionResource.scheme === Schemas.vscodeLocalChatSession) { + return this.getOrRestoreSession(chatSessionResource); + } + + const existingRef = this._sessionModels.acquireExisting(chatSessionResource); + if (existingRef) { + return existingRef; + } + + const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None); + const chatSessionType = chatSessionResource.scheme; + + // Contributed sessions do not use UI tools + const modelRef = this._sessionModels.acquireOrCreate({ + initialData: undefined, + location, + sessionResource: chatSessionResource, + canUseTools: false, + transferEditingSession: providedSession.transferredState?.editingSession, + inputState: providedSession.transferredState?.inputState, + }); + + modelRef.object.setContributedChatSession({ + chatSessionResource, + chatSessionType, + isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer) + }); + + const model = modelRef.object; + const disposables = new DisposableStore(); + disposables.add(modelRef.object.onDidDispose(() => { + disposables.dispose(); + providedSession.dispose(); + })); + + let lastRequest: ChatRequestModel | undefined; + for (const message of providedSession.history) { + if (message.type === 'request') { + if (lastRequest) { + lastRequest.response?.complete(); + } + + const requestText = message.prompt; + + const parsedRequest: IParsedChatRequest = { + text: requestText, + parts: [new ChatRequestTextPart( + new OffsetRange(0, requestText.length), + { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 }, + requestText + )] + }; + const agent = + message.participant + ? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode? + : this.chatAgentService.getAgent(chatSessionType); + lastRequest = model.addRequest(parsedRequest, + message.variableData ?? { variables: [] }, + 0, // attempt + undefined, + agent, + undefined, // slashCommand + undefined, // confirmation + undefined, // locationData + undefined, // attachments + false, // Do not treat as requests completed, else edit pills won't show. + undefined, + undefined, + message.id + ); + } else { + // response + if (lastRequest) { + for (const part of message.parts) { + model.acceptResponseProgress(lastRequest, part); + } + } + } + } + + if (providedSession.isCompleteObs?.get()) { + lastRequest?.response?.complete(); + } + + if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) { + const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); + this._pendingRequests.set(model.sessionResource, initialCancellationRequest); + const cancellationListener = disposables.add(new MutableDisposable()); + + const createCancellationListener = (token: CancellationToken) => { + return token.onCancellationRequested(() => { + providedSession.interruptActiveResponseCallback?.().then(userConfirmedInterruption => { + if (!userConfirmedInterruption) { + // User cancelled the interruption + const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); + this._pendingRequests.set(model.sessionResource, newCancellationRequest); + cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token); + } + }); + }); + }; + + cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token); + + let lastProgressLength = 0; + disposables.add(autorun(reader => { + const progressArray = providedSession.progressObs?.read(reader) ?? []; + const isComplete = providedSession.isCompleteObs?.read(reader) ?? false; + + // Process only new progress items + if (progressArray.length > lastProgressLength) { + const newProgress = progressArray.slice(lastProgressLength); + for (const progress of newProgress) { + model?.acceptResponseProgress(lastRequest, progress); + } + lastProgressLength = progressArray.length; + } + + // Handle completion + if (isComplete) { + lastRequest.response?.complete(); + cancellationListener.clear(); + } + })); + } else { + if (lastRequest && model.editingSession) { + // wait for timeline to load so that a 'changes' part is added when the response completes + await chatEditingSessionIsReady(model.editingSession); + lastRequest.response?.complete(); + } + } + + return modelRef; + } + + getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined { + const model = this._sessionModels.get(sessionResource); + if (!model) { + return; + } + const { contributedChatSession } = model; + return contributedChatSession; + } + + async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { + const model = this._sessionModels.get(request.session.sessionResource); + if (!model && model !== request.session) { + throw new Error(`Unknown session: ${request.session.sessionResource}`); + } + + const cts = this._pendingRequests.get(request.session.sessionResource); + if (cts) { + this.trace('resendRequest', `Session ${request.session.sessionResource} already has a pending request, cancelling...`); + cts.cancel(); + } + + const location = options?.location ?? model.initialLocation; + const attempt = options?.attempt ?? 0; + const enableCommandDetection = !options?.noCommandDetection; + const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!; + + model.removeRequest(request.id, ChatRequestRemovalReason.Resend); + + const resendOptions: IChatSendRequestOptions = { + ...options, + locationData: request.locationData, + attachedContext: request.attachedContext, + }; + await this._sendRequestAsync(model, model.sessionResource, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; + } + + async sendRequest(sessionResource: URI, request: string, options?: IChatSendRequestOptions): Promise { + this.trace('sendRequest', `sessionResource: ${sessionResource.toString()}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); + + + if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) { + this.trace('sendRequest', 'Rejected empty message'); + return; + } + + const model = this._sessionModels.get(sessionResource); + if (!model) { + throw new Error(`Unknown session: ${sessionResource}`); + } + + if (this._pendingRequests.has(sessionResource)) { + this.trace('sendRequest', `Session ${sessionResource} already has a pending request`); + return; + } + + const requests = model.getRequests(); + for (let i = requests.length - 1; i >= 0; i -= 1) { + const request = requests[i]; + if (request.shouldBeRemovedOnSend) { + if (request.shouldBeRemovedOnSend.afterUndoStop) { + request.response?.finalizeUndoState(); + } else { + await this.removeRequest(sessionResource, request.id); + } + } + } + + const location = options?.location ?? model.initialLocation; + const attempt = options?.attempt ?? 0; + const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!; + + const parsedRequest = this.parseChatRequest(sessionResource, request, location, options); + const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined; + const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; + const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); + + // This method is only returning whether the request was accepted - don't block on the actual request + return { + ...this._sendRequestAsync(model, sessionResource, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options), + agent, + slashCommand: agentSlashCommandPart?.command, + }; + } + + private parseChatRequest(sessionResource: URI, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { + let parserContext = options?.parserContext; + if (options?.agentId) { + const agent = this.chatAgentService.getAgent(options.agentId); + if (!agent) { + throw new Error(`Unknown agent: ${options.agentId}`); + } + parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind }; + const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; + request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; + } + + const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, request, location, parserContext); + return parsedRequest; + } + + private refreshFollowupsCancellationToken(sessionResource: URI): CancellationToken { + this._sessionFollowupCancelTokens.get(sessionResource)?.cancel(); + const newTokenSource = new CancellationTokenSource(); + this._sessionFollowupCancelTokens.set(sessionResource, newTokenSource); + + return newTokenSource.token; + } + + private _sendRequestAsync(model: ChatModel, sessionResource: URI, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { + const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionResource); + let request: ChatRequestModel; + const agentPart = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); + const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); + const commandPart = parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); + const requests = [...model.getRequests()]; + const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, { + agent: agentPart?.agent ?? defaultAgent, + agentSlashCommandPart, + commandPart, + sessionId: model.sessionId, + location: model.initialLocation, + options, + enableCommandDetection + }); + + let gotProgress = false; + const requestType = commandPart ? 'slashCommand' : 'string'; + + const responseCreated = new DeferredPromise(); + let responseCreatedComplete = false; + function completeResponseCreated(): void { + if (!responseCreatedComplete && request?.response) { + responseCreated.complete(request.response); + responseCreatedComplete = true; + } + } + + const store = new DisposableStore(); + const source = store.add(new CancellationTokenSource()); + const token = source.token; + const sendRequestInternal = async () => { + const progressCallback = (progress: IChatProgress[]) => { + if (token.isCancellationRequested) { + return; + } + + gotProgress = true; + + for (let i = 0; i < progress.length; i++) { + const isLast = i === progress.length - 1; + const progressItem = progress[i]; + + if (progressItem.kind === 'markdownContent') { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionResource}, ${progressItem.content.value.length} chars`); + } else { + this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`); + } + + model.acceptResponseProgress(request, progressItem, !isLast); + } + completeResponseCreated(); + }; + + let detectedAgent: IChatAgentData | undefined; + let detectedCommand: IChatAgentCommand | undefined; + + const stopWatch = new StopWatch(false); + store.add(token.onCancellationRequested(() => { + this.trace('sendRequest', `Request for session ${model.sessionResource} was cancelled`); + if (!request) { + return; + } + + requestTelemetry.complete({ + timeToFirstProgress: undefined, + result: 'cancelled', + // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling + totalTime: stopWatch.elapsed(), + requestType, + detectedAgent, + request, + }); + + model.cancelRequest(request); + })); + + try { + let rawResult: IChatAgentResult | null | undefined; + let agentOrCommandFollowups: Promise | undefined = undefined; + if (agentPart || (defaultAgent && !commandPart)) { + const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => { + const initVariableData: IChatRequestVariableData = { variables: [] }; + request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId, options?.userSelectedTools?.get()); + + let variableData: IChatRequestVariableData; + let message: string; + if (chatRequest) { + variableData = chatRequest.variableData; + message = getPromptText(request.message).message; + } else { + variableData = { variables: this.prepareContext(request.attachedContext) }; + model.updateRequest(request, variableData); + + const promptTextResult = getPromptText(request.message); + variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack + message = promptTextResult.message; + } + + const agentRequest: IChatAgentRequest = { + sessionResource: model.sessionResource, + requestId: request.id, + agentId: agent.id, + message, + command: command?.name, + variables: variableData, + enableCommandDetection, + isParticipantDetected, + attempt, + location, + locationData: request.locationData, + acceptedConfirmationData: options?.acceptedConfirmationData, + rejectedConfirmationData: options?.rejectedConfirmationData, + userSelectedModelId: options?.userSelectedModelId, + userSelectedTools: options?.userSelectedTools?.get(), + modeInstructions: options?.modeInfo?.modeInstructions, + editedFileEvents: request.editedFileEvents, + }; + + let isInitialTools = true; + + store.add(autorun(reader => { + const tools = options?.userSelectedTools?.read(reader); + if (isInitialTools) { + isInitialTools = false; + return; + } + + if (tools) { + this.chatAgentService.setRequestTools(agent.id, request.id, tools); + // in case the request has not been sent out yet: + agentRequest.userSelectedTools = tools; + } + })); + + return agentRequest; + }; + + if ( + this.configurationService.getValue('chat.detectParticipant.enabled') !== false && + this.chatAgentService.hasChatParticipantDetectionProviders() && + !agentPart && + !commandPart && + !agentSlashCommandPart && + enableCommandDetection && + (location !== ChatAgentLocation.EditorInline || !this.configurationService.getValue(InlineChatConfigKeys.EnableV2)) && + options?.modeInfo?.kind !== ChatModeKind.Agent && + options?.modeInfo?.kind !== ChatModeKind.Edit && + !options?.agentIdSilent + ) { + // We have no agent or command to scope history with, pass the full history to the participant detection provider + const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, location, defaultAgent.id); + + // Prepare the request object that we will send to the participant detection provider + const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); + + const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); + if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) { + // Update the response in the ChatModel to reflect the detected agent and command + request.response?.setAgent(result.agent, result.command); + detectedAgent = result.agent; + detectedCommand = result.command; + } + } + + const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; + const command = detectedCommand ?? agentSlashCommandPart?.command; + + await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); + + // Recompute history in case the agent or command changed + const history = this.getHistoryEntriesFromModel(requests, location, agent.id); + const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); + this.generateInitialChatTitleIfNeeded(model, requestProps, defaultAgent, token); + const pendingRequest = this._pendingRequests.get(sessionResource); + if (pendingRequest && !pendingRequest.requestId) { + pendingRequest.requestId = requestProps.requestId; + } + completeResponseCreated(); + + // MCP autostart: only run for native VS Code sessions (sidebar, new editors) but not for extension contributed sessions that have inputType set. + if (model.canUseTools) { + const autostartResult = new ChatMcpServersStarting(this.mcpService.autostart(token)); + if (!autostartResult.isEmpty) { + progressCallback([autostartResult]); + await autostartResult.wait(); + } + } + + const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); + rawResult = agentResult; + agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); + } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { + if (commandPart.slashCommand.silent !== true) { + request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo); + completeResponseCreated(); + } + // contributed slash commands + // TODO: spell this out in the UI + const history: IChatMessage[] = []; + for (const modelRequest of model.getRequests()) { + if (!modelRequest.response) { + continue; + } + history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] }); + history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] }); + } + const message = parsedRequest.text; + const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { + progressCallback([p]); + }), history, location, model.sessionResource, token); + agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); + rawResult = {}; + + } else { + throw new Error(`Cannot handle request`); + } + + if (token.isCancellationRequested && !rawResult) { + return; + } else { + if (!rawResult) { + this.trace('sendRequest', `Provider returned no response for session ${model.sessionResource}`); + rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + + const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' : + rawResult.errorDetails && gotProgress ? 'errorWithOutput' : + rawResult.errorDetails ? 'error' : + 'success'; + + requestTelemetry.complete({ + timeToFirstProgress: rawResult.timings?.firstProgress, + totalTime: rawResult.timings?.totalElapsed, + result, + requestType, + detectedAgent, + request, + }); + + model.setResponse(request, rawResult); + completeResponseCreated(); + this.trace('sendRequest', `Provider returned response for session ${model.sessionResource}`); + + request.response?.complete(); + if (agentOrCommandFollowups) { + agentOrCommandFollowups.then(followups => { + model.setFollowups(request, followups); + const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command; + this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0); + }); + } + } + } catch (err) { + this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); + requestTelemetry.complete({ + timeToFirstProgress: undefined, + totalTime: undefined, + result: 'error', + requestType, + detectedAgent, + request, + }); + if (request) { + const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; + model.setResponse(request, rawResult); + completeResponseCreated(); + request.response?.complete(); + } + } finally { + store.dispose(); + } + }; + const rawResponsePromise = sendRequestInternal(); + // Note- requestId is not known at this point, assigned later + this._pendingRequests.set(model.sessionResource, this.instantiationService.createInstance(CancellableRequest, source, undefined)); + rawResponsePromise.finally(() => { + this._pendingRequests.deleteAndDispose(model.sessionResource); + }); + this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource }); + return { + responseCreatedPromise: responseCreated.p, + responseCompletePromise: rawResponsePromise, + }; + } + + private generateInitialChatTitleIfNeeded(model: ChatModel, request: IChatAgentRequest, defaultAgent: IChatAgentData, token: CancellationToken): void { + // Generate a title only for the first request, and only via the default agent. + // Use a single-entry history based on the current request (no full chat history). + if (model.getRequests().length !== 1 || model.customTitle) { + return; + } + + const singleEntryHistory: IChatAgentHistoryEntry[] = [{ + request, + response: [], + result: {} + }]; + const generate = async () => { + const title = await this.chatAgentService.getChatTitle(defaultAgent.id, singleEntryHistory, token); + if (title && !model.customTitle) { + model.setCustomTitle(title); + } + }; + void generate(); + } + + private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] { + attachedContextVariables ??= []; + + // "reverse", high index first so that replacement is simple + attachedContextVariables.sort((a, b) => { + // If either range is undefined, sort it to the back + if (!a.range && !b.range) { + return 0; // Keep relative order if both ranges are undefined + } + if (!a.range) { + return 1; // a goes after b + } + if (!b.range) { + return -1; // a goes before b + } + return b.range.start - a.range.start; + }); + + return attachedContextVariables; + } + + private getHistoryEntriesFromModel(requests: IChatRequestModel[], location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { + const history: IChatAgentHistoryEntry[] = []; + const agent = this.chatAgentService.getAgent(forAgentId); + for (const request of requests) { + if (!request.response) { + continue; + } + + if (forAgentId !== request.response.agent?.id && !agent?.isDefault && !agent?.canAccessPreviousChatHistory) { + // An agent only gets to see requests that were sent to this agent. + // The default agent (the undefined case), or agents with 'canAccessPreviousChatHistory', get to see all of them. + continue; + } + + // Do not save to history inline completions + if (location === ChatAgentLocation.EditorInline) { + continue; + } + + const promptTextResult = getPromptText(request.message); + const historyRequest: IChatAgentRequest = { + sessionResource: request.session.sessionResource, + requestId: request.id, + agentId: request.response.agent?.id ?? '', + message: promptTextResult.message, + command: request.response.slashCommand?.name, + variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack + location: ChatAgentLocation.Chat, + editedFileEvents: request.editedFileEvents, + }; + history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} }); + } + + return history; + } + + async removeRequest(sessionResource: URI, requestId: string): Promise { + const model = this._sessionModels.get(sessionResource); + if (!model) { + throw new Error(`Unknown session: ${sessionResource}`); + } + + const pendingRequest = this._pendingRequests.get(sessionResource); + if (pendingRequest?.requestId === requestId) { + pendingRequest.cancel(); + this._pendingRequests.deleteAndDispose(sessionResource); + } + + model.removeRequest(requestId); + } + + async adoptRequest(sessionResource: URI, request: IChatRequestModel) { + if (!(request instanceof ChatRequestModel)) { + throw new TypeError('Can only adopt requests of type ChatRequestModel'); + } + const target = this._sessionModels.get(sessionResource); + if (!target) { + throw new Error(`Unknown session: ${sessionResource}`); + } + + const oldOwner = request.session; + target.adoptRequest(request); + + if (request.response && !request.response.isComplete) { + const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionResource); + if (cts) { + cts.requestId = request.id; + this._pendingRequests.set(target.sessionResource, cts); + } + } + } + + async addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { + this.trace('addCompleteRequest', `message: ${message}`); + + const model = this._sessionModels.get(sessionResource); + if (!model) { + throw new Error(`Unknown session: ${sessionResource}`); + } + + const parsedRequest = typeof message === 'string' ? + this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, message) : + message; + const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true); + if (typeof response.message === 'string') { + // TODO is this possible? + model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' }); + } else { + for (const part of response.message) { + model.acceptResponseProgress(request, part, true); + } + } + model.setResponse(request, response.result || {}); + if (response.followups !== undefined) { + model.setFollowups(request, response.followups); + } + request.response?.complete(); + } + + cancelCurrentRequestForSession(sessionResource: URI): void { + this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`); + this._pendingRequests.get(sessionResource)?.cancel(); + this._pendingRequests.deleteAndDispose(sessionResource); + } + + public hasSessions(): boolean { + return this._chatSessionStore.hasSessions(); + } + + async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { + if (!LocalChatSessionUri.isLocalSession(transferredSessionResource)) { + throw new Error(`Can only transfer local chat sessions. Invalid session: ${transferredSessionResource}`); + } + + const model = this._sessionModels.get(transferredSessionResource) as ChatModel | undefined; + if (!model) { + throw new Error(`Failed to transfer session. Unknown session: ${transferredSessionResource}`); + } + + if (model.initialLocation !== ChatAgentLocation.Chat) { + throw new Error(`Can only transfer chat sessions located in the Chat view. Session ${transferredSessionResource} has location=${model.initialLocation}`); + } + + await this._chatSessionStore.storeTransferSession({ + sessionResource: model.sessionResource, + timestampInMilliseconds: Date.now(), + toWorkspace: toWorkspace, + }, model); + this.chatTransferService.addWorkspaceToTransferred(toWorkspace); + this.trace('transferChatSession', `Transferred session ${model.sessionResource} to workspace ${toWorkspace.toString()}`); + } + + getChatStorageFolder(): URI { + return this._chatSessionStore.getChatStorageFolder(); + } + + logChatIndex(): void { + this._chatSessionStore.logIndex(); + } + + setTitle(sessionResource: URI, title: string): void { + this._sessionModels.get(sessionResource)?.setCustomTitle(title); + } + + appendProgress(request: IChatRequestModel, progress: IChatProgress): void { + const model = this._sessionModels.get(request.session.sessionResource); + if (!(request instanceof ChatRequestModel)) { + throw new BugIndicatingError('Can only append progress to requests of type ChatRequestModel'); + } + + model?.acceptResponseProgress(request, progress); + } + + private toLocalSessionId(sessionResource: URI) { + const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!localSessionId) { + throw new Error(`Invalid local chat session resource: ${sessionResource}`); + } + return localSessionId; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts similarity index 96% rename from src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts rename to src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts index 16d4225f19a..6cd2769fbec 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceTelemetry.ts +++ b/src/vs/workbench/contrib/chat/common/chatService/chatServiceTelemetry.ts @@ -3,16 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../base/common/uri.js'; -import { isLocation } from '../../../../editor/common/languages.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IChatAgentData } from './chatAgents.js'; -import { ChatRequestModel, IChatRequestVariableData } from './chatModel.js'; -import { ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from './chatParserTypes.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { isLocation } from '../../../../../editor/common/languages.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IChatAgentData } from '../participants/chatAgents.js'; +import { ChatRequestModel, IChatRequestVariableData } from '../model/chatModel.js'; +import { ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from '../requestParser/chatParserTypes.js'; import { ChatAgentVoteDirection, ChatCopyKind, IChatSendRequestOptions, IChatUserActionEvent } from './chatService.js'; -import { isImageVariableEntry } from './chatVariableEntries.js'; -import { ChatAgentLocation } from './constants.js'; -import { ILanguageModelsService } from './languageModels.js'; +import { isImageVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ChatAgentLocation } from '../constants.js'; +import { ILanguageModelsService } from '../languageModels.js'; type ChatVoteEvent = { direction: 'up' | 'down'; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts deleted file mode 100644 index 6eb79ae1724..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ /dev/null @@ -1,1282 +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 { DeferredPromise } from '../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { Schemas } from '../../../../base/common/network.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'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { Progress } from '../../../../platform/progress/common/progress.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IMcpService } from '../../mcp/common/mcpTypes.js'; -import { IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IChatAgentRequest, IChatAgentResult, IChatAgentService } from './chatAgents.js'; -import { IChatEditingSession } from './chatEditingService.js'; -import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData, toChatHistoryContent, updateRanges } from './chatModel.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, chatSubcommandLeader, getPromptText, IParsedChatRequest } from './chatParserTypes.js'; -import { ChatRequestParser } from './chatRequestParser.js'; -import { ChatMcpServersStarting, IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatSessionContext, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; -import { ChatRequestTelemetry, ChatServiceTelemetry } from './chatServiceTelemetry.js'; -import { IChatSessionsService } from './chatSessionsService.js'; -import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; -import { IChatSlashCommandService } from './chatSlashCommands.js'; -import { IChatTransferService } from './chatTransferService.js'; -import { LocalChatSessionUri } from './chatUri.js'; -import { IChatRequestVariableEntry } from './chatVariableEntries.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js'; -import { ChatMessageRole, IChatMessage } from './languageModels.js'; -import { ILanguageModelToolsService } from './languageModelToolsService.js'; - -const serializedChatKey = 'interactive.sessions'; - -const TransferredGlobalChatKey = 'chat.workspaceTransfer'; - -const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; - -class CancellableRequest implements IDisposable { - constructor( - public readonly cancellationTokenSource: CancellationTokenSource, - public requestId: string | undefined, - @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService - ) { } - - dispose() { - this.cancellationTokenSource.dispose(); - } - - cancel() { - if (this.requestId) { - this.toolsService.cancelToolCallsForRequest(this.requestId); - } - - this.cancellationTokenSource.cancel(); - } -} - -class ChatModelStore { - private readonly _models = new ObservableMap(); - - public get observable() { - return this._models.observable; - } - - public values(): Iterable { - return this._models.values(); - } - - public get(uri: URI): ChatModel | undefined { - return this._models.get(this.toKey(uri)); - } - - public has(uri: URI): boolean { - return this._models.has(this.toKey(uri)); - } - - public set(uri: URI, value: ChatModel): void { - this._models.set(this.toKey(uri), value); - } - - public delete(uri: URI): boolean { - return this._models.delete(this.toKey(uri)); - } - - private toKey(uri: URI): string { - return uri.toString(); - } -} - -class DisposableResourceMap extends Disposable { - - private readonly _map = this._register(new DisposableMap()); - - get(sessionResource: URI) { - return this._map.get(this.toKey(sessionResource)); - } - - set(sessionResource: URI, value: T) { - this._map.set(this.toKey(sessionResource), value); - } - - has(sessionResource: URI) { - return this._map.has(this.toKey(sessionResource)); - } - - deleteAndLeak(sessionResource: URI) { - return this._map.deleteAndLeak(this.toKey(sessionResource)); - } - - deleteAndDispose(sessionResource: URI) { - this._map.deleteAndDispose(this.toKey(sessionResource)); - } - - private toKey(uri: URI): string { - return uri.toString(); - } -} - - -export class ChatService extends Disposable implements IChatService { - declare _serviceBrand: undefined; - - private readonly _sessionModels = new ChatModelStore(); - private readonly _contentProviderSessionModels = this._register(new DisposableResourceMap<{ readonly model: IChatModel } & IDisposable>()); - private readonly _pendingRequests = this._register(new DisposableResourceMap()); - private _persistedSessions: ISerializableChatsData; - - private _transferredSessionData: IChatTransferredSessionData | undefined; - public get transferredSessionData(): IChatTransferredSessionData | undefined { - return this._transferredSessionData; - } - - private readonly _onDidSubmitRequest = this._register(new Emitter<{ readonly chatSessionResource: URI }>()); - public readonly onDidSubmitRequest = this._onDidSubmitRequest.event; - - private readonly _onDidPerformUserAction = this._register(new Emitter()); - public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; - - private readonly _onDidDisposeSession = this._register(new Emitter<{ readonly sessionResource: URI; reason: 'cleared' }>()); - public readonly onDidDisposeSession = this._onDidDisposeSession.event; - - private readonly _sessionFollowupCancelTokens = this._register(new DisposableResourceMap()); - private readonly _chatServiceTelemetry: ChatServiceTelemetry; - private readonly _chatSessionStore: ChatSessionStore; - - readonly requestInProgressObs: IObservable; - - public get edits2Enabled(): boolean { - return this.configurationService.getValue(ChatConfiguration.Edits2Enabled); - } - - private get isEmptyWindow(): boolean { - const workspace = this.workspaceContextService.getWorkspace(); - return !workspace.configuration && workspace.folders.length === 0; - } - - constructor( - @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, - @IExtensionService private readonly extensionService: IExtensionService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService, - @IChatAgentService private readonly chatAgentService: IChatAgentService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IChatTransferService private readonly chatTransferService: IChatTransferService, - @IChatSessionsService private readonly chatSessionService: IChatSessionsService, - @IMcpService private readonly mcpService: IMcpService, - ) { - super(); - - this._chatServiceTelemetry = this.instantiationService.createInstance(ChatServiceTelemetry); - - const sessionData = storageService.get(serializedChatKey, this.isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE, ''); - if (sessionData) { - this._persistedSessions = this.deserializeChats(sessionData); - const countsForLog = Object.keys(this._persistedSessions).length; - if (countsForLog > 0) { - this.trace('constructor', `Restored ${countsForLog} persisted sessions`); - } - } else { - this._persistedSessions = {}; - } - - const transferredData = this.getTransferredSessionData(); - const transferredChat = transferredData?.chat; - if (transferredChat) { - this.trace('constructor', `Transferred session ${transferredChat.sessionId}`); - this._persistedSessions[transferredChat.sessionId] = transferredChat; - this._transferredSessionData = { - sessionId: transferredChat.sessionId, - inputValue: transferredData.inputValue, - location: transferredData.location, - mode: transferredData.mode, - }; - } - - this._chatSessionStore = this._register(this.instantiationService.createInstance(ChatSessionStore)); - this._chatSessionStore.migrateDataIfNeeded(() => this._persistedSessions); - - // When using file storage, populate _persistedSessions with session metadata from the index - // This ensures that getPersistedSessionTitle() can find titles for inactive sessions - this.initializePersistedSessionsFromFileStorage(); - - this._register(storageService.onWillSaveState(() => this.saveState())); - - this.requestInProgressObs = derived(reader => { - const models = this._sessionModels.observable.read(reader).values(); - return Array.from(models).some(model => model.requestInProgressObs.read(reader)); - }); - } - - public get editingSessions() { - return [...this._sessionModels.values()].map(v => v.editingSession).filter(isDefined); - } - - isEnabled(location: ChatAgentLocation): boolean { - return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; - } - - private saveState(): void { - const liveChats = Array.from(this._sessionModels.values()) - .filter(session => { - if (!LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) { - return false; - } - return session.initialLocation === ChatAgentLocation.Chat; - }); - - this._chatSessionStore.storeSessions(liveChats); - } - - notifyUserAction(action: IChatUserActionEvent): void { - this._chatServiceTelemetry.notifyUserAction(action); - this._onDidPerformUserAction.fire(action); - if (action.action.kind === 'chatEditingSessionAction') { - const model = this._sessionModels.get(action.sessionResource); - if (model) { - model.notifyEditingAction(action.action); - } - } - } - - async setChatSessionTitle(sessionResource: URI, title: string): Promise { - const sessionId = this.toLocalSessionId(sessionResource); - const model = this._sessionModels.get(sessionResource); - if (model) { - model.setCustomTitle(title); - } - - // Update the title in the file storage - await this._chatSessionStore.setSessionTitle(sessionId, title); - // Trigger immediate save to ensure consistency - this.saveState(); - } - - private trace(method: string, message?: string): void { - if (message) { - this.logService.trace(`ChatService#${method}: ${message}`); - } else { - this.logService.trace(`ChatService#${method}`); - } - } - - private error(method: string, message: string): void { - this.logService.error(`ChatService#${method} ${message}`); - } - - private deserializeChats(sessionData: string): ISerializableChatsData { - try { - const arrayOfSessions: ISerializableChatDataIn[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data - if (!Array.isArray(arrayOfSessions)) { - throw new Error('Expected array'); - } - - const sessions = arrayOfSessions.reduce((acc, session) => { - // Revive serialized markdown strings in response data - for (const request of session.requests) { - if (Array.isArray(request.response)) { - request.response = request.response.map((response) => { - if (typeof response === 'string') { - return new MarkdownString(response); - } - return response; - }); - } else if (typeof request.response === 'string') { - request.response = [new MarkdownString(request.response)]; - } - } - - acc[session.sessionId] = normalizeSerializableChatData(session); - return acc; - }, {}); - return sessions; - } catch (err) { - this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`); - return {}; - } - } - - private getTransferredSessionData(): IChatTransfer2 | undefined { - const data: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []); - const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri; - if (!workspaceUri) { - return; - } - - const thisWorkspace = workspaceUri.toString(); - const currentTime = Date.now(); - // Only use transferred data if it was created recently - const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - // Keep data that isn't for the current workspace and that hasn't expired yet - const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - this.storageService.store(TransferredGlobalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE); - return transferred; - } - - private async initializePersistedSessionsFromFileStorage(): Promise { - - const index = await this._chatSessionStore.getIndex(); - const sessionIds = Object.keys(index); - - for (const sessionId of sessionIds) { - const metadata = index[sessionId]; - if (metadata && !this._persistedSessions[sessionId]) { - // Create a minimal session entry with the title information - // This allows getPersistedSessionTitle() to find the title without loading the full session - const minimalSession: ISerializableChatData = { - version: 3, - sessionId: sessionId, - customTitle: metadata.title, - creationDate: Date.now(), // Use current time as fallback - lastMessageDate: metadata.lastMessageDate, - isImported: metadata.isImported || false, - initialLocation: metadata.initialLocation, - requests: [], // Empty requests array - this is just for title lookup - requesterUsername: '', - responderUsername: '', - requesterAvatarIconUri: undefined, - responderAvatarIconUri: undefined, - }; - - this._persistedSessions[sessionId] = minimalSession; - } - } - } - - /** - * Returns an array of chat details for all persisted chat sessions that have at least one request. - * Chat sessions that have already been loaded into the chat view are excluded from the result. - * Imported chat sessions are also excluded from the result. - */ - async getLocalSessionHistory(): Promise { - const liveSessionItems = Array.from(this._sessionModels.values()) - .filter(session => this.shouldBeInHistory(session)) - .map((session): IChatDetail => { - const title = session.title || localize('newChat', "New Chat"); - return { - sessionResource: session.sessionResource, - title, - lastMessageDate: session.lastMessageDate, - isActive: true, - }; - }); - - const index = await this._chatSessionStore.getIndex(); - const entries = Object.values(index) - .filter(entry => !this._sessionModels.has(LocalChatSessionUri.forSession(entry.sessionId)) && this.shouldBeInHistory(entry) && !entry.isEmpty) - .map((entry): IChatDetail => { - const sessionResource = LocalChatSessionUri.forSession(entry.sessionId); - return ({ - ...entry, - sessionResource, - isActive: this._sessionModels.has(sessionResource), - }); - }); - return [...liveSessionItems, ...entries]; - } - - shouldBeInHistory(entry: Partial) { - if (entry.sessionResource) { - return !entry.isImported && LocalChatSessionUri.parseLocalSessionId(entry.sessionResource) && entry.initialLocation !== ChatAgentLocation.EditorInline; - } - return !entry.isImported && entry.initialLocation !== ChatAgentLocation.EditorInline; - } - - async removeHistoryEntry(sessionResource: URI): Promise { - await this._chatSessionStore.deleteSession(this.toLocalSessionId(sessionResource)); - } - - async clearAllHistoryEntries(): Promise { - await this._chatSessionStore.clearAllSessions(); - } - - startSession(location: ChatAgentLocation, token: CancellationToken, isGlobalEditingSession: boolean = true, options?: { canUseTools?: boolean }): ChatModel { - this.trace('startSession'); - return this._startSession(undefined, location, isGlobalEditingSession, token, options); - } - - private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: ChatAgentLocation, isGlobalEditingSession: boolean, token: CancellationToken, options?: { sessionResource?: URI; canUseTools?: boolean }, transferEditingSession?: IChatEditingSession): ChatModel { - const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, { initialLocation: location, canUseTools: options?.canUseTools ?? true, resource: options?.sessionResource }); - if (location === ChatAgentLocation.Chat) { - model.startEditingSession(isGlobalEditingSession, transferEditingSession); - } - - this._sessionModels.set(model.sessionResource, model); - this.initializeSession(model, token); - return model; - } - - private initializeSession(model: ChatModel, token: CancellationToken): void { - this.trace('initializeSession', `Initialize session ${model.sessionId}`); - - // Activate the default extension provided agent but do not wait - // for it to be ready so that the session can be used immediately - // without having to wait for the agent to be ready. - this.activateDefaultAgent(model.initialLocation).catch(e => this.logService.error(e)); - } - - async activateDefaultAgent(location: ChatAgentLocation): Promise { - await this.extensionService.whenInstalledExtensionsRegistered(); - - const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(location) ?? this.chatAgentService.getContributedDefaultAgent(ChatAgentLocation.Chat); - if (!defaultAgentData) { - throw new ErrorNoTelemetry('No default agent contributed'); - } - - // Await activation of the extension provided agent - // Using `activateById` as workaround for the issue - // https://github.com/microsoft/vscode/issues/250590 - if (!defaultAgentData.isCore) { - await this.extensionService.activateById(defaultAgentData.extensionId, { - activationEvent: `onChatParticipant:${defaultAgentData.id}`, - extensionId: defaultAgentData.extensionId, - startup: false - }); - } - - const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); - if (!defaultAgent) { - throw new ErrorNoTelemetry('No default agent registered'); - } - } - - getSession(sessionResource: URI): IChatModel | undefined { - return this._sessionModels.get(sessionResource); - } - - getSessionByLegacyId(sessionId: string): IChatModel | undefined { - return Array.from(this._sessionModels.values()).find(session => session.sessionId === sessionId); - } - - async getOrRestoreSession(sessionResource: URI): Promise { - this.trace('getOrRestoreSession', `${sessionResource}`); - const model = this._sessionModels.get(sessionResource); - if (model) { - return model; - } - - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - throw new Error(`Cannot restore non-local session ${sessionResource}`); - } - - let sessionData: ISerializableChatData | undefined; - if (this.transferredSessionData?.sessionId === sessionId) { - sessionData = revive(this._persistedSessions[sessionId]); - } else { - sessionData = revive(await this._chatSessionStore.readSession(sessionId)); - } - - if (!sessionData) { - return undefined; - } - - const session = this._startSession(sessionData, sessionData.initialLocation ?? ChatAgentLocation.Chat, true, CancellationToken.None, { canUseTools: true, sessionResource }); - - const isTransferred = this.transferredSessionData?.sessionId === sessionId; - if (isTransferred) { - this._transferredSessionData = undefined; - } - - return session; - } - - /** - * This is really just for migrating data from the edit session location to the panel. - */ - isPersistedSessionEmpty(sessionResource: URI): boolean { - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - throw new Error(`Cannot restore non-local session ${sessionResource}`); - } - - const session = this._persistedSessions[sessionId]; - if (session) { - return session.requests.length === 0; - } - - return this._chatSessionStore.isSessionEmpty(sessionId); - } - - getPersistedSessionTitle(sessionResource: URI): string | undefined { - const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!sessionId) { - return undefined; - } - - // First check the memory cache (_persistedSessions) - const session = this._persistedSessions[sessionId]; - if (session) { - const title = session.customTitle || ChatModel.getDefaultTitle(session.requests); - return title; - } - - // Try to read directly from file storage index - // This handles the case where getName() is called before initialization completes - // Access the internal synchronous index method via reflection - // This is a workaround for the timing issue where initialization hasn't completed - // eslint-disable-next-line local/code-no-any-casts - const internalGetIndex = (this._chatSessionStore as any).internalGetIndex; - if (typeof internalGetIndex === 'function') { - const indexData = internalGetIndex.call(this._chatSessionStore); - const metadata = indexData.entries[sessionId]; - if (metadata && metadata.title) { - return metadata.title; - } - } - - return undefined; - } - - loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined { - return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Chat, true, CancellationToken.None); - } - - async loadSessionForResource(chatSessionResource: URI, location: ChatAgentLocation, token: CancellationToken): Promise { - // TODO: Move this into a new ChatModelService - - if (chatSessionResource.scheme === Schemas.vscodeLocalChatSession) { - return this.getOrRestoreSession(chatSessionResource); - } - - const existing = this._contentProviderSessionModels.get(chatSessionResource); - if (existing) { - return existing.model; - } - - const providedSession = await this.chatSessionService.getOrCreateChatSession(chatSessionResource, CancellationToken.None); - const chatSessionType = chatSessionResource.scheme; - - // Contributed sessions do not use UI tools - const model = this._startSession(undefined, location, true, CancellationToken.None, { sessionResource: chatSessionResource, canUseTools: false }, providedSession.initialEditingSession); - model.setContributedChatSession({ - chatSessionResource, - chatSessionType, - isUntitled: chatSessionResource.path.startsWith('/untitled-') //TODO(jospicer) - }); - - const disposables = new DisposableStore(); - this._contentProviderSessionModels.set(chatSessionResource, { model, dispose: () => disposables.dispose() }); - - disposables.add(model.onDidDispose(() => { - this._contentProviderSessionModels.deleteAndDispose(chatSessionResource); - providedSession.dispose(); - })); - - let lastRequest: ChatRequestModel | undefined; - for (const message of providedSession.history) { - if (message.type === 'request') { - if (lastRequest) { - model.completeResponse(lastRequest); - } - - const requestText = message.prompt; - - const parsedRequest: IParsedChatRequest = { - text: requestText, - parts: [new ChatRequestTextPart( - new OffsetRange(0, requestText.length), - { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: requestText.length + 1 }, - requestText - )] - }; - const agent = - message.participant - ? this.chatAgentService.getAgent(message.participant) // TODO(jospicer): Remove and always hardcode? - : this.chatAgentService.getAgent(chatSessionType); - lastRequest = model.addRequest(parsedRequest, - message.variableData ?? { variables: [] }, - 0, // attempt - undefined, - agent, - undefined, // slashCommand - undefined, // confirmation - undefined, // locationData - undefined, // attachments - true // isCompleteAddedRequest - this indicates it's a complete request, not user input - ); - } else { - // response - if (lastRequest) { - for (const part of message.parts) { - model.acceptResponseProgress(lastRequest, part); - } - } - } - } - - if (providedSession.progressObs && lastRequest && providedSession.interruptActiveResponseCallback) { - const initialCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); - this._pendingRequests.set(model.sessionResource, initialCancellationRequest); - const cancellationListener = disposables.add(new MutableDisposable()); - - const createCancellationListener = (token: CancellationToken) => { - return token.onCancellationRequested(() => { - providedSession.interruptActiveResponseCallback?.().then(userConfirmedInterruption => { - if (!userConfirmedInterruption) { - // User cancelled the interruption - const newCancellationRequest = this.instantiationService.createInstance(CancellableRequest, new CancellationTokenSource(), undefined); - this._pendingRequests.set(model.sessionResource, newCancellationRequest); - cancellationListener.value = createCancellationListener(newCancellationRequest.cancellationTokenSource.token); - } - }); - }); - }; - - cancellationListener.value = createCancellationListener(initialCancellationRequest.cancellationTokenSource.token); - - let lastProgressLength = 0; - disposables.add(autorun(reader => { - const progressArray = providedSession.progressObs?.read(reader) ?? []; - const isComplete = providedSession.isCompleteObs?.read(reader) ?? false; - - // Process only new progress items - if (progressArray.length > lastProgressLength) { - const newProgress = progressArray.slice(lastProgressLength); - for (const progress of newProgress) { - model?.acceptResponseProgress(lastRequest, progress); - } - lastProgressLength = progressArray.length; - } - - // Handle completion - if (isComplete) { - model?.completeResponse(lastRequest); - cancellationListener.clear(); - } - })); - } else { - if (lastRequest) { - model.completeResponse(lastRequest); - } - } - - return model; - } - - getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined { - const model = this._sessionModels.get(sessionResource); - if (!model) { - return; - } - const { contributedChatSession } = model; - return contributedChatSession; - } - - async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { - const model = this._sessionModels.get(request.session.sessionResource); - if (!model && model !== request.session) { - throw new Error(`Unknown session: ${request.session.sessionResource}`); - } - - const cts = this._pendingRequests.get(request.session.sessionResource); - if (cts) { - this.trace('resendRequest', `Session ${request.session.sessionResource} already has a pending request, cancelling...`); - cts.cancel(); - } - - const location = options?.location ?? model.initialLocation; - const attempt = options?.attempt ?? 0; - const enableCommandDetection = !options?.noCommandDetection; - const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!; - - model.removeRequest(request.id, ChatRequestRemovalReason.Resend); - - const resendOptions: IChatSendRequestOptions = { - ...options, - locationData: request.locationData, - attachedContext: request.attachedContext, - }; - await this._sendRequestAsync(model, model.sessionResource, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; - } - - async sendRequest(sessionResource: URI, request: string, options?: IChatSendRequestOptions): Promise { - this.trace('sendRequest', `sessionResource: ${sessionResource.toString()}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - - - if (!request.trim() && !options?.slashCommand && !options?.agentId && !options?.agentIdSilent) { - this.trace('sendRequest', 'Rejected empty message'); - return; - } - - const model = this._sessionModels.get(sessionResource); - if (!model) { - throw new Error(`Unknown session: ${sessionResource}`); - } - - if (this._pendingRequests.has(sessionResource)) { - this.trace('sendRequest', `Session ${sessionResource} already has a pending request`); - return; - } - - const requests = model.getRequests(); - for (let i = requests.length - 1; i >= 0; i -= 1) { - const request = requests[i]; - if (request.shouldBeRemovedOnSend) { - if (request.shouldBeRemovedOnSend.afterUndoStop) { - request.response?.finalizeUndoState(); - } else { - await this.removeRequest(sessionResource, request.id); - } - } - } - - const location = options?.location ?? model.initialLocation; - const attempt = options?.attempt ?? 0; - const defaultAgent = this.chatAgentService.getDefaultAgent(location, options?.modeInfo?.kind)!; - - const parsedRequest = this.parseChatRequest(sessionResource, request, location, options); - const silentAgent = options?.agentIdSilent ? this.chatAgentService.getAgent(options.agentIdSilent) : undefined; - const agent = silentAgent ?? parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; - const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - - // This method is only returning whether the request was accepted - don't block on the actual request - return { - ...this._sendRequestAsync(model, sessionResource, parsedRequest, attempt, !options?.noCommandDetection, silentAgent ?? defaultAgent, location, options), - agent, - slashCommand: agentSlashCommandPart?.command, - }; - } - - private parseChatRequest(sessionResource: URI, request: string, location: ChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { - let parserContext = options?.parserContext; - if (options?.agentId) { - const agent = this.chatAgentService.getAgent(options.agentId); - if (!agent) { - throw new Error(`Unknown agent: ${options.agentId}`); - } - parserContext = { selectedAgent: agent, mode: options.modeInfo?.kind }; - const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; - request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; - } - - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, request, location, parserContext); - return parsedRequest; - } - - private refreshFollowupsCancellationToken(sessionResource: URI): CancellationToken { - this._sessionFollowupCancelTokens.get(sessionResource)?.cancel(); - const newTokenSource = new CancellationTokenSource(); - this._sessionFollowupCancelTokens.set(sessionResource, newTokenSource); - - return newTokenSource.token; - } - - private _sendRequestAsync(model: ChatModel, sessionResource: URI, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgentData, location: ChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { - const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionResource); - let request: ChatRequestModel; - const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); - const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); - const requests = [...model.getRequests()]; - const requestTelemetry = this.instantiationService.createInstance(ChatRequestTelemetry, { - agent: agentPart?.agent ?? defaultAgent, - agentSlashCommandPart, - commandPart, - sessionId: model.sessionId, - location: model.initialLocation, - options, - enableCommandDetection - }); - - let gotProgress = false; - const requestType = commandPart ? 'slashCommand' : 'string'; - - const responseCreated = new DeferredPromise(); - let responseCreatedComplete = false; - function completeResponseCreated(): void { - if (!responseCreatedComplete && request?.response) { - responseCreated.complete(request.response); - responseCreatedComplete = true; - } - } - - const store = new DisposableStore(); - const source = store.add(new CancellationTokenSource()); - const token = source.token; - const sendRequestInternal = async () => { - const progressCallback = (progress: IChatProgress[]) => { - if (token.isCancellationRequested) { - return; - } - - gotProgress = true; - - for (let i = 0; i < progress.length; i++) { - const isLast = i === progress.length - 1; - const progressItem = progress[i]; - - if (progressItem.kind === 'markdownContent') { - this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progressItem.content.value.length} chars`); - } else { - this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progressItem)}`); - } - - model.acceptResponseProgress(request, progressItem, !isLast); - } - completeResponseCreated(); - }; - - let detectedAgent: IChatAgentData | undefined; - let detectedCommand: IChatAgentCommand | undefined; - - const stopWatch = new StopWatch(false); - store.add(token.onCancellationRequested(() => { - this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); - if (!request) { - return; - } - - requestTelemetry.complete({ - timeToFirstProgress: undefined, - result: 'cancelled', - // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling - totalTime: stopWatch.elapsed(), - requestType, - detectedAgent, - request, - }); - - model.cancelRequest(request); - })); - - try { - let rawResult: IChatAgentResult | null | undefined; - let agentOrCommandFollowups: Promise | undefined = undefined; - let chatTitlePromise: Promise | undefined; - - if (agentPart || (defaultAgent && !commandPart)) { - const prepareChatAgentRequest = (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): IChatAgentRequest => { - const initVariableData: IChatRequestVariableData = { variables: [] }; - request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, options?.modeInfo, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, undefined, options?.userSelectedModelId, options?.userSelectedTools?.get()); - - let variableData: IChatRequestVariableData; - let message: string; - if (chatRequest) { - variableData = chatRequest.variableData; - message = getPromptText(request.message).message; - } else { - variableData = { variables: this.prepareContext(request.attachedContext) }; - model.updateRequest(request, variableData); - - const promptTextResult = getPromptText(request.message); - variableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - message = promptTextResult.message; - } - - const agentRequest: IChatAgentRequest = { - sessionId: model.sessionId, - requestId: request.id, - agentId: agent.id, - message, - command: command?.name, - variables: variableData, - enableCommandDetection, - isParticipantDetected, - attempt, - location, - locationData: request.locationData, - acceptedConfirmationData: options?.acceptedConfirmationData, - rejectedConfirmationData: options?.rejectedConfirmationData, - userSelectedModelId: options?.userSelectedModelId, - userSelectedTools: options?.userSelectedTools?.get(), - modeInstructions: options?.modeInfo?.modeInstructions, - editedFileEvents: request.editedFileEvents, - chatSummary: options?.chatSummary - }; - - let isInitialTools = true; - - store.add(autorun(reader => { - const tools = options?.userSelectedTools?.read(reader); - if (isInitialTools) { - isInitialTools = false; - return; - } - - if (tools) { - this.chatAgentService.setRequestTools(agent.id, request.id, tools); - // in case the request has not been sent out yet: - agentRequest.userSelectedTools = tools; - } - })); - - return agentRequest; - }; - - if ( - this.configurationService.getValue('chat.detectParticipant.enabled') !== false && - this.chatAgentService.hasChatParticipantDetectionProviders() && - !agentPart && - !commandPart && - !agentSlashCommandPart && - enableCommandDetection && - options?.modeInfo?.kind !== ChatModeKind.Agent && - options?.modeInfo?.kind !== ChatModeKind.Edit && - !options?.agentIdSilent - ) { - // We have no agent or command to scope history with, pass the full history to the participant detection provider - const defaultAgentHistory = this.getHistoryEntriesFromModel(requests, model.sessionId, location, defaultAgent.id); - - // Prepare the request object that we will send to the participant detection provider - const chatAgentRequest = prepareChatAgentRequest(defaultAgent, undefined, enableCommandDetection, undefined, false); - - const result = await this.chatAgentService.detectAgentOrCommand(chatAgentRequest, defaultAgentHistory, { location }, token); - if (result && this.chatAgentService.getAgent(result.agent.id)?.locations?.includes(location)) { - // Update the response in the ChatModel to reflect the detected agent and command - request.response?.setAgent(result.agent, result.command); - detectedAgent = result.agent; - detectedCommand = result.command; - } - } - - const agent = (detectedAgent ?? agentPart?.agent ?? defaultAgent)!; - const command = detectedCommand ?? agentSlashCommandPart?.command; - - await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); - - // Recompute history in case the agent or command changed - const history = this.getHistoryEntriesFromModel(requests, model.sessionId, location, agent.id); - const requestProps = prepareChatAgentRequest(agent, command, enableCommandDetection, request /* Reuse the request object if we already created it for participant detection */, !!detectedAgent); - const pendingRequest = this._pendingRequests.get(sessionResource); - if (pendingRequest && !pendingRequest.requestId) { - pendingRequest.requestId = requestProps.requestId; - } - completeResponseCreated(); - - // MCP autostart: only run for native VS Code sessions (sidebar, new editors) but not for extension contributed sessions that have inputType set. - if (model.canUseTools) { - const autostartResult = new ChatMcpServersStarting(this.mcpService.autostart(token)); - if (!autostartResult.isEmpty) { - progressCallback([autostartResult]); - await autostartResult.wait(); - } - } - - const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); - rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); - - // Use LLM to generate the chat title - if (model.getRequests().length === 1 && !model.customTitle) { - const chatHistory = this.getHistoryEntriesFromModel(model.getRequests(), model.sessionId, location, agent.id); - chatTitlePromise = this.chatAgentService.getChatTitle(agent.id, chatHistory, CancellationToken.None).then( - (title) => { - // Since not every chat agent implements title generation, we can fallback to the default agent - // which supports it - if (title === undefined) { - const defaultAgentForTitle = this.chatAgentService.getDefaultAgent(location); - if (defaultAgentForTitle) { - return this.chatAgentService.getChatTitle(defaultAgentForTitle.id, chatHistory, CancellationToken.None); - } - } - return title; - }); - } - } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { - if (commandPart.slashCommand.silent !== true) { - request = model.addRequest(parsedRequest, { variables: [] }, attempt, options?.modeInfo); - completeResponseCreated(); - } - // contributed slash commands - // TODO: spell this out in the UI - const history: IChatMessage[] = []; - for (const modelRequest of model.getRequests()) { - if (!modelRequest.response) { - continue; - } - history.push({ role: ChatMessageRole.User, content: [{ type: 'text', value: modelRequest.message.text }] }); - history.push({ role: ChatMessageRole.Assistant, content: [{ type: 'text', value: modelRequest.response.response.toString() }] }); - } - const message = parsedRequest.text; - const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { - progressCallback([p]); - }), history, location, token); - agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); - rawResult = {}; - - } else { - throw new Error(`Cannot handle request`); - } - - if (token.isCancellationRequested && !rawResult) { - return; - } else { - if (!rawResult) { - this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); - rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; - } - - const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' : - rawResult.errorDetails && gotProgress ? 'errorWithOutput' : - rawResult.errorDetails ? 'error' : - 'success'; - - requestTelemetry.complete({ - timeToFirstProgress: rawResult.timings?.firstProgress, - totalTime: rawResult.timings?.totalElapsed, - result, - requestType, - detectedAgent, - request, - }); - - model.setResponse(request, rawResult); - completeResponseCreated(); - this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); - - model.completeResponse(request); - if (agentOrCommandFollowups) { - agentOrCommandFollowups.then(followups => { - model.setFollowups(request, followups); - const commandForTelemetry = agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command; - this._chatServiceTelemetry.retrievedFollowups(agentPart?.agent.id ?? '', commandForTelemetry, followups?.length ?? 0); - }); - } - chatTitlePromise?.then(title => { - if (title) { - model.setCustomTitle(title); - } - }); - } - } catch (err) { - this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); - requestTelemetry.complete({ - timeToFirstProgress: undefined, - totalTime: undefined, - result: 'error', - requestType, - detectedAgent, - request, - }); - if (request) { - const rawResult: IChatAgentResult = { errorDetails: { message: err.message } }; - model.setResponse(request, rawResult); - completeResponseCreated(); - model.completeResponse(request); - } - } finally { - store.dispose(); - } - }; - const rawResponsePromise = sendRequestInternal(); - // Note- requestId is not known at this point, assigned later - this._pendingRequests.set(model.sessionResource, this.instantiationService.createInstance(CancellableRequest, source, undefined)); - rawResponsePromise.finally(() => { - this._pendingRequests.deleteAndDispose(model.sessionResource); - }); - this._onDidSubmitRequest.fire({ chatSessionResource: model.sessionResource }); - return { - responseCreatedPromise: responseCreated.p, - responseCompletePromise: rawResponsePromise, - }; - } - - private prepareContext(attachedContextVariables: IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] { - attachedContextVariables ??= []; - - // "reverse", high index first so that replacement is simple - attachedContextVariables.sort((a, b) => { - // If either range is undefined, sort it to the back - if (!a.range && !b.range) { - return 0; // Keep relative order if both ranges are undefined - } - if (!a.range) { - return 1; // a goes after b - } - if (!b.range) { - return -1; // a goes before b - } - return b.range.start - a.range.start; - }); - - return attachedContextVariables; - } - - private getHistoryEntriesFromModel(requests: IChatRequestModel[], sessionId: string, location: ChatAgentLocation, forAgentId: string): IChatAgentHistoryEntry[] { - const history: IChatAgentHistoryEntry[] = []; - const agent = this.chatAgentService.getAgent(forAgentId); - for (const request of requests) { - if (!request.response) { - continue; - } - - if (forAgentId !== request.response.agent?.id && !agent?.isDefault) { - // An agent only gets to see requests that were sent to this agent. - // The default agent (the undefined case) gets to see all of them. - continue; - } - - // Do not save to history inline completions - if (location === ChatAgentLocation.EditorInline) { - continue; - } - - const promptTextResult = getPromptText(request.message); - const historyRequest: IChatAgentRequest = { - sessionId: sessionId, - requestId: request.id, - agentId: request.response.agent?.id ?? '', - message: promptTextResult.message, - command: request.response.slashCommand?.name, - variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack - location: ChatAgentLocation.Chat, - editedFileEvents: request.editedFileEvents, - }; - history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} }); - } - - return history; - } - - async removeRequest(sessionResource: URI, requestId: string): Promise { - const model = this._sessionModels.get(sessionResource); - if (!model) { - throw new Error(`Unknown session: ${sessionResource}`); - } - - const pendingRequest = this._pendingRequests.get(sessionResource); - if (pendingRequest?.requestId === requestId) { - pendingRequest.cancel(); - this._pendingRequests.deleteAndDispose(sessionResource); - } - - model.removeRequest(requestId); - } - - async adoptRequest(sessionResource: URI, request: IChatRequestModel) { - if (!(request instanceof ChatRequestModel)) { - throw new TypeError('Can only adopt requests of type ChatRequestModel'); - } - const target = this._sessionModels.get(sessionResource); - if (!target) { - throw new Error(`Unknown session: ${sessionResource}`); - } - - const oldOwner = request.session; - target.adoptRequest(request); - - if (request.response && !request.response.isComplete) { - const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionResource); - if (cts) { - cts.requestId = request.id; - this._pendingRequests.set(target.sessionResource, cts); - } - } - } - - async addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { - this.trace('addCompleteRequest', `message: ${message}`); - - const model = this._sessionModels.get(sessionResource); - if (!model) { - throw new Error(`Unknown session: ${sessionResource}`); - } - - const parsedRequest = typeof message === 'string' ? - this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionResource, message) : - message; - const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0, undefined, undefined, undefined, undefined, undefined, undefined, true); - if (typeof response.message === 'string') { - // TODO is this possible? - model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' }); - } else { - for (const part of response.message) { - model.acceptResponseProgress(request, part, true); - } - } - model.setResponse(request, response.result || {}); - if (response.followups !== undefined) { - model.setFollowups(request, response.followups); - } - model.completeResponse(request); - } - - cancelCurrentRequestForSession(sessionResource: URI): void { - this.trace('cancelCurrentRequestForSession', `session: ${sessionResource}`); - this._pendingRequests.get(sessionResource)?.cancel(); - this._pendingRequests.deleteAndDispose(sessionResource); - } - - async clearSession(sessionResource: URI): Promise { - this.trace('clearSession', `session: ${sessionResource}`); - const model = this._sessionModels.get(sessionResource); - if (!model) { - throw new Error(`Unknown session: ${sessionResource}`); - } - - const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (localSessionId && (model.initialLocation === ChatAgentLocation.Chat)) { - // Always preserve sessions that have custom titles, even if empty - if (model.getRequests().length === 0 && !model.customTitle) { - await this._chatSessionStore.deleteSession(localSessionId); - } else { - await this._chatSessionStore.storeSessions([model]); - } - } - - this._sessionModels.delete(sessionResource); - model.dispose(); - this._pendingRequests.get(sessionResource)?.cancel(); - this._pendingRequests.deleteAndDispose(sessionResource); - this._onDidDisposeSession.fire({ sessionResource, reason: 'cleared' }); - } - - public hasSessions(): boolean { - return this._chatSessionStore.hasSessions(); - } - - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { - const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId); - if (!model) { - throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`); - } - - const existingRaw: IChatTransfer2[] = this.storageService.getObject(TransferredGlobalChatKey, StorageScope.PROFILE, []); - existingRaw.push({ - chat: model.toJSON(), - timestampInMilliseconds: Date.now(), - toWorkspace: toWorkspace, - inputValue: transferredSessionData.inputValue, - location: transferredSessionData.location, - mode: transferredSessionData.mode, - }); - - this.storageService.store(TransferredGlobalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); - this.chatTransferService.addWorkspaceToTransferred(toWorkspace); - this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`); - } - - getChatStorageFolder(): URI { - return this._chatSessionStore.getChatStorageFolder(); - } - - logChatIndex(): void { - this._chatSessionStore.logIndex(); - } - - private toLocalSessionId(sessionResource: URI) { - const localSessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); - if (!localSessionId) { - throw new Error(`Invalid local chat session resource: ${sessionResource}`); - } - return localSessionId; - } -} diff --git a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/chatSessionStore.ts deleted file mode 100644 index 106924c254a..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatSessionStore.ts +++ /dev/null @@ -1,473 +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 { Sequencer } from '../../../../base/common/async.js'; -import { VSBuffer } from '../../../../base/common/buffer.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { joinPath } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; -import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; -import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, normalizeSerializableChatData } from './chatModel.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; - -const maxPersistedSessions = 25; - -const ChatIndexStorageKey = 'chat.ChatSessionStore.index'; -// const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex'; - -export class ChatSessionStore extends Disposable { - private readonly storageRoot: URI; - private readonly previousEmptyWindowStorageRoot: URI | undefined; - // private readonly transferredSessionStorageRoot: URI; - - private readonly storeQueue = new Sequencer(); - - private storeTask: Promise | undefined; - private shuttingDown = false; - - constructor( - @IFileService private readonly fileService: IFileService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @ILogService private readonly logService: ILogService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IStorageService private readonly storageService: IStorageService, - @ILifecycleService private readonly lifecycleService: ILifecycleService, - @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, - ) { - super(); - - const workspace = this.workspaceContextService.getWorkspace(); - const isEmptyWindow = !workspace.configuration && workspace.folders.length === 0; - const workspaceId = this.workspaceContextService.getWorkspace().id; - this.storageRoot = isEmptyWindow ? - joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, 'emptyWindowChatSessions') : - joinPath(this.environmentService.workspaceStorageHome, workspaceId, 'chatSessions'); - - this.previousEmptyWindowStorageRoot = isEmptyWindow ? - joinPath(this.environmentService.workspaceStorageHome, 'no-workspace', 'chatSessions') : - undefined; - - // TODO tmpdir - // this.transferredSessionStorageRoot = joinPath(this.environmentService.workspaceStorageHome, 'transferredChatSessions'); - - this._register(this.lifecycleService.onWillShutdown(e => { - this.shuttingDown = true; - if (!this.storeTask) { - return; - } - - e.join(this.storeTask, { - id: 'join.chatSessionStore', - label: localize('join.chatSessionStore', "Saving chat history") - }); - })); - } - - async storeSessions(sessions: ChatModel[]): Promise { - if (this.shuttingDown) { - // Don't start this task if we missed the chance to block shutdown - return; - } - - try { - this.storeTask = this.storeQueue.queue(async () => { - try { - await Promise.all(sessions.map(session => this.writeSession(session))); - await this.trimEntries(); - await this.flushIndex(); - } catch (e) { - this.reportError('storeSessions', 'Error storing chat sessions', e); - } - }); - await this.storeTask; - } finally { - this.storeTask = undefined; - } - } - - // async storeTransferSession(transferData: IChatTransfer, session: ISerializableChatData): Promise { - // try { - // const content = JSON.stringify(session, undefined, 2); - // await this.fileService.writeFile(this.transferredSessionStorageRoot, VSBuffer.fromString(content)); - // } catch (e) { - // this.reportError('sessionWrite', 'Error writing chat session', e); - // return; - // } - - // const index = this.getTransferredSessionIndex(); - // index[transferData.toWorkspace.toString()] = transferData; - // try { - // this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); - // } catch (e) { - // this.reportError('storeTransferSession', 'Error storing chat transfer session', e); - // } - // } - - // private getTransferredSessionIndex(): IChatTransferIndex { - // try { - // const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {}); - // return data; - // } catch (e) { - // this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e); - // return {}; - // } - // } - - private async writeSession(session: ChatModel | ISerializableChatData): Promise { - try { - const index = this.internalGetIndex(); - const storageLocation = this.getStorageLocation(session.sessionId); - const content = JSON.stringify(session, undefined, 2); - await this.fileService.writeFile(storageLocation, VSBuffer.fromString(content)); - - // Write succeeded, update index - index.entries[session.sessionId] = getSessionMetadata(session); - } catch (e) { - this.reportError('sessionWrite', 'Error writing chat session', e); - } - } - - private async flushIndex(): Promise { - const index = this.internalGetIndex(); - try { - this.storageService.store(ChatIndexStorageKey, index, this.getIndexStorageScope(), StorageTarget.MACHINE); - } catch (e) { - // Only if JSON.stringify fails, AFAIK - this.reportError('indexWrite', 'Error writing index', e); - } - } - - private getIndexStorageScope(): StorageScope { - const workspace = this.workspaceContextService.getWorkspace(); - const isEmptyWindow = !workspace.configuration && workspace.folders.length === 0; - return isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE; - } - - private async trimEntries(): Promise { - const index = this.internalGetIndex(); - const entries = Object.entries(index.entries) - .sort((a, b) => b[1].lastMessageDate - a[1].lastMessageDate) - .map(([id]) => id); - - if (entries.length > maxPersistedSessions) { - const entriesToDelete = entries.slice(maxPersistedSessions); - for (const entry of entriesToDelete) { - delete index.entries[entry]; - } - - this.logService.trace(`ChatSessionStore: Trimmed ${entriesToDelete.length} old chat sessions from index`); - } - } - - private async internalDeleteSession(sessionId: string): Promise { - const index = this.internalGetIndex(); - if (!index.entries[sessionId]) { - return; - } - - const storageLocation = this.getStorageLocation(sessionId); - try { - await this.fileService.del(storageLocation); - } catch (e) { - if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { - this.reportError('sessionDelete', 'Error deleting chat session', e); - } - } finally { - delete index.entries[sessionId]; - } - } - - hasSessions(): boolean { - return Object.keys(this.internalGetIndex().entries).length > 0; - } - - isSessionEmpty(sessionId: string): boolean { - const index = this.internalGetIndex(); - return index.entries[sessionId]?.isEmpty ?? true; - } - - async deleteSession(sessionId: string): Promise { - await this.storeQueue.queue(async () => { - await this.internalDeleteSession(sessionId); - await this.flushIndex(); - }); - } - - async clearAllSessions(): Promise { - await this.storeQueue.queue(async () => { - const index = this.internalGetIndex(); - const entries = Object.keys(index.entries); - this.logService.info(`ChatSessionStore: Clearing ${entries.length} chat sessions`); - await Promise.all(entries.map(entry => this.internalDeleteSession(entry))); - await this.flushIndex(); - }); - } - - public async setSessionTitle(sessionId: string, title: string): Promise { - await this.storeQueue.queue(async () => { - const index = this.internalGetIndex(); - if (index.entries[sessionId]) { - index.entries[sessionId].title = title; - } - }); - } - - private reportError(reasonForTelemetry: string, message: string, error?: Error): void { - this.logService.error(`ChatSessionStore: ` + message, toErrorMessage(error)); - - const fileOperationReason = error && toFileOperationResult(error); - type ChatSessionStoreErrorData = { - reason: string; - fileOperationReason: number; - // error: Error; - }; - type ChatSessionStoreErrorClassification = { - owner: 'roblourens'; - comment: 'Detect issues related to managing chat sessions'; - reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; - fileOperationReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'An error code from the file service' }; - // error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; - }; - this.telemetryService.publicLog2('chatSessionStoreError', { - reason: reasonForTelemetry, - fileOperationReason: fileOperationReason ?? -1 - }); - } - - private indexCache: IChatSessionIndexData | undefined; - private internalGetIndex(): IChatSessionIndexData { - if (this.indexCache) { - return this.indexCache; - } - - const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); - if (!data) { - this.indexCache = { version: 1, entries: {} }; - return this.indexCache; - } - - try { - const index = JSON.parse(data) as unknown; - if (isChatSessionIndex(index)) { - // Success - this.indexCache = index; - } else { - this.reportError('invalidIndexFormat', `Invalid index format: ${data}`); - this.indexCache = { version: 1, entries: {} }; - } - - return this.indexCache; - } catch (e) { - // Only if JSON.parse fails - this.reportError('invalidIndexJSON', `Index corrupt: ${data}`, e); - this.indexCache = { version: 1, entries: {} }; - return this.indexCache; - } - } - - async getIndex(): Promise { - return this.storeQueue.queue(async () => { - return this.internalGetIndex().entries; - }); - } - - logIndex(): void { - const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); - this.logService.info('ChatSessionStore index: ', data); - } - - async migrateDataIfNeeded(getInitialData: () => ISerializableChatsData | undefined): Promise { - await this.storeQueue.queue(async () => { - const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); - const needsMigrationFromStorageService = !data; - if (needsMigrationFromStorageService) { - const initialData = getInitialData(); - if (initialData) { - await this.migrate(initialData); - } - } - }); - } - - private async migrate(initialData: ISerializableChatsData): Promise { - const numSessions = Object.keys(initialData).length; - this.logService.info(`ChatSessionStore: Migrating ${numSessions} chat sessions from storage service to file system`); - - await Promise.all(Object.values(initialData).map(async session => { - await this.writeSession(session); - })); - - await this.flushIndex(); - } - - public async readSession(sessionId: string): Promise { - return await this.storeQueue.queue(async () => { - let rawData: string | undefined; - const storageLocation = this.getStorageLocation(sessionId); - try { - rawData = (await this.fileService.readFile(storageLocation)).value.toString(); - } catch (e) { - this.reportError('sessionReadFile', `Error reading chat session file ${sessionId}`, e); - - if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) { - rawData = await this.readSessionFromPreviousLocation(sessionId); - } - - if (!rawData) { - return undefined; - } - } - - try { - // TODO Copied from ChatService.ts, cleanup - const session: ISerializableChatDataIn = revive(JSON.parse(rawData)); // Revive serialized URIs in session data - // Revive serialized markdown strings in response data - for (const request of session.requests) { - if (Array.isArray(request.response)) { - request.response = request.response.map((response) => { - if (typeof response === 'string') { - return new MarkdownString(response); - } - return response; - }); - } else if (typeof request.response === 'string') { - request.response = [new MarkdownString(request.response)]; - } - } - - return normalizeSerializableChatData(session); - } catch (err) { - this.reportError('malformedSession', `Malformed session data in ${storageLocation.fsPath}: [${rawData.substring(0, 20)}${rawData.length > 20 ? '...' : ''}]`, err); - return undefined; - } - }); - } - - private async readSessionFromPreviousLocation(sessionId: string): Promise { - let rawData: string | undefined; - - if (this.previousEmptyWindowStorageRoot) { - const storageLocation2 = joinPath(this.previousEmptyWindowStorageRoot, `${sessionId}.json`); - try { - rawData = (await this.fileService.readFile(storageLocation2)).value.toString(); - this.logService.info(`ChatSessionStore: Read chat session ${sessionId} from previous location`); - } catch (e) { - this.reportError('sessionReadFile', `Error reading chat session file ${sessionId} from previous location`, e); - return undefined; - } - } - - return rawData; - } - - private getStorageLocation(chatSessionId: string): URI { - return joinPath(this.storageRoot, `${chatSessionId}.json`); - } - - public getChatStorageFolder(): URI { - return this.storageRoot; - } -} - -interface IChatSessionEntryMetadata { - sessionId: string; - title: string; - lastMessageDate: number; - isImported?: boolean; - initialLocation?: ChatAgentLocation; - - /** - * This only exists because the migrated data from the storage service had empty sessions persisted, and it's impossible to know which ones are - * currently in use. Now, `clearSession` deletes empty sessions, so old ones shouldn't take up space in the store anymore, but we still need to - * filter the old ones out of history. - */ - isEmpty?: boolean; -} - -function isChatSessionEntryMetadata(obj: unknown): obj is IChatSessionEntryMetadata { - return ( - !!obj && - typeof obj === 'object' && - typeof (obj as IChatSessionEntryMetadata).sessionId === 'string' && - typeof (obj as IChatSessionEntryMetadata).title === 'string' && - typeof (obj as IChatSessionEntryMetadata).lastMessageDate === 'number' - ); -} - -export type IChatSessionIndex = Record; - -interface IChatSessionIndexData { - version: 1; - entries: IChatSessionIndex; -} - -// TODO if we update the index version: -// Don't throw away index when moving backwards in VS Code version. Try to recover it. But this scenario is hard. -function isChatSessionIndex(data: unknown): data is IChatSessionIndexData { - if (typeof data !== 'object' || data === null) { - return false; - } - - const index = data as IChatSessionIndexData; - if (index.version !== 1) { - return false; - } - - if (typeof index.entries !== 'object' || index.entries === null) { - return false; - } - - for (const key in index.entries) { - if (!isChatSessionEntryMetadata(index.entries[key])) { - return false; - } - } - - return true; -} - -function getSessionMetadata(session: ChatModel | ISerializableChatData): IChatSessionEntryMetadata { - const title = session.customTitle || (session instanceof ChatModel ? session.title : undefined); - - return { - sessionId: session.sessionId, - title: title || localize('newChat', "New Chat"), - lastMessageDate: session.lastMessageDate, - isImported: session.isImported, - initialLocation: session.initialLocation, - isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0 - }; -} - -export interface IChatTransfer { - toWorkspace: URI; - timestampInMilliseconds: number; - inputValue: string; - location: ChatAgentLocation; - mode: ChatModeKind; -} - -export interface IChatTransfer2 extends IChatTransfer { - chat: ISerializableChatData; -} - -// type IChatTransferDto = Dto; - -/** - * Map of destination workspace URI to chat transfer data - */ -// type IChatTransferIndex = Record; diff --git a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts index 49034d91ca7..ac53b31a898 100644 --- a/src/vs/workbench/contrib/chat/common/chatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/common/chatSessionsService.ts @@ -4,23 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; +import { Event, IWaitUntil } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IEditableData } from '../../../common/views.js'; -import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './chatAgents.js'; -import { IChatEditingSession } from './chatEditingService.js'; -import { IChatRequestVariableData } from './chatModel.js'; -import { IChatProgress } from './chatService.js'; +import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from './participants/chatAgents.js'; +import { IChatEditingSession } from './editing/chatEditingService.js'; +import { IChatModel, IChatRequestVariableData, ISerializableChatModelInputState } from './model/chatModel.js'; +import { IChatProgress, IChatService, IChatSessionTiming } from './chatService/chatService.js'; export const enum ChatSessionStatus { Failed = 0, Completed = 1, - InProgress = 2 + InProgress = 2, + NeedsInput = 3 } export interface IChatSessionCommandContribution { @@ -32,14 +32,40 @@ export interface IChatSessionCommandContribution { export interface IChatSessionProviderOptionItem { id: string; name: string; + description?: string; + locked?: boolean; + icon?: ThemeIcon; + default?: boolean; // [key: string]: any; } +export interface IChatSessionProviderOptionGroupCommand { + command: string; + title: string; + tooltip?: string; + arguments?: unknown[]; +} + export interface IChatSessionProviderOptionGroup { id: string; name: string; description?: string; items: IChatSessionProviderOptionItem[]; + searchable?: boolean; + onSearch?: (query: string, token: CancellationToken) => Thenable; + /** + * A context key expression that controls visibility of this option group picker. + * When specified, the picker is only visible when the expression evaluates to true. + * The expression can reference other option group values via `chatSessionOption.`. + * Example: `"chatSessionOption.models == 'gpt-4'"` + */ + when?: string; + icon?: ThemeIcon; + /** + * Custom commands to show in the option group's picker UI. + * These will be shown in a separate section at the end of the picker. + */ + commands?: IChatSessionProviderOptionGroupCommand[]; } export interface IChatSessionsExtensionPoint { @@ -57,28 +83,51 @@ export interface IChatSessionsExtensionPoint { readonly inputPlaceholder?: string; readonly capabilities?: IChatAgentAttachmentCapabilities; readonly commands?: IChatSessionCommandContribution[]; + readonly canDelegate?: boolean; + /** + * When set, the chat session will show a filtered mode picker with custom agents + * that have a matching `target` property. This enables contributed chat sessions + * to reuse the standard agent/mode dropdown with filtered custom agents. + * Custom agents without a `target` property are also shown in all filtered lists + */ + readonly customAgentTarget?: string; } + export interface IChatSessionItem { - /** @deprecated Use {@link resource} instead */ - id: string; resource: URI; label: string; iconPath?: ThemeIcon; + badge?: string | IMarkdownString; description?: string | IMarkdownString; status?: ChatSessionStatus; tooltip?: string | IMarkdownString; - timing: { - startTime: number; - endTime?: number; - }; - statistics?: { + timing: IChatSessionTiming; + changes?: { + files: number; insertions: number; deletions: number; - }; + } | readonly IChatSessionFileChange[] | readonly IChatSessionFileChange2[]; + archived?: boolean; + metadata?: { readonly [key: string]: unknown }; +} + +export interface IChatSessionFileChange { + modifiedUri: URI; + originalUri?: URI; + insertions: number; + deletions: number; +} +export interface IChatSessionFileChange2 { + readonly uri: URI; + readonly originalUri?: URI; + readonly modifiedUri?: URI; + readonly insertions: number; + readonly deletions: number; } export type IChatSessionHistoryItem = { + id?: string; type: 'request'; prompt: string; participant: string; @@ -95,6 +144,11 @@ export type IChatSessionHistoryItem = { */ export const localChatSessionType = 'local'; +/** + * The option ID used for selecting the agent in chat sessions. + */ +export const agentOptionId = 'agent'; + export interface IChatSession extends IDisposable { readonly onWillDispose: Event; @@ -104,9 +158,9 @@ export interface IChatSession extends IDisposable { /** * Session options as key-value pairs. Keys correspond to option group IDs (e.g., 'models', 'subagents') - * and values are the selected option item IDs. + * and values are either the selected option item IDs (string) or full option items (for locked state). */ - readonly options?: Record; + readonly options?: Record; readonly progressObs?: IObservable; readonly isCompleteObs?: IObservable; @@ -115,11 +169,15 @@ export interface IChatSession extends IDisposable { /** * Editing session transferred from a previously-untitled chat session in `onDidCommitChatSessionItem`. */ - initialEditingSession?: IChatEditingSession; + transferredState?: { + editingSession: IChatEditingSession | undefined; + inputState: ISerializableChatModelInputState | undefined; + }; requestHandler?: ( request: IChatAgentRequest, progress: (progress: IChatProgress[]) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any history: any[], // TODO: Nail down types token: CancellationToken ) => Promise; @@ -129,20 +187,20 @@ export interface IChatSessionItemProvider { readonly chatSessionType: string; readonly onDidChangeChatSessionItems: Event; provideChatSessionItems(token: CancellationToken): Promise; - provideNewChatSessionItem?(options: { - request: IChatAgentRequest; - metadata?: any; - }, token: CancellationToken): Promise; } export interface IChatSessionContentProvider { provideChatSessionContent(sessionResource: URI, token: CancellationToken): Promise; } -export type SessionOptionsChangedCallback = (sessionResource: URI, updates: ReadonlyArray<{ - optionId: string; - value: string; -}>) => Promise; +/** + * Event fired when session options need to be sent to the extension. + * Extends IWaitUntil to allow listeners to register async work that will be awaited. + */ +export interface IChatSessionOptionsWillNotifyExtensionEvent extends IWaitUntil { + readonly sessionResource: URI; + readonly updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>; +} export interface IChatSessionsService { readonly _serviceBrand: undefined; @@ -154,9 +212,10 @@ export interface IChatSessionsService { readonly onDidChangeAvailability: Event; readonly onDidChangeInProgress: Event; + getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined; + registerChatSessionItemProvider(provider: IChatSessionItemProvider): IDisposable; - hasChatSessionItemProvider(chatSessionType: string): Promise; - getAllChatSessionItemProviders(): IChatSessionItemProvider[]; + activateChatSessionItemProvider(chatSessionType: string): Promise; getAllChatSessionContributions(): IChatSessionsExtensionPoint[]; getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined; @@ -166,13 +225,9 @@ export interface IChatSessionsService { /** * Get the list of chat session items grouped by session type. + * @param providerTypeFilter If specified, only returns items from the given providers. If undefined, returns items from all providers. */ - getAllChatSessionItems(token: CancellationToken): Promise>; - - getNewChatSessionItem(chatSessionType: string, options: { - request: IChatAgentRequest; - metadata?: any; - }, token: CancellationToken): Promise; + getChatSessionItems(providerTypeFilter: readonly string[] | undefined, token: CancellationToken): Promise>; reportInProgress(chatSessionType: string, count: number): void; getInProgress(): { displayName: string; count: number }[]; @@ -191,24 +246,48 @@ export interface IChatSessionsService { getOrCreateChatSession(sessionResource: URI, token: CancellationToken): Promise; hasAnySessionOptions(sessionResource: URI): boolean; - getSessionOption(sessionResource: URI, optionId: string): string | undefined; - setSessionOption(sessionResource: URI, optionId: string, value: string): boolean; + getSessionOption(sessionResource: URI, optionId: string): string | IChatSessionProviderOptionItem | undefined; + setSessionOption(sessionResource: URI, optionId: string, value: string | IChatSessionProviderOptionItem): boolean; + + /** + * Fired when options for a chat session change. + */ + onDidChangeSessionOptions: Event; /** * Get the capabilities for a specific session type */ getCapabilitiesForSessionType(chatSessionType: string): IChatAgentAttachmentCapabilities | undefined; + /** + * Get the customAgentTarget for a specific session type. + * When set, the mode picker should show filtered custom agents matching this target. + */ + getCustomAgentTargetForSessionType(chatSessionType: string): string | undefined; + + onDidChangeOptionGroups: Event; + getOptionGroupsForSessionType(chatSessionType: string): IChatSessionProviderOptionGroup[] | undefined; setOptionGroupsForSessionType(chatSessionType: string, handle: number, optionGroups?: IChatSessionProviderOptionGroup[]): void; - setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void; - notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise; + /** + * Event fired when session options change and need to be sent to the extension. + * MainThreadChatSessions subscribes to this to forward changes to the extension host. + * Uses IWaitUntil pattern to allow listeners to register async work. + */ + readonly onRequestNotifyExtension: Event; + notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise; - // Editable session support - setEditableSession(sessionResource: URI, data: IEditableData | null): Promise; - getEditableData(sessionResource: URI): IEditableData | undefined; - isEditable(sessionResource: URI): boolean; - // #endregion + registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable; + getInProgressSessionDescription(chatModel: IChatModel): string | undefined; +} + +export function isSessionInProgressStatus(state: ChatSessionStatus): boolean { + return state === ChatSessionStatus.InProgress || state === ChatSessionStatus.NeedsInput; +} + +export function isIChatSessionFileChange2(obj: unknown): obj is IChatSessionFileChange2 { + const candidate = obj as IChatSessionFileChange2; + return candidate && candidate.uri instanceof URI && typeof candidate.insertions === 'number' && typeof candidate.deletions === 'number'; } export const IChatSessionsService = createDecorator('chatSessionsService'); diff --git a/src/vs/workbench/contrib/chat/common/chatUri.ts b/src/vs/workbench/contrib/chat/common/chatUri.ts deleted file mode 100644 index 9a599351f30..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatUri.ts +++ /dev/null @@ -1,75 +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 { encodeBase64, VSBuffer, decodeBase64 } from '../../../../base/common/buffer.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; -import { localChatSessionType } from './chatSessionsService.js'; - -type ChatSessionIdentifier = { - readonly chatSessionType: string; - readonly sessionId: string; -}; - - -export namespace LocalChatSessionUri { - - export const scheme = Schemas.vscodeLocalChatSession; - - export function forSession(sessionId: string): URI { - return forChatSessionTypeAndId(localChatSessionType, sessionId); - } - - export function parseLocalSessionId(resource: URI): string | undefined { - const parsed = parse(resource); - return parsed?.chatSessionType === localChatSessionType ? parsed.sessionId : undefined; - } - - /** - * @deprecated Does not support non-local sessions - */ - export function forChatSessionTypeAndId(chatSessionType: string, sessionId: string): URI { - const encodedId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(sessionId)), false, true); - // TODO: Do we need to encode the authority too? - return URI.from({ scheme, authority: chatSessionType, path: '/' + encodedId }); - } - - /** - * @deprecated Legacy parser that supports non-local sessions. - */ - export function parse(resource: URI): ChatSessionIdentifier | undefined { - if (resource.scheme !== scheme) { - return undefined; - } - - if (!resource.authority) { - return undefined; - } - - const parts = resource.path.split('/'); - if (parts.length !== 2) { - return undefined; - } - - const chatSessionType = resource.authority; - const decodedSessionId = decodeBase64(parts[1]); - return { chatSessionType, sessionId: new TextDecoder().decode(decodedSessionId.buffer) }; - } -} - -/** - * Converts a chat session resource URI to a string ID. - * - * This exists mainly for backwards compatibility with existing code that uses string IDs in telemetry and storage. - */ -export function chatSessionResourceToId(resource: URI): string { - // If we have a local session, prefer using just the id part - const localId = LocalChatSessionUri.parseLocalSessionId(resource); - if (localId) { - return localId; - } - - return resource.toString(); -} diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts deleted file mode 100644 index 1df89127dca..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatModel } from './chatModel.js'; -import { IChatContentReference, IChatProgressMessage } from './chatService.js'; -import { IDiagnosticVariableEntryFilterData, StringChatContextValue } from './chatVariableEntries.js'; -import { IToolAndToolSetEnablementMap } from './languageModelToolsService.js'; - -export interface IChatVariableData { - id: string; - name: string; - icon?: ThemeIcon; - fullName?: string; - description: string; - modelDescription?: string; - canTakeArgument?: boolean; -} - -export interface IChatRequestProblemsVariable { - id: 'vscode.problems'; - filter: IDiagnosticVariableEntryFilterData; -} - -export const isIChatRequestProblemsVariable = (obj: unknown): obj is IChatRequestProblemsVariable => - typeof obj === 'object' && obj !== null && 'id' in obj && (obj as IChatRequestProblemsVariable).id === 'vscode.problems'; - -export type IChatRequestVariableValue = string | URI | Location | Uint8Array | IChatRequestProblemsVariable | StringChatContextValue | unknown; - -export type IChatVariableResolverProgress = - | IChatContentReference - | IChatProgressMessage; - -export interface IChatVariableResolver { - (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; -} - -export const IChatVariablesService = createDecorator('IChatVariablesService'); - -export interface IChatVariablesService { - _serviceBrand: undefined; - getDynamicVariables(sessionResource: URI): ReadonlyArray; - getSelectedToolAndToolSets(sessionResource: URI): IToolAndToolSetEnablementMap; -} - -export interface IDynamicVariable { - range: IRange; - id: string; - fullName?: string; - icon?: ThemeIcon; - modelDescription?: string; - isFile?: boolean; - isDirectory?: boolean; - data: IChatRequestVariableValue; -} diff --git a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.ts deleted file mode 100644 index 4ed046e62a7..00000000000 --- a/src/vs/workbench/contrib/chat/common/chatWidgetHistoryService.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 { Emitter, Event } from '../../../../base/common/event.js'; -import { URI } from '../../../../base/common/uri.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { Memento } from '../../../common/memento.js'; -import { ModifiedFileEntryState } from './chatEditingService.js'; -import { CHAT_PROVIDER_ID } from './chatParticipantContribTypes.js'; -import { IChatRequestVariableEntry } from './chatVariableEntries.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; - -export interface IChatHistoryEntry { - text: string; - state?: IChatInputState; -} - -/** The collected input state of ChatWidget contribs + attachments */ -export interface IChatInputState { - [key: string]: any; - chatContextAttachments?: ReadonlyArray; - chatWorkingSet?: ReadonlyArray<{ uri: URI; state: ModifiedFileEntryState }>; - - /** - * This should be a mode id (ChatMode | string). - * { id: string } is the old IChatMode. This is deprecated but may still be in persisted data. - */ - chatMode?: ChatModeKind | string | { id: string }; -} - -export const IChatWidgetHistoryService = createDecorator('IChatWidgetHistoryService'); -export interface IChatWidgetHistoryService { - _serviceBrand: undefined; - - readonly onDidClearHistory: Event; - - clearHistory(): void; - getHistory(location: ChatAgentLocation): IChatHistoryEntry[]; - saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void; -} - -interface IChatHistory { - history?: { [providerId: string]: IChatHistoryEntry[] }; -} - -export const ChatInputHistoryMaxEntries = 40; - -export class ChatWidgetHistoryService implements IChatWidgetHistoryService { - _serviceBrand: undefined; - - private memento: Memento; - private viewState: IChatHistory; - - private readonly _onDidClearHistory = new Emitter(); - readonly onDidClearHistory: Event = this._onDidClearHistory.event; - - constructor( - @IStorageService storageService: IStorageService - ) { - this.memento = new Memento('interactive-session', storageService); - const loadedState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); - for (const provider in loadedState.history) { - // Migration from old format - loadedState.history[provider] = loadedState.history[provider].map(entry => typeof entry === 'string' ? { text: entry } : entry); - } - - this.viewState = loadedState; - } - - getHistory(location: ChatAgentLocation): IChatHistoryEntry[] { - const key = this.getKey(location); - return this.viewState.history?.[key] ?? []; - } - - private getKey(location: ChatAgentLocation): string { - // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. - return location === ChatAgentLocation.Chat ? CHAT_PROVIDER_ID : location; - } - - saveHistory(location: ChatAgentLocation, history: IChatHistoryEntry[]): void { - if (!this.viewState.history) { - this.viewState.history = {}; - } - - const key = this.getKey(location); - this.viewState.history[key] = history.slice(-ChatInputHistoryMaxEntries); - this.memento.saveMemento(); - } - - clearHistory(): void { - this.viewState.history = {}; - this.memento.saveMemento(); - this._onDidClearHistory.fire(); - } -} diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index aab321d6abb..13056d6991a 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -9,22 +9,41 @@ import { ServicesAccessor } from '../../../../platform/instantiation/common/inst import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; export enum ChatConfiguration { + AIDisabled = 'chat.disableAIFeatures', AgentEnabled = 'chat.agent.enabled', + AgentStatusEnabled = 'chat.agentsControl.enabled', + EditorAssociations = 'chat.editorAssociations', + UnifiedAgentsBar = 'chat.unifiedAgentsBar.enabled', + AgentSessionProjectionEnabled = 'chat.agentSessionProjection.enabled', + EditModeHidden = 'chat.editMode.hidden', + AlternativeToolAction = 'chat.alternativeToolAction.enabled', Edits2Enabled = 'chat.edits2.enabled', ExtensionToolsEnabled = 'chat.extensionTools.enabled', + RepoInfoEnabled = 'chat.repoInfo.enabled', EditRequests = 'chat.editRequests', + InlineReferencesStyle = 'chat.inlineReferences.style', GlobalAutoApprove = 'chat.tools.global.autoApprove', AutoApproveEdits = 'chat.tools.edits.autoApprove', + AutoApprovedUrls = 'chat.tools.urls.autoApprove', + EligibleForAutoApproval = 'chat.tools.eligibleForAutoApproval', EnableMath = 'chat.math.enabled', CheckpointsEnabled = 'chat.checkpoints.enabled', - AgentSessionsViewLocation = 'chat.agentSessionsViewLocation', ThinkingStyle = 'chat.agent.thinkingStyle', + ThinkingGenerateTitles = 'chat.agent.thinking.generateTitles', + TerminalToolsInThinking = 'chat.agent.thinking.terminalTools', + AutoExpandToolFailures = 'chat.tools.autoExpandFailures', TodosShowWidget = 'chat.tools.todos.showWidget', - UseCloudButtonV2 = 'chat.useCloudButtonV2', - ShowAgentSessionsViewDescription = 'chat.showAgentSessionsViewDescription', - EmptyStateHistoryEnabled = 'chat.emptyState.history.enabled', NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', + ChatViewSessionsEnabled = 'chat.viewSessions.enabled', + ChatViewSessionsGrouping = 'chat.viewSessions.grouping', + ChatViewSessionsOrientation = 'chat.viewSessions.orientation', + ChatViewProgressBadgeEnabled = 'chat.viewProgressBadge.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', + ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', + RestoreLastPanelSession = 'chat.restoreLastPanelSession', + ExitAfterDelegation = 'chat.exitAfterDelegation', + AgentsControlClickBehavior = 'chat.agentsControl.clickBehavior', + ExplainChangesEnabled = 'chat.editing.explainChanges.enabled', } /** @@ -58,6 +77,18 @@ export enum ThinkingDisplayMode { FixedScrolling = 'fixedScrolling', } +export enum CollapsedToolsDisplayMode { + Off = 'off', + WithThinking = 'withThinking', + Always = 'always', +} + +export enum AgentsControlClickBehavior { + Default = 'default', + Cycle = 'cycle', + Focus = 'focus', +} + export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook' | 'editing-session'; export enum ChatAgentLocation { @@ -118,7 +149,6 @@ export function isSupportedChatFileScheme(accessor: ServicesAccessor, scheme: st return true; } -export const AGENT_SESSIONS_VIEWLET_ID = 'workbench.view.chat.sessions'; // TODO@bpasero clear once settled export const MANAGE_CHAT_COMMAND_ID = 'workbench.action.chat.manage'; export const ChatEditorTitleMaxLength = 30; diff --git a/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts new file mode 100644 index 00000000000..7c6b273acd3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/contextContrib/chatContext.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; + +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; + +export interface IChatContextItem { + icon: ThemeIcon; + label: string; + modelDescription?: string; + tooltip?: IMarkdownString; + handle: number; + value?: string; + command?: { + id: string; + }; +} + +export interface IChatWorkspaceContextProvider { + provideWorkspaceChatContext(token: CancellationToken): Promise; +} + +export interface IChatExplicitContextProvider { + provideChatContext(token: CancellationToken): Promise; + resolveChatContext(context: IChatContextItem, token: CancellationToken): Promise; +} + +export interface IChatResourceContextProvider { + provideChatContext(resource: URI, withValue: boolean, token: CancellationToken): Promise; + resolveChatContext(context: IChatContextItem, token: CancellationToken): Promise; +} diff --git a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts b/src/vs/workbench/contrib/chat/common/editing/chatCodeMapperService.ts similarity index 82% rename from src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts rename to src/vs/workbench/contrib/chat/common/editing/chatCodeMapperService.ts index 749850a7aee..438168778c5 100644 --- a/src/vs/workbench/contrib/chat/common/chatCodeMapperService.ts +++ b/src/vs/workbench/contrib/chat/common/editing/chatCodeMapperService.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; export interface ICodeMapperResponse { textEdit: (resource: URI, textEdit: TextEdit[]) => void; @@ -25,7 +25,7 @@ export interface ICodeMapperRequest { readonly codeBlocks: ICodeMapperCodeBlock[]; readonly chatRequestId?: string; readonly chatRequestModel?: string; - readonly chatSessionId?: string; + readonly chatSessionResource?: URI; readonly location?: string; } diff --git a/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts new file mode 100644 index 00000000000..58b49b13fa5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts @@ -0,0 +1,470 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { decodeHex, encodeHex, VSBuffer } from '../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../../base/common/errors.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js'; +import { hasKey } from '../../../../../base/common/types.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../../editor/common/model.js'; +import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; +import { localize } from '../../../../../nls.js'; +import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IEditorPane } from '../../../../common/editor.js'; +import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatProgress, IChatWorkspaceEdit } from '../chatService/chatService.js'; +import { ChatModel, IChatRequestDisablement, IChatResponseModel } from '../model/chatModel.js'; +import { IChatAgentResult } from '../participants/chatAgents.js'; + +export const IChatEditingService = createDecorator('chatEditingService'); + +export interface IChatEditingService { + + _serviceBrand: undefined; + + startOrContinueGlobalEditingSession(chatModel: ChatModel): IChatEditingSession; + + getEditingSession(chatSessionResource: URI): IChatEditingSession | undefined; + + /** + * All editing sessions, sorted by recency, e.g the last created session comes first. + */ + readonly editingSessionsObs: IObservable; + + /** + * Creates a new short lived editing session + */ + createEditingSession(chatModel: ChatModel): IChatEditingSession; + + /** + * Creates an editing session with state transferred from the provided session. + */ + transferEditingSession(chatModel: ChatModel, session: IChatEditingSession): IChatEditingSession; +} + +export interface WorkingSetDisplayMetadata { + state: ModifiedFileEntryState; + description?: string; +} + +export interface IStreamingEdits { + pushText(edits: TextEdit[], isLastEdits: boolean): void; + pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void; + pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void; + /** Marks edits as done, idempotent */ + complete(): void; +} + +export interface IModifiedEntryTelemetryInfo { + readonly agentId: string | undefined; + readonly command: string | undefined; + readonly sessionResource: URI; + readonly requestId: string; + readonly result: IChatAgentResult | undefined; + readonly modelId: string | undefined; + readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined; + readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined; + readonly feature: 'sideBarChat' | 'inlineChat' | undefined; +} + +export interface ISnapshotEntry { + readonly resource: URI; + readonly languageId: string; + readonly snapshotUri: URI; + readonly original: string; + readonly current: string; + readonly state: ModifiedFileEntryState; + telemetryInfo: IModifiedEntryTelemetryInfo; + /** True if this entry represents a deleted file */ + readonly isDeleted?: boolean; +} + +export interface IChatEditingSession extends IDisposable { + readonly isGlobalEditingSession: boolean; + readonly chatSessionResource: URI; + readonly onDidDispose: Event; + readonly state: IObservable; + readonly entries: IObservable; + /** Requests disabled by undo/redo in the session */ + readonly requestDisablement: IObservable; + + show(previousChanges?: boolean): Promise; + accept(...uris: URI[]): Promise; + reject(...uris: URI[]): Promise; + getEntry(uri: URI): IModifiedFileEntry | undefined; + readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined; + + restoreSnapshot(requestId: string, stopId: string | undefined): Promise; + + /** + * Marks all edits to the given resources as agent edits until + * {@link stopExternalEdits} is called with the same ID. This is used for + * agents that make changes on-disk rather than streaming edits through the + * chat session. + */ + startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise; + stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise; + + /** + * Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop. + * @param uri File in the workspace + */ + getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined; + + getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise; + getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise; + + /** + * Will lead to this object getting disposed + */ + stop(clearState?: boolean): Promise; + + /** + * Starts making edits to the resource. + * @param resource URI that's being edited + * @param responseModel The response model making the edits + * @param inUndoStop The undo stop the edits will be grouped in + */ + startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits; + + /** + * Applies a workspace edit (file deletions, creations, renames). + * @param edit The workspace edit containing file operations + * @param responseModel The response model making the edit + * @param undoStopId The undo stop ID for this edit + */ + applyWorkspaceEdit(edit: IChatWorkspaceEdit, responseModel: IChatResponseModel, undoStopId: string): void; + + /** + * Gets the document diff of a change made to a URI between one undo stop and + * the next one. + * @returns The observable or undefined if there is no diff between the stops. + */ + getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable | undefined; + + /** + * Gets the document diff of a change made to a URI between one request to another one. + * @returns The observable or undefined if there is no diff between the requests. + */ + getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable; + + /** + * Gets the diff of each file modified in this session, comparing the initial + * baseline to the current state. + */ + getDiffsForFilesInSession(): IObservable; + + /** + * Gets the diff of each file modified in the request. + */ + getDiffsForFilesInRequest(requestId: string): IObservable; + + /** + * Whether there are any edits made in the given request. + */ + hasEditsInRequest(requestId: string, reader?: IReader): boolean; + + /** + * Gets the aggregated diff stats for all files modified in this session. + */ + getDiffForSession(): IObservable; + + readonly canUndo: IObservable; + readonly canRedo: IObservable; + undoInteraction(): Promise; + redoInteraction(): Promise; + + /** + * Triggers generation of explanations for all modified files in the session. + */ + triggerExplanationGeneration(): Promise; + + /** + * Clears any active explanation generation. + */ + clearExplanations(): void; + + /** + * Whether explanations are currently being generated or displayed. + */ + hasExplanations(): boolean; +} + +export function chatEditingSessionIsReady(session: IChatEditingSession): Promise { + return new Promise(resolve => { + autorunSelfDisposable(reader => { + const state = session.state.read(reader); + if (state !== ChatEditingSessionState.Initial) { + reader.dispose(); + resolve(); + } + }); + }); +} + +export function editEntriesToMultiDiffData(entriesObs: IObservable): IChatMultiDiffData { + const multiDiffData = entriesObs.map(entries => ({ + title: localize('chatMultidiff.autoGenerated', 'Changes to {0} files', entries.length), + resources: entries.map(entry => ({ + originalUri: entry.originalURI, + modifiedUri: entry.modifiedURI, + goToFileUri: entry.modifiedURI, + added: entry.added, + removed: entry.removed, + })) + })); + + return { + kind: 'multiDiffData', + collapsed: true, + multiDiffData, + toJSON(): IChatMultiDiffDataSerialized { + return { + kind: 'multiDiffData', + collapsed: this.collapsed, + multiDiffData: multiDiffData.get(), + }; + } + }; +} + +export function awaitCompleteChatEditingDiff(diff: IObservable, token?: CancellationToken): Promise; +export function awaitCompleteChatEditingDiff(diff: IObservable, token?: CancellationToken): Promise; +export function awaitCompleteChatEditingDiff(diff: IObservable, token?: CancellationToken): Promise { + return new Promise((resolve, reject) => { + autorunSelfDisposable(reader => { + if (token) { + if (token.isCancellationRequested) { + reader.dispose(); + return reject(new CancellationError()); + } + reader.store.add(token.onCancellationRequested(() => { + reader.dispose(); + reject(new CancellationError()); + })); + } + + const current = diff.read(reader); + if (current instanceof Array) { + if (!current.some(c => c.isBusy)) { + reader.dispose(); + resolve(current); + } + } else if (!current.isBusy) { + reader.dispose(); + resolve(current); + } + }); + }); +} + +export interface IEditSessionDiffStats { + /** Added data (e.g. line numbers) to show in the UI */ + added: number; + /** Removed data (e.g. line numbers) to show in the UI */ + removed: number; +} + +export interface IEditSessionEntryDiff extends IEditSessionDiffStats { + /** LHS and RHS of a diff editor, if opened: */ + originalURI: URI; + modifiedURI: URI; + + /** Diff state information: */ + quitEarly: boolean; + identical: boolean; + + /** True if nothing else will be added to this diff. */ + isFinal: boolean; + + /** True if the diff is currently being computed or updated. */ + isBusy: boolean; +} + +export function emptySessionEntryDiff(originalURI: URI, modifiedURI: URI): IEditSessionEntryDiff { + return { + originalURI, + modifiedURI, + added: 0, + removed: 0, + quitEarly: false, + identical: false, + isFinal: false, + isBusy: false, + }; +} + +export const enum ModifiedFileEntryState { + Modified, + Accepted, + Rejected, +} + +/** + * Represents a part of a change + */ +export interface IModifiedFileEntryChangeHunk { + accept(): Promise; + reject(): Promise; +} + +export interface IModifiedFileEntryEditorIntegration extends IDisposable { + + /** + * The index of a change + */ + currentIndex: IObservable; + + /** + * Reveal the first (`true`) or last (`false`) change + */ + reveal(firstOrLast: boolean, preserveFocus?: boolean): void; + + /** + * Go to next change and increate `currentIndex` + * @param wrap When at the last, start over again or not + * @returns If it went next + */ + next(wrap: boolean): boolean; + + /** + * @see `next` + */ + previous(wrap: boolean): boolean; + + /** + * Enable the accessible diff viewer for this editor + */ + enableAccessibleDiffView(): void; + + /** + * Accept the change given or the nearest + * @param change An opaque change object + */ + acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; + + /** + * @see `acceptNearestChange` + */ + rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise; + + /** + * Toggle between diff-editor and normal editor + * @param change An opaque change object + * @param show Optional boolean to control if the diff should show + */ + toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise; +} + +export interface IModifiedFileEntry { + readonly entryId: string; + readonly originalURI: URI; + readonly modifiedURI: URI; + readonly isDeletion?: boolean; + + readonly lastModifyingRequestId: string; + + readonly state: IObservable; + readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>; + readonly lastModifyingResponse: IObservable; + readonly rewriteRatio: IObservable; + + readonly waitsForLastEdits: IObservable; + + accept(): Promise; + reject(): Promise; + + reviewMode: IObservable; + autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>; + enableReviewModeUntilSettled(): void; + + /** + * Number of changes for this file + */ + readonly changesCount: IObservable; + + /** + * Diff information for this entry + */ + readonly diffInfo?: IObservable; + + /** + * Number of lines added in this entry. + */ + readonly linesAdded?: IObservable; + + /** + * Number of lines removed in this entry + */ + readonly linesRemoved?: IObservable; + + getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration; + /** + * Gets the document diff info, waiting for any ongoing promises to flush. + */ + getDiffInfo?(): Promise; +} + +export interface IChatEditingSessionStream { + textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void; + notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void; +} + +export const enum ChatEditingSessionState { + Initial = 0, + StreamingEdits = 1, + Idle = 2, + Disposed = 3 +} + +export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source'; + +export const chatEditingWidgetFileStateContextKey = new RawContextKey('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget")); +export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)")); +export const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); +export const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); +export const inChatEditingSessionContextKey = new RawContextKey('inChatEditingSession', undefined); +export const hasUndecidedChatEditingResourceContextKey = new RawContextKey('hasUndecidedChatEditingResource', false); +export const hasAppliedChatEditsContextKey = new RawContextKey('hasAppliedChatEdits', false); +export const applyingChatEditsFailedContextKey = new RawContextKey('applyingChatEditsFailed', false); + +export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit'; +export const defaultChatEditingMaxFileLimit = 10; + +export const enum ChatEditKind { + Created, + Modified, + Deleted, +} + +export interface IChatEditingActionContext { + // The chat session that this editing session is associated with + sessionResource: URI; +} + +export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext { + return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true }); +} + +export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI { + return URI.from({ + scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, + authority: encodeHex(VSBuffer.fromString(session.chatSessionResource.toString())), + query: showPreviousChanges ? 'previous' : undefined, + }); +} + +export function parseChatMultiDiffUri(uri: URI): { chatSessionResource: URI; showPreviousChanges: boolean } { + const chatSessionResource = URI.parse(decodeHex(uri.authority).toString()); + const showPreviousChanges = uri.query === 'previous'; + + return { chatSessionResource, showPreviousChanges }; +} diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsConfirmationService.ts deleted file mode 100644 index c00132a8353..00000000000 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsConfirmationService.ts +++ /dev/null @@ -1,95 +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 { IDisposable } from '../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IQuickInputButton, IQuickTreeItem } from '../../../../platform/quickinput/common/quickInput.js'; -import { ConfirmedReason } from './chatService.js'; -import { IToolData, ToolDataSource } from './languageModelToolsService.js'; - -export interface ILanguageModelToolConfirmationActions { - /** Label for the action */ - label: string; - /** Action detail (e.g. tooltip) */ - detail?: string; - /** Show a separator before this action */ - divider?: boolean; - /** Selects this action. Resolves true if the action should be confirmed after selection */ - select(): Promise; -} - -export interface ILanguageModelToolConfirmationRef { - toolId: string; - source: ToolDataSource; - parameters: unknown; -} - -export interface ILanguageModelToolConfirmationActionProducer { - getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined; - getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined; - - /** Gets the selectable actions to take to memorize confirmation changes */ - getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[]; - getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[]; -} - -export interface ILanguageModelToolConfirmationContributionQuickTreeItem extends IQuickTreeItem { - onDidTriggerItemButton?(button: IQuickInputButton): void; - onDidChangeChecked?(checked: boolean): void; -} - -/** - * Type that can be registered to provide more specific confirmation - * actions for a specific tool. - */ -export type ILanguageModelToolConfirmationContribution = Partial & { - /** - * Gets items to be shown in the `manageConfirmationPreferences` quick tree. - * These are added under the tool's category. - */ - getManageActions?(): ILanguageModelToolConfirmationContributionQuickTreeItem[]; - - /** - * Defaults to true. If false, the "Always Allow" options will not be shown - * and _only_ your custom manage actions will be shown. - */ - canUseDefaultApprovals?: boolean; - - /** - * Reset all confirmation settings for this tool. - */ - reset?(): void; -}; - -/** - * Handles language model tool confirmation. - * - * - By default, all tools can have their confirmation preferences saved within - * a session, workspace, or globally. - * - Tools with ToolDataSource from an extension or MCP can have that entire - * source's preference saved within a session, workspace, or globally. - * - Contributable confirmations may also be registered for specific behaviors. - * - * Note: this interface MUST NOT depend in the ILanguageModelToolsService. - * The ILanguageModelToolsService depends on this service instead in order to - * call getPreConfirmAction/getPostConfirmAction. - */ -export interface ILanguageModelToolsConfirmationService extends ILanguageModelToolConfirmationActionProducer { - readonly _serviceBrand: undefined; - - /** Opens an IQuickTree to let the user manage their preferences. */ - manageConfirmationPreferences(tools: Readonly[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void; - - /** - * Registers a contribution that provides more specific confirmation logic - * for a tool, in addition to the default confirmation handling. - */ - registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable; - - /** Resets all tool and server confirmation preferences */ - resetToolAutoConfirmation(): void; -} - -export const ILanguageModelToolsConfirmationService = createDecorator('ILanguageModelToolsConfirmationService'); diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts deleted file mode 100644 index f7a17c11860..00000000000 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ /dev/null @@ -1,397 +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 { Separator } from '../../../../base/common/actions.js'; -import { VSBuffer } from '../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { derived, IObservable, IReader, ITransaction, ObservableSet } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { localize } from '../../../../nls.js'; -import { ContextKeyExpression } from '../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { ByteSize } from '../../../../platform/files/common/files.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IProgress } from '../../../../platform/progress/common/progress.js'; -import { UserSelectedTools } from './chatAgents.js'; -import { IVariableReference } from './chatModes.js'; -import { IChatExtensionsContent, IChatTodoListContent, IChatToolInputInvocationData, type IChatTerminalToolInvocationData } from './chatService.js'; -import { ChatRequestToolReferenceEntry } from './chatVariableEntries.js'; -import { LanguageModelPartAudience } from './languageModels.js'; -import { PromptElementJSON, stringifyPromptElementJSON } from './tools/promptTsxTypes.js'; - -export interface IToolData { - id: string; - source: ToolDataSource; - toolReferenceName?: string; - icon?: { dark: URI; light?: URI } | ThemeIcon; - when?: ContextKeyExpression; - tags?: string[]; - displayName: string; - userDescription?: string; - modelDescription: string; - inputSchema?: IJSONSchema; - canBeReferencedInPrompt?: boolean; - /** - * True if the tool runs in the (possibly remote) workspace, false if it runs - * on the host, undefined if known. - */ - runsInWorkspace?: boolean; - alwaysDisplayInputOutput?: boolean; - /** True if this tool might ask for pre-approval */ - canRequestPreApproval?: boolean; - /** True if this tool might ask for post-approval */ - canRequestPostApproval?: boolean; -} - -export interface IToolProgressStep { - readonly message: string | IMarkdownString | undefined; - /** 0-1 progress of the tool call */ - readonly progress?: number; -} - -export type ToolProgress = IProgress; - -export type ToolDataSource = - | { - type: 'extension'; - label: string; - extensionId: ExtensionIdentifier; - } - | { - type: 'mcp'; - label: string; - serverLabel: string | undefined; - instructions: string | undefined; - collectionId: string; - definitionId: string; - } - | { - type: 'user'; - label: string; - file: URI; - } - | { - type: 'internal'; - label: string; - } | { - type: 'external'; - label: string; - }; - -export namespace ToolDataSource { - - export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' }; - - /** External tools may not be contributed or invoked, but may be invoked externally and described in an IChatToolInvocationSerialized */ - export const External: ToolDataSource = { type: 'external', label: 'External' }; - - export function toKey(source: ToolDataSource): string { - switch (source.type) { - case 'extension': return `extension:${source.extensionId.value}`; - case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`; - case 'user': return `user:${source.file.toString()}`; - case 'internal': return 'internal'; - case 'external': return 'external'; - } - } - - export function equals(a: ToolDataSource, b: ToolDataSource): boolean { - return toKey(a) === toKey(b); - } - - export function classify(source: ToolDataSource): { readonly ordinal: number; readonly label: string } { - if (source.type === 'internal') { - return { ordinal: 1, label: localize('builtin', 'Built-In') }; - } else if (source.type === 'mcp') { - return { ordinal: 2, label: source.label }; - } else if (source.type === 'user') { - return { ordinal: 0, label: localize('user', 'User Defined') }; - } else { - return { ordinal: 3, label: source.label }; - } - } -} - -export interface IToolInvocation { - callId: string; - toolId: string; - parameters: Object; - tokenBudget?: number; - 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; - userSelectedTools?: UserSelectedTools; -} - -export interface IToolInvocationContext { - sessionId: string; -} - -export function isToolInvocationContext(obj: any): obj is IToolInvocationContext { - return typeof obj === 'object' && typeof obj.sessionId === 'string'; -} - -export interface IToolInvocationPreparationContext { - parameters: any; - chatRequestId?: string; - chatSessionId?: string; - chatInteractionId?: string; -} - -export type ToolInputOutputBase = { - /** Mimetype of the value, optional */ - mimeType?: string; - /** URI of the resource on the MCP server. */ - uri?: URI; - /** If true, this part came in as a resource reference rather than direct data. */ - asResource?: boolean; - /** Audience of the data part */ - audience?: LanguageModelPartAudience[]; -}; - -export type ToolInputOutputEmbedded = ToolInputOutputBase & { - type: 'embed'; - value: string; - /** If true, value is text. If false or not given, value is base64 */ - isText?: boolean; -}; - -export type ToolInputOutputReference = ToolInputOutputBase & { type: 'ref'; uri: URI }; - -export interface IToolResultInputOutputDetails { - readonly input: string; - readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[]; - readonly isError?: boolean; -} - -export interface IToolResultOutputDetails { - readonly output: { type: 'data'; mimeType: string; value: VSBuffer }; -} - -export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails { - return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output)); -} - -export function isToolResultOutputDetails(obj: any): obj is IToolResultOutputDetails { - return typeof obj === 'object' && typeof obj?.output === 'object' && typeof obj?.output?.mimeType === 'string' && obj?.output?.type === 'data'; -} - -export interface IToolResult { - content: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[]; - toolResultMessage?: string | IMarkdownString; - toolResultDetails?: Array | IToolResultInputOutputDetails | IToolResultOutputDetails; - toolResultError?: string; - toolMetadata?: unknown; - /** Whether to ask the user to confirm these tool results. Overrides {@link IToolConfirmationMessages.confirmResults}. */ - confirmResults?: boolean; -} - -export function toolContentToA11yString(part: IToolResult['content']) { - return part.map(p => { - switch (p.kind) { - case 'promptTsx': - return stringifyPromptTsxPart(p); - case 'text': - return p.value; - case 'data': - return localize('toolResultDataPartA11y', "{0} of {1} binary data", ByteSize.formatSize(p.value.data.byteLength), p.value.mimeType || 'unknown'); - } - }).join(', '); -} - -export function toolResultHasBuffers(result: IToolResult): boolean { - return result.content.some(part => part.kind === 'data'); -} - -export interface IToolResultPromptTsxPart { - kind: 'promptTsx'; - value: unknown; -} - -export function stringifyPromptTsxPart(part: IToolResultPromptTsxPart): string { - return stringifyPromptElementJSON(part.value as PromptElementJSON); -} - -export interface IToolResultTextPart { - kind: 'text'; - value: string; - audience?: LanguageModelPartAudience[]; -} - -export interface IToolResultDataPart { - kind: 'data'; - value: { - mimeType: string; - data: VSBuffer; - }; - audience?: LanguageModelPartAudience[]; -} - -export interface IToolConfirmationMessages { - /** Title for the confirmation. If set, the user will be asked to confirm execution of the tool */ - title?: string | IMarkdownString; - /** MUST be set if `title` is also set */ - message?: string | IMarkdownString; - disclaimer?: string | IMarkdownString; - allowAutoConfirm?: boolean; - terminalCustomActions?: ToolConfirmationAction[]; - /** If true, confirmation will be requested after the tool executes and before results are sent to the model */ - confirmResults?: boolean; -} - -export interface IToolConfirmationAction { - label: string; - disabled?: boolean; - tooltip?: string; - data: any; -} - -export type ToolConfirmationAction = IToolConfirmationAction | Separator; - -export enum ToolInvocationPresentation { - Hidden = 'hidden', - HiddenAfterComplete = 'hiddenAfterComplete' -} - -export interface IPreparedToolInvocation { - invocationMessage?: string | IMarkdownString; - pastTenseMessage?: string | IMarkdownString; - originMessage?: string | IMarkdownString; - confirmationMessages?: IToolConfirmationMessages; - presentation?: ToolInvocationPresentation; - toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent; -} - -export interface IToolImpl { - invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; - prepareToolInvocation?(context: IToolInvocationPreparationContext, token: CancellationToken): Promise; -} - -export type IToolAndToolSetEnablementMap = ReadonlyMap; - -export class ToolSet { - - protected readonly _tools = new ObservableSet(); - - protected readonly _toolSets = new ObservableSet(); - - /** - * A homogenous tool set only contains tools from the same source as the tool set itself - */ - readonly isHomogenous: IObservable; - - constructor( - readonly id: string, - readonly referenceName: string, - readonly icon: ThemeIcon, - readonly source: ToolDataSource, - readonly description?: string, - ) { - - this.isHomogenous = derived(r => { - return !Iterable.some(this._tools.observable.read(r), tool => !ToolDataSource.equals(tool.source, this.source)) - && !Iterable.some(this._toolSets.observable.read(r), toolSet => !ToolDataSource.equals(toolSet.source, this.source)); - }); - } - - addTool(data: IToolData, tx?: ITransaction): IDisposable { - this._tools.add(data, tx); - return toDisposable(() => { - this._tools.delete(data); - }); - } - - addToolSet(toolSet: ToolSet, tx?: ITransaction): IDisposable { - if (toolSet === this) { - return Disposable.None; - } - this._toolSets.add(toolSet, tx); - return toDisposable(() => { - this._toolSets.delete(toolSet); - }); - } - - getTools(r?: IReader): Iterable { - return Iterable.concat( - this._tools.observable.read(r), - ...Iterable.map(this._toolSets.observable.read(r), toolSet => toolSet.getTools(r)) - ); - } -} - - -export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); - -export type CountTokensCallback = (input: string, token: CancellationToken) => Promise; - -export interface ILanguageModelToolsService { - _serviceBrand: undefined; - readonly onDidChangeTools: Event; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionId: string; readonly toolData: IToolData }>; - registerToolData(toolData: IToolData): IDisposable; - registerToolImplementation(id: string, tool: IToolImpl): IDisposable; - registerTool(toolData: IToolData, tool: IToolImpl): IDisposable; - getTools(): Iterable>; - getTool(id: string): IToolData | undefined; - getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined; - invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; - cancelToolCallsForRequest(requestId: string): void; - /** Flush any pending tool updates to the extension hosts. */ - flushToolUpdates(): void; - - 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; - mapGithubToolName(githubToolName: string): string; - - toToolAndToolSetEnablementMap(qualifiedToolOrToolSetNames: readonly string[], target: string | undefined): IToolAndToolSetEnablementMap; - toQualifiedToolNames(map: IToolAndToolSetEnablementMap): string[]; - toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; -} - -export function createToolInputUri(toolCallId: string): URI { - return URI.from({ scheme: Schemas.inMemory, path: `/lm/tool/${toolCallId}/tool_input.json` }); -} - -export function createToolSchemaUri(toolOrId: IToolData | string): URI { - if (typeof toolOrId !== 'string') { - toolOrId = toolOrId.id; - } - return URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolOrId}` }); -} - -export namespace GithubCopilotToolReference { - export const shell = 'shell'; - export const edit = 'edit'; - export const search = 'search'; - export const customAgent = 'custom-agent'; -} - -export namespace VSCodeToolReference { - export const runCommands = 'runCommands'; - export const runSubagent = 'runSubagent'; -} diff --git a/src/vs/workbench/contrib/chat/common/languageModels.ts b/src/vs/workbench/contrib/chat/common/languageModels.ts index 69c135d78fd..0df77c1014f 100644 --- a/src/vs/workbench/contrib/chat/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/common/languageModels.ts @@ -6,24 +6,33 @@ import { SequencerByKey } from '../../../../base/common/async.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; +import { CancellationError, getErrorMessage, isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; +import { hash } from '../../../../base/common/hash.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { IJSONSchema, TypeFromJsonSchema } from '../../../../base/common/jsonSchema.js'; import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { isFalsyOrWhitespace } from '../../../../base/common/strings.js'; +import { equals } from '../../../../base/common/objects.js'; +import Severity from '../../../../base/common/severity.js'; +import { format, isFalsyOrWhitespace } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { IQuickInputService, QuickInputHideReason } from '../../../../platform/quickinput/common/quickInput.js'; +import { ISecretStorageService } from '../../../../platform/secrets/common/secrets.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ChatEntitlement, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { ExtensionsRegistry } from '../../../services/extensions/common/extensionsRegistry.js'; -import { ChatContextKeys } from './chatContextKeys.js'; +import { ChatContextKeys } from './actions/chatContextKeys.js'; +import { ChatAgentLocation } from './constants.js'; +import { ILanguageModelsProviderGroup, ILanguageModelsConfigurationService } from './languageModelsConfiguration.js'; export const enum ChatMessageRole { System, @@ -52,6 +61,7 @@ export interface IChatMessageThinkingPart { type: 'thinking'; value: string | string[]; id?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata?: { readonly [key: string]: any }; } @@ -131,6 +141,7 @@ export interface IChatResponseToolUsePart { type: 'tool_use'; name: string; toolCallId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any parameters: any; } @@ -138,6 +149,7 @@ export interface IChatResponseThinkingPart { type: 'thinking'; value: string | string[]; id?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any metadata?: { readonly [key: string]: any }; } @@ -163,11 +175,12 @@ export interface ILanguageModelChatMetadata { readonly version: string; readonly tooltip?: string; readonly detail?: string; + readonly multiplier?: string; readonly family: string; readonly maxInputTokens: number; readonly maxOutputTokens: number; - readonly isDefault?: boolean; + readonly isDefaultForLocation: { [K in ChatAgentLocation]?: boolean }; readonly isUserSelectable?: boolean; readonly statusIcon?: ThemeIcon; readonly modelPickerCategory: { label: string; order: number } | undefined; @@ -203,19 +216,20 @@ export namespace ILanguageModelChatMetadata { export interface ILanguageModelChatResponse { stream: AsyncIterable; + // eslint-disable-next-line @typescript-eslint/no-explicit-any result: Promise; } export interface ILanguageModelChatProvider { readonly onDidChange: Event; - provideLanguageModelChatInfo(options: { silent: boolean }, token: CancellationToken): Promise; - sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; + provideLanguageModelChatInfo(options: ILanguageModelChatInfoOptions, token: CancellationToken): Promise; + sendChatRequest(modelId: string, messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise; provideTokenCount(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise; } export interface ILanguageModelChat { metadata: ILanguageModelChatMetadata; - sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; + sendChatRequest(messages: IChatMessage[], from: ExtensionIdentifier, options: { [name: string]: unknown }, token: CancellationToken): Promise; provideTokenCount(message: string | IChatMessage, token: CancellationToken): Promise; } @@ -229,6 +243,23 @@ export interface ILanguageModelChatSelector { readonly extension?: ExtensionIdentifier; } + +export function isILanguageModelChatSelector(value: unknown): value is ILanguageModelChatSelector { + if (typeof value !== 'object' || value === null) { + return false; + } + const obj = value as Record; + return ( + (obj.name === undefined || typeof obj.name === 'string') && + (obj.id === undefined || typeof obj.id === 'string') && + (obj.vendor === undefined || typeof obj.vendor === 'string') && + (obj.version === undefined || typeof obj.version === 'string') && + (obj.family === undefined || typeof obj.family === 'string') && + (obj.tokens === undefined || typeof obj.tokens === 'number') && + (obj.extension === undefined || typeof obj.extension === 'object') + ); +} + export const ILanguageModelsService = createDecorator('ILanguageModelsService'); export interface ILanguageModelChatMetadataAndIdentifier { @@ -236,33 +267,65 @@ export interface ILanguageModelChatMetadataAndIdentifier { identifier: string; } +export interface ILanguageModelChatInfoOptions { + readonly group?: string; + readonly silent: boolean; + readonly configuration?: IStringDictionary; +} + +export interface ILanguageModelsGroup { + readonly group?: ILanguageModelsProviderGroup; + readonly modelIdentifiers: string[]; + readonly status?: { + readonly message: string; + readonly severity: Severity; + }; +} + export interface ILanguageModelsService { readonly _serviceBrand: undefined; - // TODO @lramos15 - Make this a richer event in the future. Right now it just indicates some change happened, but not what + readonly onDidChangeLanguageModelVendors: Event; readonly onDidChangeLanguageModels: Event; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void; getLanguageModelIds(): string[]; - getVendors(): IUserFriendlyLanguageModel[]; + getVendors(): ILanguageModelProviderDescriptor[]; lookupLanguageModel(modelId: string): ILanguageModelChatMetadata | undefined; + /** + * Find a model by its qualified name. The qualified name is what is used in prompt and agent files and is in the format "Model Name (Vendor)". + */ + lookupLanguageModelByQualifiedName(qualifiedName: string): ILanguageModelChatMetadata | undefined; + + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[]; + /** * Given a selector, returns a list of model identifiers * @param selector The selector to lookup for language models. If the selector is empty, all language models are returned. - * @param allowPromptingUser If true the user may be prompted for things like API keys for us to select the model. */ - selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise; + selectLanguageModels(selector: ILanguageModelChatSelector): Promise; registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable; + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; computeTokenLength(modelId: string, message: string | IChatMessage, token: CancellationToken): Promise; + + addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise; + + removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise; + + configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise; + + migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise; } const languageModelChatProviderType = { @@ -277,9 +340,46 @@ const languageModelChatProviderType = { type: 'string', description: localize('vscode.extension.contributes.languageModels.displayName', "The display name of the language model chat provider.") }, + configuration: { + type: 'object', + description: localize('vscode.extension.contributes.languageModels.configuration', "Configuration options for the language model chat provider."), + anyOf: [ + { + $ref: 'http://json-schema.org/draft-07/schema#' + }, + { + properties: { + properties: { + type: 'object', + additionalProperties: { + $ref: 'http://json-schema.org/draft-07/schema#', + properties: { + secret: { + type: 'boolean', + description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.") + } + } + } + }, + additionalProperties: { + $ref: 'http://json-schema.org/draft-07/schema#', + properties: { + secret: { + type: 'boolean', + description: localize('vscode.extension.contributes.languageModels.configuration.secret', "Whether the property is a secret.") + } + } + } + } + } + ] + + }, managementCommand: { type: 'string', - description: localize('vscode.extension.contributes.languageModels.managementCommand', "A command to manage the language model chat provider, e.g. 'Manage Copilot models'. This is used in the chat model picker. If not provided, a gear icon is not rendered during vendor selection.") + description: localize('vscode.extension.contributes.languageModels.managementCommand', "A command to manage the language model chat provider, e.g. 'Manage Copilot models'. This is used in the chat model picker. If not provided, a gear icon is not rendered during vendor selection."), + deprecated: true, + deprecationMessage: localize('vscode.extension.contributes.languageModels.managementCommand.deprecated', "The managementCommand property is deprecated and will be removed in a future release. Use the new configuration property instead.") }, when: { type: 'string', @@ -290,6 +390,10 @@ const languageModelChatProviderType = { export type IUserFriendlyLanguageModel = TypeFromJsonSchema; +export interface ILanguageModelProviderDescriptor extends IUserFriendlyLanguageModel { + readonly isDefault: boolean; +} + export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'languageModelChatProviders', jsonSchema: { @@ -309,19 +413,29 @@ export const languageModelChatProviderExtensionPoint = ExtensionsRegistry.regist } }); +const CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY = 'chatModelPickerPreferences'; + export class LanguageModelsService implements ILanguageModelsService { + private static SECRET_KEY_PREFIX = 'chat.lm.secret.'; + private static SECRET_INPUT = '${input:{0}}'; + readonly _serviceBrand: undefined; private readonly _store = new DisposableStore(); private readonly _providers = new Map(); + private readonly _vendors = new Map(); + + private readonly _onDidChangeLanguageModelVendors = this._store.add(new Emitter()); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + + private readonly _modelsGroups = new Map(); private readonly _modelCache = new Map(); - private readonly _vendors = new Map(); private readonly _resolveLMSequencer = new SequencerByKey(); - private _modelPickerUserPreferences: Record = {}; + private _modelPickerUserPreferences: IStringDictionary = {}; private readonly _hasUserSelectableModels: IContextKey; - private readonly _contextKeyService: IContextKeyService; + private readonly _onLanguageModelChange = this._store.add(new Emitter()); readonly onDidChangeLanguageModels: Event = this._onLanguageModelChange.event; @@ -329,33 +443,23 @@ export class LanguageModelsService implements ILanguageModelsService { @IExtensionService private readonly _extensionService: IExtensionService, @ILogService private readonly _logService: ILogService, @IStorageService private readonly _storageService: IStorageService, - @IContextKeyService _contextKeyService: IContextKeyService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IChatEntitlementService private readonly _chatEntitlementService: IChatEntitlementService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ILanguageModelsConfigurationService private readonly _languageModelsConfigurationService: ILanguageModelsConfigurationService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISecretStorageService private readonly _secretStorageService: ISecretStorageService, ) { this._hasUserSelectableModels = ChatContextKeys.languageModelsAreUserSelectable.bindTo(_contextKeyService); - this._contextKeyService = _contextKeyService; - this._modelPickerUserPreferences = this._storageService.getObject>('chatModelPickerPreferences', StorageScope.PROFILE, this._modelPickerUserPreferences); - // TODO @lramos15 - Remove after a few releases, as this is just cleaning a bad storage state - const entitlementChangeHandler = () => { - if ((this._chatEntitlementService.entitlement === ChatEntitlement.Business || this._chatEntitlementService.entitlement === ChatEntitlement.Enterprise) && !this._chatEntitlementService.isInternal) { - this._modelPickerUserPreferences = {}; - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); - } - }; + this._modelPickerUserPreferences = this._readModelPickerPreferences(); + this._store.add(this._storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._store)(() => this._onDidChangeModelPickerPreferences())); - entitlementChangeHandler(); - this._store.add(this._chatEntitlementService.onDidChangeEntitlement(entitlementChangeHandler)); + this._store.add(this.onDidChangeLanguageModels(() => this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)))); + this._store.add(this._languageModelsConfigurationService.onDidChangeLanguageModelGroups(changedGroups => this._onDidChangeLanguageModelGroups(changedGroups))); - this._store.add(this.onDidChangeLanguageModels(() => { - this._hasUserSelectableModels.set(this._modelCache.size > 0 && Array.from(this._modelCache.values()).some(model => model.isUserSelectable)); - })); - - this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions) => { + this._store.add(languageModelChatProviderExtensionPoint.setHandler((extensions, { added, removed }) => { + const addedVendors: IUserFriendlyLanguageModel[] = []; + const removedVendors: IUserFriendlyLanguageModel[] = []; - this._vendors.clear(); - - for (const extension of extensions) { + for (const extension of added) { for (const item of Iterable.wrap(extension.value)) { if (this._vendors.has(item.vendor)) { extension.collector.error(localize('vscode.extension.contributes.languageModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); @@ -369,30 +473,132 @@ export class LanguageModelsService implements ILanguageModelsService { extension.collector.error(localize('vscode.extension.contributes.languageModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); continue; } - this._vendors.set(item.vendor, item); - // Have some models we want from this vendor, so activate the extension - if (this._hasStoredModelForVendor(item.vendor)) { - this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); - } + addedVendors.push(item); } } - for (const [vendor, _] of this._providers) { - if (!this._vendors.has(vendor)) { - this._providers.delete(vendor); + + for (const extension of removed) { + for (const item of Iterable.wrap(extension.value)) { + removedVendors.push(item); } } + + this.deltaLanguageModelChatProviderDescriptors(addedVendors, removedVendors); })); } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + const addedVendorIds: string[] = []; + const removedVendorIds: string[] = []; + + for (const item of added) { + if (this._vendors.has(item.vendor)) { + this._logService.error(`The vendor '${item.vendor}' is already registered and cannot be registered twice`); + continue; + } + if (isFalsyOrWhitespace(item.vendor)) { + this._logService.error('The vendor field cannot be empty.'); + continue; + } + if (item.vendor.trim() !== item.vendor) { + this._logService.error('The vendor field cannot start or end with whitespace.'); + continue; + } + const vendor: ILanguageModelProviderDescriptor = { + vendor: item.vendor, + displayName: item.displayName, + configuration: item.configuration, + managementCommand: item.managementCommand, + when: item.when, + isDefault: item.vendor === 'copilot' + }; + this._vendors.set(item.vendor, vendor); + addedVendorIds.push(item.vendor); + // Have some models we want from this vendor, so activate the extension + if (this._hasStoredModelForVendor(item.vendor)) { + this._extensionService.activateByEvent(`onLanguageModelChatProvider:${item.vendor}`); + } + } + + for (const item of removed) { + this._vendors.delete(item.vendor); + this._providers.delete(item.vendor); + this._clearModelCache(item.vendor); + removedVendorIds.push(item.vendor); + } + + for (const [vendor, _] of this._providers) { + if (!this._vendors.has(vendor)) { + this._providers.delete(vendor); + } + } + + if (addedVendorIds.length > 0 || removedVendorIds.length > 0) { + this._onDidChangeLanguageModelVendors.fire([...addedVendorIds, ...removedVendorIds]); + if (removedVendorIds.length > 0) { + for (const vendor of removedVendorIds) { + this._onLanguageModelChange.fire(vendor); + } + } + } + } + + private async _onDidChangeLanguageModelGroups(changedGroups: readonly ILanguageModelsProviderGroup[]): Promise { + const changedVendors = new Set(changedGroups.map(g => g.vendor)); + await Promise.all(Array.from(changedVendors).map(vendor => this._resolveAllLanguageModels(vendor, true))); + } + + private _readModelPickerPreferences(): IStringDictionary { + return this._storageService.getObject>(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, StorageScope.PROFILE, {}); + } + + private _onDidChangeModelPickerPreferences(): void { + const newPreferences = this._readModelPickerPreferences(); + const oldPreferences = this._modelPickerUserPreferences; + + // Check if there are any changes by computing diff + const affectedVendors = new Set(); + let hasChanges = false; + + // Check for added or updated keys + for (const modelId in newPreferences) { + if (oldPreferences[modelId] !== newPreferences[modelId]) { + hasChanges = true; + const model = this._modelCache.get(modelId); + if (model) { + affectedVendors.add(model.vendor); + } + } + } + + // Check for removed keys + for (const modelId in oldPreferences) { + if (!newPreferences.hasOwnProperty(modelId)) { + hasChanges = true; + const model = this._modelCache.get(modelId); + if (model) { + affectedVendors.add(model.vendor); + } + } + } + + if (hasChanges) { + this._logService.trace('[LM] Updated model picker preferences from storage'); + this._modelPickerUserPreferences = newPreferences; + for (const vendor of affectedVendors) { + this._onLanguageModelChange.fire(vendor); + } + } + } + private _hasStoredModelForVendor(vendor: string): boolean { return Object.keys(this._modelPickerUserPreferences).some(modelId => { return modelId.startsWith(vendor); }); } - dispose() { - this._store.dispose(); - this._providers.clear(); + private _saveModelPickerPreferences(): void { + this._storageService.store(CHAT_MODEL_PICKER_PREFERENCES_STORAGE_KEY, this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); } updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { @@ -405,22 +611,23 @@ export class LanguageModelsService implements ILanguageModelsService { this._modelPickerUserPreferences[modelIdentifier] = showInModelPicker; if (showInModelPicker === model.isUserSelectable) { delete this._modelPickerUserPreferences[modelIdentifier]; - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + this._saveModelPickerPreferences(); } else if (model.isUserSelectable !== showInModelPicker) { - this._storageService.store('chatModelPickerPreferences', this._modelPickerUserPreferences, StorageScope.PROFILE, StorageTarget.USER); + this._saveModelPickerPreferences(); } this._onLanguageModelChange.fire(model.vendor); this._logService.trace(`[LM] Updated model picker preference for ${modelIdentifier} to ${showInModelPicker}`); } - getVendors(): IUserFriendlyLanguageModel[] { - return Array.from(this._vendors.values()).filter(vendor => { - if (!vendor.when) { - return true; // No when clause means always visible - } - const whenClause = ContextKeyExpr.deserialize(vendor.when); - return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false; - }); + getVendors(): ILanguageModelProviderDescriptor[] { + return Array.from(this._vendors.values()) + .filter(vendor => { + if (!vendor.when) { + return true; // No when clause means always visible + } + const whenClause = ContextKeyExpr.deserialize(vendor.when); + return whenClause ? this._contextKeyService.contextMatchesRules(whenClause) : false; + }); } getLanguageModelIds(): string[] { @@ -429,61 +636,132 @@ export class LanguageModelsService implements ILanguageModelsService { lookupLanguageModel(modelIdentifier: string): ILanguageModelChatMetadata | undefined { const model = this._modelCache.get(modelIdentifier); - if (model && this._configurationService.getValue('chat.experimentalShowAllModels')) { - return { ...model, isUserSelectable: true }; - } if (model && this._modelPickerUserPreferences[modelIdentifier] !== undefined) { return { ...model, isUserSelectable: this._modelPickerUserPreferences[modelIdentifier] }; } return model; } - private _clearModelCache(vendor: string): void { - for (const [id, model] of this._modelCache.entries()) { - if (model.vendor === vendor) { - this._modelCache.delete(id); + lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadata | undefined { + for (const model of this._modelCache.values()) { + if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, model)) { + return model; } } + return undefined; } - private async _resolveLanguageModels(vendor: string, silent: boolean): Promise { + private async _resolveAllLanguageModels(vendorId: string, silent: boolean): Promise { + + const vendor = this._vendors.get(vendorId); + + if (!vendor) { + return; + } + // Activate extensions before requesting to resolve the models - await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`); - const provider = this._providers.get(vendor); + await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendorId}`); + + const provider = this._providers.get(vendorId); if (!provider) { - this._logService.warn(`[LM] No provider registered for vendor ${vendor}`); + this._logService.warn(`[LM] No provider registered for vendor ${vendorId}`); return; } - return this._resolveLMSequencer.queue(vendor, async () => { + + return this._resolveLMSequencer.queue(vendorId, async () => { + + const allModels: ILanguageModelChatMetadataAndIdentifier[] = []; + const languageModelsGroups: ILanguageModelsGroup[] = []; + try { - let modelsAndIdentifiers = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None); - // This is a bit of a hack, when prompting user if the provider returns any models that are user selectable then we only want to show those and not the entire model list - if (!silent && modelsAndIdentifiers.some(m => m.metadata.isUserSelectable)) { - modelsAndIdentifiers = modelsAndIdentifiers.filter(m => m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true); - } - this._clearModelCache(vendor); - for (const modelAndIdentifier of modelsAndIdentifiers) { - if (this._modelCache.has(modelAndIdentifier.identifier)) { - this._logService.warn(`[LM] Model ${modelAndIdentifier.identifier} is already registered. Skipping.`); - continue; + const models = await provider.provideLanguageModelChatInfo({ silent }, CancellationToken.None); + if (models.length) { + allModels.push(...models); + const modelIdentifiers = []; + for (const m of models) { + if (vendor.isDefault) { + // Special case for copilot models - they are all user selectable unless marked otherwise + if (m.metadata.isUserSelectable || this._modelPickerUserPreferences[m.identifier] === true) { + modelIdentifiers.push(m.identifier); + } else { + this._logService.trace(`[LM] Skipping model ${m.identifier} from model picker as it is not user selectable.`); + } + } else { + modelIdentifiers.push(m.identifier); + } } - this._modelCache.set(modelAndIdentifier.identifier, modelAndIdentifier.metadata); + languageModelsGroups.push({ modelIdentifiers }); } - this._logService.trace(`[LM] Resolved language models for vendor ${vendor}`, modelsAndIdentifiers); } catch (error) { - this._logService.error(`[LM] Error resolving language models for vendor ${vendor}:`, error); + languageModelsGroups.push({ + modelIdentifiers: [], + status: { + message: getErrorMessage(error), + severity: Severity.Error + } + }); + } + + const groups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups(); + for (const group of groups) { + if (group.vendor !== vendorId) { + continue; + } + + const configuration = await this._resolveConfiguration(group, vendor.configuration); + + try { + const models = await provider.provideLanguageModelChatInfo({ group: group.name, silent, configuration }, CancellationToken.None); + if (models.length) { + allModels.push(...models); + languageModelsGroups.push({ group, modelIdentifiers: models.map(m => m.identifier) }); + } + } catch (error) { + languageModelsGroups.push({ + group, + modelIdentifiers: [], + status: { + message: getErrorMessage(error), + severity: Severity.Error + } + }); + } + } + + this._modelsGroups.set(vendorId, languageModelsGroups); + const oldModels = this._clearModelCache(vendorId); + let hasChanges = false; + for (const model of allModels) { + if (this._modelCache.has(model.identifier)) { + this._logService.warn(`[LM] Model ${model.identifier} is already registered. Skipping.`); + continue; + } + this._modelCache.set(model.identifier, model.metadata); + hasChanges = hasChanges || !equals(oldModels.get(model.identifier), model.metadata); + oldModels.delete(model.identifier); + } + this._logService.trace(`[LM] Resolved language models for vendor ${vendorId}`, allModels); + hasChanges = hasChanges || oldModels.size > 0; + + if (hasChanges) { + this._onLanguageModelChange.fire(vendorId); + } else { + this._logService.trace(`[LM] No changes in language models for vendor ${vendorId}`); } - this._onLanguageModelChange.fire(vendor); }); } - async selectLanguageModels(selector: ILanguageModelChatSelector, allowPromptingUser?: boolean): Promise { + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { + return this._modelsGroups.get(vendor) ?? []; + } + + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { if (selector.vendor) { - await this._resolveLanguageModels(selector.vendor, !allowPromptingUser); + await this._resolveAllLanguageModels(selector.vendor, true); } else { const allVendors = Array.from(this._vendors.keys()); - await Promise.all(allVendors.map(vendor => this._resolveLanguageModels(vendor, !allowPromptingUser))); + await Promise.all(allVendors.map(vendor => this._resolveAllLanguageModels(vendor, true))); } const result: string[] = []; @@ -515,11 +793,11 @@ export class LanguageModelsService implements ILanguageModelsService { this._providers.set(vendor, provider); if (this._hasStoredModelForVendor(vendor)) { - this._resolveLanguageModels(vendor, true); + this._resolveAllLanguageModels(vendor, true); } - const modelChangeListener = provider.onDidChange(async () => { - await this._resolveLanguageModels(vendor, true); + const modelChangeListener = provider.onDidChange(() => { + this._resolveAllLanguageModels(vendor, true); }); return toDisposable(() => { @@ -530,6 +808,7 @@ export class LanguageModelsService implements ILanguageModelsService { }); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any async sendChatRequest(modelId: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { const provider = this._providers.get(this._modelCache.get(modelId)?.vendor || ''); if (!provider) { @@ -549,4 +828,437 @@ export class LanguageModelsService implements ILanguageModelsService { } return provider.provideTokenCount(modelId, message, token); } + + async configureLanguageModelsProviderGroup(vendorId: string, providerGroupName?: string): Promise { + + const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); + if (!vendor) { + throw new Error(`Vendor ${vendorId} not found.`); + } + + if (vendor.managementCommand) { + await this._resolveAllLanguageModels(vendor.vendor, false); + return; + } + + const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups(); + const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName); + + const name = await this.promptForName(languageModelProviderGroups, vendor, existing); + if (!name) { + return; + } + + const existingConfiguration = existing ? await this._resolveConfiguration(existing, vendor.configuration) : undefined; + + try { + const configuration = vendor.configuration ? await this.promptForConfiguration(name, vendor.configuration, existingConfiguration) : undefined; + if (vendor.configuration && !configuration) { + return; + } + + const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration); + const saved = existing + ? await this._languageModelsConfigurationService.updateLanguageModelsProviderGroup(existing, languageModelProviderGroup) + : await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup); + + if (vendor.configuration && this.requireConfiguring(vendor.configuration)) { + const snippet = this.getSnippetForFirstUnconfiguredProperty(configuration ?? {}, vendor.configuration); + await this._languageModelsConfigurationService.configureLanguageModels({ group: saved, snippet }); + } + } catch (error) { + if (isCancellationError(error)) { + return; + } + throw error; + } + } + + async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { + const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); + if (!vendor) { + throw new Error(`Vendor ${vendorId} not found.`); + } + + const languageModelProviderGroup = await this._resolveLanguageModelProviderGroup(name, vendorId, configuration, vendor.configuration); + await this._languageModelsConfigurationService.addLanguageModelsProviderGroup(languageModelProviderGroup); + } + + async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise { + const vendor = this.getVendors().find(({ vendor }) => vendor === vendorId); + if (!vendor) { + throw new Error(`Vendor ${vendorId} not found.`); + } + + const languageModelProviderGroups = this._languageModelsConfigurationService.getLanguageModelsProviderGroups(); + const existing = languageModelProviderGroups.find(g => g.vendor === vendorId && g.name === providerGroupName); + + if (!existing) { + throw new Error(`Language model provider group ${providerGroupName} for vendor ${vendorId} not found.`); + } + + await this._deleteSecretsInConfiguration(existing, vendor.configuration); + await this._languageModelsConfigurationService.removeLanguageModelsProviderGroup(existing); + } + + private requireConfiguring(schema: IJSONSchema): boolean { + if (schema.additionalProperties) { + return true; + } + if (!schema.properties) { + return false; + } + for (const property of Object.keys(schema.properties)) { + if (!this.canPromptForProperty(schema.properties[property])) { + return true; + } + } + return false; + } + + private getSnippetForFirstUnconfiguredProperty(configuration: IStringDictionary, schema: IJSONSchema): string | undefined { + if (!schema.properties) { + return undefined; + } + for (const property of Object.keys(schema.properties)) { + if (configuration[property] === undefined) { + const propertySchema = schema.properties[property]; + if (propertySchema && typeof propertySchema !== 'boolean' && propertySchema.defaultSnippets?.[0]) { + const snippet = propertySchema.defaultSnippets[0]; + let bodyText = snippet.bodyText ?? JSON.stringify(snippet.body, null, '\t'); + // Handle ^ prefix for raw values (numbers/booleans) - remove quotes around ^-prefixed values + bodyText = bodyText.replace(/"(\^[^"]*)"/g, (_, value) => value.substring(1)); + return `"${property}": ${bodyText}`; + } + } + } + return undefined; + } + + private async promptForName(languageModelProviderGroups: readonly ILanguageModelsProviderGroup[], vendor: IUserFriendlyLanguageModel, existing: ILanguageModelsProviderGroup | undefined): Promise { + let providerGroupName = existing?.name; + if (!providerGroupName) { + providerGroupName = vendor.displayName; + let count = 1; + while (languageModelProviderGroups.some(g => g.vendor === vendor.vendor && g.name === providerGroupName)) { + count++; + providerGroupName = `${vendor.displayName} ${count}`; + } + } + + let result: string | undefined; + const disposables = new DisposableStore(); + try { + await new Promise(resolve => { + const inputBox = disposables.add(this._quickInputService.createInputBox()); + inputBox.title = localize('configureLanguageModelGroup', "Group Name"); + inputBox.placeholder = localize('languageModelGroupName', "Enter a name for the group"); + inputBox.value = providerGroupName; + inputBox.ignoreFocusOut = true; + + disposables.add(inputBox.onDidChangeValue(value => { + if (!value) { + inputBox.validationMessage = localize('enterName', "Please enter a name"); + inputBox.severity = Severity.Error; + return; + } + if (!existing && languageModelProviderGroups.some(g => g.name === value)) { + inputBox.validationMessage = localize('nameExists', "A language models group with this name already exists"); + inputBox.severity = Severity.Error; + return; + } + inputBox.validationMessage = undefined; + inputBox.severity = Severity.Ignore; + })); + disposables.add(inputBox.onDidAccept(async () => { + result = inputBox.value; + inputBox.hide(); + })); + disposables.add(inputBox.onDidHide(() => resolve())); + inputBox.show(); + }); + } finally { + disposables.dispose(); + } + return result; + } + + private async promptForConfiguration(groupName: string, configuration: IJSONSchema, existing: IStringDictionary | undefined): Promise | undefined> { + if (!configuration.properties) { + return; + } + + const result: IStringDictionary = existing ? { ...existing } : {}; + + for (const property of Object.keys(configuration.properties)) { + const propertySchema = configuration.properties[property]; + const required = !!configuration.required?.includes(property); + const value = await this.promptForValue(groupName, property, propertySchema, required, existing); + if (value !== undefined) { + result[property] = value; + } + } + + return result; + } + + private async promptForValue(groupName: string, property: string, propertySchema: IJSONSchema | undefined, required: boolean, existing: IStringDictionary | undefined): Promise { + if (!propertySchema) { + return undefined; + } + + if (!this.canPromptForProperty(propertySchema)) { + return undefined; + } + + if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) { + const selectedItems = await this.promptForArray(groupName, property, propertySchema); + if (selectedItems === undefined) { + return undefined; + } + return selectedItems; + } + + const value = await this.promptForInput(groupName, property, propertySchema, required, existing); + if (value === undefined) { + return undefined; + } + + return value; + } + + private canPromptForProperty(propertySchema: IJSONSchema | undefined): boolean { + if (!propertySchema || typeof propertySchema === 'boolean') { + return false; + } + + if (propertySchema.type === 'array' && propertySchema.items && !Array.isArray(propertySchema.items) && propertySchema.items.enum) { + return true; + } + + if (propertySchema.type === 'string' || propertySchema.type === 'number' || propertySchema.type === 'integer' || propertySchema.type === 'boolean') { + return true; + } + + return false; + } + + private async promptForArray(groupName: string, property: string, propertySchema: IJSONSchema): Promise { + if (!propertySchema.items || Array.isArray(propertySchema.items) || !propertySchema.items.enum) { + return undefined; + } + const items = propertySchema.items.enum; + const disposables = new DisposableStore(); + try { + return await new Promise(resolve => { + const quickPick = disposables.add(this._quickInputService.createQuickPick()); + quickPick.title = `${groupName}: ${propertySchema.title ?? property}`; + quickPick.items = items.map(item => ({ label: item })); + quickPick.placeholder = propertySchema.description ?? localize('selectValue', "Select value for {0}", property); + quickPick.canSelectMany = true; + quickPick.ignoreFocusOut = true; + + disposables.add(quickPick.onDidAccept(() => { + resolve(quickPick.selectedItems.map(item => item.label)); + quickPick.hide(); + })); + disposables.add(quickPick.onDidHide(() => { + resolve(undefined); + })); + quickPick.show(); + }); + } finally { + disposables.dispose(); + } + } + + private async promptForInput(groupName: string, property: string, propertySchema: IJSONSchema, required: boolean, existing: IStringDictionary | undefined): Promise { + const disposables = new DisposableStore(); + try { + const value = await new Promise((resolve, reject) => { + const inputBox = disposables.add(this._quickInputService.createInputBox()); + inputBox.title = `${groupName}: ${propertySchema.title ?? property}`; + inputBox.placeholder = localize('enterValue', "Enter value for {0}", property); + inputBox.password = !!propertySchema.secret; + inputBox.ignoreFocusOut = true; + if (existing?.[property]) { + inputBox.value = String(existing?.[property]); + } else if (propertySchema.default) { + inputBox.value = String(propertySchema.default); + } + if (propertySchema.description) { + inputBox.prompt = propertySchema.description; + } + + disposables.add(inputBox.onDidChangeValue(value => { + if (!value && required) { + inputBox.validationMessage = localize('valueRequired', "Value is required"); + inputBox.severity = Severity.Error; + return; + } + if (propertySchema.type === 'number' || propertySchema.type === 'integer') { + if (isNaN(Number(value))) { + inputBox.validationMessage = localize('numberRequired', "Please enter a number"); + inputBox.severity = Severity.Error; + return; + } + } + if (propertySchema.type === 'boolean') { + if (value !== 'true' && value !== 'false') { + inputBox.validationMessage = localize('booleanRequired', "Please enter true or false"); + inputBox.severity = Severity.Error; + return; + } + } + inputBox.validationMessage = undefined; + inputBox.severity = Severity.Ignore; + })); + + disposables.add(inputBox.onDidAccept(() => { + if (!inputBox.value && required) { + inputBox.validationMessage = localize('valueRequired', "Value is required"); + inputBox.severity = Severity.Error; + return; + } + resolve(inputBox.value); + inputBox.hide(); + })); + + disposables.add(inputBox.onDidHide((e) => { + if (e.reason === QuickInputHideReason.Gesture) { + reject(new CancellationError()); + } else { + resolve(undefined); + } + })); + + inputBox.show(); + }); + + if (!value) { + return undefined; // User cancelled + } + + if (propertySchema.type === 'number' || propertySchema.type === 'integer') { + return Number(value); + } else if (propertySchema.type === 'boolean') { + return value === 'true'; + } else { + return value; + } + + } finally { + disposables.dispose(); + } + } + + private encodeSecretKey(property: string): string { + return format(LanguageModelsService.SECRET_INPUT, property); + } + + private decodeSecretKey(secretInput: unknown): string | undefined { + if (!isString(secretInput)) { + return undefined; + } + return secretInput.substring(secretInput.indexOf(':') + 1, secretInput.length - 1); + } + + private _clearModelCache(vendor: string): Map { + const removed = new Map(); + for (const [id, model] of this._modelCache.entries()) { + if (model.vendor === vendor) { + removed.set(id, model); + this._modelCache.delete(id); + } + } + return removed; + } + + private async _resolveConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise> { + if (!schema) { + return {}; + } + + const result: IStringDictionary = {}; + for (const key in group) { + if (key === 'vendor' || key === 'name' || key === 'range') { + continue; + } + let value = group[key]; + if (schema.properties?.[key]?.secret) { + const secretKey = this.decodeSecretKey(value); + value = secretKey ? await this._secretStorageService.get(secretKey) : undefined; + } + result[key] = value; + } + + return result; + } + + private async _resolveLanguageModelProviderGroup(name: string, vendor: string, configuration: IStringDictionary | undefined, schema: IJSONSchema | undefined): Promise { + if (!schema) { + return { name, vendor }; + } + + const result: IStringDictionary = {}; + for (const key in configuration) { + let value = configuration[key]; + if (schema.properties?.[key]?.secret && isString(value)) { + const secretKey = `${LanguageModelsService.SECRET_KEY_PREFIX}${hash(generateUuid()).toString(16)}`; + await this._secretStorageService.set(secretKey, value); + value = this.encodeSecretKey(secretKey); + } + result[key] = value; + } + + return { name, vendor, ...result }; + } + + private async _deleteSecretsInConfiguration(group: ILanguageModelsProviderGroup, schema: IJSONSchema | undefined): Promise { + if (!schema) { + return; + } + + const { vendor, name, range, ...configuration } = group; + for (const key in configuration) { + const value = group[key]; + if (schema.properties?.[key]?.secret) { + const secretKey = this.decodeSecretKey(value); + if (secretKey) { + await this._secretStorageService.delete(secretKey); + } + } + } + } + + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { + const { vendor, name, ...configuration } = languageModelsProviderGroup; + if (!this._vendors.get(vendor)) { + throw new Error(`Vendor ${vendor} not found.`); + } + + await this._extensionService.activateByEvent(`onLanguageModelChatProvider:${vendor}`); + const provider = this._providers.get(vendor); + if (!provider) { + throw new Error(`Chat model provider for vendor ${vendor} is not registered.`); + } + + const models = await provider.provideLanguageModelChatInfo({ group: name, silent: false, configuration }, CancellationToken.None); + for (const model of models) { + const oldIdentifier = `${vendor}/${model.metadata.id}`; + if (this._modelPickerUserPreferences[oldIdentifier] === true) { + this._modelPickerUserPreferences[model.identifier] = true; + } + delete this._modelPickerUserPreferences[oldIdentifier]; + } + this._saveModelPickerPreferences(); + + await this.addLanguageModelsProviderGroup(name, vendor, configuration); + } + + dispose() { + this._store.dispose(); + this._providers.clear(); + } + } diff --git a/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts new file mode 100644 index 00000000000..4f0dfa41691 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/languageModelsConfiguration.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { URI } from '../../../../base/common/uri.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; + +export const ILanguageModelsConfigurationService = createDecorator('ILanguageModelsConfigurationService'); + +export interface ConfigureLanguageModelsOptions { + group: ILanguageModelsProviderGroup; + snippet?: string; +} + +export interface ILanguageModelsConfigurationService { + readonly _serviceBrand: undefined; + + readonly configurationFile: URI; + + readonly onDidChangeLanguageModelGroups: Event; + + getLanguageModelsProviderGroups(): readonly ILanguageModelsProviderGroup[]; + + addLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise; + + updateLanguageModelsProviderGroup(from: ILanguageModelsProviderGroup, to: ILanguageModelsProviderGroup): Promise; + + removeLanguageModelsProviderGroup(languageModelGroup: ILanguageModelsProviderGroup): Promise; + + configureLanguageModels(options?: ConfigureLanguageModelsOptions): Promise; +} + +export interface ILanguageModelsProviderGroup extends IStringDictionary { + readonly name: string; + readonly vendor: string; + readonly range?: IRange; +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts new file mode 100644 index 00000000000..343dfce4a59 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -0,0 +1,2507 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { asArray } from '../../../../../base/common/arrays.js'; +import { softAssertNever } from '../../../../../base/common/assert.js'; +import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffer.js'; +import { BugIndicatingError } from '../../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { revive } from '../../../../../base/common/marshalling.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { equals } from '../../../../../base/common/objects.js'; +import { IObservable, autorun, autorunSelfDisposable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js'; +import { basename, isEqual } from '../../../../../base/common/resources.js'; +import { hasKey, WithDefinedProps } from '../../../../../base/common/types.js'; +import { URI, UriDto } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { ISelection } from '../../../../../editor/common/core/selection.js'; +import { TextEdit } from '../../../../../editor/common/languages.js'; +import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js'; +import { localize } from '../../../../../nls.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js'; +import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js'; +import { migrateLegacyTerminalToolSpecificData } from '../chat.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, isIUsedContext } from '../chatService/chatService.js'; +import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../editing/chatEditingService.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js'; +import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { LocalChatSessionUri } from './chatUri.js'; +import { ObjectMutationLog } from './objectMutationLog.js'; + + +export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record = { + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif', + webp: 'image/webp', +}; + +export function getAttachableImageExtension(mimeType: string): string | undefined { + return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0]; +} + +export interface IChatRequestVariableData { + variables: readonly IChatRequestVariableEntry[]; +} + +export namespace IChatRequestVariableData { + export function toExport(data: IChatRequestVariableData): IChatRequestVariableData { + return { variables: data.variables.map(IChatRequestVariableEntry.toExport) }; + } +} + +export interface IChatRequestModel { + readonly id: string; + readonly timestamp: number; + readonly version: number; + readonly modeInfo?: IChatRequestModeInfo; + readonly session: IChatModel; + readonly message: IParsedChatRequest; + readonly attempt: number; + readonly variableData: IChatRequestVariableData; + readonly confirmation?: string; + readonly locationData?: IChatLocationData; + readonly attachedContext?: IChatRequestVariableEntry[]; + readonly isCompleteAddedRequest: boolean; + readonly response?: IChatResponseModel; + readonly editedFileEvents?: IChatAgentEditedFileEvent[]; + shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + readonly shouldBeBlocked: IObservable; + setShouldBeBlocked(value: boolean): void; + readonly modelId?: string; + readonly userSelectedTools?: UserSelectedTools; +} + +export interface ICodeBlockInfo { + readonly suggestionId: EditSuggestionId; +} + +export interface IChatTextEditGroupState { + sha1: string; + applied: number; +} + +export interface IChatTextEditGroup { + uri: URI; + edits: TextEdit[][]; + state?: IChatTextEditGroupState; + kind: 'textEditGroup'; + done: boolean | undefined; + isExternalEdit?: boolean; +} + +export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation { + const candidate = value as ICellTextEditOperation; + return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri); +} + +export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] { + return value.some(isCellTextEditOperation); +} + +export interface ICellTextEditOperation { + edit: TextEdit; + uri: URI; +} + +export interface IChatNotebookEditGroup { + uri: URI; + edits: (ICellTextEditOperation[] | ICellEditOperation[])[]; + state?: IChatTextEditGroupState; + kind: 'notebookEditGroup'; + done: boolean | undefined; + isExternalEdit?: boolean; +} + +/** + * Progress kinds that are included in the history of a response. + * Excludes "internal" types that are included in history. + */ +export type IChatProgressHistoryResponseContent = + | IChatMarkdownContent + | IChatAgentMarkdownContentWithVulnerability + | IChatResponseCodeblockUriPart + | IChatTreeData + | IChatMultiDiffDataSerialized + | IChatContentInlineReference + | IChatProgressMessage + | IChatCommandButton + | IChatWarningMessage + | IChatTask + | IChatTaskSerialized + | IChatTextEditGroup + | IChatNotebookEditGroup + | IChatConfirmation + | IChatQuestionCarousel + | IChatExtensionsContent + | IChatThinkingPart + | IChatPullRequestContent + | IChatWorkspaceEdit; + +/** + * "Normal" progress kinds that are rendered as parts of the stream of content. + */ +export type IChatProgressResponseContent = + | IChatProgressHistoryResponseContent + | IChatToolInvocation + | IChatToolInvocationSerialized + | IChatMultiDiffData + | IChatUndoStop + | IChatElicitationRequest + | IChatElicitationRequestSerialized + | IChatClearToPreviousToolInvocation + | IChatMcpServersStarting + | IChatMcpServersStartingSerialized; + +export type IChatProgressResponseContentSerialized = Exclude; + +const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']); +function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent { + return !nonHistoryKinds.has(content.kind); +} + +export function toChatHistoryContent(content: ReadonlyArray): IChatProgressHistoryResponseContent[] { + return content.filter(isChatProgressHistoryResponseContent); +} + +export type IChatProgressRenderableResponseContent = Exclude; + +export interface IResponse { + readonly value: ReadonlyArray; + getMarkdown(): string; + toString(): string; +} + +export interface IChatResponseModel { + readonly onDidChange: Event; + readonly id: string; + readonly requestId: string; + readonly request: IChatRequestModel | undefined; + readonly username: string; + readonly session: IChatModel; + readonly agent?: IChatAgentData; + readonly usedContext: IChatUsedContext | undefined; + readonly contentReferences: ReadonlyArray; + readonly codeCitations: ReadonlyArray; + readonly progressMessages: ReadonlyArray; + readonly slashCommand?: IChatAgentCommand; + readonly agentOrSlashCommandDetected: boolean; + /** View of the response shown to the user, may have parts omitted from undo stops. */ + readonly response: IResponse; + /** Entire response from the model. */ + readonly entireResponse: IResponse; + /** Milliseconds timestamp when this chat response was created. */ + readonly timestamp: number; + /** Milliseconds timestamp when this chat response was completed or cancelled. */ + readonly completedAt?: number; + /** The state of this response */ + readonly state: ResponseModelState; + /** @internal */ + readonly stateT: ResponseModelStateT; + /** + * Adjusted millisecond timestamp that excludes the duration during which + * the model was pending user confirmation. `Date.now() - confirmationAdjustedTimestamp` + * will return the amount of time the response was busy generating content. + * This is updated only when `isPendingConfirmation` changes state. + */ + readonly confirmationAdjustedTimestamp: IObservable; + readonly isComplete: boolean; + readonly isCanceled: boolean; + readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>; + readonly isInProgress: IObservable; + readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + readonly shouldBeBlocked: IObservable; + readonly isCompleteAddedRequest: boolean; + /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ + readonly isStale: boolean; + readonly vote: ChatAgentVoteDirection | undefined; + readonly voteDownReason: ChatAgentVoteDownReason | undefined; + readonly followups?: IChatFollowup[] | undefined; + readonly result?: IChatAgentResult; + readonly usage?: IChatUsage; + readonly codeBlockInfos: ICodeBlockInfo[] | undefined; + + initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void; + addUndoStop(undoStop: IChatUndoStop): void; + setVote(vote: ChatAgentVoteDirection): void; + setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void; + setUsage(usage: IChatUsage): void; + setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean; + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void; + /** + * Adopts any partially-undo {@link response} as the {@link entireResponse}. + * Only valid when {@link isComplete}. This is needed because otherwise an + * undone and then diverged state would start showing old data because the + * undo stops would no longer exist in the model. + */ + finalizeUndoState(): void; +} + +export type ChatResponseModelChangeReason = + | { reason: 'other' } + | { reason: 'completedRequest' } + | { reason: 'undoStop'; id: string }; + +export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' }; + +export interface IChatRequestModeInfo { + kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply' + isBuiltin: boolean; + modeInstructions: IChatRequestModeInstructions | undefined; + modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined; + applyCodeBlockSuggestionId: EditSuggestionId | undefined; +} + +export interface IChatRequestModeInstructions { + readonly name: string; + readonly content: string; + readonly toolReferences: readonly ChatRequestToolReferenceEntry[]; + readonly metadata?: Record; +} + +export interface IChatRequestModelParameters { + session: ChatModel; + message: IParsedChatRequest; + variableData: IChatRequestVariableData; + timestamp: number; + attempt?: number; + modeInfo?: IChatRequestModeInfo; + confirmation?: string; + locationData?: IChatLocationData; + attachedContext?: IChatRequestVariableEntry[]; + isCompleteAddedRequest?: boolean; + modelId?: string; + restoredId?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; + userSelectedTools?: UserSelectedTools; +} + +export class ChatRequestModel implements IChatRequestModel { + public readonly id: string; + public response: ChatResponseModel | undefined; + public shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + public readonly timestamp: number; + public readonly message: IParsedChatRequest; + public readonly isCompleteAddedRequest: boolean; + public readonly modelId?: string; + public readonly modeInfo?: IChatRequestModeInfo; + public readonly userSelectedTools?: UserSelectedTools; + + private readonly _shouldBeBlocked = observableValue(this, false); + public get shouldBeBlocked(): IObservable { + return this._shouldBeBlocked; + } + + public setShouldBeBlocked(value: boolean): void { + this._shouldBeBlocked.set(value, undefined); + } + + private _session: ChatModel; + private readonly _attempt: number; + private _variableData: IChatRequestVariableData; + private readonly _confirmation?: string; + private readonly _locationData?: IChatLocationData; + private readonly _attachedContext?: IChatRequestVariableEntry[]; + private readonly _editedFileEvents?: IChatAgentEditedFileEvent[]; + + public get session(): ChatModel { + return this._session; + } + + public get attempt(): number { + return this._attempt; + } + + public get variableData(): IChatRequestVariableData { + return this._variableData; + } + + public set variableData(v: IChatRequestVariableData) { + this._version++; + this._variableData = v; + } + + public get confirmation(): string | undefined { + return this._confirmation; + } + + public get locationData(): IChatLocationData | undefined { + return this._locationData; + } + + public get attachedContext(): IChatRequestVariableEntry[] | undefined { + return this._attachedContext; + } + + public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined { + return this._editedFileEvents; + } + + private _version = 0; + public get version(): number { + return this._version; + } + + constructor(params: IChatRequestModelParameters) { + this._session = params.session; + this.message = params.message; + this._variableData = params.variableData; + this.timestamp = params.timestamp; + this._attempt = params.attempt ?? 0; + this.modeInfo = params.modeInfo; + this._confirmation = params.confirmation; + this._locationData = params.locationData; + this._attachedContext = params.attachedContext; + this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; + this.modelId = params.modelId; + this.id = params.restoredId ?? 'request_' + generateUuid(); + this._editedFileEvents = params.editedFileEvents; + this.userSelectedTools = params.userSelectedTools; + } + + adoptTo(session: ChatModel) { + this._session = session; + } +} + +class AbstractResponse implements IResponse { + protected _responseParts: IChatProgressResponseContent[]; + + /** + * A stringified representation of response data which might be presented to a screenreader or used when copying a response. + */ + protected _responseRepr = ''; + + /** + * Just the markdown content of the response, used for determining the rendering rate of markdown + */ + protected _markdownContent = ''; + + get value(): IChatProgressResponseContent[] { + return this._responseParts; + } + + constructor(value: IChatProgressResponseContent[]) { + this._responseParts = value; + this._updateRepr(); + } + + toString(): string { + return this._responseRepr; + } + + /** + * _Just_ the content of markdown parts in the response + */ + getMarkdown(): string { + return this._markdownContent; + } + + protected _updateRepr() { + this._responseRepr = this.partsToRepr(this._responseParts); + + this._markdownContent = this._responseParts.map(part => { + if (part.kind === 'inlineReference') { + return this.inlineRefToRepr(part); + } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { + return part.content.value; + } else { + return ''; + } + }) + .filter(s => s.length > 0) + .join(''); + } + + private partsToRepr(parts: readonly IChatProgressResponseContent[]): string { + const blocks: string[] = []; + let currentBlockSegments: string[] = []; + let hasEditGroupsAfterLastClear = false; + + for (const part of parts) { + let segment: { text: string; isBlock?: boolean } | undefined; + switch (part.kind) { + case 'clearToPreviousToolInvocation': + currentBlockSegments = []; + blocks.length = 0; + hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing + continue; + case 'treeData': + case 'progressMessage': + case 'codeblockUri': + case 'extensions': + case 'pullRequest': + case 'undoStop': + case 'workspaceEdit': + case 'elicitation2': + case 'elicitationSerialized': + case 'thinking': + case 'multiDiffData': + case 'mcpServersStarting': + case 'questionCarousel': + // Ignore + continue; + case 'toolInvocation': + case 'toolInvocationSerialized': + // Include tool invocations in the copy text + segment = this.getToolInvocationText(part); + break; + case 'inlineReference': + segment = { text: this.inlineRefToRepr(part) }; + break; + case 'command': + segment = { text: part.command.title, isBlock: true }; + break; + case 'textEditGroup': + case 'notebookEditGroup': + // Mark that we have edit groups after the last clear + hasEditGroupsAfterLastClear = true; + // Skip individual edit groups to avoid duplication + continue; + case 'confirmation': + if (part.message instanceof MarkdownString) { + segment = { text: `${part.title}\n${part.message.value}`, isBlock: true }; + break; + } + segment = { text: `${part.title}\n${part.message}`, isBlock: true }; + break; + case 'markdownContent': + case 'markdownVuln': + case 'progressTask': + case 'progressTaskSerialized': + case 'warning': + segment = { text: part.content.value }; + break; + default: + // Ignore any unknown/obsolete parts, but assert that all are handled: + softAssertNever(part); + continue; + } + + if (segment.isBlock) { + if (currentBlockSegments.length) { + blocks.push(currentBlockSegments.join('')); + currentBlockSegments = []; + } + blocks.push(segment.text); + } else { + currentBlockSegments.push(segment.text); + } + } + + if (currentBlockSegments.length) { + blocks.push(currentBlockSegments.join('')); + } + + // Add consolidated edit summary at the end if there were any edit groups after the last clear + if (hasEditGroupsAfterLastClear) { + blocks.push(localize('editsSummary', "Made changes.")); + } + + return blocks.join('\n\n'); + } + + private inlineRefToRepr(part: IChatContentInlineReference) { + if ('uri' in part.inlineReference) { + return this.uriToRepr(part.inlineReference.uri); + } + + return 'name' in part.inlineReference + ? '`' + part.inlineReference.name + '`' + : this.uriToRepr(part.inlineReference); + } + + private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } { + // Extract the message and input details + let message = ''; + let input = ''; + + if (toolInvocation.pastTenseMessage) { + message = typeof toolInvocation.pastTenseMessage === 'string' + ? toolInvocation.pastTenseMessage + : toolInvocation.pastTenseMessage.value; + } else { + message = typeof toolInvocation.invocationMessage === 'string' + ? toolInvocation.invocationMessage + : toolInvocation.invocationMessage.value; + } + + // Handle different types of tool invocations + if (toolInvocation.toolSpecificData) { + if (toolInvocation.toolSpecificData.kind === 'terminal') { + message = 'Ran terminal command'; + const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData); + input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original; + } + } + + // Format the tool invocation text + let text = message; + if (input) { + text += `: ${input}`; + } + + // For completed tool invocations, also include the result details if available + if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isComplete(toolInvocation))) { + const resultDetails = IChatToolInvocation.resultDetails(toolInvocation); + if (resultDetails && 'input' in resultDetails) { + const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || IChatToolInvocation.isComplete(toolInvocation) ? 'Completed' : 'Errored'; + text += `\n${resultPrefix} with input: ${resultDetails.input}`; + } + } + + return { text, isBlock: true }; + } + + private uriToRepr(uri: URI): string { + if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) { + return uri.toString(false); + } + + return basename(uri); + } +} + +/** A view of a subset of a response */ +class ResponseView extends AbstractResponse { + constructor( + _response: IResponse, + public readonly undoStop: string, + ) { + let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop); + // Undo stops are inserted before `codeblockUri`'s, which are preceeded by a + // markdownContent containing the opening code fence. Adjust the index + // backwards to avoid a buggy response if it looked like this happened. + if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') { + idx--; + } + + super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx)); + } +} + +export class Response extends AbstractResponse implements IDisposable { + private _onDidChangeValue = new Emitter(); + public get onDidChangeValue() { + return this._onDidChangeValue.event; + } + + private _citations: IChatCodeCitation[] = []; + + + constructor(value: IMarkdownString | ReadonlyArray) { + super(asArray(value).map((v) => ( + 'kind' in v ? v : + isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : + { kind: 'treeData', treeData: v } + ))); + } + + dispose(): void { + this._onDidChangeValue.dispose(); + } + + + clear(): void { + this._responseParts = []; + this._updateRepr(true); + } + + clearToPreviousToolInvocation(message?: string): void { + // look through the response parts and find the last tool invocation, then slice the response parts to that point + let lastToolInvocationIndex = -1; + for (let i = this._responseParts.length - 1; i >= 0; i--) { + const part = this._responseParts[i]; + if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') { + lastToolInvocationIndex = i; + break; + } + } + if (lastToolInvocationIndex !== -1) { + this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1); + } else { + this._responseParts = []; + } + if (message) { + this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) }); + } + this._updateRepr(true); + } + + updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void { + if (progress.kind === 'clearToPreviousToolInvocation') { + if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) { + this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt.")); + } else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) { + this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt.")); + } else { + this.clearToPreviousToolInvocation(); + } + return; + } else if (progress.kind === 'markdownContent') { + + // last response which is NOT a text edit group because we do want to support heterogenous streaming but not have + // the MD be chopped up by text edit groups (and likely other non-renderable parts) + const lastResponsePart = this._responseParts + .filter(p => p.kind !== 'textEditGroup') + .at(-1); + + if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) { + // The last part can't be merged with- not markdown, or markdown with different permissions + this._responseParts.push(progress); + } else { + // Don't modify the current object, since it's being diffed by the renderer + const idx = this._responseParts.indexOf(lastResponsePart); + this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) }; + } + this._updateRepr(quiet); + } else if (progress.kind === 'thinking') { + + // tries to split thinking chunks if it is an array. only while certain models give us array chunks. + const lastResponsePart = this._responseParts + .filter(p => p.kind !== 'textEditGroup') + .at(-1); + + const lastText = lastResponsePart && lastResponsePart.kind === 'thinking' + ? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || '')) + : ''; + const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || ''); + const isEmpty = (s: string) => s.length === 0; + + // Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking + if (!lastResponsePart + || lastResponsePart.kind !== 'thinking' + || isEmpty(currText) + || isEmpty(lastText) + || !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) { + this._responseParts.push(progress); + } else { + const idx = this._responseParts.indexOf(lastResponsePart); + this._responseParts[idx] = { + ...lastResponsePart, + value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value + }; + } + this._updateRepr(quiet); + } else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') { + // merge edits for the same file no matter when they come in + const notebookUri = CellUri.parse(progress.uri)?.notebook; + const uri = notebookUri ?? progress.uri; + const isExternalEdit = progress.isExternalEdit; + + if (progress.kind === 'textEdit' && !notebookUri) { + // Text edits to a regular (non-notebook) file + this._mergeOrPushTextEditGroup(uri, progress.edits, progress.done, isExternalEdit); + } else if (progress.kind === 'textEdit') { + // Text edits to a notebook cell - convert to ICellTextEditOperation + const cellEdits = progress.edits.map(edit => ({ uri: progress.uri, edit })); + this._mergeOrPushNotebookEditGroup(uri, cellEdits, progress.done, isExternalEdit); + } else { + // Notebook cell edits (ICellEditOperation) + this._mergeOrPushNotebookEditGroup(uri, progress.edits, progress.done, isExternalEdit); + } + this._updateRepr(quiet); + } else if (progress.kind === 'progressTask') { + // Add a new resolving part + const responsePosition = this._responseParts.push(progress) - 1; + this._updateRepr(quiet); + + const disp = progress.onDidAddProgress(() => { + this._updateRepr(false); + }); + + progress.task?.().then((content) => { + // Stop listening for progress updates once the task settles + disp.dispose(); + + // Replace the resolving part's content with the resolved response + if (typeof content === 'string') { + (this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content); + } + this._updateRepr(false); + }); + + } else if (progress.kind === 'toolInvocation') { + autorunSelfDisposable(reader => { + progress.state.read(reader); // update repr when state changes + this._updateRepr(false); + + if (IChatToolInvocation.isComplete(progress, reader)) { + reader.dispose(); + } + }); + this._responseParts.push(progress); + this._updateRepr(quiet); + } else { + this._responseParts.push(progress); + this._updateRepr(quiet); + } + } + + public addCitation(citation: IChatCodeCitation) { + this._citations.push(citation); + this._updateRepr(); + } + + private _mergeOrPushTextEditGroup(uri: URI, edits: TextEdit[], done: boolean | undefined, isExternalEdit: boolean | undefined): void { + for (const candidate of this._responseParts) { + if (candidate.kind === 'textEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) { + candidate.edits.push(edits); + candidate.done = done; + return; + } + } + this._responseParts.push({ kind: 'textEditGroup', uri, edits: [edits], done, isExternalEdit }); + } + + private _mergeOrPushNotebookEditGroup(uri: URI, edits: ICellTextEditOperation[] | ICellEditOperation[], done: boolean | undefined, isExternalEdit: boolean | undefined): void { + for (const candidate of this._responseParts) { + if (candidate.kind === 'notebookEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) { + candidate.edits.push(edits); + candidate.done = done; + return; + } + } + this._responseParts.push({ kind: 'notebookEditGroup', uri, edits: [edits], done, isExternalEdit }); + } + + protected override _updateRepr(quiet?: boolean) { + super._updateRepr(); + if (!this._onDidChangeValue) { + return; // called from parent constructor + } + + this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : ''; + + if (!quiet) { + this._onDidChangeValue.fire(); + } + } +} + +export interface IChatResponseModelParameters { + responseContent: IMarkdownString | ReadonlyArray; + session: ChatModel; + agent?: IChatAgentData; + slashCommand?: IChatAgentCommand; + requestId: string; + timestamp?: number; + vote?: ChatAgentVoteDirection; + voteDownReason?: ChatAgentVoteDownReason; + result?: IChatAgentResult; + followups?: ReadonlyArray; + isCompleteAddedRequest?: boolean; + shouldBeRemovedOnSend?: IChatRequestDisablement; + shouldBeBlocked?: boolean; + restoredId?: string; + modelState?: ResponseModelStateT; + timeSpentWaiting?: number; + /** + * undefined means it will be set later. + */ + codeBlockInfos: ICodeBlockInfo[] | undefined; +} + +export type ResponseModelStateT = + | { value: ResponseModelState.Pending } + | { value: ResponseModelState.NeedsInput } + | { value: ResponseModelState.Complete | ResponseModelState.Cancelled | ResponseModelState.Failed; completedAt: number }; + +export class ChatResponseModel extends Disposable implements IChatResponseModel { + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + public readonly id: string; + public readonly requestId: string; + private _session: ChatModel; + private _agent: IChatAgentData | undefined; + private _slashCommand: IChatAgentCommand | undefined; + private _modelState = observableValue(this, { value: ResponseModelState.Pending }); + private _vote?: ChatAgentVoteDirection; + private _voteDownReason?: ChatAgentVoteDownReason; + private _result?: IChatAgentResult; + private _usage?: IChatUsage; + private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined; + public readonly isCompleteAddedRequest: boolean; + private readonly _shouldBeBlocked = observableValue(this, false); + private readonly _timestamp: number; + private _timeSpentWaitingAccumulator: number; + + public confirmationAdjustedTimestamp: IObservable; + + public get shouldBeBlocked(): IObservable { + return this._shouldBeBlocked; + } + + public get request(): IChatRequestModel | undefined { + return this.session.getRequests().find(r => r.id === this.requestId); + } + + public get session() { + return this._session; + } + + public get shouldBeRemovedOnSend() { + return this._shouldBeRemovedOnSend; + } + + public get isComplete(): boolean { + return this._modelState.get().value !== ResponseModelState.Pending && this._modelState.get().value !== ResponseModelState.NeedsInput; + } + + public get timestamp(): number { + return this._timestamp; + } + + public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) { + if (this._shouldBeRemovedOnSend === disablement) { + return; + } + + this._shouldBeRemovedOnSend = disablement; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + public get isCanceled(): boolean { + return this._modelState.get().value === ResponseModelState.Cancelled; + } + + public get completedAt(): number | undefined { + const state = this._modelState.get(); + if (state.value === ResponseModelState.Complete || state.value === ResponseModelState.Cancelled || state.value === ResponseModelState.Failed) { + return state.completedAt; + } + return undefined; + } + + public get state(): ResponseModelState { + const state = this._modelState.get().value; + if (state === ResponseModelState.Complete && !!this._result?.errorDetails && this.result?.errorDetails?.code !== 'canceled') { + // This check covers sessions created in previous vscode versions which saved a failed response as 'Complete' + return ResponseModelState.Failed; + } + + return state; + } + + public get stateT(): ResponseModelStateT { + return this._modelState.get(); + } + + public get vote(): ChatAgentVoteDirection | undefined { + return this._vote; + } + + public get voteDownReason(): ChatAgentVoteDownReason | undefined { + return this._voteDownReason; + } + + public get followups(): IChatFollowup[] | undefined { + return this._followups; + } + + private _response: Response; + private _finalizedResponse?: IResponse; + public get entireResponse(): IResponse { + return this._finalizedResponse || this._response; + } + + public get result(): IChatAgentResult | undefined { + return this._result; + } + + public get usage(): IChatUsage | undefined { + return this._usage; + } + + public get username(): string { + return this.session.responderUsername; + } + + private _followups?: IChatFollowup[]; + + public get agent(): IChatAgentData | undefined { + return this._agent; + } + + public get slashCommand(): IChatAgentCommand | undefined { + return this._slashCommand; + } + + private _agentOrSlashCommandDetected: boolean | undefined; + public get agentOrSlashCommandDetected(): boolean { + return this._agentOrSlashCommandDetected ?? false; + } + + private _usedContext: IChatUsedContext | undefined; + public get usedContext(): IChatUsedContext | undefined { + return this._usedContext; + } + + private readonly _contentReferences: IChatContentReference[] = []; + public get contentReferences(): ReadonlyArray { + return Array.from(this._contentReferences); + } + + private readonly _codeCitations: IChatCodeCitation[] = []; + public get codeCitations(): ReadonlyArray { + return this._codeCitations; + } + + private readonly _progressMessages: IChatProgressMessage[] = []; + public get progressMessages(): ReadonlyArray { + return this._progressMessages; + } + + private _isStale: boolean = false; + public get isStale(): boolean { + return this._isStale; + } + + + readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>; + + readonly isInProgress: IObservable; + + private _responseView?: ResponseView; + public get response(): IResponse { + const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop; + if (!undoStop) { + return this._finalizedResponse || this._response; + } + + if (this._responseView?.undoStop !== undoStop) { + this._responseView = new ResponseView(this._response, undoStop); + } + + return this._responseView; + } + + private _codeBlockInfos: ICodeBlockInfo[] | undefined; + public get codeBlockInfos(): ICodeBlockInfo[] | undefined { + return this._codeBlockInfos; + } + + constructor(params: IChatResponseModelParameters) { + super(); + + this._session = params.session; + this._agent = params.agent; + this._slashCommand = params.slashCommand; + this.requestId = params.requestId; + this._timestamp = params.timestamp || Date.now(); + if (params.modelState) { + this._modelState.set(params.modelState, undefined); + } + this._timeSpentWaitingAccumulator = params.timeSpentWaiting || 0; + this._vote = params.vote; + this._voteDownReason = params.voteDownReason; + this._result = params.result; + this._followups = params.followups ? [...params.followups] : undefined; + this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; + this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend; + this._shouldBeBlocked.set(params.shouldBeBlocked ?? false, undefined); + + // If we are creating a response with some existing content, consider it stale + this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0); + + this._response = this._register(new Response(params.responseContent)); + this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined; + + const signal = observableSignalFromEvent(this, this.onDidChange); + + const _pendingInfo = signal.map((_value, r): string | undefined => { + signal.read(r); + + for (const part of this._response.value) { + if (part.kind === 'toolInvocation') { + const state = part.state.read(r); + if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) { + const title = state.confirmationMessages?.title; + return title ? (isMarkdownString(title) ? title.value : title) : undefined; + } + if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) { + return localize('waitingForPostApproval', "Approve tool result?"); + } + } + if (part.kind === 'confirmation' && !part.isUsed) { + return part.title; + } + if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) { + const title = part.title; + return isMarkdownString(title) ? title.value : title; + } + } + + return undefined; + }); + + const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined); + this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined); + + this.isInProgress = signal.map((_value, r) => { + + signal.read(r); + + return !_pendingInfo.read(r) + && !this.shouldBeRemovedOnSend + && (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput); + }); + + this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason))); + this.id = params.restoredId ?? 'response_' + generateUuid(); + + let lastStartedWaitingAt: number | undefined = undefined; + this.confirmationAdjustedTimestamp = derived(reader => { + const pending = this.isPendingConfirmation.read(reader); + if (pending) { + this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined); + if (!lastStartedWaitingAt) { + lastStartedWaitingAt = pending.startedWaitingAt; + } + } else if (lastStartedWaitingAt) { + // Restore state to Pending if it was set to NeedsInput by this observable + if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) { + this._modelState.set({ value: ResponseModelState.Pending }, undefined); + } + this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt; + lastStartedWaitingAt = undefined; + } + + return this._timestamp + this._timeSpentWaitingAccumulator; + }).recomputeInitiallyAndOnChange(this._store); + } + + initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void { + if (this._codeBlockInfos) { + throw new BugIndicatingError('Code block infos have already been initialized'); + } + this._codeBlockInfos = [...codeBlockInfo]; + } + + setBlockedState(isBlocked: boolean): void { + this._shouldBeBlocked.set(isBlocked, undefined); + } + + /** + * Apply a progress update to the actual response content. + */ + updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) { + this._response.updateContent(responsePart, quiet); + } + + /** + * Adds an undo stop at the current position in the stream. + */ + addUndoStop(undoStop: IChatUndoStop) { + this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id }); + this._response.updateContent(undoStop, true); + } + + /** + * Apply one of the progress updates that are not part of the actual response content. + */ + applyReference(progress: IChatUsedContext | IChatContentReference) { + if (progress.kind === 'usedContext') { + this._usedContext = progress; + } else if (progress.kind === 'reference') { + this._contentReferences.push(progress); + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + } + + applyCodeCitation(progress: IChatCodeCitation) { + this._codeCitations.push(progress); + this._response.addCitation(progress); + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { + this._agent = agent; + this._slashCommand = slashCommand; + this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + setResult(result: IChatAgentResult): void { + this._result = result; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + setUsage(usage: IChatUsage): void { + this._usage = usage; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + complete(): void { + // No-op if it's already complete + if (this.isComplete) { + return; + } + if (this._result?.errorDetails?.responseIsRedacted) { + this._response.clear(); + } + + // Canceled sessions can be considered 'Complete' + const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete; + this._modelState.set({ value: state, completedAt: Date.now() }, undefined); + this._onDidChange.fire({ reason: 'completedRequest' }); + } + + cancel(): void { + this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined); + this._onDidChange.fire({ reason: 'completedRequest' }); + } + + setFollowups(followups: IChatFollowup[] | undefined): void { + this._followups = followups; + this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row + } + + setVote(vote: ChatAgentVoteDirection): void { + this._vote = vote; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void { + this._voteDownReason = reason; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean { + if (!this.response.value.includes(edit)) { + return false; + } + if (!edit.state) { + return false; + } + edit.state.applied = editCount; // must not be edit.edits.length + this._onDidChange.fire(defaultChatResponseModelChangeReason); + return true; + } + + adoptTo(session: ChatModel) { + this._session = session; + this._onDidChange.fire(defaultChatResponseModelChangeReason); + } + + + finalizeUndoState(): void { + this._finalizedResponse = this.response; + this._responseView = undefined; + this._shouldBeRemovedOnSend = undefined; + } + + toJSON(): ISerializableChatResponseData { + const modelState = this._modelState.get(); + const pendingConfirmation = this.isPendingConfirmation.get(); + + return { + responseId: this.id, + result: this.result, + responseMarkdownInfo: this.codeBlockInfos?.map(info => ({ suggestionId: info.suggestionId })), + followups: this.followups, + modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState, + vote: this.vote, + voteDownReason: this.voteDownReason, + slashCommand: this.slashCommand, + usedContext: this.usedContext, + contentReferences: this.contentReferences, + codeCitations: this.codeCitations, + timestamp: this._timestamp, + timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator, + } satisfies WithDefinedProps; + } +} + + +export interface IChatRequestDisablement { + requestId: string; + afterUndoStop?: string; +} + +/** + * Information about a chat request that needs user input to continue. + */ +export interface IChatRequestNeedsInputInfo { + /** The chat session title */ + readonly title: string; + /** Optional detail message, e.g., " needs approval to run." */ + readonly detail?: string; +} + +export interface IChatModel extends IDisposable { + readonly onDidDispose: Event; + readonly onDidChange: Event; + /** @deprecated Use {@link sessionResource} instead */ + readonly sessionId: string; + /** Milliseconds timestamp this chat model was created. */ + readonly timestamp: number; + readonly timing: IChatSessionTiming; + readonly sessionResource: URI; + readonly initialLocation: ChatAgentLocation; + readonly title: string; + readonly hasCustomTitle: boolean; + readonly responderUsername: string; + /** True whenever a request is currently running */ + readonly requestInProgress: IObservable; + /** Provides session information when a request needs user interaction to continue */ + readonly requestNeedsInput: IObservable; + readonly inputPlaceholder?: string; + readonly editingSession?: IChatEditingSession | undefined; + readonly checkpoint: IChatRequestModel | undefined; + startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void; + /** Input model for managing input state */ + readonly inputModel: IInputModel; + readonly hasRequests: boolean; + readonly lastRequest: IChatRequestModel | undefined; + /** Whether this model will be kept alive while it is running or has edits */ + readonly willKeepAlive: boolean; + readonly lastRequestObs: IObservable; + getRequests(): IChatRequestModel[]; + setCheckpoint(requestId: string | undefined): void; + + toExport(): IExportableChatData; + toJSON(): ISerializableChatData; + readonly contributedChatSession: IChatSessionContext | undefined; + + readonly repoData: IExportableRepoData | undefined; + setRepoData(data: IExportableRepoData | undefined): void; +} + +export interface ISerializableChatsData { + [sessionId: string]: ISerializableChatData; +} + +export type ISerializableChatAgentData = UriDto; + +interface ISerializableChatResponseData { + responseId?: string; + result?: IChatAgentResult; // Optional for backcompat + responseMarkdownInfo?: ISerializableMarkdownInfo[]; + followups?: ReadonlyArray; + modelState?: ResponseModelStateT; + vote?: ChatAgentVoteDirection; + voteDownReason?: ChatAgentVoteDownReason; + timestamp?: number; + slashCommand?: IChatAgentCommand; + /** For backward compat: should be optional */ + usedContext?: IChatUsedContext; + contentReferences?: ReadonlyArray; + codeCitations?: ReadonlyArray; + timeSpentWaiting?: number; +} + +export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized; + +export interface ISerializableChatRequestData extends ISerializableChatResponseData { + requestId: string; + message: string | IParsedChatRequest; // string => old format + /** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */ + variableData: IChatRequestVariableData; + response: ReadonlyArray | undefined; + + /**Old, persisted name for shouldBeRemovedOnSend */ + isHidden?: boolean; + shouldBeRemovedOnSend?: IChatRequestDisablement; + agent?: ISerializableChatAgentData; + // responseErrorDetails: IChatResponseErrorDetails | undefined; + /** @deprecated modelState is used instead now */ + isCanceled?: boolean; + timestamp?: number; + confirmation?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; + modelId?: string; +} + +export interface ISerializableMarkdownInfo { + readonly suggestionId: EditSuggestionId; +} + +/** + * Repository state captured for chat session export. + * Enables reproducing the workspace state by cloning, checking out the commit, and applying diffs. + */ +export interface IExportableRepoData { + /** + * Classification of the workspace's version control state. + * - `remote-git`: Git repo with a configured remote URL + * - `local-git`: Git repo without any remote (local only) + * - `plain-folder`: Not a git repository + */ + workspaceType: 'remote-git' | 'local-git' | 'plain-folder'; + + /** + * Sync status between local and remote. + * - `synced`: Local HEAD matches remote tracking branch (fully pushed) + * - `unpushed`: Local has commits not pushed to the remote tracking branch + * - `unpublished`: Local branch has no remote tracking branch configured + * - `local-only`: No remote configured (local git repo only) + * - `no-git`: Not a git repository + */ + syncStatus: 'synced' | 'unpushed' | 'unpublished' | 'local-only' | 'no-git'; + + /** + * Remote URL of the repository (e.g., https://github.com/org/repo.git). + * Undefined if no remote is configured. + */ + remoteUrl?: string; + + /** + * Vendor/host of the remote repository. + * Undefined if no remote is configured. + */ + remoteVendor?: 'github' | 'ado' | 'other'; + + /** + * Remote tracking branch for the current branch (e.g., "origin/feature/my-work"). + * Undefined if branch is unpublished or no remote. + */ + remoteTrackingBranch?: string; + + /** + * Default remote branch used as base for unpublished branches (e.g., "origin/main"). + * Helpful for computing merge-base when branch has no tracking. + */ + remoteBaseBranch?: string; + + /** + * Commit hash of the remote tracking branch HEAD. + * Undefined if branch has no remote tracking branch. + */ + remoteHeadCommit?: string; + + /** + * Name of the current local branch (e.g., "feature/my-work"). + */ + localBranch?: string; + + /** + * Commit hash of the local HEAD when captured. + */ + localHeadCommit?: string; + + /** + * Working tree diffs (uncommitted changes). + */ + diffs?: IExportableRepoDiff[]; + + /** + * Status of the diffs collection. + * - `included`: Diffs were successfully captured and included + * - `tooManyChanges`: Diffs skipped because >100 files changed (degenerate case like mass renames) + * - `tooLarge`: Diffs skipped because total size exceeded 900KB + * - `trimmedForStorage`: Diffs were trimmed to save storage (older session) + * - `noChanges`: No working tree changes detected + * - `notCaptured`: Diffs not captured (default/undefined case) + */ + diffsStatus?: 'included' | 'tooManyChanges' | 'tooLarge' | 'trimmedForStorage' | 'noChanges' | 'notCaptured'; + + /** + * Number of changed files detected, even if diffs were not included. + */ + changedFileCount?: number; +} + +/** + * A file change exported as a unified diff patch compatible with `git apply`. + */ +export interface IExportableRepoDiff { + relativePath: string; + changeType: 'added' | 'modified' | 'deleted' | 'renamed'; + oldRelativePath?: string; + unifiedDiff?: string; + status: string; +} + +export interface IExportableChatData { + initialLocation: ChatAgentLocation | undefined; + requests: ISerializableChatRequestData[]; + responderUsername: string; +} + +/* + NOTE: every time the serialized data format is updated, we need to create a new interface, because we may need to handle any old data format when parsing. +*/ + +export interface ISerializableChatData1 extends IExportableChatData { + sessionId: string; + creationDate: number; +} + +export interface ISerializableChatData2 extends ISerializableChatData1 { + version: 2; + computedTitle: string | undefined; +} + +export interface ISerializableChatData3 extends Omit { + version: 3; + customTitle: string | undefined; + /** + * Whether the session had pending edits when it was stored. + * todo@connor4312 This will be cleaned up with the globalization of edits. + */ + hasPendingEdits?: boolean; + /** Current draft input state (added later, fully backwards compatible) */ + inputState?: ISerializableChatModelInputState; + repoData?: IExportableRepoData; +} + +/** + * Input model for managing chat input state independently from the chat model. + * This keeps display logic separated from the core chat model. + * + * The input model: + * - Manages the current draft state (text, attachments, mode, model selection, cursor/selection) + * - Provides an observable interface for reactive UI updates + * - Automatically persists through the chat model's serialization + * - Enables bidirectional sync between the UI (ChatInputPart) and the model + * - Uses `undefined` state to indicate no persisted state (new/empty chat) + * + * This architecture ensures that: + * - Input state is preserved when moving chats between editor/sidebar/window + * - No manual state transfer is needed when switching contexts + * - The UI stays in sync with the persisted state + * - New chats use UI defaults (persisted preferences) instead of hardcoded values + */ +export interface IInputModel { + /** Observable for current input state (undefined for new/uninitialized chats) */ + readonly state: IObservable; + + /** Update the input state (partial update) */ + setState(state: Partial): void; + + /** Clear input state (after sending or clearing) */ + clearState(): void; + + /** Serializes the state */ + toJSON(): ISerializableChatModelInputState | undefined; +} + +/** + * Represents the current state of the chat input that hasn't been sent yet. + * This is the "draft" state that should be preserved across sessions. + */ +export interface IChatModelInputState { + /** Current attachments in the input */ + attachments: readonly IChatRequestVariableEntry[]; + + /** Currently selected chat mode */ + mode: { + /** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */ + id: string; + /** Mode kind for builtin modes */ + kind: ChatModeKind | undefined; + }; + + /** Currently selected language model, if any */ + selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined; + + /** Current input text */ + inputText: string; + + /** Current selection ranges */ + selections: ISelection[]; + + /** Contributed stored state */ + contrib: Record; +} + +/** + * Serializable version of IChatModelInputState + */ +export interface ISerializableChatModelInputState { + attachments: readonly IChatRequestVariableEntry[]; + mode: { + id: string; + kind: ChatModeKind | undefined; + }; + selectedModel: { + identifier: string; + metadata: ILanguageModelChatMetadata; + } | undefined; + inputText: string; + selections: ISelection[]; + contrib: Record; +} + +/** +* Chat data that has been parsed and normalized to the current format. +*/ +export type ISerializableChatData = ISerializableChatData3; + +export type IChatDataSerializerLog = ObjectMutationLog; + +export interface ISerializedChatDataReference { + value: ISerializableChatData | IExportableChatData; + serializer: IChatDataSerializerLog; +} + +/** + * Chat data that has been loaded but not normalized, and could be any format + */ +export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3; + +/** + * Normalize chat data from storage to the current format. + * TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too. + */ +export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData { + normalizeOldFields(raw); + + if (!('version' in raw)) { + return { + version: 3, + ...raw, + customTitle: undefined, + }; + } + + if (raw.version === 2) { + return { + ...raw, + version: 3, + customTitle: raw.computedTitle + }; + } + + return raw; +} + +function normalizeOldFields(raw: ISerializableChatDataIn): void { + // Fill in fields that very old chat data may be missing + if (!raw.sessionId) { + raw.sessionId = generateUuid(); + } + + if (!raw.creationDate) { + raw.creationDate = getLastYearDate(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts + if ((raw.initialLocation as any) === 'editing-session') { + raw.initialLocation = ChatAgentLocation.Chat; + } +} + +function getLastYearDate(): number { + const lastYearDate = new Date(); + lastYearDate.setFullYear(lastYearDate.getFullYear() - 1); + return lastYearDate.getTime(); +} + +export function isExportableSessionData(obj: unknown): obj is IExportableChatData { + return !!obj && + Array.isArray((obj as IExportableChatData).requests) && + typeof (obj as IExportableChatData).responderUsername === 'string'; +} + +export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { + const data = obj as ISerializableChatData; + return isExportableSessionData(obj) && + typeof data.creationDate === 'number' && + typeof data.sessionId === 'string' && + obj.requests.every((request: ISerializableChatRequestData) => + !request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext) + ); +} + +export type IChatChangeEvent = + | IChatInitEvent + | IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent + | IChatAddResponseEvent + | IChatSetAgentEvent + | IChatMoveEvent + | IChatSetHiddenEvent + | IChatCompletedRequestEvent + | IChatSetCustomTitleEvent + ; + +export interface IChatAddRequestEvent { + kind: 'addRequest'; + request: IChatRequestModel; +} + +export interface IChatChangedRequestEvent { + kind: 'changedRequest'; + request: IChatRequestModel; +} + +export interface IChatCompletedRequestEvent { + kind: 'completedRequest'; + request: IChatRequestModel; +} + +export interface IChatAddResponseEvent { + kind: 'addResponse'; + response: IChatResponseModel; +} + +export const enum ChatRequestRemovalReason { + /** + * "Normal" remove + */ + Removal, + + /** + * Removed because the request will be resent + */ + Resend, + + /** + * Remove because the request is moving to another model + */ + Adoption +} + +export interface IChatRemoveRequestEvent { + kind: 'removeRequest'; + requestId: string; + responseId?: string; + reason: ChatRequestRemovalReason; +} + +export interface IChatSetHiddenEvent { + kind: 'setHidden'; +} + +export interface IChatMoveEvent { + kind: 'move'; + target: URI; + range: IRange; +} + +export interface IChatSetAgentEvent { + kind: 'setAgent'; + agent: IChatAgentData; + command?: IChatAgentCommand; +} + +export interface IChatSetCustomTitleEvent { + kind: 'setCustomTitle'; + title: string; +} + +export interface IChatInitEvent { + kind: 'initialize'; +} + +/** + * Internal implementation of IInputModel + */ +class InputModel implements IInputModel { + private readonly _state: ReturnType>; + readonly state: IObservable; + + constructor(initialState: IChatModelInputState | undefined) { + this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState); + this.state = this._state; + } + + setState(state: Partial): void { + const current = this._state.get(); + this._state.set({ + // If current is undefined, provide defaults for required fields + attachments: [], + mode: { id: 'agent', kind: ChatModeKind.Agent }, + selectedModel: undefined, + inputText: '', + selections: [], + contrib: {}, + ...current, + ...state + }, undefined); + } + + clearState(): void { + this._state.set(undefined, undefined); + } + + toJSON(): ISerializableChatModelInputState | undefined { + const value = this.state.get(); + if (!value) { + return undefined; + } + + // Filter out extension-contributed context items (kind: 'string' or implicit entries with StringChatContextValue) + // These have handles that become invalid after window reload and cannot be properly restored. + const persistableAttachments = value.attachments.filter(attachment => { + if (isStringVariableEntry(attachment)) { + return false; + } + if (isImplicitVariableEntry(attachment) && isStringImplicitContextValue(attachment.value)) { + return false; + } + return true; + }); + + return { + contrib: value.contrib, + attachments: persistableAttachments, + mode: value.mode, + selectedModel: value.selectedModel ? { + identifier: value.selectedModel.identifier, + metadata: value.selectedModel.metadata + } : undefined, + inputText: value.inputText, + selections: value.selections + }; + } +} + +export class ChatModel extends Disposable implements IChatModel { + static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string { + const firstRequestMessage = requests.at(0)?.message ?? ''; + const message = typeof firstRequestMessage === 'string' ? + firstRequestMessage : + firstRequestMessage.text; + return message.split('\n')[0].substring(0, 200); + } + + private readonly _onDidDispose = this._register(new Emitter()); + readonly onDidDispose = this._onDidDispose.event; + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + private _requests: ChatRequestModel[]; + + private _contributedChatSession: IChatSessionContext | undefined; + public get contributedChatSession(): IChatSessionContext | undefined { + return this._contributedChatSession; + } + public setContributedChatSession(session: IChatSessionContext | undefined) { + this._contributedChatSession = session; + } + + private _repoData: IExportableRepoData | undefined; + public get repoData(): IExportableRepoData | undefined { + return this._repoData; + } + public setRepoData(data: IExportableRepoData | undefined): void { + this._repoData = data; + } + + readonly lastRequestObs: IObservable; + + // TODO to be clear, this is not the same as the id from the session object, which belongs to the provider. + // It's easier to be able to identify this model before its async initialization is complete + private readonly _sessionId: string; + /** @deprecated Use {@link sessionResource} instead */ + get sessionId(): string { + return this._sessionId; + } + + private readonly _sessionResource: URI; + get sessionResource(): URI { + return this._sessionResource; + } + + readonly requestInProgress: IObservable; + readonly requestNeedsInput: IObservable; + + /** Input model for managing input state */ + readonly inputModel: InputModel; + + get hasRequests(): boolean { + return this._requests.length > 0; + } + + get lastRequest(): ChatRequestModel | undefined { + return this._requests.at(-1); + } + + private _timestamp: number; + get timestamp(): number { + return this._timestamp; + } + + get timing(): IChatSessionTiming { + const lastRequest = this._requests.at(-1); + const lastResponse = lastRequest?.response; + const lastRequestStarted = lastRequest?.timestamp; + const lastRequestEnded = lastResponse?.completedAt ?? lastResponse?.timestamp; + return { + created: this._timestamp, + lastRequestStarted, + lastRequestEnded, + }; + } + + get lastMessageDate(): number { + return this._requests.at(-1)?.timestamp ?? this._timestamp; + } + + private get _defaultAgent() { + return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask); + } + + private readonly _initialResponderUsername: string | undefined; + get responderUsername(): string { + return this._defaultAgent?.fullName ?? + this._initialResponderUsername ?? ''; + } + + private _isImported = false; + get isImported(): boolean { + return this._isImported; + } + + private _customTitle: string | undefined; + get customTitle(): string | undefined { + return this._customTitle; + } + + get title(): string { + return this._customTitle || ChatModel.getDefaultTitle(this._requests); + } + + get hasCustomTitle(): boolean { + return this._customTitle !== undefined; + } + + private _editingSession: IChatEditingSession | undefined; + + get editingSession(): IChatEditingSession | undefined { + return this._editingSession; + } + + private readonly _initialLocation: ChatAgentLocation; + get initialLocation(): ChatAgentLocation { + return this._initialLocation; + } + + private readonly _canUseTools: boolean = true; + get canUseTools(): boolean { + return this._canUseTools; + } + + private _disableBackgroundKeepAlive: boolean; + get willKeepAlive(): boolean { + return !this._disableBackgroundKeepAlive; + } + + public dataSerializer?: IChatDataSerializerLog; + + constructor( + dataRef: ISerializedChatDataReference | undefined, + initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean }, + @ILogService private readonly logService: ILogService, + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatEditingService private readonly chatEditingService: IChatEditingService, + @IChatService private readonly chatService: IChatService, + ) { + super(); + + const initialData = dataRef?.value; + const isValidExportedData = isExportableSessionData(initialData); + const isValidFullData = isValidExportedData && isSerializableSessionData(initialData); + if (initialData && !isValidExportedData) { + this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`); + } + + this._isImported = !!initialData && isValidExportedData && !isValidFullData; + this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid(); + this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId); + this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false; + + this._requests = initialData ? this._deserialize(initialData) : []; + this._timestamp = (isValidFullData && initialData.creationDate) || Date.now(); + this._customTitle = isValidFullData ? initialData.customTitle : undefined; + + // Initialize input model from serialized data (undefined for new chats) + const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined); + this.inputModel = new InputModel(serializedInputState && { + attachments: serializedInputState.attachments, + mode: serializedInputState.mode, + selectedModel: serializedInputState.selectedModel && { + identifier: serializedInputState.selectedModel.identifier, + metadata: serializedInputState.selectedModel.metadata + }, + contrib: serializedInputState.contrib, + inputText: serializedInputState.inputText, + selections: serializedInputState.selections + }); + + this.dataSerializer = dataRef?.serializer; + this._initialResponderUsername = initialData?.responderUsername; + + this._repoData = isValidFullData && initialData.repoData ? initialData.repoData : undefined; + + this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation; + + this._canUseTools = initialModelProps.canUseTools; + + this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1)); + + this._register(autorun(reader => { + const request = this.lastRequestObs.read(reader); + if (!request?.response) { + return; + } + + reader.store.add(request.response.onDidChange(async ev => { + if (!this._editingSession || ev.reason !== 'completedRequest') { + return; + } + + this._onDidChange.fire({ kind: 'completedRequest', request }); + })); + })); + + this.requestInProgress = this.lastRequestObs.map((request, r) => { + return request?.response?.isInProgress.read(r) ?? false; + }); + + this.requestNeedsInput = this.lastRequestObs.map((request, r) => { + const pendingInfo = request?.response?.isPendingConfirmation.read(r); + if (!pendingInfo) { + return undefined; + } + return { + title: this.title, + detail: pendingInfo.detail, + }; + }); + + // Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background + // only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often? + if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) { + const selfRef = this._register(new MutableDisposable()); + this._register(autorun(r => { + const inProgress = this.requestInProgress.read(r); + const needsInput = this.requestNeedsInput.read(r); + const shouldStayAlive = inProgress || !!needsInput; + if (shouldStayAlive && !selfRef.value) { + selfRef.value = chatService.getActiveSessionReference(this._sessionResource); + } else if (!shouldStayAlive && selfRef.value) { + selfRef.clear(); + } + })); + } + } + + startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void { + const session = this._editingSession ??= this._register( + transferFromSession + ? this.chatEditingService.transferEditingSession(this, transferFromSession) + : isGlobalEditingSession + ? this.chatEditingService.startOrContinueGlobalEditingSession(this) + : this.chatEditingService.createEditingSession(this) + ); + + if (!this._disableBackgroundKeepAlive) { + // todo@connor4312: hold onto a reference so background sessions don't + // trigger early disposal. This will be cleaned up with the globalization of edits. + const selfRef = this._register(new MutableDisposable()); + this._register(autorun(r => { + const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified); + if (hasModified && !selfRef.value) { + selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource); + } else if (!hasModified && selfRef.value) { + selfRef.clear(); + } + })); + } + + this._register(autorun(reader => { + this._setDisabledRequests(session.requestDisablement.read(reader)); + })); + } + + private currentEditedFileEvents = new ResourceMap(); + notifyEditingAction(action: IChatEditingSessionAction): void { + const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep : + action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo : + action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null; + if (state === null) { + return; + } + + if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) { + this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri }); + } + } + + private _deserialize(obj: IExportableChatData | ISerializedChatDataReference): ChatRequestModel[] { + const requests = hasKey(obj, { serializer: true }) ? obj.value.requests : obj.requests; + if (!Array.isArray(requests)) { + this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`); + return []; + } + + try { + return requests.map((raw: ISerializableChatRequestData) => { + const parsedRequest = + typeof raw.message === 'string' + ? this.getParsedRequestFromString(raw.message) + : reviveParsedChatRequest(raw.message); + + // Old messages don't have variableData, or have it in the wrong (non-array) shape + const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); + const request = new ChatRequestModel({ + session: this, + message: parsedRequest, + variableData, + timestamp: raw.timestamp ?? -1, + restoredId: raw.requestId, + confirmation: raw.confirmation, + editedFileEvents: raw.editedFileEvents, + modelId: raw.modelId, + }); + request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts + if (raw.response || raw.result || (raw as any).responseErrorDetails) { + const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format + reviveSerializedAgent(raw.agent) : undefined; + + // Port entries from old format + const result = 'responseErrorDetails' in raw ? + // eslint-disable-next-line local/code-no-dangerous-type-assertions + { errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result; + let modelState = raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: Date.now() }; + if (modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput) { + modelState = { value: ResponseModelState.Cancelled, completedAt: Date.now() }; + } + + request.response = new ChatResponseModel({ + responseContent: raw.response ?? [new MarkdownString(raw.response)], + session: this, + agent, + slashCommand: raw.slashCommand, + requestId: request.id, + modelState, + vote: raw.vote, + timestamp: raw.timestamp, + voteDownReason: raw.voteDownReason, + result, + followups: raw.followups, + restoredId: raw.responseId, + timeSpentWaiting: raw.timeSpentWaiting, + shouldBeBlocked: request.shouldBeBlocked.get(), + codeBlockInfos: raw.responseMarkdownInfo?.map(info => ({ suggestionId: info.suggestionId })), + }); + request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; + if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? + request.response.applyReference(revive(raw.usedContext)); + } + + raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r))); + raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c))); + } + return request; + }); + } catch (error) { + this.logService.error('Failed to parse chat data', error); + return []; + } + } + + private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData { + const variableData = raw && Array.isArray(raw.variables) + ? raw : + { variables: [] }; + + variableData.variables = variableData.variables.map(IChatRequestVariableEntry.fromExport); + + return variableData; + } + + private getParsedRequestFromString(message: string): IParsedChatRequest { + // TODO These offsets won't be used, but chat replies need to go through the parser as well + const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; + return { + text: message, + parts + }; + } + + + + getRequests(): ChatRequestModel[] { + return this._requests; + } + + resetCheckpoint(): void { + for (const request of this._requests) { + request.setShouldBeBlocked(false); + } + } + + setCheckpoint(requestId: string | undefined) { + let checkpoint: ChatRequestModel | undefined; + let checkpointIndex = -1; + if (requestId !== undefined) { + this._requests.forEach((request, index) => { + if (request.id === requestId) { + checkpointIndex = index; + checkpoint = request; + request.setShouldBeBlocked(true); + } + }); + + if (!checkpoint) { + return; // Invalid request ID + } + } + + for (let i = this._requests.length - 1; i >= 0; i -= 1) { + const request = this._requests[i]; + if (this._checkpoint && !checkpoint) { + request.setShouldBeBlocked(false); + } else if (checkpoint && i >= checkpointIndex) { + request.setShouldBeBlocked(true); + if (request.response) { + request.response.setBlockedState(true); + } + } else if (checkpoint && i < checkpointIndex) { + request.setShouldBeBlocked(false); + } + } + + this._checkpoint = checkpoint; + } + + private _checkpoint: ChatRequestModel | undefined = undefined; + public get checkpoint() { + return this._checkpoint; + } + + private _setDisabledRequests(requestIds: IChatRequestDisablement[]) { + this._requests.forEach((request) => { + const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id); + request.shouldBeRemovedOnSend = shouldBeRemovedOnSend; + if (request.response) { + request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend; + } + }); + + this._onDidChange.fire({ kind: 'setHidden' }); + } + + addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools, id?: string): ChatRequestModel { + const editedFileEvents = [...this.currentEditedFileEvents.values()]; + this.currentEditedFileEvents.clear(); + const request = new ChatRequestModel({ + restoredId: id, + session: this, + message, + variableData, + timestamp: Date.now(), + attempt, + modeInfo, + confirmation, + locationData, + attachedContext: attachments, + isCompleteAddedRequest, + modelId, + editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined, + userSelectedTools, + }); + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + agent: chatAgent, + slashCommand, + requestId: request.id, + isCompleteAddedRequest, + codeBlockInfos: undefined, + }); + + this._requests.push(request); + this._onDidChange.fire({ kind: 'addRequest', request }); + return request; + } + + public setCustomTitle(title: string): void { + this._customTitle = title; + this._onDidChange.fire({ kind: 'setCustomTitle', title }); + } + + updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) { + request.variableData = variableData; + this._onDidChange.fire({ kind: 'changedRequest', request }); + } + + adoptRequest(request: ChatRequestModel): void { + // this doesn't use `removeRequest` because it must not dispose the request object + const oldOwner = request.session; + const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id); + + if (index === -1) { + return; + } + + oldOwner._requests.splice(index, 1); + + request.adoptTo(this); + request.response?.adoptTo(this); + this._requests.push(request); + + oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption }); + this._onDidChange.fire({ kind: 'addRequest', request }); + } + + acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void { + if (!request.response) { + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + requestId: request.id, + codeBlockInfos: undefined, + }); + } + + if (request.response.isComplete) { + throw new Error('acceptResponseProgress: Adding progress to a completed response'); + } + + if (progress.kind === 'usedContext' || progress.kind === 'reference') { + request.response.applyReference(progress); + } else if (progress.kind === 'codeCitation') { + request.response.applyCodeCitation(progress); + } else if (progress.kind === 'move') { + this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range }); + } else if (progress.kind === 'codeblockUri' && progress.isEdit) { + request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' }); + request.response.updateContent(progress, quiet); + } else if (progress.kind === 'progressTaskResult') { + // Should have been handled upstream, not sent to model + this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); + } else { + request.response.updateContent(progress, quiet); + } + } + + removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void { + const index = this._requests.findIndex(request => request.id === id); + const request = this._requests[index]; + + if (index !== -1) { + this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason }); + this._requests.splice(index, 1); + request.response?.dispose(); + } + } + + cancelRequest(request: ChatRequestModel): void { + if (request.response) { + request.response.cancel(); + } + } + + setResponse(request: ChatRequestModel, result: IChatAgentResult): void { + if (!request.response) { + request.response = new ChatResponseModel({ + responseContent: [], + session: this, + requestId: request.id, + codeBlockInfos: undefined, + }); + } + + request.response.setResult(result); + } + + setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void { + if (!request.response) { + // Maybe something went wrong? + return; + } + request.response.setFollowups(followups); + } + + setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void { + request.response = response; + this._onDidChange.fire({ kind: 'addResponse', response }); + } + + toExport(): IExportableChatData { + return { + responderUsername: this.responderUsername, + initialLocation: this.initialLocation, + requests: this._requests.map((r): ISerializableChatRequestData => { + const message = { + ...r.message, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p) + }; + const agent = r.response?.agent; + const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() : + agent ? { ...agent } : undefined; + return { + requestId: r.id, + message, + variableData: IChatRequestVariableData.toExport(r.variableData), + response: r.response ? + r.response.entireResponse.value.map(item => { + // Keeping the shape of the persisted data the same for back compat + if (item.kind === 'treeData') { + return item.treeData; + } else if (item.kind === 'markdownContent') { + return item.content; + } else { + // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + return item as any; // TODO + } + }) + : undefined, + shouldBeRemovedOnSend: r.shouldBeRemovedOnSend, + agent: agentJson, + timestamp: r.timestamp, + confirmation: r.confirmation, + editedFileEvents: r.editedFileEvents, + modelId: r.modelId, + ...r.response?.toJSON(), + }; + }), + }; + } + + toJSON(): ISerializableChatData { + return { + version: 3, + ...this.toExport(), + sessionId: this.sessionId, + creationDate: this._timestamp, + customTitle: this._customTitle, + inputState: this.inputModel.toJSON(), + repoData: this._repoData, + }; + } + + override dispose() { + this._requests.forEach(r => r.response?.dispose()); + this._onDidDispose.fire(); + + super.dispose(); + } +} + +export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData { + return { + variables: variableData.variables.map(v => ({ + ...v, + range: v.range && { + start: v.range.start - diff, + endExclusive: v.range.endExclusive - diff + } + })) + }; +} + +export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean { + if (md1.baseUri && md2.baseUri) { + const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme + && md1.baseUri.authority === md2.baseUri.authority + && md1.baseUri.path === md2.baseUri.path + && md1.baseUri.query === md2.baseUri.query + && md1.baseUri.fragment === md2.baseUri.fragment; + if (!baseUriEquals) { + return false; + } + } else if (md1.baseUri || md2.baseUri) { + return false; + } + + return equals(md1.isTrusted, md2.isTrusted) && + md1.supportHtml === md2.supportHtml && + md1.supportThemeIcons === md2.supportThemeIcons; +} + +export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { + const appendedValue = typeof md2 === 'string' ? md2 : md2.value; + return { + value: md1.value + appendedValue, + isTrusted: md1.isTrusted, + supportThemeIcons: md1.supportThemeIcons, + supportHtml: md1.supportHtml, + baseUri: md1.baseUri + }; +} + +export function getCodeCitationsMessage(citations: ReadonlyArray): string { + if (citations.length === 0) { + return ''; + } + + const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set()); + const label = licenseTypes.size === 1 ? + localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) : + localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size); + return label; +} + +export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, +} + +export interface IChatAgentEditedFileEvent { + readonly uri: URI; + readonly eventKind: ChatRequestEditedFileEventKind; +} + +/** URI for a resource embedded in a chat request/response */ +export namespace ChatResponseResource { + export const scheme = 'vscode-chat-response-resource'; + + export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI { + return URI.from({ + scheme: ChatResponseResource.scheme, + authority: encodeHex(VSBuffer.fromString(sessionResource.toString())), + path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''), + }); + } + + export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } { + if (uri.scheme !== ChatResponseResource.scheme) { + return undefined; + } + + const parts = uri.path.split('/'); + if (parts.length < 5) { + return undefined; + } + + const [, kind, toolCallId, index] = parts; + if (kind !== 'tool') { + return undefined; + } + + let sessionResource: URI; + try { + sessionResource = URI.parse(decodeHex(uri.authority).toString()); + } catch (e) { + if (e instanceof SyntaxError) { // pre-1.108 local session ID + sessionResource = LocalChatSessionUri.forSession(uri.authority); + } else { + throw e; + } + } + + return { + sessionResource, + toolCallId: toolCallId, + index: Number(index), + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts new file mode 100644 index 00000000000..42305065ed5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatModelStore.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../base/common/event.js'; +import { DisposableStore, IDisposable, IReference, ReferenceCollection } from '../../../../../base/common/lifecycle.js'; +import { ObservableMap } from '../../../../../base/common/observable.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { ChatAgentLocation } from '../constants.js'; +import { IChatEditingSession } from '../editing/chatEditingService.js'; +import { ChatModel, ISerializableChatModelInputState, ISerializedChatDataReference } from './chatModel.js'; + +export interface IStartSessionProps { + readonly initialData?: ISerializedChatDataReference; + readonly location: ChatAgentLocation; + readonly sessionResource: URI; + readonly sessionId?: string; + readonly canUseTools: boolean; + readonly transferEditingSession?: IChatEditingSession; + readonly disableBackgroundKeepAlive?: boolean; + readonly inputState?: ISerializableChatModelInputState; +} + +export interface ChatModelStoreDelegate { + createModel: (props: IStartSessionProps) => ChatModel; + willDisposeModel: (model: ChatModel) => Promise; +} + +export class ChatModelStore extends ReferenceCollection implements IDisposable { + private readonly _store = new DisposableStore(); + + private readonly _models = new ObservableMap(); + private readonly _modelsToDispose = new Set(); + private readonly _pendingDisposals = new Set>(); + + private readonly _onDidDisposeModel = this._store.add(new Emitter()); + public readonly onDidDisposeModel = this._onDidDisposeModel.event; + + private readonly _onDidCreateModel = this._store.add(new Emitter()); + public readonly onDidCreateModel = this._onDidCreateModel.event; + + constructor( + private readonly delegate: ChatModelStoreDelegate, + @ILogService private readonly logService: ILogService, + ) { + super(); + } + + public get observable() { + return this._models.observable; + } + + public values(): Iterable { + return this._models.values(); + } + + /** + * Get a ChatModel directly without acquiring a reference. + */ + public get(uri: URI): ChatModel | undefined { + return this._models.get(this.toKey(uri)); + } + + public has(uri: URI): boolean { + return this._models.has(this.toKey(uri)); + } + + public acquireExisting(uri: URI): IReference | undefined { + const key = this.toKey(uri); + if (!this._models.has(key)) { + return undefined; + } + return this.acquire(key); + } + + public acquireOrCreate(props: IStartSessionProps): IReference { + return this.acquire(this.toKey(props.sessionResource), props); + } + + protected createReferencedObject(key: string, props?: IStartSessionProps): ChatModel { + this._modelsToDispose.delete(key); + const existingModel = this._models.get(key); + if (existingModel) { + return existingModel; + } + + if (!props) { + throw new Error(`No start session props provided for chat session ${key}`); + } + + this.logService.trace(`Creating chat session ${key}`); + const model = this.delegate.createModel(props); + if (model.sessionResource.toString() !== key) { + throw new Error(`Chat session key mismatch for ${key}`); + } + this._models.set(key, model); + this._onDidCreateModel.fire(model); + return model; + } + + protected destroyReferencedObject(key: string, object: ChatModel): void { + this._modelsToDispose.add(key); + const promise = this.doDestroyReferencedObject(key, object); + this._pendingDisposals.add(promise); + promise.finally(() => { + this._pendingDisposals.delete(promise); + }); + } + + private async doDestroyReferencedObject(key: string, object: ChatModel): Promise { + try { + await this.delegate.willDisposeModel(object); + } catch (error) { + this.logService.error(error); + } finally { + if (this._modelsToDispose.has(key)) { + this.logService.trace(`Disposing chat session ${key}`); + this._models.delete(key); + this._onDidDisposeModel.fire(object); + object.dispose(); + } + this._modelsToDispose.delete(key); + } + } + + /** + * For test use only + */ + async waitForModelDisposals(): Promise { + await Promise.all(this._pendingDisposals); + } + + private toKey(uri: URI): string { + return uri.toString(); + } + + dispose(): void { + this._store.dispose(); + this._models.forEach(model => model.dispose()); + } +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatElicitationRequestPart.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatElicitationRequestPart.ts new file mode 100644 index 00000000000..69913fbf76e --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatElicitationRequestPart.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 { IAction } from '../../../../../../base/common/actions.js'; +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { ElicitationState, IChatElicitationRequest, IChatElicitationRequestSerialized } from '../../chatService/chatService.js'; +import { ToolDataSource } from '../../tools/languageModelToolsService.js'; + +export class ChatElicitationRequestPart implements IChatElicitationRequest { + public readonly kind = 'elicitation2'; + public state = observableValue('state', ElicitationState.Pending); + public acceptedResult?: Record; + + private readonly _isHiddenValue = observableValue('isHidden', false); + public readonly isHidden: IObservable = this._isHiddenValue; + public reject?: (() => Promise) | undefined; + + constructor( + public readonly title: string | IMarkdownString, + public readonly message: string | IMarkdownString, + public readonly subtitle: string | IMarkdownString, + public readonly acceptButtonLabel: string, + public readonly rejectButtonLabel: string | undefined, + // True when the primary action is accepted, otherwise the action that was selected + private readonly _accept: (value: IAction | true) => Promise, + reject?: () => Promise, + public readonly source?: ToolDataSource, + public readonly moreActions?: IAction[], + public readonly onHide?: () => void, + ) { + if (reject) { + this.reject = async () => { + const state = await reject!(); + this.state.set(state, undefined); + }; + } + } + + accept(value: IAction | true): Promise { + return this._accept(value).then(state => { + this.state.set(state, undefined); + }); + } + + hide(): void { + if (this._isHiddenValue.get()) { + return; + } + this._isHiddenValue.set(true, undefined, undefined); + this.onHide?.(); + if (this.state.get() === ElicitationState.Pending) { + this.state.set(ElicitationState.Rejected, undefined); + } + } + + public toJSON() { + const state = this.state.get(); + + return { + kind: 'elicitationSerialized', + title: this.title, + message: this.message, + state: state === ElicitationState.Pending ? ElicitationState.Rejected : state, + acceptedResult: this.acceptedResult, + subtitle: this.subtitle, + source: this.source, + isHidden: this._isHiddenValue.get(), + } satisfies IChatElicitationRequestSerialized; + } +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts new file mode 100644 index 00000000000..c0421469402 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatProgressTypes/chatToolInvocation.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { encodeBase64 } from '../../../../../../base/common/buffer.js'; +import { IMarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IObservable, ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { localize } from '../../../../../../nls.js'; +import { ConfirmedReason, IChatExtensionsContent, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind, type IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; +import { IPreparedToolInvocation, isToolResultOutputDetails, IToolConfirmationMessages, IToolData, IToolProgressStep, IToolResult, ToolDataSource } from '../../tools/languageModelToolsService.js'; + +export interface IStreamingToolCallOptions { + toolCallId: string; + toolId: string; + toolData: IToolData; + subagentInvocationId?: string; + chatRequestId?: string; +} + +export class ChatToolInvocation implements IChatToolInvocation { + public readonly kind: 'toolInvocation' = 'toolInvocation'; + + public invocationMessage: string | IMarkdownString; + public readonly originMessage: string | IMarkdownString | undefined; + public pastTenseMessage: string | IMarkdownString | undefined; + public confirmationMessages: IToolConfirmationMessages | undefined; + public presentation: IPreparedToolInvocation['presentation']; + public readonly toolId: string; + public source: ToolDataSource; + public readonly subAgentInvocationId: string | undefined; + public parameters: unknown; + public generatedTitle?: string; + public readonly chatRequestId?: string; + + public toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData; + + private readonly _progress = observableValue<{ message?: string | IMarkdownString; progress: number | undefined }>(this, { progress: 0 }); + private readonly _state: ISettableObservable; + + // Streaming-related observables + private readonly _partialInput = observableValue(this, undefined); + private readonly _streamingMessage = observableValue(this, undefined); + + public get state(): IObservable { + return this._state; + } + + /** + * Create a tool invocation in streaming state. + * Use this when the tool call is beginning to stream partial input from the LM. + */ + public static createStreaming(options: IStreamingToolCallOptions): ChatToolInvocation { + return new ChatToolInvocation(undefined, options.toolData, options.toolCallId, options.subagentInvocationId, undefined, true, options.chatRequestId); + } + + constructor( + preparedInvocation: IPreparedToolInvocation | undefined, + toolData: IToolData, + public readonly toolCallId: string, + subAgentInvocationId: string | undefined, + parameters: unknown, + isStreaming: boolean = false, + chatRequestId?: string + ) { + // For streaming invocations, use a default message until handleToolStream provides one + const defaultStreamingMessage = isStreaming ? localize('toolInvocationMessage', "Using \"{0}\"", toolData.displayName) : ''; + this.invocationMessage = preparedInvocation?.invocationMessage ?? defaultStreamingMessage; + this.pastTenseMessage = preparedInvocation?.pastTenseMessage; + this.originMessage = preparedInvocation?.originMessage; + this.confirmationMessages = preparedInvocation?.confirmationMessages; + this.presentation = preparedInvocation?.presentation; + this.toolSpecificData = preparedInvocation?.toolSpecificData; + this.toolId = toolData.id; + this.source = toolData.source; + this.subAgentInvocationId = subAgentInvocationId; + this.parameters = parameters; + this.chatRequestId = chatRequestId; + + if (isStreaming) { + // Start in streaming state + this._state = observableValue(this, { + type: IChatToolInvocation.StateKind.Streaming, + partialInput: this._partialInput, + streamingMessage: this._streamingMessage, + }); + } else if (!this.confirmationMessages?.title) { + this._state = observableValue(this, { + type: IChatToolInvocation.StateKind.Executing, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }); + } else { + this._state = observableValue(this, { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + confirm: reason => { + if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + } + }); + } + } + + /** + * Update the partial input observable during streaming. + */ + public updatePartialInput(input: unknown): void { + if (this._state.get().type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only update in streaming state + } + this._partialInput.set(input, undefined); + } + + /** + * Update the streaming message (from handleToolStream). + */ + public updateStreamingMessage(message: string | IMarkdownString): void { + const state = this._state.get(); + if (state.type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only update in streaming state + } + this._streamingMessage.set(message, undefined); + } + + /** + * Transition from streaming state to prepared/executing state. + * Called when the full tool call is ready. + */ + public transitionFromStreaming(preparedInvocation: IPreparedToolInvocation | undefined, parameters: unknown, autoConfirmed: ConfirmedReason | undefined): void { + const currentState = this._state.get(); + if (currentState.type !== IChatToolInvocation.StateKind.Streaming) { + return; // Only transition from streaming state + } + + // Preserve the last streaming message if no new invocation message is provided + const lastStreamingMessage = this._streamingMessage.get(); + if (lastStreamingMessage && !preparedInvocation?.invocationMessage) { + this.invocationMessage = lastStreamingMessage; + } + + // Update fields from prepared invocation + this.parameters = parameters; + if (preparedInvocation) { + if (preparedInvocation.invocationMessage) { + this.invocationMessage = preparedInvocation.invocationMessage; + } + this.pastTenseMessage = preparedInvocation.pastTenseMessage; + this.confirmationMessages = preparedInvocation.confirmationMessages; + this.presentation = preparedInvocation.presentation; + this.toolSpecificData = preparedInvocation.toolSpecificData; + } + + const confirm = (reason: ConfirmedReason) => { + if (reason.type === ToolConfirmKind.Denied || reason.type === ToolConfirmKind.Skipped) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: reason.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: reason, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + }; + + // Transition to the appropriate state + if (autoConfirmed) { + confirm(autoConfirmed); + } else if (!this.confirmationMessages?.title) { + this._state.set({ + type: IChatToolInvocation.StateKind.Executing, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded, reason: this.confirmationMessages?.confirmationNotNeededReason }, + progress: this._progress, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + confirm, + }, undefined); + } + } + + private _setCompleted(result: IToolResult | undefined, postConfirmed?: ConfirmedReason | undefined) { + if (postConfirmed && (postConfirmed.type === ToolConfirmKind.Denied || postConfirmed.type === ToolConfirmKind.Skipped)) { + this._state.set({ + type: IChatToolInvocation.StateKind.Cancelled, + reason: postConfirmed.type, + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + return; + } + + this._state.set({ + type: IChatToolInvocation.StateKind.Completed, + confirmed: IChatToolInvocation.executionConfirmedOrDenied(this) || { type: ToolConfirmKind.ConfirmationNotNeeded }, + resultDetails: result?.toolResultDetails, + postConfirmed, + contentForModel: result?.content || [], + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + + public async didExecuteTool(result: IToolResult | undefined, final?: boolean, checkIfResultAutoApproved?: () => Promise): Promise { + if (result?.toolResultMessage) { + this.pastTenseMessage = result.toolResultMessage; + } else if (this._progress.get().message) { + this.pastTenseMessage = this._progress.get().message; + } + + if (this.confirmationMessages?.confirmResults && !result?.toolResultError && result?.confirmResults !== false && !final) { + const autoApproved = await checkIfResultAutoApproved?.(); + if (autoApproved) { + this._setCompleted(result, autoApproved); + } else { + this._state.set({ + type: IChatToolInvocation.StateKind.WaitingForPostApproval, + confirmed: IChatToolInvocation.executionConfirmedOrDenied(this) || { type: ToolConfirmKind.ConfirmationNotNeeded }, + resultDetails: result?.toolResultDetails, + contentForModel: result?.content || [], + confirm: reason => this._setCompleted(result, reason), + parameters: this.parameters, + confirmationMessages: this.confirmationMessages, + }, undefined); + } + } else { + this._setCompleted(result); + } + + return this._state.get(); + } + + public acceptProgress(step: IToolProgressStep) { + const prev = this._progress.get(); + this._progress.set({ + progress: step.progress || prev.progress || 0, + message: step.message, + }, undefined); + } + + public toJSON(): IChatToolInvocationSerialized { + // persist the serialized call as 'skipped' if we were waiting for postapproval + const waitingForPostApproval = this.state.get().type === IChatToolInvocation.StateKind.WaitingForPostApproval; + const details = waitingForPostApproval ? undefined : IChatToolInvocation.resultDetails(this); + + return { + kind: 'toolInvocationSerialized', + presentation: this.presentation, + invocationMessage: this.invocationMessage, + pastTenseMessage: this.pastTenseMessage, + originMessage: this.originMessage, + isConfirmed: waitingForPostApproval ? { type: ToolConfirmKind.Skipped } : IChatToolInvocation.executionConfirmedOrDenied(this), + isComplete: true, + source: this.source, + resultDetails: isToolResultOutputDetails(details) + ? { output: { type: 'data', mimeType: details.output.mimeType, base64Data: encodeBase64(details.output.value) } } + : details, + toolSpecificData: this.toolSpecificData, + toolCallId: this.toolCallId, + toolId: this.toolId, + subAgentInvocationId: this.subAgentInvocationId, + generatedTitle: this.generatedTitle, + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts new file mode 100644 index 00000000000..dcd4a9978db --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -0,0 +1,179 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { isMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { equals as objectsEqual } from '../../../../../base/common/objects.js'; +import { isEqual as _urisEqual } from '../../../../../base/common/resources.js'; +import { hasKey } from '../../../../../base/common/types.js'; +import { URI, UriComponents } from '../../../../../base/common/uri.js'; +import { IChatMarkdownContent, ResponseModelState } from '../chatService/chatService.js'; +import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; +import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { IChatAgentEditedFileEvent, IChatDataSerializerLog, IChatModel, IChatProgressResponseContent, IChatRequestModel, IChatRequestVariableData, ISerializableChatData, ISerializableChatModelInputState, ISerializableChatRequestData, SerializedChatResponsePart } from './chatModel.js'; +import * as Adapt from './objectMutationLog.js'; + +/** + * ChatModel has lots of properties and lots of ways those properties can mutate. + * The naive way to store the ChatModel is serializing it to JSON and calling it + * a day. However, chats can get very, very long, and thus doing so is slow. + * + * In this file, we define a `storageSchema` that adapters from the `IChatModel` + * into the serializable format. This schema tells us what properties in the chat + * model correspond to the serialized properties, *and how they change*. For + * example, `Adapt.constant(...)` defines a property that will never be checked + * for changes after it's written, and `Adapt.primitive(...)` defines a property + * that will be checked for changes using strict equality each time we store it. + * + * We can then use this to generate a log of mutations that we can append to + * cheaply without rewriting and reserializing the entire request each time. + */ + +const toJson = (obj: T): T extends { toJSON?(): infer R } ? R : T => { + const cast = obj as { toJSON?: () => T }; + // eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any + return (cast && typeof cast.toJSON === 'function' ? cast.toJSON() : obj) as any; +}; + +const responsePartSchema = Adapt.v( + (obj): SerializedChatResponsePart => obj.kind === 'markdownContent' ? obj.content : toJson(obj), + (a, b) => { + if (isMarkdownString(a) && isMarkdownString(b)) { + return a.value === b.value; + } + + if (hasKey(a, { kind: true }) && hasKey(b, { kind: true })) { + if (a.kind !== b.kind) { + return false; + } + + switch (a.kind) { + case 'markdownContent': + return a.content === (b as IChatMarkdownContent).content; + + // Dynamic types that can change after initial push need deep equality + // Note: these are the *serialized* kind names (e.g. toolInvocationSerialized not toolInvocation) + case 'toolInvocationSerialized': + case 'elicitationSerialized': + case 'progressTaskSerialized': + case 'textEditGroup': + case 'multiDiffData': + case 'mcpServersStarting': + return objectsEqual(a, b); + + // Static types that won't change after being pushed can use strict equality. + case 'clearToPreviousToolInvocation': + case 'codeblockUri': + case 'command': + case 'confirmation': + case 'extensions': + case 'inlineReference': + case 'markdownVuln': + case 'notebookEditGroup': + case 'progressMessage': + case 'pullRequest': + case 'questionCarousel': + case 'thinking': + case 'undoStop': + case 'warning': + case 'treeData': + case 'workspaceEdit': + return a.kind === b.kind; + + default: { + // Hello developer! You are probably here because you added a new chat response type. + // This logic controls when we'll update chat parts stored on disk as part of the session. + // If it's a 'static' type that is not expected to change, add it to the 'return true' + // block above. However it's a type that is going to change, add it to the 'objectsEqual' + // block or make something more tailored. + assertNever(a); + } + } + } + + return false; + } +); + +const urisEqual = (a: UriComponents, b: UriComponents): boolean => { + return _urisEqual(URI.from(a), URI.from(b)); +}; + +const messageSchema = Adapt.object({ + text: Adapt.v(m => m.text), + parts: Adapt.v(m => m.parts, (a, b) => a.length === b.length && a.every((part, i) => part.text === b[i].text)), +}); + +const agentEditedFileEventSchema = Adapt.object({ + uri: Adapt.v(e => e.uri, urisEqual), + eventKind: Adapt.v(e => e.eventKind), +}); + +const chatVariableSchema = Adapt.object({ + variables: Adapt.t(v => v.variables, Adapt.array(Adapt.value((a, b) => a.name === b.name))), +}); + +const requestSchema = Adapt.object({ + // request parts + requestId: Adapt.t(m => m.id, Adapt.key()), + timestamp: Adapt.v(m => m.timestamp), + confirmation: Adapt.v(m => m.confirmation), + message: Adapt.t(m => m.message, messageSchema), + shouldBeRemovedOnSend: Adapt.v(m => m.shouldBeRemovedOnSend, objectsEqual), + agent: Adapt.v(m => m.response?.agent, (a, b) => a?.id === b?.id), + modelId: Adapt.v(m => m.modelId), + editedFileEvents: Adapt.t(m => m.editedFileEvents, Adapt.array(agentEditedFileEventSchema)), + variableData: Adapt.t(m => m.variableData, chatVariableSchema), + isHidden: Adapt.v(() => undefined), // deprecated, always undefined for new data + isCanceled: Adapt.v(() => undefined), // deprecated, modelState is used instead + + // response parts (from ISerializableChatResponseData via response.toJSON()) + response: Adapt.t(m => m.response?.entireResponse.value, Adapt.array(responsePartSchema)), + responseId: Adapt.v(m => m.response?.id), + result: Adapt.v(m => m.response?.result, objectsEqual), + responseMarkdownInfo: Adapt.v( + m => m.response?.codeBlockInfos?.map(info => ({ suggestionId: info.suggestionId })), + objectsEqual, + ), + followups: Adapt.v(m => m.response?.followups, objectsEqual), + modelState: Adapt.v(m => m.response?.stateT, objectsEqual), + vote: Adapt.v(m => m.response?.vote), + voteDownReason: Adapt.v(m => m.response?.voteDownReason), + slashCommand: Adapt.t(m => m.response?.slashCommand, Adapt.value((a, b) => a?.name === b?.name)), + usedContext: Adapt.v(m => m.response?.usedContext, objectsEqual), + contentReferences: Adapt.v(m => m.response?.contentReferences, objectsEqual), + codeCitations: Adapt.v(m => m.response?.codeCitations, objectsEqual), + timeSpentWaiting: Adapt.v(m => m.response?.timestamp), // based on response timestamp +}, { + sealed: (o) => o.modelState?.value === ResponseModelState.Cancelled || o.modelState?.value === ResponseModelState.Failed || o.modelState?.value === ResponseModelState.Complete, +}); + +const inputStateSchema = Adapt.object({ + attachments: Adapt.v(i => i.attachments, objectsEqual), + mode: Adapt.v(i => i.mode, (a, b) => a.id === b.id), + selectedModel: Adapt.v(i => i.selectedModel, (a, b) => a?.identifier === b?.identifier), + inputText: Adapt.v(i => i.inputText), + selections: Adapt.v(i => i.selections, objectsEqual), + contrib: Adapt.v(i => i.contrib, objectsEqual), +}); + +export const storageSchema = Adapt.object({ + version: Adapt.v(() => 3), + creationDate: Adapt.v(m => m.timestamp), + customTitle: Adapt.v(m => m.hasCustomTitle ? m.title : undefined), + initialLocation: Adapt.v(m => m.initialLocation), + inputState: Adapt.t(m => m.inputModel.toJSON(), inputStateSchema), + responderUsername: Adapt.v(m => m.responderUsername), + sessionId: Adapt.v(m => m.sessionId), + requests: Adapt.t(m => m.getRequests(), Adapt.array(requestSchema)), + hasPendingEdits: Adapt.v(m => m.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)), + repoData: Adapt.v(m => m.repoData, objectsEqual), +}); + +export class ChatSessionOperationLog extends Adapt.ObjectMutationLog implements IChatDataSerializerLog { + constructor() { + super(storageSchema, 1024); + } +} diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts new file mode 100644 index 00000000000..3bb3abaaafe --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionStore.ts @@ -0,0 +1,733 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Sequencer } from '../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { toErrorMessage } from '../../../../../base/common/errorMessage.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { revive } from '../../../../../base/common/marshalling.js'; +import { joinPath } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize } from '../../../../../nls.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../../platform/files/common/files.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IUserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { Dto } from '../../../../services/extensions/common/proxyIdentifier.js'; +import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; +import { awaitStatsForSession } from '../chat.js'; +import { IChatSessionStats, IChatSessionTiming, ResponseModelState } from '../chatService/chatService.js'; +import { ChatAgentLocation } from '../constants.js'; +import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; +import { ChatModel, ISerializableChatData, ISerializableChatDataIn, ISerializableChatsData, ISerializedChatDataReference, normalizeSerializableChatData } from './chatModel.js'; +import { ChatSessionOperationLog } from './chatSessionOperationLog.js'; +import { LocalChatSessionUri } from './chatUri.js'; + +const maxPersistedSessions = 50; + +const ChatIndexStorageKey = 'chat.ChatSessionStore.index'; +const ChatTransferIndexStorageKey = 'ChatSessionStore.transferIndex'; + +export class ChatSessionStore extends Disposable { + private readonly storageRoot: URI; + private readonly previousEmptyWindowStorageRoot: URI | undefined; + private readonly transferredSessionStorageRoot: URI; + + private readonly storeQueue = new Sequencer(); + + private storeTask: Promise | undefined; + private shuttingDown = false; + + constructor( + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @ILogService private readonly logService: ILogService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageService private readonly storageService: IStorageService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + + const workspace = this.workspaceContextService.getWorkspace(); + const isEmptyWindow = !workspace.configuration && workspace.folders.length === 0; + const workspaceId = this.workspaceContextService.getWorkspace().id; + this.storageRoot = isEmptyWindow ? + joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, 'emptyWindowChatSessions') : + joinPath(this.environmentService.workspaceStorageHome, workspaceId, 'chatSessions'); + + this.previousEmptyWindowStorageRoot = isEmptyWindow ? + joinPath(this.environmentService.workspaceStorageHome, 'no-workspace', 'chatSessions') : + undefined; + + this.transferredSessionStorageRoot = joinPath(this.userDataProfilesService.defaultProfile.globalStorageHome, 'transferredChatSessions'); + + this._register(this.lifecycleService.onWillShutdown(e => { + this.shuttingDown = true; + if (!this.storeTask) { + return; + } + + e.join(this.storeTask, { + id: 'join.chatSessionStore', + label: localize('join.chatSessionStore', "Saving chat history") + }); + })); + } + + async storeSessions(sessions: ChatModel[]): Promise { + if (this.shuttingDown) { + // Don't start this task if we missed the chance to block shutdown + return; + } + + try { + this.storeTask = this.storeQueue.queue(async () => { + try { + await Promise.all(sessions.map(session => this.writeSession(session))); + await this.trimEntries(); + await this.flushIndex(); + } catch (e) { + this.reportError('storeSessions', 'Error storing chat sessions', e); + } + }); + await this.storeTask; + } finally { + this.storeTask = undefined; + } + } + + async storeSessionsMetadataOnly(sessions: ChatModel[]): Promise { + if (this.shuttingDown) { + // Don't start this task if we missed the chance to block shutdown + return; + } + + try { + this.storeTask = this.storeQueue.queue(async () => { + try { + await Promise.all(sessions.map(session => this.writeSessionMetadataOnly(session))); + await this.flushIndex(); + } catch (e) { + this.reportError('storeSessions', 'Error storing chat sessions', e); + } + }); + await this.storeTask; + } finally { + this.storeTask = undefined; + } + } + + async storeTransferSession(transferData: IChatTransfer, session: ChatModel): Promise { + const index = this.getTransferredSessionIndex(); + const workspaceKey = transferData.toWorkspace.toString(); + + // Clean up any preexisting transferred session for this workspace + const existingTransfer = index[workspaceKey]; + if (existingTransfer) { + try { + const existingSessionResource = URI.revive(existingTransfer.sessionResource); + if (existingSessionResource && LocalChatSessionUri.parseLocalSessionId(existingSessionResource)) { + const existingStorageLocation = this.getTransferredSessionStorageLocation(existingSessionResource); + await this.fileService.del(existingStorageLocation); + } + } catch (e) { + if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { + this.reportError('storeTransferSession', 'Error deleting old transferred session file', e); + } + } + } + + try { + const content = JSON.stringify(session, undefined, 2); + const storageLocation = this.getTransferredSessionStorageLocation(session.sessionResource); + await this.fileService.writeFile(storageLocation, VSBuffer.fromString(content)); + } catch (e) { + this.reportError('sessionWrite', 'Error writing chat session', e); + return; + } + + index[workspaceKey] = transferData; + try { + this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); + } catch (e) { + this.reportError('storeTransferSession', 'Error storing chat transfer session', e); + } + } + + private getTransferredSessionIndex(): IChatTransferIndex { + try { + const data: IChatTransferIndex = this.storageService.getObject(ChatTransferIndexStorageKey, StorageScope.PROFILE, {}); + return data; + } catch (e) { + this.reportError('getTransferredSessionIndex', 'Error reading chat transfer index', e); + return {}; + } + } + + private static readonly TRANSFER_EXPIRATION_MS = 60 * 1000 * 5; + + getTransferredSessionData(): URI | undefined { + try { + const index = this.getTransferredSessionIndex(); + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + if (workspaceFolders.length !== 1) { + // Can only transfer sessions to single-folder workspaces + return undefined; + } + + const workspaceKey = workspaceFolders[0].uri.toString(); + const transferredSessionForWorkspace: IChatTransferDto = index[workspaceKey]; + if (!transferredSessionForWorkspace) { + return undefined; + } + + // Check if the transfer has expired + const revivedTransferData = revive(transferredSessionForWorkspace); + if (Date.now() - transferredSessionForWorkspace.timestampInMilliseconds > ChatSessionStore.TRANSFER_EXPIRATION_MS) { + this.logService.info('ChatSessionStore: Transferred session has expired'); + this.cleanupTransferredSession(revivedTransferData.sessionResource); + return undefined; + } + return !!LocalChatSessionUri.parseLocalSessionId(revivedTransferData.sessionResource) && revivedTransferData.sessionResource; + } catch (e) { + this.reportError('getTransferredSession', 'Error getting transferred chat session URI', e); + return undefined; + } + } + + async readTransferredSession(sessionResource: URI): Promise { + try { + const storageLocation = this.getTransferredSessionStorageLocation(sessionResource); + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + return undefined; + } + + const sessionData = await this.readSessionFromLocation(storageLocation, undefined, sessionId); + + // Clean up the transferred session after reading + await this.cleanupTransferredSession(sessionResource); + + return sessionData; + } catch (e) { + this.reportError('getTransferredSession', 'Error getting transferred chat session', e); + return undefined; + } + } + + private async cleanupTransferredSession(sessionResource: URI): Promise { + try { + // Remove from index + const index = this.getTransferredSessionIndex(); + const workspaceFolders = this.workspaceContextService.getWorkspace().folders; + if (workspaceFolders.length === 1) { + const workspaceKey = workspaceFolders[0].uri.toString(); + delete index[workspaceKey]; + this.storageService.store(ChatTransferIndexStorageKey, index, StorageScope.PROFILE, StorageTarget.MACHINE); + } + + // Delete the transferred session file + const storageLocation = this.getTransferredSessionStorageLocation(sessionResource); + await this.fileService.del(storageLocation); + } catch (e) { + if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { + this.reportError('cleanupTransferredSession', 'Error cleaning up transferred session', e); + } + } + } + + private async writeSession(session: ChatModel | ISerializableChatData): Promise { + try { + const index = this.internalGetIndex(); + const storageLocation = this.getStorageLocation(session.sessionId); + if (storageLocation.log) { + if (session instanceof ChatModel) { + if (!session.dataSerializer) { + session.dataSerializer = new ChatSessionOperationLog(); + } + + const { op, data } = session.dataSerializer.write(session); + if (data.byteLength > 0) { + await this.fileService.writeFile(storageLocation.log, data, { append: op === 'append' }); + } + } else { + const content = new ChatSessionOperationLog().createInitialFromSerialized(session); + await this.fileService.writeFile(storageLocation.log, content); + } + } else { + await this.fileService.writeFile(storageLocation.flat, VSBuffer.fromString(JSON.stringify(session))); + } + + // Write succeeded, update index + const newMetadata = await getSessionMetadata(session); + index.entries[session.sessionId] = newMetadata; + } catch (e) { + this.reportError('sessionWrite', 'Error writing chat session', e); + } + } + + private async writeSessionMetadataOnly(session: ChatModel): Promise { + // Only to be used for external sessions + if (LocalChatSessionUri.parseLocalSessionId(session.sessionResource)) { + return; + } + + try { + const index = this.internalGetIndex(); + + // TODO get this class on sessionResource + const externalSessionId = session.sessionResource.toString(); + index.entries[externalSessionId] = await getSessionMetadata(session); + } catch (e) { + this.reportError('sessionMetadataWrite', 'Error writing chat session metadata', e); + } + } + + private async flushIndex(): Promise { + const index = this.internalGetIndex(); + try { + this.storageService.store(ChatIndexStorageKey, index, this.getIndexStorageScope(), StorageTarget.MACHINE); + } catch (e) { + // Only if JSON.stringify fails, AFAIK + this.reportError('indexWrite', 'Error writing index', e); + } + } + + private getIndexStorageScope(): StorageScope { + const workspace = this.workspaceContextService.getWorkspace(); + const isEmptyWindow = !workspace.configuration && workspace.folders.length === 0; + return isEmptyWindow ? StorageScope.APPLICATION : StorageScope.WORKSPACE; + } + + private async trimEntries(): Promise { + const index = this.internalGetIndex(); + const entries = Object.entries(index.entries) + .filter(([_id, entry]) => !entry.isExternal) + .sort((a, b) => b[1].lastMessageDate - a[1].lastMessageDate) + .map(([id]) => id); + + if (entries.length > maxPersistedSessions) { + const entriesToDelete = entries.slice(maxPersistedSessions); + for (const entry of entriesToDelete) { + delete index.entries[entry]; + } + + this.logService.trace(`ChatSessionStore: Trimmed ${entriesToDelete.length} old chat sessions from index`); + } + } + + private async internalDeleteSession(sessionId: string): Promise { + const index = this.internalGetIndex(); + if (!index.entries[sessionId]) { + return; + } + + const storageLocation = this.getStorageLocation(sessionId); + for (const uri of [storageLocation.flat, storageLocation.log]) { + try { + if (uri) { + await this.fileService.del(uri); + } + } catch (e) { + if (toFileOperationResult(e) !== FileOperationResult.FILE_NOT_FOUND) { + this.reportError('sessionDelete', 'Error deleting chat session', e); + } + } + + delete index.entries[sessionId]; + } + } + + hasSessions(): boolean { + return Object.keys(this.internalGetIndex().entries).length > 0; + } + + isSessionEmpty(sessionId: string): boolean { + const index = this.internalGetIndex(); + return index.entries[sessionId]?.isEmpty ?? true; + } + + async deleteSession(sessionId: string): Promise { + await this.storeQueue.queue(async () => { + await this.internalDeleteSession(sessionId); + await this.flushIndex(); + }); + } + + async clearAllSessions(): Promise { + await this.storeQueue.queue(async () => { + const index = this.internalGetIndex(); + const entries = Object.keys(index.entries); + this.logService.info(`ChatSessionStore: Clearing ${entries.length} chat sessions`); + await Promise.all(entries.map(entry => this.internalDeleteSession(entry))); + await this.flushIndex(); + }); + } + + public async setSessionTitle(sessionId: string, title: string): Promise { + await this.storeQueue.queue(async () => { + const index = this.internalGetIndex(); + if (index.entries[sessionId]) { + index.entries[sessionId].title = title; + } + }); + } + + private reportError(reasonForTelemetry: string, message: string, error?: Error): void { + const fileOperationReason = error && toFileOperationResult(error); + + if (fileOperationReason === FileOperationResult.FILE_NOT_FOUND) { + // Expected case (e.g. reading a non-existent session); keep noise low + this.logService.trace(`ChatSessionStore: ` + message, toErrorMessage(error)); + } else { + // Unexpected or serious error; surface at error level + this.logService.error(`ChatSessionStore: ` + message, toErrorMessage(error)); + } + type ChatSessionStoreErrorData = { + reason: string; + fileOperationReason: number; + // error: Error; + }; + type ChatSessionStoreErrorClassification = { + owner: 'roblourens'; + comment: 'Detect issues related to managing chat sessions'; + reason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; + fileOperationReason: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'An error code from the file service' }; + // error: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Info about the error that occurred' }; + }; + this.telemetryService.publicLog2('chatSessionStoreError', { + reason: reasonForTelemetry, + fileOperationReason: fileOperationReason ?? -1 + }); + } + + private indexCache: IChatSessionIndexData | undefined; + private internalGetIndex(): IChatSessionIndexData { + if (this.indexCache) { + return this.indexCache; + } + + const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); + if (!data) { + this.indexCache = { version: 1, entries: {} }; + return this.indexCache; + } + + try { + const index = JSON.parse(data) as unknown; + if (isChatSessionIndex(index)) { + // Success + this.indexCache = index; + } else { + this.reportError('invalidIndexFormat', `Invalid index format: ${data}`); + this.indexCache = { version: 1, entries: {} }; + } + + } catch (e) { + // Only if JSON.parse fails + this.reportError('invalidIndexJSON', `Index corrupt: ${data}`, e); + this.indexCache = { version: 1, entries: {} }; + } + + // Convert from pre-1.109 format which lacks timing + for (const entry of Object.values(this.indexCache.entries)) { + entry.timing ??= { + created: entry.lastMessageDate, + lastRequestStarted: undefined, + lastRequestEnded: entry.lastMessageDate, + }; + + // TODO@connor4312: the check for Pending/NeedsInput guards old sessions from Insiders pre PR #288161 and it can be safely removed after a transition period, to only backfill the "complete" state when missing. + entry.lastResponseState ??= entry.lastResponseState === ResponseModelState.Pending || entry.lastResponseState === ResponseModelState.NeedsInput ? ResponseModelState.Complete : entry.lastResponseState || ResponseModelState.Complete; + } + + return this.indexCache; + } + + async getIndex(): Promise { + return this.storeQueue.queue(async () => { + return this.internalGetIndex().entries; + }); + } + + getMetadataForSessionSync(sessionResource: URI): IChatSessionEntryMetadata | undefined { + const index = this.internalGetIndex(); + return index.entries[this.getIndexKey(sessionResource)]; + } + + private getIndexKey(sessionResource: URI): string { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + return sessionId ?? sessionResource.toString(); + } + + logIndex(): void { + const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); + this.logService.info('ChatSessionStore index: ', data); + } + + async migrateDataIfNeeded(getInitialData: () => ISerializableChatsData | undefined): Promise { + await this.storeQueue.queue(async () => { + const data = this.storageService.get(ChatIndexStorageKey, this.getIndexStorageScope(), undefined); + const needsMigrationFromStorageService = !data; + if (needsMigrationFromStorageService) { + const initialData = getInitialData(); + if (initialData) { + await this.migrate(initialData); + } + } + }); + } + + private async migrate(initialData: ISerializableChatsData): Promise { + const numSessions = Object.keys(initialData).length; + this.logService.info(`ChatSessionStore: Migrating ${numSessions} chat sessions from storage service to file system`); + + await Promise.all(Object.values(initialData).map(async session => { + await this.writeSession(session); + })); + + await this.flushIndex(); + } + + public async readSession(sessionId: string): Promise { + return await this.storeQueue.queue(async () => { + const storageLocation = this.getStorageLocation(sessionId); + return this.readSessionFromLocation(storageLocation.flat, storageLocation.log, sessionId); + }); + } + + private async readSessionFromLocation(flatStorageLocation: URI, logStorageLocation: URI | undefined, sessionId: string): Promise { + let fromLocation = flatStorageLocation; + let rawData: VSBuffer | undefined; + + if (logStorageLocation) { + try { + rawData = (await this.fileService.readFile(logStorageLocation)).value; + fromLocation = logStorageLocation; + } catch (e) { + this.reportError('sessionReadFile', `Error reading log chat session file ${sessionId}`, e); + } + } + + if (!rawData) { + try { + rawData = (await this.fileService.readFile(flatStorageLocation)).value; + fromLocation = flatStorageLocation; + } catch (e) { + this.reportError('sessionReadFile', `Error reading flat chat session file ${sessionId}`, e); + + if (toFileOperationResult(e) === FileOperationResult.FILE_NOT_FOUND && this.previousEmptyWindowStorageRoot) { + rawData = await this.readSessionFromPreviousLocation(sessionId); + } + } + } + + if (!rawData) { + return undefined; + } + + try { + let session: ISerializableChatDataIn; + const log = new ChatSessionOperationLog(); + if (fromLocation === logStorageLocation) { + session = revive(log.read(rawData)); + } else { + session = revive(JSON.parse(rawData.toString())); + } + + // TODO Copied from ChatService.ts, cleanup + // Revive serialized markdown strings in response data + for (const request of session.requests) { + if (Array.isArray(request.response)) { + request.response = request.response.map((response) => { + if (typeof response === 'string') { + return new MarkdownString(response); + } + return response; + }); + } else if (typeof request.response === 'string') { + request.response = [new MarkdownString(request.response)]; + } + } + + return { value: normalizeSerializableChatData(session), serializer: log }; + } catch (err) { + this.reportError('malformedSession', `Malformed session data in ${fromLocation.fsPath}: [${rawData.slice(0, 20).toString()}${rawData.byteLength > 20 ? '...' : ''}]`, err); + return undefined; + } + } + + private async readSessionFromPreviousLocation(sessionId: string): Promise { + let rawData: VSBuffer | undefined; + + if (this.previousEmptyWindowStorageRoot) { + const storageLocation2 = joinPath(this.previousEmptyWindowStorageRoot, `${sessionId}.json`); + try { + rawData = (await this.fileService.readFile(storageLocation2)).value; + this.logService.info(`ChatSessionStore: Read chat session ${sessionId} from previous location`); + } catch (e) { + this.reportError('sessionReadFile', `Error reading chat session file ${sessionId} from previous location`, e); + return undefined; + } + } + + return rawData; + } + + private getStorageLocation(chatSessionId: string): { + /** <1.109 flat JSON file */ + flat: URI; + /** >=1.109 append log */ + log?: URI; + } { + return { + flat: joinPath(this.storageRoot, `${chatSessionId}.json`), + // todo@connor4312: remove after stabilizing + log: this.configurationService.getValue('chat.useLogSessionStorage') !== false ? joinPath(this.storageRoot, `${chatSessionId}.jsonl`) : undefined, + }; + } + + private getTransferredSessionStorageLocation(sessionResource: URI): URI { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + return joinPath(this.transferredSessionStorageRoot, `${sessionId}.json`); + } + + public getChatStorageFolder(): URI { + return this.storageRoot; + } +} + +export interface IChatSessionEntryMetadata { + sessionId: string; + title: string; + lastMessageDate: number; + timing: IChatSessionTiming; + initialLocation?: ChatAgentLocation; + hasPendingEdits?: boolean; + stats?: IChatSessionStats; + lastResponseState: ResponseModelState; + + /** + * This only exists because the migrated data from the storage service had empty sessions persisted, and it's impossible to know which ones are + * currently in use. Now, `clearSession` deletes empty sessions, so old ones shouldn't take up space in the store anymore, but we still need to + * filter the old ones out of history. + */ + isEmpty?: boolean; + + /** + * Whether this session was loaded from an external provider (eg background/cloud sessions). + */ + isExternal?: boolean; +} + +function isChatSessionEntryMetadata(obj: unknown): obj is IChatSessionEntryMetadata { + return ( + !!obj && + typeof obj === 'object' && + typeof (obj as IChatSessionEntryMetadata).sessionId === 'string' && + typeof (obj as IChatSessionEntryMetadata).title === 'string' && + typeof (obj as IChatSessionEntryMetadata).lastMessageDate === 'number' + ); +} + +export type IChatSessionIndex = Record; + +interface IChatSessionIndexData { + version: 1; + entries: IChatSessionIndex; +} + +// TODO if we update the index version: +// Don't throw away index when moving backwards in VS Code version. Try to recover it. But this scenario is hard. +function isChatSessionIndex(data: unknown): data is IChatSessionIndexData { + if (typeof data !== 'object' || data === null) { + return false; + } + + const index = data as IChatSessionIndexData; + if (index.version !== 1) { + return false; + } + + if (typeof index.entries !== 'object' || index.entries === null) { + return false; + } + + for (const key in index.entries) { + if (!isChatSessionEntryMetadata(index.entries[key])) { + return false; + } + } + + return true; +} + +async function getSessionMetadata(session: ChatModel | ISerializableChatData): Promise { + const title = session.customTitle || (session instanceof ChatModel ? session.title : undefined); + + let stats: IChatSessionStats | undefined; + if (session instanceof ChatModel) { + stats = await awaitStatsForSession(session); + } + + const lastMessageDate = session instanceof ChatModel ? + session.lastMessageDate : + session.requests.at(-1)?.timestamp ?? session.creationDate; + + const timing: IChatSessionTiming = session instanceof ChatModel ? + session.timing : + // session is only ISerializableChatData in the old pre-fs storage data migration scenario + { + created: session.creationDate, + lastRequestStarted: session.requests.at(-1)?.timestamp, + lastRequestEnded: lastMessageDate, + }; + + let lastResponseState = session instanceof ChatModel ? + (session.lastRequest?.response?.state ?? ResponseModelState.Complete) : + ResponseModelState.Complete; + + if (lastResponseState === ResponseModelState.Pending || lastResponseState === ResponseModelState.NeedsInput) { + lastResponseState = ResponseModelState.Cancelled; + } + + return { + sessionId: session.sessionId, + title: title || localize('newChat', "New Chat"), + lastMessageDate, + timing, + initialLocation: session.initialLocation, + hasPendingEdits: session instanceof ChatModel ? (session.editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)) : false, + isEmpty: session instanceof ChatModel ? session.getRequests().length === 0 : session.requests.length === 0, + stats, + isExternal: session instanceof ChatModel && !LocalChatSessionUri.parseLocalSessionId(session.sessionResource), + lastResponseState, + }; +} + +export interface IChatTransfer { + toWorkspace: URI; + sessionResource: URI; + timestampInMilliseconds: number; +} + +export interface IChatTransfer2 extends IChatTransfer { + chat: ISerializableChatData; +} + +type IChatTransferDto = Dto; + +/** + * Map of destination workspace URI to chat transfer data + */ +type IChatTransferIndex = Record; diff --git a/src/vs/workbench/contrib/chat/common/model/chatStreamStats.ts b/src/vs/workbench/contrib/chat/common/model/chatStreamStats.ts new file mode 100644 index 00000000000..03766819e26 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatStreamStats.ts @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILogService } from '../../../../../platform/log/common/log.js'; + +export interface IChatStreamStats { + impliedWordLoadRate: number; + lastWordCount: number; +} + +export interface IChatStreamStatsInternal extends IChatStreamStats { + totalTime: number; + lastUpdateTime: number; + firstMarkdownTime: number | undefined; + bootstrapActive: boolean; + wordCountAtBootstrapExit: number | undefined; + updatesWithNewWords: number; +} + +export interface IChatStreamUpdate { + totalWordCount: number; +} + +const MIN_BOOTSTRAP_TOTAL_TIME = 250; +const LARGE_BOOTSTRAP_MIN_TOTAL_TIME = 500; +const MAX_INTERVAL_TIME = 250; +const LARGE_UPDATE_MAX_INTERVAL_TIME = 1000; +const WORDS_FOR_LARGE_CHUNK = 10; +const MIN_UPDATES_FOR_STABLE_RATE = 2; + +/** + * Estimates the loading rate of a chat response stream so that we can try to match the rendering rate to + * the rate at which text is actually produced by the model. This can only be an estimate for various reasons- + * reasoning summaries don't represent real generated tokens, we don't have full visibility into tool calls, + * some model providers send text in large chunks rather than a steady stream, e.g. Gemini, we don't know about + * latency between agent requests, etc. + * + * When the first text is received, we don't know how long it actually took to generate. So we apply an assumed + * minimum time, until we have received enough data to make a stable estimate. This is the "bootstrap" phase. + * + * Since we don't have visibility into when the model started generated tool call args, or when the client was running + * a tool, we ignore long pauses. The ignore period is longer for large chunks, since those naturally take longer + * to generate anyway. + * + * After that, the word load rate is estimated using the words received since the end of the bootstrap phase. + */ +export class ChatStreamStatsTracker { + private _data: IChatStreamStatsInternal; + private _publicData: IChatStreamStats; + + constructor( + @ILogService private readonly logService: ILogService + ) { + const start = Date.now(); + this._data = { + totalTime: 0, + lastUpdateTime: start, + impliedWordLoadRate: 0, + lastWordCount: 0, + firstMarkdownTime: undefined, + bootstrapActive: true, + wordCountAtBootstrapExit: undefined, + updatesWithNewWords: 0 + }; + this._publicData = { impliedWordLoadRate: 0, lastWordCount: 0 }; + } + + get data(): IChatStreamStats { + return this._publicData; + } + + get internalData(): IChatStreamStatsInternal { + return this._data; + } + + update(totals: IChatStreamUpdate): IChatStreamStats | undefined { + const { totalWordCount: wordCount } = totals; + if (wordCount === this._data.lastWordCount) { + this.trace('Update- no new words'); + return undefined; + } + + const now = Date.now(); + const newWords = wordCount - this._data.lastWordCount; + const hadNoWordsBeforeUpdate = this._data.lastWordCount === 0; + let firstMarkdownTime = this._data.firstMarkdownTime; + let wordCountAtBootstrapExit = this._data.wordCountAtBootstrapExit; + if (typeof firstMarkdownTime !== 'number' && wordCount > 0) { + firstMarkdownTime = now; + } + const updatesWithNewWords = this._data.updatesWithNewWords + 1; + + if (hadNoWordsBeforeUpdate) { + this._data.lastUpdateTime = now; + } + + const intervalCap = newWords > WORDS_FOR_LARGE_CHUNK ? LARGE_UPDATE_MAX_INTERVAL_TIME : MAX_INTERVAL_TIME; + const timeDiff = Math.min(now - this._data.lastUpdateTime, intervalCap); + let totalTime = this._data.totalTime + timeDiff; + const minBootstrapTotalTime = hadNoWordsBeforeUpdate && wordCount > WORDS_FOR_LARGE_CHUNK ? LARGE_BOOTSTRAP_MIN_TOTAL_TIME : MIN_BOOTSTRAP_TOTAL_TIME; + + let bootstrapActive = this._data.bootstrapActive; + if (bootstrapActive) { + const stableStartTime = firstMarkdownTime; + const hasStableData = typeof stableStartTime === 'number' + && updatesWithNewWords >= MIN_UPDATES_FOR_STABLE_RATE + && wordCount >= WORDS_FOR_LARGE_CHUNK; + if (hasStableData) { + bootstrapActive = false; + totalTime = Math.max(now - stableStartTime, timeDiff); + wordCountAtBootstrapExit = this._data.lastWordCount; + this.trace('Has stable data'); + } else { + totalTime = Math.max(totalTime, minBootstrapTotalTime); + } + } + + const wordsSinceBootstrap = typeof wordCountAtBootstrapExit === 'number' ? Math.max(wordCount - wordCountAtBootstrapExit, 0) : wordCount; + const effectiveTime = totalTime; + const effectiveWordCount = bootstrapActive ? wordCount : wordsSinceBootstrap; + const impliedWordLoadRate = effectiveTime > 0 ? effectiveWordCount / (effectiveTime / 1000) : 0; + this._data = { + totalTime, + lastUpdateTime: now, + impliedWordLoadRate, + lastWordCount: wordCount, + firstMarkdownTime, + bootstrapActive, + wordCountAtBootstrapExit, + updatesWithNewWords + }; + this._publicData = { + impliedWordLoadRate, + lastWordCount: wordCount + }; + + const traceWords = bootstrapActive ? wordCount : wordsSinceBootstrap; + this.trace(`Update- got ${traceWords} words over last ${totalTime}ms = ${impliedWordLoadRate} words/s`); + return this._data; + } + + private trace(message: string): void { + this.logService.trace(`ChatStreamStatsTracker#update: ${message}`); + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatTransferService.ts b/src/vs/workbench/contrib/chat/common/model/chatTransferService.ts similarity index 77% rename from src/vs/workbench/contrib/chat/common/chatTransferService.ts rename to src/vs/workbench/contrib/chat/common/model/chatTransferService.ts index bbc21070343..0d2f65a0ad7 100644 --- a/src/vs/workbench/contrib/chat/common/chatTransferService.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatTransferService.ts @@ -2,13 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { areWorkspaceFoldersEmpty } from '../../../services/workspaces/common/workspaceUtils.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { URI } from '../../../../base/common/uri.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js'; +import { areWorkspaceFoldersEmpty } from '../../../../services/workspaces/common/workspaceUtils.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { URI } from '../../../../../base/common/uri.js'; export const IChatTransferService = createDecorator('chatTransferService'); const transferredWorkspacesKey = 'chat.transferedWorkspaces'; @@ -30,7 +30,7 @@ export class ChatTransferService implements IChatTransferService { @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService ) { } - deleteWorkspaceFromTransferredList(workspace: URI): void { + private deleteWorkspaceFromTransferredList(workspace: URI): void { const transferredWorkspaces = this.storageService.getObject(transferredWorkspacesKey, StorageScope.PROFILE, []); const updatedWorkspaces = transferredWorkspaces.filter(uri => uri !== workspace.toString()); this.storageService.store(transferredWorkspacesKey, updatedWorkspaces, StorageScope.PROFILE, StorageTarget.MACHINE); @@ -54,7 +54,7 @@ export class ChatTransferService implements IChatTransferService { } } - isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean { + private isChatTransferredWorkspace(workspace: URI, storageService: IStorageService): boolean { if (!workspace) { return false; } diff --git a/src/vs/workbench/contrib/chat/common/model/chatUri.ts b/src/vs/workbench/contrib/chat/common/model/chatUri.ts new file mode 100644 index 00000000000..8bbc1ff00a6 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/chatUri.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { encodeBase64, VSBuffer, decodeBase64 } from '../../../../../base/common/buffer.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localChatSessionType } from '../chatSessionsService.js'; + +type ChatSessionIdentifier = { + readonly chatSessionType: string; + readonly sessionId: string; +}; + + +export namespace LocalChatSessionUri { + + export const scheme = Schemas.vscodeLocalChatSession; + + export function forSession(sessionId: string): URI { + const encodedId = encodeBase64(VSBuffer.wrap(new TextEncoder().encode(sessionId)), false, true); + return URI.from({ scheme, authority: localChatSessionType, path: '/' + encodedId }); + } + + export function parseLocalSessionId(resource: URI): string | undefined { + const parsed = parse(resource); + return parsed?.chatSessionType === localChatSessionType ? parsed.sessionId : undefined; + } + + export function isLocalSession(resource: URI): boolean { + return !!parseLocalSessionId(resource); + } + + function parse(resource: URI): ChatSessionIdentifier | undefined { + if (resource.scheme !== scheme) { + return undefined; + } + + if (!resource.authority) { + return undefined; + } + + const parts = resource.path.split('/'); + if (parts.length !== 2) { + return undefined; + } + + const chatSessionType = resource.authority; + const decodedSessionId = decodeBase64(parts[1]); + return { chatSessionType, sessionId: new TextDecoder().decode(decodedSessionId.buffer) }; + } +} + +/** + * Converts a chat session resource URI to a string ID. + * + * This exists mainly for backwards compatibility with existing code that uses string IDs in telemetry and storage. + */ +export function chatSessionResourceToId(resource: URI): string { + // If we have a local session, prefer using just the id part + const localId = LocalChatSessionUri.parseLocalSessionId(resource); + if (localId) { + return localId; + } + + return resource.toString(); +} + +/** + * Extracts the chat session type from a resource URI. + * + * @param resource - The chat session resource URI + * @returns The session type string. Returns `localChatSessionType` for local sessions + * (vscodeChatEditor and vscodeLocalChatSession schemes), or the scheme/authority + * for contributed sessions. + */ +export function getChatSessionType(resource: URI): string { + if (resource.scheme === Schemas.vscodeChatEditor) { + return localChatSessionType; + } + + if (resource.scheme === LocalChatSessionUri.scheme) { + return resource.authority || localChatSessionType; + } + + return resource.scheme; +} diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts similarity index 75% rename from src/vs/workbench/contrib/chat/common/chatViewModel.ts rename to src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index ad6288fe8a8..fb7bd5d8a3d 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -3,23 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../base/common/event.js'; -import { hash } from '../../../../base/common/hash.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable, dispose } from '../../../../base/common/lifecycle.js'; -import * as marked from '../../../../base/common/marked/marked.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { annotateVulnerabilitiesInText } from './annotations.js'; -import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from './chatAgents.js'; +import { Codicon } from '../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Disposable, dispose } from '../../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatMcpServersStarting, IChatProgressMessage, IChatQuestionCarousel, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from '../chatService/chatService.js'; +import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from '../participants/chatAgents.js'; +import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; +import { CodeBlockModelCollection } from '../widget/codeBlockModelCollection.js'; import { IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js'; -import { IChatRequestVariableEntry } from './chatVariableEntries.js'; -import { IParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatChangesSummary, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatMcpServersStarting, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from './chatService.js'; +import { ChatStreamStatsTracker, IChatStreamStats } from './chatStreamStats.js'; import { countWords } from './chatWordCounter.js'; -import { CodeBlockModelCollection } from './codeBlockModelCollection.js'; export function isRequestVM(item: unknown): item is IChatRequestViewModel { return !!item && typeof item === 'object' && 'message' in item; @@ -39,7 +38,7 @@ export function assertIsResponseVM(item: unknown): asserts item is IChatResponse } } -export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | IChatSetCheckpointEvent | null; +export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | null; export interface IChatAddRequestEvent { kind: 'addRequest'; @@ -57,18 +56,11 @@ export interface IChatSetHiddenEvent { kind: 'setHidden'; } -export interface IChatSetCheckpointEvent { - kind: 'setCheckpoint'; -} - export interface IChatViewModel { readonly model: IChatModel; - /** @deprecated Use {@link sessionResource} instead */ - readonly sessionId: string; readonly sessionResource: URI; readonly onDidDisposeModel: Event; readonly onDidChange: Event; - readonly requestInProgress: boolean; readonly inputPlaceholder?: string; getItems(): (IChatRequestViewModel | IChatResponseViewModel)[]; setInputPlaceholder(text: string): void; @@ -79,8 +71,6 @@ export interface IChatViewModel { export interface IChatRequestViewModel { readonly id: string; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; @@ -89,7 +79,7 @@ export interface IChatRequestViewModel { readonly message: IParsedChatRequest | IChatFollowup; readonly messageText: string; readonly attempt: number; - readonly variables: IChatRequestVariableEntry[]; + readonly variables: readonly IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; readonly contentReferences?: ReadonlyArray; readonly confirmation?: string; @@ -98,8 +88,9 @@ export interface IChatRequestViewModel { readonly isCompleteAddedRequest: boolean; readonly slashCommand: IChatAgentCommand | undefined; readonly agentOrSlashCommandDetected: boolean; - readonly shouldBeBlocked?: boolean; + readonly shouldBeBlocked: IObservable; readonly modelId?: string; + readonly timestamp: number; } export interface IChatResponseMarkdownRenderData { @@ -178,34 +169,25 @@ export interface IChatErrorDetailsPart { export interface IChatChangesSummaryPart { readonly kind: 'changesSummary'; - readonly fileChanges: ReadonlyArray; + readonly requestId: string; + readonly sessionResource: URI; } /** * Type for content parts rendered by IChatListRenderer (not necessarily in the model) */ -export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations | IChatErrorDetailsPart | IChatChangesSummaryPart | IChatWorkingProgress | IChatMcpServersStarting; - -export interface IChatLiveUpdateData { - totalTime: number; - lastUpdateTime: number; - impliedWordLoadRate: number; - lastWordCount: number; -} +export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations | IChatErrorDetailsPart | IChatChangesSummaryPart | IChatWorkingProgress | IChatMcpServersStarting | IChatQuestionCarousel; export interface IChatResponseViewModel { readonly model: IChatResponseModel; readonly id: string; readonly session: IChatViewModel; - /** @deprecated */ - readonly sessionId: string; readonly sessionResource: URI; /** This ID updates every time the underlying data changes */ readonly dataId: string; /** The ID of the associated IChatRequestViewModel */ readonly requestId: string; readonly username: string; - readonly avatarIcon?: URI | ThemeIcon; readonly agent?: IChatAgentData; readonly slashCommand?: IChatAgentCommand; readonly agentOrSlashCommandDetected: boolean; @@ -222,7 +204,7 @@ export interface IChatResponseViewModel { readonly replyFollowups?: IChatFollowup[]; readonly errorDetails?: IChatResponseErrorDetails; readonly result?: IChatAgentResult; - readonly contentUpdateTimings?: IChatLiveUpdateData; + readonly contentUpdateTimings?: IChatStreamStats; readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined; readonly isCompleteAddedRequest: boolean; renderData?: IChatResponseRenderData; @@ -232,7 +214,15 @@ export interface IChatResponseViewModel { usedReferencesExpanded?: boolean; vulnerabilitiesListExpanded: boolean; setEditApplied(edit: IChatTextEditGroup, editCount: number): void; - readonly shouldBeBlocked: boolean; + readonly shouldBeBlocked: IObservable; +} + +export interface IChatViewModelOptions { + /** + * Maximum number of items to return from getItems(). + * When set, only the last N items are returned (most recent request/response pairs). + */ + readonly maxVisibleItems?: number; } export class ChatViewModel extends Disposable implements IChatViewModel { @@ -264,22 +254,14 @@ export class ChatViewModel extends Disposable implements IChatViewModel { this._onDidChange.fire({ kind: 'changePlaceholder' }); } - /** @deprecated Use {@link sessionResource} instead */ - get sessionId() { - return this._model.sessionId; - } - get sessionResource(): URI { return this._model.sessionResource; } - get requestInProgress(): boolean { - return this._model.requestInProgress; - } - constructor( private readonly _model: IChatModel, public readonly codeBlockModelCollection: CodeBlockModelCollection, + private readonly _options: IChatViewModelOptions | undefined, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -287,7 +269,6 @@ export class ChatViewModel extends Disposable implements IChatViewModel { _model.getRequests().forEach((request, i) => { const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); this._items.push(requestModel); - this.updateCodeBlockTextModels(requestModel); if (request.response) { this.onAddResponse(request.response); @@ -299,7 +280,6 @@ export class ChatViewModel extends Disposable implements IChatViewModel { if (e.kind === 'addRequest') { const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); this._items.push(requestModel); - this.updateCodeBlockTextModels(requestModel); if (e.request.response) { this.onAddResponse(e.request.response); @@ -334,17 +314,17 @@ export class ChatViewModel extends Disposable implements IChatViewModel { private onAddResponse(responseModel: IChatResponseModel) { const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel, this); this._register(response.onDidChange(() => { - if (response.isComplete) { - this.updateCodeBlockTextModels(response); - } return this._onDidChange.fire(null); })); this._items.push(response); - this.updateCodeBlockTextModels(response); } getItems(): (IChatRequestViewModel | IChatResponseViewModel)[] { - return this._items.filter((item) => !item.shouldBeRemovedOnSend || item.shouldBeRemovedOnSend.afterUndoStop); + const items = this._items.filter((item) => !item.shouldBeRemovedOnSend || item.shouldBeRemovedOnSend.afterUndoStop); + if (this._options?.maxVisibleItems !== undefined && items.length > this._options.maxVisibleItems) { + return items.slice(-this._options.maxVisibleItems); + } + return items; } @@ -365,24 +345,6 @@ export class ChatViewModel extends Disposable implements IChatViewModel { super.dispose(); dispose(this._items.filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel)); } - - updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) { - let content: string; - if (isRequestVM(model)) { - content = model.messageText; - } else { - content = annotateVulnerabilitiesInText(model.response.value).map(x => x.content.value).join(''); - } - - let codeBlockIndex = 0; - marked.walkTokens(marked.lexer(content), token => { - if (token.type === 'code') { - const lang = token.lang || ''; - const text = token.text; - this.codeBlockModelCollection.update(this._model.sessionResource, model, codeBlockIndex++, { text, languageId: lang, isComplete: true }); - } - }); - } } export class ChatRequestViewModel implements IChatRequestViewModel { @@ -390,13 +352,11 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.id; } + /** + * An ID that changes when the request should be re-rendered. + */ get dataId() { - return this.id + `_${hash(this.variables)}_${hash(this.isComplete)}`; - } - - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; + return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`; } get sessionResource() { @@ -404,11 +364,11 @@ export class ChatRequestViewModel implements IChatRequestViewModel { } get username() { - return this._model.username; + return 'User'; } - get avatarIcon() { - return this._model.avatarIconUri; + get avatarIcon(): ThemeIcon { + return Codicon.account; } get message() { @@ -465,6 +425,10 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.modelId; } + get timestamp() { + return this._model.timestamp; + } + constructor( private readonly _model: IChatRequestModel, ) { } @@ -490,11 +454,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi (this.isLast ? '_last' : ''); } - /** @deprecated */ - get sessionId() { - return this._model.session.sessionId; - } - get sessionResource(): URI { return this._model.session.sessionResource; } @@ -512,10 +471,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.username; } - get avatarIcon() { - return this._model.avatarIcon; - } - get agent() { return this._model.agent; } @@ -625,55 +580,28 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi this._vulnerabilitiesListExpanded = v; } - private _contentUpdateTimings: IChatLiveUpdateData | undefined = undefined; - get contentUpdateTimings(): IChatLiveUpdateData | undefined { - return this._contentUpdateTimings; - } + private readonly liveUpdateTracker: ChatStreamStatsTracker | undefined; + get contentUpdateTimings(): IChatStreamStats | undefined { + return this.liveUpdateTracker?.data; + } constructor( private readonly _model: IChatResponseModel, public readonly session: IChatViewModel, - @ILogService private readonly logService: ILogService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService, ) { super(); if (!_model.isComplete) { - this._contentUpdateTimings = { - totalTime: 0, - lastUpdateTime: Date.now(), - impliedWordLoadRate: 0, - lastWordCount: 0, - }; + this.liveUpdateTracker = this.instantiationService.createInstance(ChatStreamStatsTracker); } this._register(_model.onDidChange(() => { - // This is set when the response is loading, but the model can change later for other reasons - if (this._contentUpdateTimings) { - const now = Date.now(); + if (this.liveUpdateTracker) { const wordCount = countWords(_model.entireResponse.getMarkdown()); - - if (wordCount === this._contentUpdateTimings.lastWordCount) { - this.trace('onDidChange', `Update- no new words`); - } else { - if (this._contentUpdateTimings.lastWordCount === 0) { - this._contentUpdateTimings.lastUpdateTime = now; - } - - const timeDiff = Math.min(now - this._contentUpdateTimings.lastUpdateTime, 500); - const newTotalTime = Math.max(this._contentUpdateTimings.totalTime + timeDiff, 250); - const impliedWordLoadRate = wordCount / (newTotalTime / 1000); - this.trace('onDidChange', `Update- got ${wordCount} words over last ${newTotalTime}ms = ${impliedWordLoadRate} words/s`); - this._contentUpdateTimings = { - totalTime: this._contentUpdateTimings.totalTime !== 0 || this.response.value.some(v => v.kind === 'markdownContent') ? - newTotalTime : - this._contentUpdateTimings.totalTime, - lastUpdateTime: now, - impliedWordLoadRate, - lastWordCount: wordCount - }; - } + this.liveUpdateTracker.update({ totalWordCount: wordCount }); } // new data -> new id, new content to render @@ -683,10 +611,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi })); } - private trace(tag: string, message: string) { - this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); - } - setVote(vote: ChatAgentVoteDirection): void { this._modelChangeCount++; this._model.setVote(vote); diff --git a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts b/src/vs/workbench/contrib/chat/common/model/chatWordCounter.ts similarity index 96% rename from src/vs/workbench/contrib/chat/common/chatWordCounter.ts rename to src/vs/workbench/contrib/chat/common/model/chatWordCounter.ts index 82a94408020..5f95217c5a5 100644 --- a/src/vs/workbench/contrib/chat/common/chatWordCounter.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatWordCounter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as markedKatexExtension from '../../markdown/common/markedKatexExtension.js'; +import * as markedKatexExtension from '../../../markdown/common/markedKatexExtension.js'; export interface IWordCountResult { value: string; diff --git a/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts b/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts new file mode 100644 index 00000000000..01ce16ae67a --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/model/objectMutationLog.ts @@ -0,0 +1,489 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBuffer } from '../../../../../base/common/buffer.js'; +import { isUndefinedOrNull } from '../../../../../base/common/types.js'; + + +/** IMPORTANT: `Key` comes first. Then we should sort in order of least->most expensive to diff */ +const enum TransformKind { + Key, + Primitive, + Array, + Object, +} + +/** Schema entries sorted with key properties first */ +export type SchemaEntries = [string, Transform][]; + +interface TransformBase { + readonly kind: TransformKind; + /** Extracts the serializable value from the source object */ + extract(from: TFrom): TTo; +} + +/** Transform for primitive values (keys and values) that can be compared for equality */ +export interface TransformValue extends TransformBase { + readonly kind: TransformKind.Key | TransformKind.Primitive; + /** Compares two serialized values for equality */ + equals(a: TTo, b: TTo): boolean; +} + +/** Transform for arrays with an item schema */ +export interface TransformArray extends TransformBase { + readonly kind: TransformKind.Array; + /** The schema for array items */ + readonly itemSchema: TransformObject | TransformValue; +} + +/** Transform for objects with child properties */ +export interface TransformObject extends TransformBase { + readonly kind: TransformKind.Object; + /** Schema entries sorted with Key properties first */ + readonly children: SchemaEntries; + /** Checks if the object is sealed (won't change). */ + sealed?(obj: TTo, wasSerialized: boolean): boolean; +} + +export type Transform = + | TransformValue + | TransformArray + | TransformObject; + +export type Schema = { + [K in keyof Required]: Transform +}; + +/** + * A primitive that will be tracked and compared first. If this is changed, the entire + * object is thrown out and re-stored. + */ +export function key(comparator?: (a: R, b: R) => boolean): TransformValue { + return { + kind: TransformKind.Key, + extract: (from: T) => from as unknown as R, + equals: comparator ?? ((a, b) => a === b), + }; +} + +/** A value that will be tracked and replaced if the comparator is not equal. */ +export function value(): TransformValue; +export function value(comparator: (a: R, b: R) => boolean): TransformValue; +export function value(comparator?: (a: R, b: R) => boolean): TransformValue { + return { + kind: TransformKind.Primitive, + extract: (from: T) => { + let value = from as unknown as R; + // We map the object to JSON for two reasons (a) reduce issues with references to + // mutable type that could be held internally in the LogAdapter and (b) to make + // object comparison work with the data we re-hydrate from disk (e.g. if using + // objectsEqual, a hydrated URI is not equal to the serialized UriComponents) + if (!!value && typeof value === 'object') { + value = JSON.parse(JSON.stringify(value)); + } + + return value; + }, + equals: comparator ?? ((a, b) => a === b), + }; +} + +/** An array that will use the schema to compare items positionally. */ +export function array(schema: TransformObject | TransformValue): TransformArray { + return { + kind: TransformKind.Array, + itemSchema: schema, + extract: from => from?.map(item => schema.extract(item)), + }; +} + +export interface ObjectOptions { + /** + * Returns true if the object is sealed and will never change again. + * When comparing two sealed objects, only key fields are compared + * (to detect replacement), but other fields are not diffed. + */ + sealed?: (obj: R, wasSerialized: boolean) => boolean; +} + +/** An object schema. */ +export function object(schema: Schema, options?: ObjectOptions): TransformObject { + // Sort entries with key properties first for fast key checking + const entries = (Object.entries(schema) as [string, Transform][]).sort(([, a], [, b]) => a.kind - b.kind); + return { + kind: TransformKind.Object, + children: entries as SchemaEntries, + sealed: options?.sealed, + extract: (from: T) => { + if (isUndefinedOrNull(from)) { + return from as unknown as R; + } + + const result: Record = Object.create(null); + for (const [key, transform] of entries) { + result[key] = transform.extract(from); + } + return result as R; + }, + }; +} + +/** + * Defines a getter on the object to extract a value, compared with the given schema. + * It should return the value that will get serialized in the resulting log file. + */ +export function t(getter: (obj: T) => O, schema: Transform): Transform { + return { + ...schema, + extract: (from: T) => schema.extract(getter(from)), + }; +} + +/** Shortcut for t(fn, value()) */ +export function v(getter: (obj: T) => R): TransformValue; +export function v(getter: (obj: T) => R, comparator: (a: R, b: R) => boolean): TransformValue; +export function v(getter: (obj: T) => R, comparator?: (a: R, b: R) => boolean): TransformValue { + const inner = value(comparator!); + return { + ...inner, + extract: (from: T) => inner.extract(getter(from)), + }; +} + + +const enum EntryKind { + /** Initial complete object state, valid only as the first entry */ + Initial = 0, + /** Property update */ + Set = 1, + /** Array push/splice. */ + Push = 2, + /** Delete a property */ + Delete = 3, +} + +type ObjectPath = (string | number)[]; + +type Entry = + | { kind: EntryKind.Initial; v: unknown } + /** Update a property of an object, replacing it entirely */ + | { kind: EntryKind.Set; k: ObjectPath; v: unknown } + /** Delete a property of an object */ + | { kind: EntryKind.Delete; k: ObjectPath } + /** Pushes 0 or more new entries to an array. If `i` is set, everything after that index is removed */ + | { kind: EntryKind.Push; k: ObjectPath; v?: unknown[]; i?: number }; + +const LF = VSBuffer.fromString('\n'); + +/** + * An implementation of an append-based mutation logger. Given a `Transform` + * definition of an object, it can recreate it from a file on disk. It is + * then stateful, and given a `write` call it can update the log in a minimal + * way. + */ +export class ObjectMutationLog { + private _previous: TTo | undefined; + private _entryCount = 0; + + constructor( + private readonly _transform: Transform, + private readonly _compactAfterEntries = 512, + ) { } + + /** + * Creates an initial log file from the given object. + */ + createInitial(current: TFrom): VSBuffer { + return this.createInitialFromSerialized(this._transform.extract(current)); + } + + + /** + * Creates an initial log file from the serialized object. + */ + createInitialFromSerialized(value: TTo): VSBuffer { + this._previous = value; + this._entryCount = 1; + const entry: Entry = { kind: EntryKind.Initial, v: value }; + return VSBuffer.fromString(JSON.stringify(entry) + '\n'); + } + + /** + * Reads and reconstructs the state from a log file. + */ + read(content: VSBuffer): TTo { + let state: unknown; + let lineCount = 0; + + let start = 0; + const len = content.byteLength; + while (start < len) { + let end = content.indexOf(LF, start); + if (end === -1) { + end = len; + } + + if (end > start) { + const line = content.slice(start, end); + if (line.byteLength > 0) { + lineCount++; + const entry = JSON.parse(line.toString()) as Entry; + switch (entry.kind) { + case EntryKind.Initial: + state = entry.v; + break; + case EntryKind.Set: + this._applySet(state, entry.k, entry.v); + break; + case EntryKind.Push: + this._applyPush(state, entry.k, entry.v, entry.i); + break; + case EntryKind.Delete: + this._applySet(state, entry.k, undefined); + break; + default: + assertNever(entry); + } + } + } + start = end + 1; + } + + if (lineCount === 0) { + throw new Error('Empty log file'); + } + + this._previous = state as TTo; + this._entryCount = lineCount; + return state as TTo; + } + + /** + * Writes updates to the log. Returns the operation type and data to write. + */ + write(current: TFrom): { op: 'append' | 'replace'; data: VSBuffer } { + const currentValue = this._transform.extract(current); + + if (!this._previous || this._entryCount > this._compactAfterEntries) { + // No previous state, create initial + this._previous = currentValue; + this._entryCount = 1; + const entry: Entry = { kind: EntryKind.Initial, v: currentValue }; + return { op: 'replace', data: VSBuffer.fromString(JSON.stringify(entry) + '\n') }; + } + + // Generate diff entries + const entries: Entry[] = []; + const path: ObjectPath = []; + this._diff(this._transform, path, this._previous, currentValue, entries); + + if (entries.length === 0) { + // No changes + return { op: 'append', data: VSBuffer.fromString('') }; + } + + this._entryCount += entries.length; + this._previous = currentValue; + + // Append entries - build string directly + let data = ''; + for (const e of entries) { + data += JSON.stringify(e) + '\n'; + } + return { op: 'append', data: VSBuffer.fromString(data) }; + } + + private _applySet(state: unknown, path: ObjectPath, value: unknown): void { + if (path.length === 0) { + return; // Root replacement handled by caller + } + + let current = state as Record; + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]] as Record; + } + + current[path[path.length - 1]] = value; + } + + private _applyPush(state: unknown, path: ObjectPath, values: unknown[] | undefined, startIndex: number | undefined): void { + let current = state as Record; + for (let i = 0; i < path.length - 1; i++) { + current = current[path[i]] as Record; + } + + const arrayKey = path[path.length - 1]; + const arr = current[arrayKey] as unknown[] || []; + + if (startIndex !== undefined) { + arr.length = startIndex; + } + + if (values && values.length > 0) { + arr.push(...values); + } + + current[arrayKey] = arr; + } + + private _diff( + transform: Transform, + path: ObjectPath, + prev: R, + curr: R, + entries: Entry[] + ): void { + if (transform.kind === TransformKind.Key || transform.kind === TransformKind.Primitive) { + // Simple value change - copy path since we're storing it + if (!transform.equals(prev, curr)) { + entries.push({ kind: EntryKind.Set, k: path.slice(), v: curr }); + } + } else if (isUndefinedOrNull(prev) || isUndefinedOrNull(curr)) { + if (prev !== curr) { + if (curr === undefined) { + entries.push({ kind: EntryKind.Delete, k: path.slice() }); + } else if (curr === null) { + entries.push({ kind: EntryKind.Set, k: path.slice(), v: null }); + } else { + entries.push({ kind: EntryKind.Set, k: path.slice(), v: curr }); + } + } + } else if (transform.kind === TransformKind.Array) { + this._diffArray(transform, path, prev as unknown[], curr as unknown[], entries); + } else if (transform.kind === TransformKind.Object) { + this._diffObject(transform.children, path, prev, curr, entries, transform.sealed as ((obj: unknown, wasSerialized: boolean) => boolean) | undefined); + } else { + throw new Error(`Unknown transform kind ${JSON.stringify(transform)}`); + } + } + + private _diffObject( + children: SchemaEntries, + path: ObjectPath, + prev: unknown, + curr: unknown, + entries: Entry[], + sealed?: (obj: unknown, wasSerialized: boolean) => boolean, + ): void { + const prevObj = prev as Record | undefined; + const currObj = curr as Record; + + // First check key fields (sorted to front) - if any key changed, replace the entire object + let i = 0; + for (; i < children.length; i++) { + const [key, transform] = children[i]; + if (transform.kind !== TransformKind.Key) { + break; // Keys are sorted to front, so we can stop + } + if (!transform.equals(prevObj?.[key], currObj[key])) { + // Key changed, replace entire object + entries.push({ kind: EntryKind.Set, k: path.slice(), v: curr }); + return; + } + } + + // If both objects are sealed, we've already verified keys match above, + // so we can skip diffing the other properties since sealed objects don't change + if (sealed && sealed(prev, true) && sealed(curr, false)) { + return; + } + + // Diff each property using mutable path + for (; i < children.length; i++) { + const [key, transform] = children[i]; + path.push(key); + this._diff(transform, path, prevObj?.[key], currObj[key], entries); + path.pop(); + } + } + + private _diffArray( + transform: TransformArray, + path: ObjectPath, + prev: unknown[] | undefined, + curr: unknown[] | undefined, + entries: Entry[] + ): void { + const prevArr = prev || []; + const currArr = curr || []; + + const itemSchema = transform.itemSchema; + const minLen = Math.min(prevArr.length, currArr.length); + + // If the item schema is an object, we can recurse into it to diff individual + // properties instead of replacing the entire item. However, we only do this + // if the key fields match. + if (itemSchema.kind === TransformKind.Object) { + const childEntries = itemSchema.children; + + // Diff common elements by recursing into them + for (let i = 0; i < minLen; i++) { + const prevItem = prevArr[i]; + const currItem = currArr[i]; + + // Check if key fields match - if not, we need to replace from this point + if (this._hasKeyMismatch(childEntries, prevItem, currItem)) { + // Key mismatch: replace from this point onward + const newItems = currArr.slice(i); + entries.push({ kind: EntryKind.Push, k: path.slice(), v: newItems.length > 0 ? newItems : undefined, i }); + return; + } + + // Keys match, recurse into the object + path.push(i); + this._diffObject(childEntries, path, prevItem, currItem, entries, itemSchema.sealed); + path.pop(); + } + + // Handle length changes + if (currArr.length > prevArr.length) { + entries.push({ kind: EntryKind.Push, k: path.slice(), v: currArr.slice(prevArr.length) }); + } else if (currArr.length < prevArr.length) { + entries.push({ kind: EntryKind.Push, k: path.slice(), i: currArr.length }); + } + } else { + // No children schema, use the original positional comparison + let firstMismatch = -1; + + for (let i = 0; i < minLen; i++) { + if (!itemSchema.equals(prevArr[i], currArr[i])) { + firstMismatch = i; + break; + } + } + + if (firstMismatch === -1) { + // All common elements match + if (currArr.length > prevArr.length) { + // New items appended + entries.push({ kind: EntryKind.Push, k: path.slice(), v: currArr.slice(prevArr.length) }); + } else if (currArr.length < prevArr.length) { + // Items removed from end + entries.push({ kind: EntryKind.Push, k: path.slice(), i: currArr.length }); + } + // else: same length, all match - no change + } else { + // Mismatch found, rewrite from that point + const newItems = currArr.slice(firstMismatch); + entries.push({ kind: EntryKind.Push, k: path.slice(), v: newItems.length > 0 ? newItems : undefined, i: firstMismatch }); + } + } + } + + private _hasKeyMismatch(children: SchemaEntries, prev: unknown, curr: unknown): boolean { + const prevObj = prev as Record | undefined; + const currObj = curr as Record; + for (const [key, transform] of children) { + if (transform.kind !== TransformKind.Key) { + break; // Keys are sorted to front, so we can stop + } + if (!transform.equals(prevObj?.[key], currObj[key])) { + return true; + } + } + return false; + } +} diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts similarity index 90% rename from src/vs/workbench/contrib/chat/common/chatAgents.ts rename to src/vs/workbench/contrib/chat/common/participants/chatAgents.ts index 38325cbda81..303f24a1f3b 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/participants/chatAgents.ts @@ -3,32 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { findLast } from '../../../../base/common/arraysFind.js'; -import { timeout } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.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'; -import { URI } from '../../../../base/common/uri.js'; -import { Command } from '../../../../editor/common/languages.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; -import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ChatContextKeys } from './chatContextKeys.js'; -import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestModeInstructions, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; +import { findLast } from '../../../../../base/common/arraysFind.js'; +import { timeout } from '../../../../../base/common/async.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.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'; +import { URI } from '../../../../../base/common/uri.js'; +import { Command } from '../../../../../editor/common/languages.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../platform/log/common/log.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { asJson, IRequestService } from '../../../../../platform/request/common/request.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { ChatContextKeys } from '../actions/chatContextKeys.js'; +import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestModeInstructions, IChatRequestVariableData, ISerializableChatAgentData } from '../model/chatModel.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; -import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from './constants.js'; +import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from '../chatService/chatService.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; //#region agent service, commands etc @@ -70,6 +70,7 @@ export interface IChatAgentData { isDynamic?: boolean; /** This agent is contributed from core and not from an extension */ isCore?: boolean; + canAccessPreviousChatHistory?: boolean; metadata: IChatAgentMetadata; slashCommands: IChatAgentCommand[]; locations: ChatAgentLocation[]; @@ -114,15 +115,6 @@ export interface IChatAgentCommand extends IRawChatCommandContribution { followupPlaceholder?: string; } -export interface IChatRequesterInformation { - name: string; - - /** - * A full URI for the icon of the requester. - */ - icon?: URI; -} - export interface IChatAgentMetadata { helpTextPrefix?: string | IMarkdownString; helpTextPostfix?: string | IMarkdownString; @@ -133,7 +125,6 @@ export interface IChatAgentMetadata { supportIssueReporting?: boolean; followupPlaceholder?: string; isSticky?: boolean; - requester?: IChatRequesterInformation; additionalWelcomeMessage?: string | IMarkdownString; } @@ -141,7 +132,7 @@ export type UserSelectedTools = Record; export interface IChatAgentRequest { - sessionId: string; + sessionResource: URI; requestId: string; agentId: string; command?: string; @@ -152,21 +143,21 @@ export interface IChatAgentRequest { variables: IChatRequestVariableData; location: ChatAgentLocation; locationData?: Revived; - acceptedConfirmationData?: any[]; - rejectedConfirmationData?: any[]; + acceptedConfirmationData?: unknown[]; + rejectedConfirmationData?: unknown[]; userSelectedModelId?: string; userSelectedTools?: UserSelectedTools; modeInstructions?: IChatRequestModeInstructions; editedFileEvents?: IChatAgentEditedFileEvent[]; - isSubagent?: boolean; - /** - * Summary data for chat sessions context + * Unique ID for the subagent invocation, used to group tool calls from the same subagent run together. */ - chatSummary?: { - prompt?: string; - history?: string; - }; + subAgentInvocationId?: string; + /** + * Display name of the subagent that is invoking this request. + */ + subAgentName?: string; + } export interface IChatQuestion { @@ -180,11 +171,23 @@ export interface IChatAgentResultTimings { totalElapsed: number; } +export interface IChatAgentPromptTokenDetail { + category: string; + label: string; + percentageOfPrompt: number; +} + +export interface IChatAgentResultUsage { + promptTokens: number; + completionTokens: number; + promptTokenDetails?: readonly IChatAgentPromptTokenDetail[]; +} + export interface IChatAgentResult { errorDetails?: IChatResponseErrorDetails; timings?: IChatAgentResultTimings; /** Extra properties that the agent can use to identify a result */ - readonly metadata?: { readonly [key: string]: any }; + readonly metadata?: { readonly [key: string]: unknown }; readonly details?: string; nextQuestion?: IChatQuestion; } @@ -366,7 +369,7 @@ export class ChatAgentService extends Disposable implements IChatAgentService { this._onDidChangeAgents.fire(undefined); if (entry.data.isDefault) { - this._hasDefaultAgent.set(Iterable.some(this._agents.values(), agent => agent.data.isDefault)); + this._hasDefaultAgent.set(Iterable.some(this._agents.values(), agent => agent.data.isDefault && !!agent.impl)); } }); } @@ -759,8 +762,12 @@ interface IOldSerializedChatAgentData extends Omit, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; +export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> }; export const IChatSlashCommandService = createDecorator('chatSlashCommandService'); @@ -52,7 +53,7 @@ export interface IChatSlashCommandService { _serviceBrand: undefined; readonly onDidChangeCommands: Event; registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; + executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>; getCommands(location: ChatAgentLocation, mode: ChatModeKind): Array; hasCommand(id: string): boolean; } @@ -102,7 +103,7 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom return this._commands.has(id); } - async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { + async executeCommand(id: string, prompt: string, progress: IProgress, history: IChatMessage[], location: ChatAgentLocation, sessionResource: URI, token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> { const data = this._commands.get(id); if (!data) { throw new Error('No command with id ${id} NOT registered'); @@ -114,6 +115,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom throw new Error(`No command with id ${id} NOT resolved`); } - return await data.command(prompt, progress, history, location, token); + return await data.command(prompt, progress, history, location, sessionResource, token); } } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts index a4478135735..6468c60cdfa 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/chatPromptFilesContribution.ts @@ -3,22 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; import { localize } from '../../../../../nls.js'; +import { ExtensionIdentifier, IExtensionManifest } from '../../../../../platform/extensions/common/extensions.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { joinPath, isEqualOrParent } from '../../../../../base/common/resources.js'; -import { IPromptsService } from './service/promptsService.js'; +import { IPromptsService, PromptsStorage } from './service/promptsService.js'; import { PromptsType } from './promptTypes.js'; -import { DisposableMap } from '../../../../../base/common/lifecycle.js'; +import { UriComponents } from '../../../../../base/common/uri.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { Registry } from '../../../../../platform/registry/common/platform.js'; +import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; interface IRawChatFileContribution { - readonly name: string; readonly path: string; - readonly description?: string; // reserved for future use + readonly name?: string; + readonly description?: string; } -type ChatContributionPoint = 'chatPromptFiles' | 'chatInstructions' | 'chatAgents'; +enum ChatContributionPoint { + chatInstructions = 'chatInstructions', + chatAgents = 'chatAgents', + chatPromptFiles = 'chatPromptFiles', + chatSkills = 'chatSkills' +} function registerChatFilesExtensionPoint(point: ChatContributionPoint) { return extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ @@ -31,24 +43,23 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { type: 'object', defaultSnippets: [{ body: { - name: 'exampleName', path: './relative/path/to/file.md', - description: 'Optional description' } }], - required: ['name', 'path'], + required: ['path'], properties: { - name: { - description: localize('chatContribution.property.name', 'Identifier for this file. Must be unique within this extension for this contribution point.'), - type: 'string', - pattern: '^[\\w.-]+$' - }, path: { description: localize('chatContribution.property.path', 'Path to the file relative to the extension root.'), type: 'string' }, + name: { + description: localize('chatContribution.property.name', '(Optional) Name for this entry.'), + deprecationMessage: localize('chatContribution.property.name.deprecated', 'Specify "name" in the prompt file itself instead.'), + type: 'string' + }, description: { - description: localize('chatContribution.property.description', '(Optional) Description of the file.'), + description: localize('chatContribution.property.description', '(Optional) Description of the entry.'), + deprecationMessage: localize('chatContribution.property.description.deprecated', 'Specify "description" in the prompt file itself instead.'), type: 'string' } } @@ -57,20 +68,22 @@ function registerChatFilesExtensionPoint(point: ChatContributionPoint) { }); } -const epPrompt = registerChatFilesExtensionPoint('chatPromptFiles'); -const epInstructions = registerChatFilesExtensionPoint('chatInstructions'); -const epAgents = registerChatFilesExtensionPoint('chatAgents'); +const epPrompt = registerChatFilesExtensionPoint(ChatContributionPoint.chatPromptFiles); +const epInstructions = registerChatFilesExtensionPoint(ChatContributionPoint.chatInstructions); +const epAgents = registerChatFilesExtensionPoint(ChatContributionPoint.chatAgents); +const epSkills = registerChatFilesExtensionPoint(ChatContributionPoint.chatSkills); function pointToType(contributionPoint: ChatContributionPoint): PromptsType { switch (contributionPoint) { - case 'chatPromptFiles': return PromptsType.prompt; - case 'chatInstructions': return PromptsType.instructions; - case 'chatAgents': return PromptsType.agent; + case ChatContributionPoint.chatPromptFiles: return PromptsType.prompt; + case ChatContributionPoint.chatInstructions: return PromptsType.instructions; + case ChatContributionPoint.chatAgents: return PromptsType.agent; + case ChatContributionPoint.chatSkills: return PromptsType.skill; } } -function key(extensionId: ExtensionIdentifier, type: PromptsType, name: string) { - return `${extensionId.value}/${type}/${name}`; +function key(extensionId: ExtensionIdentifier, type: PromptsType, path: string) { + return `${extensionId.value}/${type}/${path}`; } export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribution { @@ -81,9 +94,10 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut constructor( @IPromptsService private readonly promptsService: IPromptsService, ) { - this.handle(epPrompt, 'chatPromptFiles'); - this.handle(epInstructions, 'chatInstructions'); - this.handle(epAgents, 'chatAgents'); + this.handle(epPrompt, ChatContributionPoint.chatPromptFiles); + this.handle(epInstructions, ChatContributionPoint.chatInstructions); + this.handle(epAgents, ChatContributionPoint.chatAgents); + this.handle(epSkills, ChatContributionPoint.chatSkills); } private handle(extensionPoint: extensionsRegistry.IExtensionPoint, contributionPoint: ChatContributionPoint) { @@ -91,38 +105,140 @@ export class ChatPromptFilesExtensionPointHandler implements IWorkbenchContribut for (const ext of delta.added) { const type = pointToType(contributionPoint); for (const raw of ext.value) { - if (!raw.name || !raw.name.match(/^[\w.-]+$/)) { - ext.collector.error(localize('extension.invalid.name', "Extension '{0}' cannot register {1} entry with invalid name '{2}'.", ext.description.identifier.value, contributionPoint, raw.name)); - continue; - } if (!raw.path) { - ext.collector.error(localize('extension.missing.path', "Extension '{0}' cannot register {1} entry '{2}' without path.", ext.description.identifier.value, contributionPoint, raw.name)); - continue; - } - if (!raw.description) { - ext.collector.error(localize('extension.missing.description', "Extension '{0}' cannot register {1} entry '{2}' without description.", ext.description.identifier.value, contributionPoint, raw.name)); + ext.collector.error(localize('extension.missing.path', "Extension '{0}' cannot register {1} entry without path.", ext.description.identifier.value, contributionPoint)); continue; } const fileUri = joinPath(ext.description.extensionLocation, raw.path); if (!isEqualOrParent(fileUri, ext.description.extensionLocation)) { - ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' path resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.name)); + ext.collector.error(localize('extension.invalid.path', "Extension '{0}' {1} entry '{2}' resolves outside the extension.", ext.description.identifier.value, contributionPoint, raw.path)); continue; } try { - const d = this.promptsService.registerContributedFile(type, raw.name, raw.description, fileUri, ext.description); - this.registrations.set(key(ext.description.identifier, type, raw.name), d); + const d = this.promptsService.registerContributedFile(type, fileUri, ext.description, raw.name, raw.description); + this.registrations.set(key(ext.description.identifier, type, raw.path), d); } catch (e) { const msg = e instanceof Error ? e.message : String(e); - ext.collector.error(localize('extension.registration.failed', "Failed to register {0} entry '{1}': {2}", contributionPoint, raw.name, msg)); + ext.collector.error(localize('extension.registration.failed', "Extension '{0}' {1}. Failed to register {2}: {3}", ext.description.identifier.value, contributionPoint, raw.path, msg)); } } } for (const ext of delta.removed) { const type = pointToType(contributionPoint); for (const raw of ext.value) { - this.registrations.deleteAndDispose(key(ext.description.identifier, type, raw.name)); + this.registrations.deleteAndDispose(key(ext.description.identifier, type, raw.path)); } } }); } } + +/** + * Result type for the extension prompt file provider command. + */ +export interface IExtensionPromptFileResult { + readonly uri: UriComponents; + readonly type: PromptsType; +} + +/** + * Register the command to list all extension-contributed prompt files. + */ +CommandsRegistry.registerCommand('_listExtensionPromptFiles', async (accessor): Promise => { + const promptsService = accessor.get(IPromptsService); + + // Get extension prompt files for all prompt types in parallel + const [agents, instructions, prompts, skills] = await Promise.all([ + promptsService.listPromptFiles(PromptsType.agent, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.instructions, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.prompt, CancellationToken.None), + promptsService.listPromptFiles(PromptsType.skill, CancellationToken.None), + ]); + + // Combine all files and collect extension-contributed ones + const result: IExtensionPromptFileResult[] = []; + for (const file of [...agents, ...instructions, ...prompts, ...skills]) { + if (file.storage === PromptsStorage.extension) { + result.push({ uri: file.uri.toJSON(), type: file.type }); + } + } + + return result; +}); + +class ChatPromptFilesDataRenderer extends Disposable implements IExtensionFeatureTableRenderer { + readonly type = 'table'; + + constructor(private readonly contributionPoint: ChatContributionPoint) { + super(); + } + + shouldRender(manifest: IExtensionManifest): boolean { + return !!manifest.contributes?.[this.contributionPoint]; + } + + render(manifest: IExtensionManifest): IRenderedData { + const contributions = manifest.contributes?.[this.contributionPoint] ?? []; + if (!contributions.length) { + return { data: { headers: [], rows: [] }, dispose: () => { } }; + } + + const headers = [ + localize('chatFilesName', "Name"), + localize('chatFilesDescription', "Description"), + localize('chatFilesPath', "Path"), + ]; + + const rows: IRowData[][] = contributions.map(d => { + return [ + d.name ?? '-', + d.description ?? '-', + d.path, + ]; + }); + + return { + data: { + headers, + rows + }, + dispose: () => { } + }; + } +} + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatPromptFiles, + label: localize('chatPromptFiles', "Chat Prompt Files"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatPromptFiles]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatInstructions, + label: localize('chatInstructions', "Chat Instructions"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatInstructions]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatAgents, + label: localize('chatAgents', "Chat Agents"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatAgents]), +}); + +Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ + id: ChatContributionPoint.chatSkills, + label: localize('chatSkills', "Chat Skills"), + access: { + canToggle: false + }, + renderer: new SyncDescriptor(ChatPromptFilesDataRenderer, [ChatContributionPoint.chatSkills]), +}); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts index 6afc0bb8187..070cbefa7ef 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/computeAutomaticInstructions.ts @@ -16,13 +16,17 @@ import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind } from '../chatVariableEntries.js'; -import { IToolData } from '../languageModelToolsService.js'; +import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, toPromptFileVariableEntry, toPromptTextVariableEntry, PromptFileVariableKind, IPromptTextVariableEntry, ChatRequestToolReferenceEntry, toToolVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ILanguageModelToolsService, IToolData, VSCodeToolReference } from '../tools/languageModelToolsService.js'; import { PromptsConfig } from './config/config.js'; import { isPromptOrInstructionsFile } from './config/promptFileLocations.js'; import { PromptsType } from './promptTypes.js'; import { ParsedPromptFile } from './promptFileParser.js'; -import { IPromptPath, IPromptsService } from './service/promptsService.js'; +import { ICustomAgent, IPromptPath, IPromptsService } from './service/promptsService.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { ChatConfiguration, ChatModeKind } from '../constants.js'; +import { UserSelectedTools } from '../participants/chatAgents.js'; +import { IChatMode } from '../chatModes.js'; export type InstructionsCollectionEvent = { applyingInstructionsCount: number; @@ -50,7 +54,9 @@ export class ComputeAutomaticInstructions { private _parseResults: ResourceMap = new ResourceMap(); constructor( - private readonly _readFileTool: IToolData | undefined, + private readonly _agent: IChatMode, + private readonly _enabledTools: UserSelectedTools | undefined, + private readonly _enabledSubagents: (readonly string[]) | undefined, @IPromptsService private readonly _promptsService: IPromptsService, @ILogService public readonly _logService: ILogService, @ILabelService private readonly _labelService: ILabelService, @@ -58,6 +64,7 @@ export class ComputeAutomaticInstructions { @IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService, @IFileService private readonly _fileService: IFileService, @ITelemetryService private readonly _telemetryService: ITelemetryService, + @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, ) { } @@ -94,10 +101,9 @@ export class ComputeAutomaticInstructions { // get copilot instructions await this._addAgentInstructions(variables, telemetryEvent, token); - const instructionsWithPatternsList = await this._getInstructionsWithPatternsList(instructionFiles, variables, token); - if (instructionsWithPatternsList.length > 0) { - const text = instructionsWithPatternsList.join('\n'); - variables.add(toPromptTextVariableEntry(text, true)); + const instructionsListVariable = await this._getInstructionsWithPatternsList(instructionFiles, variables, token); + if (instructionsListVariable) { + variables.add(instructionsListVariable); telemetryEvent.listedInstructionsCount++; } @@ -112,6 +118,11 @@ export class ComputeAutomaticInstructions { /** public for testing */ public async addApplyingInstructions(instructionFiles: readonly IPromptPath[], context: { files: ResourceSet; instructions: ResourceSet }, variables: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { + const includeApplyingInstructions = this._configurationService.getValue(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS); + if (!includeApplyingInstructions && this._agent.kind !== ChatModeKind.Edit) { + this._logService.trace(`[InstructionsContextComputer] includeApplyingInstructions is disabled and agent kind is not Edit. No applying instructions will be added.`); + return; + } for (const { uri } of instructionFiles) { const parsedFile = await this._parseInstructionsFile(uri, token); @@ -234,51 +245,166 @@ export class ComputeAutomaticInstructions { return undefined; } - private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise { - if (!this._readFileTool) { - this._logService.trace('[InstructionsContextComputer] No readFile tool available, skipping instructions with patterns list.'); - return []; + private _getTool(referenceName: string): { tool: IToolData; variable: string } | undefined { + if (!this._enabledTools) { + return undefined; } - const searchNestedAgentMd = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD); - const agentsMdPromise = searchNestedAgentMd ? this._promptsService.findAgentMDsInWorkspace(token) : Promise.resolve([]); + const tool = this._languageModelToolsService.getToolByName(referenceName); + if (tool && this._enabledTools[tool.id]) { + return { tool, variable: `#tool:${this._languageModelToolsService.getFullReferenceName(tool)}` }; + } + return undefined; + } + + private async _getInstructionsWithPatternsList(instructionFiles: readonly IPromptPath[], _existingVariables: ChatRequestVariableSet, token: CancellationToken): Promise { + const readTool = this._getTool('readFile'); + const runSubagentTool = this._getTool(VSCodeToolReference.runSubagent); const entries: string[] = []; - for (const { uri } of instructionFiles) { - const parsedFile = await this._parseInstructionsFile(uri, token); - if (parsedFile) { - const applyTo = parsedFile.header?.applyTo ?? '**/*'; - const description = parsedFile.header?.description ?? ''; - entries.push(`| '${getFilePath(uri)}' | ${applyTo} | ${description} |`); + if (readTool) { + + const searchNestedAgentMd = this._configurationService.getValue(PromptsConfig.USE_NESTED_AGENT_MD); + const agentsMdPromise = searchNestedAgentMd ? this._promptsService.findAgentMDsInWorkspace(token) : Promise.resolve([]); + + entries.push(''); + entries.push('Here is a list of instruction files that contain rules for working with this codebase.'); + entries.push('These files are important for understanding the codebase structure, conventions, and best practices.'); + entries.push('Please make sure to follow the rules specified in these files when working with the codebase.'); + entries.push(`If the file is not already available as attachment, use the ${readTool.variable} tool to acquire it.`); + entries.push('Make sure to acquire the instructions before working with the codebase.'); + let hasContent = false; + for (const { uri } of instructionFiles) { + const parsedFile = await this._parseInstructionsFile(uri, token); + if (parsedFile) { + entries.push(''); + if (parsedFile.header) { + const { description, applyTo } = parsedFile.header; + if (description) { + entries.push(`${description}`); + } + entries.push(`${getFilePath(uri)}`); + if (applyTo) { + entries.push(`${applyTo}`); + } + } else { + entries.push(`${getFilePath(uri)}`); + } + entries.push(''); + hasContent = true; + } } - } - const agentsMdFiles = await agentsMdPromise; - for (const uri of agentsMdFiles) { - if (uri) { + const agentsMdFiles = await agentsMdPromise; + for (const uri of agentsMdFiles) { const folderName = this._labelService.getUriLabel(dirname(uri), { relative: true }); const description = folderName.trim().length === 0 ? localize('instruction.file.description.agentsmd.root', 'Instructions for the workspace') : localize('instruction.file.description.agentsmd.folder', 'Instructions for folder \'{0}\'', folderName); - entries.push(`| '${getFilePath(uri)}' | | ${description} |`); + entries.push(''); + entries.push(`${description}`); + entries.push(`${getFilePath(uri)}`); + entries.push(''); + hasContent = true; + } - } + if (!hasContent) { + entries.length = 0; // clear entries + } else { + entries.push('', '', ''); // add trailing newline + } + const agentSkills = await this._promptsService.findAgentSkills(token); + if (agentSkills && agentSkills.length > 0) { + const useSkillAdherencePrompt = this._configurationService.getValue(PromptsConfig.USE_SKILL_ADHERENCE_PROMPT); + entries.push(''); + if (useSkillAdherencePrompt) { + // Stronger skill adherence prompt for experimental feature + entries.push('Skills provide specialized capabilities, domain knowledge, and refined workflows for producing high-quality outputs. Each skill folder contains tested instructions for specific domains like testing strategies, API design, or performance optimization. Multiple skills can be combined when a task spans different domains.'); + entries.push(`BLOCKING REQUIREMENT: When a skill applies to the user's request, you MUST load and read the SKILL.md file IMMEDIATELY as your first action, BEFORE generating any other response or taking action on the task. Use ${readTool.variable} to load the relevant skill(s).`); + entries.push('NEVER just mention or reference a skill in your response without actually reading it first. If a skill is relevant, load it before proceeding.'); + entries.push('How to determine if a skill applies:'); + entries.push('1. Review the available skills below and match their descriptions against the user\'s request'); + entries.push('2. If any skill\'s domain overlaps with the task, load that skill immediately'); + entries.push('3. When multiple skills apply (e.g., a flowchart in documentation), load all relevant skills'); + entries.push('Examples:'); + entries.push(`- "Help me write unit tests for this module" -> Load the testing skill via ${readTool.variable} FIRST, then proceed`); + entries.push(`- "Optimize this slow function" -> Load the performance-profiling skill via ${readTool.variable} FIRST, then proceed`); + entries.push(`- "Add a discount code field to checkout" -> Load both the checkout-flow and form-validation skills FIRST`); + entries.push('Available skills:'); + } else { + entries.push('Here is a list of skills that contain domain specific knowledge on a variety of topics.'); + entries.push('Each skill comes with a description of the topic and a file path that contains the detailed instructions.'); + entries.push(`When a user asks you to perform a task that falls within the domain of a skill, use the ${readTool.variable} tool to acquire the full instructions from the file URI.`); + } + for (const skill of agentSkills) { + entries.push(''); + entries.push(`${skill.name}`); + if (skill.description) { + entries.push(`${skill.description}`); + } + entries.push(`${getFilePath(skill.uri)}`); + entries.push(''); + } + entries.push('', '', ''); // add trailing newline + } + } + if (runSubagentTool && this._configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + const canUseAgent = (() => { + if (!this._enabledSubagents || this._enabledSubagents.includes('*')) { + return (agent: ICustomAgent) => agent.visibility.agentInvokable; + } else { + const subagents = this._enabledSubagents; + return (agent: ICustomAgent) => subagents.includes(agent.name); + } + })(); + const agents = await this._promptsService.getCustomAgents(token); + if (agents.length > 0) { + entries.push(''); + entries.push('Here is a list of agents that can be used when running a subagent.'); + entries.push('Each agent has optionally a description with the agent\'s purpose and expertise. When asked to run a subagent, choose the most appropriate agent from this list.'); + entries.push(`Use the ${runSubagentTool.variable} tool with the agent name to run the subagent.`); + for (const agent of agents) { + if (canUseAgent(agent)) { + entries.push(''); + entries.push(`${agent.name}`); + if (agent.description) { + entries.push(`${agent.description}`); + } + if (agent.argumentHint) { + entries.push(`${agent.argumentHint}`); + } + entries.push(''); + } + } + entries.push('', '', ''); // add trailing newline + } + } if (entries.length === 0) { - return entries; + return undefined; } - const toolName = 'read_file'; // workaround https://github.com/microsoft/vscode/issues/252167 - return [ - 'Here is a list of instruction files that contain rules for modifying or creating new code.', - 'These files are important for ensuring that the code is modified or created correctly.', - 'Please make sure to follow the rules specified in these files when working with the codebase.', - `If the file is not already available as attachment, use the \`${toolName}\` tool to acquire it.`, - 'Make sure to acquire the instructions before making any changes to the code.', - '| File | Applies To | Description |', - '| ------- | --------- | ----------- |', - ].concat(entries); + const content = entries.join('\n'); + const toolReferences: ChatRequestToolReferenceEntry[] = []; + const collectToolReference = (tool: { tool: IToolData; variable: string } | undefined) => { + if (tool) { + let offset = content.indexOf(tool.variable); + while (offset >= 0) { + toolReferences.push(toToolVariableEntry(tool.tool, new OffsetRange(offset, offset + tool.variable.length))); + offset = content.indexOf(tool.variable, offset + 1); + } + } + }; + collectToolReference(readTool); + collectToolReference(runSubagentTool); + return toPromptTextVariableEntry(content, true, toolReferences); } private async _addReferencedInstructions(attachedContext: ChatRequestVariableSet, telemetryEvent: InstructionsCollectionEvent, token: CancellationToken): Promise { + const includeReferencedInstructions = this._configurationService.getValue(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS); + if (!includeReferencedInstructions && this._agent.kind !== ChatModeKind.Edit) { + this._logService.trace(`[InstructionsContextComputer] includeReferencedInstructions is disabled and agent kind is not Edit. No referenced instructions will be added.`); + return; + } + const seen = new ResourceSet(); const todo: URI[] = []; for (const variable of attachedContext.asArray()) { 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 a1b91611fdb..c543b912bd9 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -6,7 +6,8 @@ import type { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.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'; +import { getPromptFileDefaultLocations, IPromptSourceFolder, PromptFileSource } from './promptFileLocations.js'; +import { PromptsStorage } from '../service/promptsService.js'; /** * Configuration helper for the `reusable prompts` feature. @@ -55,9 +56,20 @@ export namespace PromptsConfig { export const INSTRUCTIONS_LOCATION_KEY = 'chat.instructionsFilesLocations'; /** * Configuration key for the locations of mode files. + * @deprecated Use {@link AGENTS_LOCATION_KEY} instead */ export const MODE_LOCATION_KEY = 'chat.modeFilesLocations'; + /** + * Configuration key for the locations of agent files (with simplified path support). + */ + export const AGENTS_LOCATION_KEY = 'chat.agentFilesLocations'; + + /** + * Configuration key for the locations of skill folders. + */ + export const SKILLS_LOCATION_KEY = 'chat.agentSkillsLocations'; + /** * Configuration key for prompt file suggestions. */ @@ -78,9 +90,29 @@ export namespace PromptsConfig { */ export const USE_NESTED_AGENT_MD = 'chat.useNestedAgentsMdFiles'; + /** + * Configuration key for agent skills usage. + */ + export const USE_AGENT_SKILLS = 'chat.useAgentSkills'; + + /** + * Configuration key for enabling stronger skill adherence prompt (experimental). + */ + export const USE_SKILL_ADHERENCE_PROMPT = 'chat.experimental.useSkillAdherencePrompt'; + + /** + * Configuration key for including applying instructions. + */ + export const INCLUDE_APPLYING_INSTRUCTIONS = 'chat.includeApplyingInstructions'; + + /** + * Configuration key for including referenced instructions. + */ + export const INCLUDE_REFERENCED_INSTRUCTIONS = 'chat.includeReferencedInstructions'; + /** * Get value of the `reusable prompt locations` configuration setting. - * @see {@link PROMPT_LOCATIONS_CONFIG_KEY}, {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}, {@link MODE_LOCATIONS_CONFIG_KEY}. + * @see {@link PROMPT_LOCATIONS_CONFIG_KEY}, {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}, {@link MODE_LOCATIONS_CONFIG_KEY}, {@link SKILLS_LOCATION_KEY}. */ export function getLocationsValue(configService: IConfigurationService, type: PromptsType): Record | undefined { const key = getPromptFileLocationsConfigKey(type); @@ -114,29 +146,34 @@ export namespace PromptsConfig { /** * Gets list of source folders for prompt files. - * Defaults to {@link PROMPT_DEFAULT_SOURCE_FOLDER}, {@link INSTRUCTIONS_DEFAULT_SOURCE_FOLDER} or {@link MODE_DEFAULT_SOURCE_FOLDER}. + * Defaults to {@link PROMPT_DEFAULT_SOURCE_FOLDER}, {@link INSTRUCTIONS_DEFAULT_SOURCE_FOLDER}, {@link MODE_DEFAULT_SOURCE_FOLDER} or {@link SKILLS_LOCATION_KEY}. */ - export function promptSourceFolders(configService: IConfigurationService, type: PromptsType): string[] { + export function promptSourceFolders(configService: IConfigurationService, type: PromptsType): IPromptSourceFolder[] { const value = getLocationsValue(configService, type); - const defaultSourceFolder = getPromptFileDefaultLocation(type); + const defaultSourceFolders = getPromptFileDefaultLocations(type); // note! the `value &&` part handles the `undefined`, `null`, and `false` cases if (value && (typeof value === 'object')) { - const paths: string[] = []; + const paths: IPromptSourceFolder[] = []; + const defaultFolderPathsSet = new Set(defaultSourceFolders.map(f => f.path)); - // if the default source folder is not explicitly disabled, add it - if (value[defaultSourceFolder] !== false) { - paths.push(defaultSourceFolder); + // add default source folders that are not explicitly disabled + for (const defaultFolder of defaultSourceFolders) { + if (value[defaultFolder.path] !== false) { + paths.push(defaultFolder); + } } // copy all the enabled paths to the result list for (const [path, enabledValue] of Object.entries(value)) { - // we already added the default source folder, so skip it - if ((enabledValue === false) || (path === defaultSourceFolder)) { + // we already added the default source folders, so skip them + if ((enabledValue === false) || defaultFolderPathsSet.has(path)) { continue; } - paths.push(path); + // determine location type in the general case + const storage = isTildePath(path) ? PromptsStorage.user : PromptsStorage.local; + paths.push({ path, source: storage === PromptsStorage.local ? PromptFileSource.ConfigPersonal : PromptFileSource.ConfigWorkspace, storage }); } return paths; @@ -205,7 +242,9 @@ export function getPromptFileLocationsConfigKey(type: PromptsType): string { case PromptsType.prompt: return PromptsConfig.PROMPT_LOCATIONS_KEY; case PromptsType.agent: - return PromptsConfig.MODE_LOCATION_KEY; + return PromptsConfig.AGENTS_LOCATION_KEY; + case PromptsType.skill: + return PromptsConfig.SKILLS_LOCATION_KEY; default: throw new Error('Unknown prompt type'); } @@ -239,3 +278,15 @@ export function asBoolean(value: unknown): boolean | undefined { return undefined; } + +/** + * Helper to check if a path starts with tilde (user home). + * Only supports Unix-style (`~/`) paths for cross-platform sharing. + * Backslash paths (`~\`) are not supported to ensure paths are shareable in repos. + * + * @param path - path to check + * @returns `true` if the path starts with `~/` + */ +export function isTildePath(path: string): boolean { + return path.startsWith('~/'); +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 0122c3244d5..713ddaaad3b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -6,6 +6,7 @@ import { URI } from '../../../../../../base/common/uri.js'; import { basename, dirname } from '../../../../../../base/common/path.js'; import { PromptsType } from '../promptTypes.js'; +import { PromptsStorage } from '../service/promptsService.js'; /** * File extension for the reusable prompt files. @@ -27,6 +28,11 @@ export const LEGACY_MODE_FILE_EXTENSION = '.chatmode.md'; */ export const AGENT_FILE_EXTENSION = '.agent.md'; +/** + * Skill file name (case insensitive). + */ +export const SKILL_FILENAME = 'SKILL.md'; + /** * Copilot custom instructions file name. */ @@ -53,6 +59,87 @@ export const LEGACY_MODE_DEFAULT_SOURCE_FOLDER = '.github/chatmodes'; */ export const AGENTS_SOURCE_FOLDER = '.github/agents'; +/** + * Tracks where prompt files originate from. + */ +export enum PromptFileSource { + GitHubWorkspace = 'github-workspace', + CopilotPersonal = 'copilot-personal', + ClaudePersonal = 'claude-personal', + ClaudeWorkspace = 'claude-workspace', + ConfigWorkspace = 'config-workspace', + ConfigPersonal = 'config-personal', + ExtensionContribution = 'extension-contribution', + ExtensionAPI = 'extension-api', +} + +/** + * Prompt source folder path with source and storage type. + */ +export interface IPromptSourceFolder { + readonly path: string; + readonly source: PromptFileSource; + readonly storage: PromptsStorage; +} + +/** + * Resolved prompt folder with source and storage type. + */ +export interface IResolvedPromptSourceFolder { + readonly uri: URI; + readonly source: PromptFileSource; + readonly storage: PromptsStorage; + /** + * The original path string before resolution (e.g., '~/.copilot/agents' or '.github/agents'). + * Used for display purposes. + */ + readonly displayPath?: string; + /** + * Whether this is a default location (vs user-configured). + */ + readonly isDefault?: boolean; +} + +/** + * Resolved prompt markdown file with source and storage type. + */ +export interface IResolvedPromptFile { + readonly fileUri: URI; + readonly source: PromptFileSource; + readonly storage: PromptsStorage; +} + +/** + * All default skill source folders (both workspace and user home). + */ +export const DEFAULT_SKILL_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ + { path: '.github/skills', source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, + { path: '.claude/skills', source: PromptFileSource.ClaudeWorkspace, storage: PromptsStorage.local }, + { path: '~/.copilot/skills', source: PromptFileSource.CopilotPersonal, storage: PromptsStorage.user }, + { path: '~/.claude/skills', source: PromptFileSource.ClaudePersonal, storage: PromptsStorage.user }, +]; + +/** + * Default instructions source folders. + */ +export const DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ + { path: INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, +]; + +/** + * Default prompt source folders. + */ +export const DEFAULT_PROMPT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ + { path: PROMPT_DEFAULT_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, +]; + +/** + * Default agent source folders. + */ +export const DEFAULT_AGENT_SOURCE_FOLDERS: readonly IPromptSourceFolder[] = [ + { path: AGENTS_SOURCE_FOLDER, source: PromptFileSource.GitHubWorkspace, storage: PromptsStorage.local }, +]; + /** * Helper function to check if a file is directly in the .github/agents/ folder (not in subfolders). */ @@ -79,8 +166,13 @@ export function getPromptFileType(fileUri: URI): PromptsType | undefined { return PromptsType.agent; } + if (filename.toLowerCase() === SKILL_FILENAME.toLowerCase()) { + return PromptsType.skill; + } + // Check if it's a .md file in the .github/agents/ folder - if (filename.endsWith('.md') && isInAgentsFolder(fileUri)) { + // Exclude README.md to allow documentation files + if (filename.endsWith('.md') && filename !== 'README.md' && isInAgentsFolder(fileUri)) { return PromptsType.agent; } @@ -102,19 +194,23 @@ export function getPromptFileExtension(type: PromptsType): string { return PROMPT_FILE_EXTENSION; case PromptsType.agent: return AGENT_FILE_EXTENSION; + case PromptsType.skill: + return SKILL_FILENAME; default: throw new Error('Unknown prompt type'); } } -export function getPromptFileDefaultLocation(type: PromptsType): string { +export function getPromptFileDefaultLocations(type: PromptsType): readonly IPromptSourceFolder[] { switch (type) { case PromptsType.instructions: - return INSTRUCTIONS_DEFAULT_SOURCE_FOLDER; + return DEFAULT_INSTRUCTIONS_SOURCE_FOLDERS; case PromptsType.prompt: - return PROMPT_DEFAULT_SOURCE_FOLDER; + return DEFAULT_PROMPT_SOURCE_FOLDERS; case PromptsType.agent: - return AGENTS_SOURCE_FOLDER; + return DEFAULT_AGENT_SOURCE_FOLDERS; + case PromptsType.skill: + return DEFAULT_SKILL_SOURCE_FOLDERS; default: throw new Error('Unknown prompt type'); } @@ -144,8 +240,14 @@ export function getCleanPromptName(fileUri: URI): string { return basename(fileUri.path, '.md'); } + // For SKILL.md files (case insensitive), return 'SKILL' + if (fileName.toLowerCase() === SKILL_FILENAME.toLowerCase()) { + return basename(fileUri.path, '.md'); + } + // For .md files in .github/agents/ folder, treat them as agent files - if (fileName.endsWith('.md') && isInAgentsFolder(fileUri)) { + // Exclude README.md to allow documentation files + if (fileName.endsWith('.md') && fileName !== 'README.md' && isInAgentsFolder(fileUri)) { return basename(fileUri.path, '.md'); } 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 42ccfc02811..2ccd9e56124 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptBodyAutocompletion.ts @@ -13,8 +13,8 @@ import { CompletionContext, CompletionItem, CompletionItemKind, CompletionItemPr import { Range } from '../../../../../../editor/common/core/range.js'; import { CharCode } from '../../../../../../base/common/charCode.js'; import { getWordAtText } from '../../../../../../editor/common/core/wordHelper.js'; -import { chatVariableLeader } from '../../chatParserTypes.js'; -import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { chatVariableLeader } from '../../requestParser/chatParserTypes.js'; +import { ILanguageModelToolsService } from '../../tools/languageModelToolsService.js'; /** * Provides autocompletion for the variables inside prompt bodies. @@ -77,7 +77,7 @@ export class PromptBodyAutocompletion implements CompletionItemProvider { } private async collectToolCompletions(model: ITextModel, position: Position, toolRange: Range, suggestions: CompletionItem[]): Promise { - for (const toolName of this.languageModelToolsService.getQualifiedToolNames()) { + for (const toolName of this.languageModelToolsService.getFullReferenceNames()) { suggestions.push({ label: toolName, kind: CompletionItemKind.Value, diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts index 4902446ac48..f229164fb33 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptCodeActions.ts @@ -8,7 +8,7 @@ import { Range } from '../../../../../../editor/common/core/range.js'; import { CodeAction, CodeActionContext, CodeActionList, CodeActionProvider, IWorkspaceFileEdit, IWorkspaceTextEdit, TextEdit } from '../../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../../editor/common/model.js'; import { localize } from '../../../../../../nls.js'; -import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../../tools/languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; @@ -16,8 +16,9 @@ import { Selection } from '../../../../../../editor/common/core/selection.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; import { LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { isGithubTarget } from './promptValidator.js'; +import { isGithubTarget, MARKERS_OWNER_ID } from './promptValidator.js'; +import { IMarkerData, IMarkerService } from '../../../../../../platform/markers/common/markers.js'; +import { CodeActionKind } from '../../../../../../editor/contrib/codeAction/common/types.js'; export class PromptCodeActionProvider implements CodeActionProvider { /** @@ -29,6 +30,7 @@ export class PromptCodeActionProvider implements CodeActionProvider { @IPromptsService private readonly promptsService: IPromptsService, @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, @IFileService private readonly fileService: IFileService, + @IMarkerService private readonly markerService: IMarkerService, ) { } @@ -45,7 +47,7 @@ export class PromptCodeActionProvider implements CodeActionProvider { switch (promptType) { case PromptsType.agent: this.getUpdateToolsCodeActions(promptAST, promptType, model, range, result); - await this.getMigrateModeFileCodeActions(model.uri, result); + await this.getMigrateModeFileCodeActions(model, result); break; case PromptsType.prompt: this.getUpdateModeCodeActions(promptAST, model, range, result); @@ -63,31 +65,42 @@ export class PromptCodeActionProvider implements CodeActionProvider { } + private getMarkers(model: ITextModel, range: Range): IMarkerData[] { + const markers = this.markerService.read({ resource: model.uri, owner: MARKERS_OWNER_ID }); + return markers.filter(marker => range.containsRange(marker)); + } + + private createCodeAction(model: ITextModel, range: Range, title: string, edits: Array): CodeAction { + return { + title, + edit: { edits }, + ranges: [range], + diagnostics: this.getMarkers(model, range), + kind: CodeActionKind.QuickFix.value + }; + } + private getUpdateModeCodeActions(promptFile: ParsedPromptFile, model: ITextModel, range: Range, result: CodeAction[]): void { const modeAttr = promptFile.header?.getAttribute(PromptHeaderAttributes.mode); if (!modeAttr?.range.containsRange(range)) { return; } const keyRange = new Range(modeAttr.range.startLineNumber, modeAttr.range.startColumn, modeAttr.range.startLineNumber, modeAttr.range.startColumn + modeAttr.key.length); - result.push({ - title: localize('renameToAgent', "Rename to 'agent'"), - edit: { - edits: [asWorkspaceTextEdit(model, { range: keyRange, text: 'agent' })] - } - }); + result.push(this.createCodeAction(model, keyRange, + localize('renameToAgent', "Rename to 'agent'"), + [asWorkspaceTextEdit(model, { range: keyRange, text: 'agent' })] + )); } - private async getMigrateModeFileCodeActions(uri: URI, result: CodeAction[]): Promise { - if (uri.path.endsWith(LEGACY_MODE_FILE_EXTENSION)) { - const location = this.promptsService.getAgentFileURIFromModeFile(uri); - if (location && await this.fileService.canMove(uri, location)) { - const edit: IWorkspaceFileEdit = { oldResource: uri, newResource: location, options: { overwrite: false, copy: false } }; - result.push({ - title: localize('migrateToAgent', "Migrate to custom agent file"), - edit: { - edits: [edit] - } - }); + private async getMigrateModeFileCodeActions(model: ITextModel, result: CodeAction[]): Promise { + if (model.uri.path.endsWith(LEGACY_MODE_FILE_EXTENSION)) { + const location = this.promptsService.getAgentFileURIFromModeFile(model.uri); + if (location && await this.fileService.canMove(model.uri, location)) { + const edit: IWorkspaceFileEdit = { oldResource: model.uri, newResource: location, options: { overwrite: false, copy: false } }; + result.push(this.createCodeAction(model, new Range(1, 1, 1, 4), + localize('migrateToAgent', "Migrate to custom agent file"), + [edit] + )); } } } @@ -103,37 +116,59 @@ export class PromptCodeActionProvider implements CodeActionProvider { } const values = toolsAttr.value.items; - const deprecatedNames = new Lazy(() => this.languageModelToolsService.getDeprecatedQualifiedToolNames()); + const deprecatedNames = new Lazy(() => this.languageModelToolsService.getDeprecatedFullReferenceNames()); const edits: TextEdit[] = []; for (const item of values) { if (item.type !== 'string') { continue; } - const newName = deprecatedNames.value.get(item.value); - if (newName) { + const newNames = deprecatedNames.value.get(item.value); + if (newNames && newNames.size > 0) { const quote = model.getValueInRange(new Range(item.range.startLineNumber, item.range.startColumn, item.range.endLineNumber, item.range.startColumn + 1)); - const text = (quote === `'` || quote === '"') ? (quote + newName + quote) : newName; - const edit = { range: item.range, text }; - edits.push(edit); - - if (item.range.containsRange(range)) { - result.push({ - title: localize('updateToolName', "Update to '{0}'", newName), - edit: { - edits: [asWorkspaceTextEdit(model, edit)] - } - }); + + if (newNames.size === 1) { + const newName = Array.from(newNames)[0]; + const text = (quote === `'` || quote === '"') ? (quote + newName + quote) : newName; + const edit = { range: item.range, text }; + edits.push(edit); + + if (item.range.containsRange(range)) { + result.push(this.createCodeAction(model, item.range, + localize('updateToolName', "Update to '{0}'", newName), + [asWorkspaceTextEdit(model, edit)] + )); + } + } else { + // Multiple new names - expand to include all of them + const newNamesArray = Array.from(newNames).sort((a, b) => a.localeCompare(b)); + const separator = model.getValueInRange(new Range(item.range.startLineNumber, item.range.endColumn, item.range.endLineNumber, item.range.endColumn + 2)); + const useCommaSpace = separator.includes(','); + const delimiterText = useCommaSpace ? ', ' : ','; + + const newNamesText = newNamesArray.map(name => + (quote === `'` || quote === '"') ? (quote + name + quote) : name + ).join(delimiterText); + + const edit = { range: item.range, text: newNamesText }; + edits.push(edit); + + if (item.range.containsRange(range)) { + result.push(this.createCodeAction(model, item.range, + localize('expandToolNames', "Expand to {0} tools", newNames.size), + [asWorkspaceTextEdit(model, edit)] + )); + } } } } if (edits.length && result.length === 0 || edits.length > 1) { - result.push({ - title: localize('updateAllToolNames', "Update all tool names"), - edit: { - edits: edits.map(edit => asWorkspaceTextEdit(model, edit)) - } - }); + result.push( + this.createCodeAction(model, toolsAttr.value.range, + localize('updateAllToolNames', "Update all tool names"), + edits.map(edit => asWorkspaceTextEdit(model, edit)) + ) + ); } } } 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 540097cec9a..64463f76fa6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHeaderAutocompletion.ts @@ -10,13 +10,13 @@ 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 { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; -import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../../tools/languageModelToolsService.js'; import { IChatModeService } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; import { Iterable } from '../../../../../../base/common/iterator.js'; -import { PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; -import { getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; +import { IHeaderAttribute, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { getAttributeDescription, getValidAttributeNames, isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; import { localize } from '../../../../../../nls.js'; export class PromptHeaderAutocompletion implements CompletionItemProvider { @@ -55,6 +55,24 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return undefined; } + if (/^\s*$/.test(model.getValue())) { + return { + suggestions: [{ + label: localize('promptHeaderAutocompletion.addHeader', "Add Prompt Header"), + kind: CompletionItemKind.Snippet, + insertText: [ + `---`, + `description: $1`, + `---`, + `$0` + ].join('\n'), + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: model.getFullModelRange(), + }] + }; + } + + const parsedAST = this.promptsService.getParsedPromptFile(model); const header = parsedAST.header; if (!header) { @@ -109,6 +127,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { for (const attribute of attributesToPropose) { const item: CompletionItem = { label: attribute, + documentation: getAttributeDescription(attribute, promptType), kind: CompletionItemKind.Property, insertText: getInsertText(attribute), insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, @@ -129,30 +148,42 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { ): Promise { const suggestions: CompletionItem[] = []; - const lineContent = model.getLineContent(position.lineNumber); - const attribute = lineContent.substring(0, colonPosition.column - 1).trim(); + const attribute = header.attributes.find(attr => attr.range.containsPosition(position)); + if (!attribute) { + return undefined; + } const isGitHubTarget = isGithubTarget(promptType, header.target); - if (!getValidAttributeNames(promptType, true, isGitHubTarget).includes(attribute)) { + if (!getValidAttributeNames(promptType, true, isGitHubTarget).includes(attribute.key)) { return undefined; } if (promptType === PromptsType.prompt || promptType === PromptsType.agent) { - // if the position is inside the tools metadata, we provide tool name completions - const result = this.provideToolCompletions(model, position, header, isGitHubTarget); - if (result) { - return result; + if (attribute.key === PromptHeaderAttributes.model) { + if (attribute.value.type === 'array') { + // if the position is inside the tools metadata, we provide tool name completions + const getValues = async () => this.getModelNames(promptType === PromptsType.agent); + return this.provideArrayCompletions(model, position, attribute, getValues); + } + } + if (attribute.key === PromptHeaderAttributes.tools) { + if (attribute.value.type === 'array') { + // if the position is inside the tools metadata, we provide tool name completions + const getValues = async () => isGitHubTarget ? knownGithubCopilotTools : Array.from(this.languageModelToolsService.getFullReferenceNames()); + return this.provideArrayCompletions(model, position, attribute, getValues); + } } } - - const bracketIndex = lineContent.indexOf('['); - if (bracketIndex !== -1 && bracketIndex <= position.column - 1) { - // if the value is already inside a bracket, we don't provide value completions - return undefined; + if (promptType === PromptsType.agent) { + if (attribute.key === PromptHeaderAttributes.agents && !isGitHubTarget) { + if (attribute.value.type === 'array') { + return this.provideArrayCompletions(model, position, attribute, async () => (await this.promptsService.getCustomAgents(CancellationToken.None)).map(agent => agent.name)); + } + } } - + const lineContent = model.getLineContent(attribute.range.startLineNumber); const whilespaceAfterColon = (lineContent.substring(colonPosition.column).match(/^\s*/)?.[0].length) ?? 0; - const values = this.getValueSuggestions(promptType, attribute); + const values = this.getValueSuggestions(promptType, attribute.key); for (const value of values) { const item: CompletionItem = { label: value, @@ -162,7 +193,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { }; suggestions.push(item); } - if (attribute === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) { + if (attribute.key === PromptHeaderAttributes.handOffs && (promptType === PromptsType.agent)) { const value = [ '', ' - label: Start Implementation', @@ -199,6 +230,7 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { } return suggestions; } + break; case PromptHeaderAttributes.target: if (promptType === PromptsType.agent) { return ['vscode', 'github-copilot']; @@ -213,6 +245,27 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { if (promptType === PromptsType.prompt || promptType === PromptsType.agent) { return this.getModelNames(promptType === PromptsType.agent); } + break; + case PromptHeaderAttributes.infer: + if (promptType === PromptsType.agent) { + return ['true', 'false']; + } + break; + case PromptHeaderAttributes.agents: + if (promptType === PromptsType.agent) { + return ['["*"]']; + } + break; + case PromptHeaderAttributes.userInvokable: + if (promptType === PromptsType.agent) { + return ['true', 'false']; + } + break; + case PromptHeaderAttributes.disableModelInvocation: + if (promptType === PromptsType.agent) { + return ['true', 'false']; + } + break; } return []; } @@ -230,14 +283,13 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return result; } - private provideToolCompletions(model: ITextModel, position: Position, header: PromptHeader, isGitHubTarget: boolean): CompletionList | undefined { - const toolsAttr = header.getAttribute(PromptHeaderAttributes.tools); - if (!toolsAttr || toolsAttr.value.type !== 'array' || !toolsAttr.range.containsPosition(position)) { + private async provideArrayCompletions(model: ITextModel, position: Position, agentsAttr: IHeaderAttribute, getValues: () => Promise): Promise { + if (agentsAttr.value.type !== 'array') { return undefined; } - const getSuggestions = (toolRange: Range) => { + const getSuggestions = async (toolRange: Range) => { const suggestions: CompletionItem[] = []; - const toolNames = isGitHubTarget ? Object.keys(knownGithubCopilotTools) : this.languageModelToolsService.getQualifiedToolNames(); + const toolNames = await getValues(); for (const toolName of toolNames) { let insertText: string; if (!toolRange.isEmpty()) { @@ -257,16 +309,16 @@ export class PromptHeaderAutocompletion implements CompletionItemProvider { return { suggestions }; }; - for (const toolNameNode of toolsAttr.value.items) { + for (const toolNameNode of agentsAttr.value.items) { if (toolNameNode.range.containsPosition(position)) { // if the position is inside a tool range, we provide tool name completions - return getSuggestions(toolNameNode.range); + return await getSuggestions(toolNameNode.range); } } const prefix = model.getValueInRange(new Range(position.lineNumber, 1, position.lineNumber, position.column)); if (prefix.match(/[,[]\s*$/)) { // if the position is after a comma or bracket - return getSuggestions(new Range(position.lineNumber, position.column, position.lineNumber, position.column)); + return await getSuggestions(new Range(position.lineNumber, position.column, position.lineNumber, position.column)); } return undefined; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts index 0a09b8bd1d6..66faf44b6c8 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts @@ -10,13 +10,13 @@ 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 { localize } from '../../../../../../nls.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; -import { ILanguageModelToolsService, ToolSet } from '../../languageModelToolsService.js'; +import { ILanguageModelsService } from '../../languageModels.js'; +import { ILanguageModelToolsService, isToolSet, IToolSet } from '../../tools/languageModelToolsService.js'; import { IChatModeService, isBuiltinChatMode } from '../../chatModes.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; import { IPromptsService } from '../service/promptsService.js'; -import { IHeaderAttribute, PromptBody, PromptHeader, PromptHeaderAttributes, Target } from '../promptFileParser.js'; -import { isGithubTarget, knownGithubCopilotTools } from './promptValidator.js'; +import { IHeaderAttribute, PromptBody, PromptHeader, PromptHeaderAttributes } from '../promptFileParser.js'; +import { getAttributeDescription, isGithubTarget } from './promptValidator.js'; export class PromptHoverProvider implements HoverProvider { /** @@ -68,58 +68,24 @@ export class PromptHoverProvider implements HoverProvider { } private async provideHeaderHover(position: Position, promptType: PromptsType, header: PromptHeader): Promise { - if (promptType === PromptsType.instructions) { - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { + for (const attribute of header.attributes) { + if (attribute.range.containsPosition(position)) { + const description = getAttributeDescription(attribute.key, promptType); + if (description) { switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'), attribute.range); - case PromptHeaderAttributes.description: - 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.'), attribute.range); - case PromptHeaderAttributes.applyTo: - 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/**`'), attribute.range); - } - } - } - } else if (promptType === PromptsType.agent) { - const isGitHubTarget = isGithubTarget(promptType, header.target); - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'), attribute.range); case PromptHeaderAttributes.model: - return this.getModelHover(attribute, attribute.range, localize('promptHeader.agent.model', 'Specify the model that runs this custom agent.'), isGitHubTarget); + return this.getModelHover(attribute, position, description, promptType === PromptsType.agent && isGithubTarget(promptType, header.target)); case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'), header.target); - case PromptHeaderAttributes.handOffs: - return this.getHandsOffHover(attribute, position, isGitHubTarget); - case PromptHeaderAttributes.target: - return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'), attribute.range); - } - } - } - } else { - for (const attribute of header.attributes) { - if (attribute.range.containsPosition(position)) { - switch (attribute.key) { - case PromptHeaderAttributes.name: - return this.createHover(localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'), attribute.range); - case PromptHeaderAttributes.description: - return this.createHover(localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'), attribute.range); - case PromptHeaderAttributes.argumentHint: - return this.createHover(localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'), attribute.range); - case PromptHeaderAttributes.model: - return this.getModelHover(attribute, attribute.range, localize('promptHeader.prompt.model', 'The model to use in this prompt.'), false); - case PromptHeaderAttributes.tools: - return this.getToolHover(attribute, position, localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'), Target.VSCode); + return this.getToolHover(attribute, position, description); case PromptHeaderAttributes.agent: case PromptHeaderAttributes.mode: - return this.getAgentHover(attribute, position); + return this.getAgentHover(attribute, position, description); + case PromptHeaderAttributes.handOffs: + return this.getHandsOffHover(attribute, position, promptType === PromptsType.agent && isGithubTarget(promptType, header.target)); + case PromptHeaderAttributes.infer: + return this.createHover(description + '\n\n' + localize('promptHeader.attribute.infer.hover', 'Deprecated: Use `user-invokable` and `disable-model-invocation` instead.'), attribute.range); + default: + return this.createHover(description, attribute.range); } } } @@ -127,25 +93,13 @@ export class PromptHoverProvider implements HoverProvider { return undefined; } - private getToolHover(node: IHeaderAttribute, position: Position, baseMessage: string, target: string | undefined): Hover | undefined { + 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)) { - let toolNameValue = toolName.value; - if (target === undefined) { - toolNameValue = this.languageModelToolsService.mapGithubToolName(toolNameValue); - } - if (target === Target.VSCode || target === undefined) { - const description = this.getToolHoverByName(toolNameValue, toolName.range); - if (description) { - return description; - } - } - if (target === Target.GitHubCopilot || target === undefined) { - const description = knownGithubCopilotTools[toolNameValue]; - if (description) { - return this.createHover(description, toolName.range); - } + const description = this.getToolHoverByName(toolName.value, toolName.range); + if (description) { + return description; } } } @@ -154,9 +108,9 @@ export class PromptHoverProvider implements HoverProvider { } private getToolHoverByName(toolName: string, range: Range): Hover | undefined { - const tool = this.languageModelToolsService.getToolByQualifiedName(toolName); + const tool = this.languageModelToolsService.getToolByFullReferenceName(toolName); if (tool !== undefined) { - if (tool instanceof ToolSet) { + if (isToolSet(tool)) { return this.getToolsetHover(tool, range); } else { return this.createHover(tool.userDescription ?? tool.modelDescription, range); @@ -165,7 +119,7 @@ export class PromptHoverProvider implements HoverProvider { return undefined; } - private getToolsetHover(toolSet: ToolSet, range: Range): Hover | undefined { + private getToolsetHover(toolSet: IToolSet, range: Range): Hover | undefined { const lines: string[] = []; lines.push(localize('toolSetName', 'ToolSet: {0}\n\n', toolSet.referenceName)); if (toolSet.description) { @@ -177,30 +131,44 @@ export class PromptHoverProvider implements HoverProvider { return this.createHover(lines.join('\n'), range); } - private getModelHover(node: IHeaderAttribute, range: Range, baseMessage: string, isGitHubTarget: boolean): Hover | undefined { + private getModelHover(node: IHeaderAttribute, position: Position, baseMessage: string, isGitHubTarget: boolean): Hover | undefined { if (isGitHubTarget) { - return this.createHover(baseMessage + '\n\n' + localize('promptHeader.agent.model.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), range); + return this.createHover(baseMessage + '\n\n' + localize('promptHeader.agent.model.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), node.range); } + const modelHoverContent = (modelName: string): Hover | undefined => { + const meta = this.languageModelsService.lookupLanguageModelByQualifiedName(modelName); + if (meta) { + const lines: string[] = []; + lines.push(baseMessage + '\n'); + lines.push(localize('modelName', '- Name: {0}', meta.name)); + lines.push(localize('modelFamily', '- Family: {0}', meta.family)); + lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor)); + if (meta.tooltip) { + lines.push('', '', meta.tooltip); + } + return this.createHover(lines.join('\n'), node.range); + } + return undefined; + }; if (node.value.type === 'string') { - for (const id of this.languageModelsService.getLanguageModelIds()) { - const meta = this.languageModelsService.lookupLanguageModel(id); - if (meta && ILanguageModelChatMetadata.matchesQualifiedName(node.value.value, meta)) { - const lines: string[] = []; - lines.push(baseMessage + '\n'); - lines.push(localize('modelName', '- Name: {0}', meta.name)); - lines.push(localize('modelFamily', '- Family: {0}', meta.family)); - lines.push(localize('modelVendor', '- Vendor: {0}', meta.vendor)); - if (meta.tooltip) { - lines.push('', '', meta.tooltip); + const hover = modelHoverContent(node.value.value); + if (hover) { + return hover; + } + } else if (node.value.type === 'array') { + for (const item of node.value.items) { + if (item.type === 'string' && item.range.containsPosition(position)) { + const hover = modelHoverContent(item.value); + if (hover) { + return hover; } - return this.createHover(lines.join('\n'), range); } } } - return this.createHover(baseMessage, range); + return this.createHover(baseMessage, node.range); } - private getAgentHover(agentAttribute: IHeaderAttribute, position: Position): Hover | undefined { + private getAgentHover(agentAttribute: IHeaderAttribute, position: Position, baseMessage: string): Hover | undefined { const lines: string[] = []; const value = agentAttribute.value; if (value.type === 'string' && value.range.containsPosition(position)) { @@ -211,7 +179,7 @@ export class PromptHoverProvider implements HoverProvider { } } else { const agents = this.chatModeService.getModes(); - lines.push(localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.')); + lines.push(baseMessage); lines.push(''); // Built-in agents @@ -234,7 +202,7 @@ export class PromptHoverProvider implements HoverProvider { } private getHandsOffHover(attribute: IHeaderAttribute, position: Position, isGitHubTarget: boolean): Hover | undefined { - const handoffsBaseMessage = localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'); + const handoffsBaseMessage = getAttributeDescription(PromptHeaderAttributes.handOffs, PromptsType.agent)!; if (isGitHubTarget) { return this.createHover(handoffsBaseMessage + '\n\n' + localize('promptHeader.agent.handoffs.githubCopilot', 'Note: This attribute is not used when target is github-copilot.'), attribute.range); } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts index 3dcdf132686..d582ca6b76b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts @@ -11,12 +11,12 @@ import { IModelService } from '../../../../../../editor/common/services/model.js import { localize } from '../../../../../../nls.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 { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js'; import { ChatModeKind } from '../../constants.js'; import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../languageModels.js'; -import { ILanguageModelToolsService } from '../../languageModelToolsService.js'; +import { ILanguageModelToolsService, SpecedToolAliases } from '../../tools/languageModelToolsService.js'; import { getPromptsTypeForLanguageId, PromptsType } from '../promptTypes.js'; -import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PROMPT_NAME_REGEXP, PromptHeaderAttributes, Target } from '../promptFileParser.js'; +import { GithubPromptHeaderAttributes, IArrayValue, IHeaderAttribute, IStringValue, ParsedPromptFile, PromptHeader, PromptHeaderAttributes, Target } from '../promptFileParser.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { Delayer } from '../../../../../../base/common/async.js'; import { ResourceMap } from '../../../../../../base/common/map.js'; @@ -25,8 +25,9 @@ import { IPromptsService } from '../service/promptsService.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { AGENTS_SOURCE_FOLDER, LEGACY_MODE_FILE_EXTENSION } from '../config/promptFileLocations.js'; import { Lazy } from '../../../../../../base/common/lazy.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; -const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; +export const MARKERS_OWNER_ID = 'prompts-diagnostics-provider'; export class PromptValidator { constructor( @@ -40,9 +41,10 @@ export class PromptValidator { 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.validateHeader(promptAST, promptType, report); await this.validateBody(promptAST, promptType, report); await this.validateFileName(promptAST, promptType, report); + await this.validateSkillFolderName(promptAST, promptType, report); } private async validateFileName(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { @@ -56,6 +58,36 @@ export class PromptValidator { } } + private async validateSkillFolderName(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { + if (promptType !== PromptsType.skill) { + return; + } + + const nameAttribute = promptAST.header?.attributes.find(attr => attr.key === PromptHeaderAttributes.name); + if (!nameAttribute || nameAttribute.value.type !== 'string') { + return; + } + + const skillName = nameAttribute.value.value.trim(); + if (!skillName) { + return; + } + + // Extract folder name from path (e.g., .github/skills/my-skill/SKILL.md -> my-skill) + const pathParts = promptAST.uri.path.split('/'); + const skillIndex = pathParts.findIndex(part => part === 'SKILL.md'); + if (skillIndex > 0) { + const folderName = pathParts[skillIndex - 1]; + if (folderName && skillName !== folderName) { + report(toMarker( + localize('promptValidator.skillNameFolderMismatch', "The skill name '{0}' should match the folder name '{1}'.", skillName, folderName), + nameAttribute.value.range, + MarkerSeverity.Warning + )); + } + } + } + private async validateBody(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { const body = promptAST.body; if (!body) { @@ -92,20 +124,28 @@ export class PromptValidator { if (body.variableReferences.length && !isGitHubTarget) { const headerTools = promptAST.header?.tools; const headerTarget = promptAST.header?.target; - const headerToolsMap = headerTools ? this.languageModelToolsService.toToolAndToolSetEnablementMap(headerTools, headerTarget) : undefined; + const headerToolsMap = headerTools ? this.languageModelToolsService.toToolAndToolSetEnablementMap(headerTools, headerTarget, undefined) : undefined; - const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); - const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + const available = new Set(this.languageModelToolsService.getFullReferenceNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedFullReferenceNames(); 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}' has been renamed, use '{1}' instead.", variable.name, currentName), variable.range, MarkerSeverity.Info)); + const currentNames = deprecatedNames.get(variable.name); + if (currentNames && currentNames.size > 0) { + if (currentNames.size === 1) { + const newName = Array.from(currentNames)[0]; + report(toMarker(localize('promptValidator.deprecatedVariableReference', "Tool or toolset '{0}' has been renamed, use '{1}' instead.", variable.name, newName), variable.range, MarkerSeverity.Info)); + } else { + const newNames = Array.from(currentNames).sort((a, b) => a.localeCompare(b)).join(', '); + report(toMarker(localize('promptValidator.deprecatedVariableReferenceMultipleNames', "Tool or toolset '{0}' has been renamed, use the following tools instead: {1}", variable.name, newNames), variable.range, MarkerSeverity.Info)); + } + } } else { report(toMarker(localize('promptValidator.unknownVariableReference', "Unknown tool or toolset '{0}'.", variable.name), variable.range, MarkerSeverity.Warning)); } } else if (headerToolsMap) { - const tool = this.languageModelToolsService.getToolByQualifiedName(variable.name); + const tool = this.languageModelToolsService.getToolByFullReferenceName(variable.name); if (tool && headerToolsMap.get(tool) === false) { report(toMarker(localize('promptValidator.disabledTool', "Tool or toolset '{0}' also needs to be enabled in the header.", variable.name), variable.range, MarkerSeverity.Warning)); } @@ -116,7 +156,7 @@ export class PromptValidator { await Promise.all(fileReferenceChecks); } - private validateHeader(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): void { + private async validateHeader(promptAST: ParsedPromptFile, promptType: PromptsType, report: (markers: IMarkerData) => void): Promise { const header = promptAST.header; if (!header) { return; @@ -142,14 +182,22 @@ export class PromptValidator { case PromptsType.agent: { this.validateTarget(attributes, report); + this.validateInfer(attributes, report); + this.validateUserInvokable(attributes, report); + this.validateDisableModelInvocation(attributes, report); this.validateTools(attributes, ChatModeKind.Agent, header.target, report); if (!isGitHubTarget) { this.validateModel(attributes, ChatModeKind.Agent, report); this.validateHandoffs(attributes, report); + await this.validateAgentsAttribute(attributes, header, report); } break; } + case PromptsType.skill: + // Skill-specific validations (currently none beyond name/description) + break; + } } @@ -177,6 +225,9 @@ export class PromptValidator { case PromptsType.instructions: report(toMarker(localize('promptValidator.unknownAttribute.instructions', "Attribute '{0}' is not supported in instructions files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); break; + case PromptsType.skill: + report(toMarker(localize('promptValidator.unknownAttribute.skill', "Attribute '{0}' is not supported in skill files. Supported: {1}.", attribute.key, supportedNames.value), attribute.range, MarkerSeverity.Warning)); + break; } } } @@ -197,9 +248,6 @@ export class PromptValidator { report(toMarker(localize('promptValidator.nameShouldNotBeEmpty', "The 'name' attribute must not be empty."), nameAttribute.value.range, MarkerSeverity.Error)); return; } - if (!PROMPT_NAME_REGEXP.test(nameAttribute.value.value)) { - report(toMarker(localize('promptValidator.nameInvalidCharacters', "The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods."), nameAttribute.value.range, MarkerSeverity.Error)); - } } private validateDescription(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): void { @@ -237,36 +285,58 @@ export class PromptValidator { 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)); + if (attribute.value.type !== 'string' && attribute.value.type !== 'array') { + report(toMarker(localize('promptValidator.modelMustBeStringOrArray', "The 'model' attribute must be a string or an array of strings."), 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 modelNames: [string, Range][] = []; + if (attribute.value.type === 'string') { + 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; + } + modelNames.push([modelName, attribute.value.range]); + } else if (attribute.value.type === 'array') { + if (attribute.value.items.length === 0) { + report(toMarker(localize('promptValidator.modelArrayMustNotBeEmpty', "The 'model' array must not be empty."), attribute.value.range, MarkerSeverity.Error)); + return; + } + for (const item of attribute.value.items) { + if (item.type !== 'string') { + report(toMarker(localize('promptValidator.modelArrayMustContainStrings', "The 'model' array must contain only strings."), item.range, MarkerSeverity.Error)); + return; + } + const modelName = item.value.trim(); + if (modelName.length === 0) { + report(toMarker(localize('promptValidator.modelArrayItemMustBeNonEmpty', "Model names in the array must be non-empty strings."), item.range, MarkerSeverity.Error)); + return; + } + modelNames.push([modelName, item.range]); + } } - const languageModes = this.languageModelsService.getLanguageModelIds(); - if (languageModes.length === 0) { + const languageModels = this.languageModelsService.getLanguageModelIds(); + if (languageModels.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 (agentKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) { - report(toMarker(localize('promptValidator.modelNotSuited', "Model '{0}' is not suited for agent mode.", modelName), attribute.value.range, MarkerSeverity.Warning)); + for (const [modelName, range] of modelNames) { + const modelMetadata = this.findModelByName(modelName); + if (!modelMetadata) { + report(toMarker(localize('promptValidator.modelNotFound', "Unknown model '{0}'.", modelName), range, MarkerSeverity.Warning)); + } else if (agentKind === ChatModeKind.Agent && !ILanguageModelChatMetadata.suitableForAgentMode(modelMetadata)) { + report(toMarker(localize('promptValidator.modelNotSuited', "Model '{0}' is not suited for agent mode.", modelName), 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; - } + private findModelByName(modelName: string): ILanguageModelChatMetadata | undefined { + const metadata = this.languageModelsService.lookupLanguageModelByQualifiedName(modelName); + if (metadata && metadata.isUserSelectable !== false) { + return metadata; } return undefined; } @@ -339,19 +409,24 @@ export class PromptValidator { private validateVSCodeTools(valueItem: IArrayValue, target: string | undefined, report: (markers: IMarkerData) => void) { if (valueItem.items.length > 0) { - const available = new Set(this.languageModelToolsService.getQualifiedToolNames()); - const deprecatedNames = this.languageModelToolsService.getDeprecatedQualifiedToolNames(); + const available = new Set(this.languageModelToolsService.getFullReferenceNames()); + const deprecatedNames = this.languageModelToolsService.getDeprecatedFullReferenceNames(); 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) { - const toolName = target === undefined ? this.languageModelToolsService.mapGithubToolName(item.value) : item.value; - if (!available.has(toolName)) { - if (deprecatedNames.has(toolName)) { - const currentName = deprecatedNames.get(toolName); - report(toMarker(localize('promptValidator.toolDeprecated', "Tool or toolset '{0}' has been renamed, use '{1}' instead.", toolName, currentName), item.range, MarkerSeverity.Info)); + if (!available.has(item.value)) { + const currentNames = deprecatedNames.get(item.value); + if (currentNames) { + if (currentNames?.size === 1) { + const newName = Array.from(currentNames)[0]; + report(toMarker(localize('promptValidator.toolDeprecated', "Tool or toolset '{0}' has been renamed, use '{1}' instead.", item.value, newName), item.range, MarkerSeverity.Info)); + } else { + const newNames = Array.from(currentNames).sort((a, b) => a.localeCompare(b)).join(', '); + report(toMarker(localize('promptValidator.toolDeprecatedMultipleNames', "Tool or toolset '{0}' has been renamed, use the following tools instead: {1}", item.value, newNames), item.range, MarkerSeverity.Info)); + } } else { - report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}'.", toolName), item.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.toolNotFound', "Unknown tool '{0}'.", item.value), item.range, MarkerSeverity.Warning)); } } } @@ -392,8 +467,8 @@ export class PromptValidator { if (!attribute) { return; } - if (attribute.value.type !== 'array') { - report(toMarker(localize('promptValidator.excludeAgentMustBeArray', "The 'excludeAgent' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + if (attribute.value.type !== 'array' && attribute.value.type !== 'string') { + report(toMarker(localize('promptValidator.excludeAgentMustBeArray', "The 'excludeAgent' attribute must be an string or array."), attribute.value.range, MarkerSeverity.Error)); return; } } @@ -437,8 +512,18 @@ export class PromptValidator { report(toMarker(localize('promptValidator.handoffSendMustBeBoolean', "The 'send' property in a handoff must be a boolean."), prop.value.range, MarkerSeverity.Error)); } break; + case 'showContinueOn': + if (prop.value.type !== 'boolean') { + report(toMarker(localize('promptValidator.handoffShowContinueOnMustBeBoolean', "The 'showContinueOn' property in a handoff must be a boolean."), prop.value.range, MarkerSeverity.Error)); + } + break; + case 'model': + if (prop.value.type !== 'string') { + report(toMarker(localize('promptValidator.handoffModelMustBeString', "The 'model' property in a handoff must be a string."), prop.value.range, MarkerSeverity.Error)); + } + break; default: - report(toMarker(localize('promptValidator.unknownHandoffProperty', "Unknown property '{0}' in handoff object. Supported properties are 'label', 'agent', 'prompt' and optional 'send'.", prop.key.value), prop.value.range, MarkerSeverity.Warning)); + report(toMarker(localize('promptValidator.unknownHandoffProperty', "Unknown property '{0}' in handoff object. Supported properties are 'label', 'agent', 'prompt' and optional 'send', 'showContinueOn', 'model'.", prop.key.value), prop.value.range, MarkerSeverity.Warning)); } required.delete(prop.key.value); } @@ -448,6 +533,14 @@ export class PromptValidator { } } + private validateInfer(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.infer); + if (!attribute) { + return; + } + report(toMarker(localize('promptValidator.inferDeprecated', "The 'infer' attribute is deprecated in favour of 'user-invokable' and 'disable-model-invocation'."), attribute.value.range, MarkerSeverity.Error)); + } + private validateTarget(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.target); if (!attribute) { @@ -467,18 +560,79 @@ export class PromptValidator { report(toMarker(localize('promptValidator.targetInvalidValue', "The 'target' attribute must be one of: {0}.", validTargets.join(', ')), attribute.value.range, MarkerSeverity.Error)); } } + + private validateUserInvokable(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.userInvokable); + if (!attribute) { + return; + } + if (attribute.value.type !== 'boolean') { + report(toMarker(localize('promptValidator.userInvokableMustBeBoolean', "The 'user-invokable' attribute must be a boolean."), attribute.value.range, MarkerSeverity.Error)); + return; + } + } + + private validateDisableModelInvocation(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.disableModelInvocation); + if (!attribute) { + return; + } + if (attribute.value.type !== 'boolean') { + report(toMarker(localize('promptValidator.disableModelInvocationMustBeBoolean', "The 'disable-model-invocation' attribute must be a boolean."), attribute.value.range, MarkerSeverity.Error)); + return; + } + } + + private async validateAgentsAttribute(attributes: IHeaderAttribute[], header: PromptHeader, report: (markers: IMarkerData) => void): Promise { + const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.agents); + if (!attribute) { + return; + } + if (attribute.value.type !== 'array') { + report(toMarker(localize('promptValidator.agentsMustBeArray', "The 'agents' attribute must be an array."), attribute.value.range, MarkerSeverity.Error)); + return; + } + + // Collect available agent names + const agents = await this.promptsService.getCustomAgents(CancellationToken.None); + const availableAgentNames = new Set(agents.map(agent => agent.name)); + availableAgentNames.add(ChatMode.Agent.name.get()); // include default agent + + // Check each item is a string and agent exists + const agentNames: string[] = []; + for (const item of attribute.value.items) { + if (item.type !== 'string') { + report(toMarker(localize('promptValidator.eachAgentMustBeString', "Each agent name in the 'agents' attribute must be a string."), item.range, MarkerSeverity.Error)); + } else if (item.value) { + agentNames.push(item.value); + if (item.value !== '*' && !availableAgentNames.has(item.value)) { + report(toMarker(localize('promptValidator.agentInAgentsNotFound', "Unknown agent '{0}'. Available agents: {1}.", item.value, Array.from(availableAgentNames).join(', ')), item.range, MarkerSeverity.Warning)); + } + } + } + + // If not wildcard and not empty, check that 'agent' tool is available + if (agentNames.length > 0) { + const tools = header.tools; + if (tools && !tools.includes(SpecedToolAliases.agent)) { + report(toMarker(localize('promptValidator.agentsRequiresAgentTool', "When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute."), attribute.value.range, MarkerSeverity.Warning)); + } + } + } } const allAttributeNames = { [PromptsType.prompt]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint], [PromptsType.instructions]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent], - [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target] + [PromptsType.agent]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target, PromptHeaderAttributes.infer, PromptHeaderAttributes.agents, PromptHeaderAttributes.userInvokable, PromptHeaderAttributes.disableModelInvocation], + [PromptsType.skill]: [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.license, PromptHeaderAttributes.compatibility, PromptHeaderAttributes.metadata], }; -const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers]; +const githubCopilotAgentAttributeNames = [PromptHeaderAttributes.name, PromptHeaderAttributes.description, PromptHeaderAttributes.tools, PromptHeaderAttributes.target, GithubPromptHeaderAttributes.mcpServers, PromptHeaderAttributes.infer]; const recommendedAttributeNames = { [PromptsType.prompt]: allAttributeNames[PromptsType.prompt].filter(name => !isNonRecommendedAttribute(name)), [PromptsType.instructions]: allAttributeNames[PromptsType.instructions].filter(name => !isNonRecommendedAttribute(name)), - [PromptsType.agent]: allAttributeNames[PromptsType.agent].filter(name => !isNonRecommendedAttribute(name)) + [PromptsType.agent]: allAttributeNames[PromptsType.agent].filter(name => !isNonRecommendedAttribute(name)), + [PromptsType.skill]: allAttributeNames[PromptsType.skill].filter(name => !isNonRecommendedAttribute(name)), }; export function getValidAttributeNames(promptType: PromptsType, includeNonRecommended: boolean, isGitHubTarget: boolean): string[] { @@ -489,16 +643,81 @@ export function getValidAttributeNames(promptType: PromptsType, includeNonRecomm } export function isNonRecommendedAttribute(attributeName: string): boolean { - return attributeName === PromptHeaderAttributes.advancedOptions || attributeName === PromptHeaderAttributes.excludeAgent || attributeName === PromptHeaderAttributes.mode; + return attributeName === PromptHeaderAttributes.advancedOptions || attributeName === PromptHeaderAttributes.excludeAgent || attributeName === PromptHeaderAttributes.mode || attributeName === PromptHeaderAttributes.infer; +} + +export function getAttributeDescription(attributeName: string, promptType: PromptsType): string | undefined { + switch (promptType) { + case PromptsType.instructions: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.instructions.name', 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'); + case PromptHeaderAttributes.description: + return 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.'); + case PromptHeaderAttributes.applyTo: + return 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/**`'); + } + break; + case PromptsType.skill: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.skill.name', 'The name of the skill.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.skill.description', 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'); + } + break; + case PromptsType.agent: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.agent.name', 'The name of the agent as shown in the UI.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.agent.description', 'The description of the custom agent, what it does and when to use it.'); + case PromptHeaderAttributes.argumentHint: + return localize('promptHeader.agent.argumentHint', 'The argument-hint describes what inputs the custom agent expects or supports.'); + case PromptHeaderAttributes.model: + return localize('promptHeader.agent.model', 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.'); + case PromptHeaderAttributes.tools: + return localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'); + case PromptHeaderAttributes.handOffs: + return localize('promptHeader.agent.handoffs', 'Possible handoff actions when the agent has completed its task.'); + case PromptHeaderAttributes.target: + return localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'); + case PromptHeaderAttributes.infer: + return localize('promptHeader.agent.infer', 'Controls visibility of the agent.'); + case PromptHeaderAttributes.agents: + return localize('promptHeader.agent.agents', 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + case PromptHeaderAttributes.userInvokable: + return localize('promptHeader.agent.userInvokable', 'Whether the agent can be selected and invoked by users in the UI.'); + case PromptHeaderAttributes.disableModelInvocation: + return localize('promptHeader.agent.disableModelInvocation', 'If true, prevents the agent from being invoked as a subagent.'); + } + break; + case PromptsType.prompt: + switch (attributeName) { + case PromptHeaderAttributes.name: + return localize('promptHeader.prompt.name', 'The name of the prompt. This is also the name of the slash command that will run this prompt.'); + case PromptHeaderAttributes.description: + return localize('promptHeader.prompt.description', 'The description of the reusable prompt, what it does and when to use it.'); + case PromptHeaderAttributes.argumentHint: + return localize('promptHeader.prompt.argumentHint', 'The argument-hint describes what inputs the prompt expects or supports.'); + case PromptHeaderAttributes.model: + return localize('promptHeader.prompt.model', 'The model to use in this prompt. Can also be a list of models. The first available model will be used.'); + case PromptHeaderAttributes.tools: + return localize('promptHeader.prompt.tools', 'The tools to use in this prompt.'); + case PromptHeaderAttributes.agent: + case PromptHeaderAttributes.mode: + return localize('promptHeader.prompt.agent.description', 'The agent to use when running this prompt.'); + } + break; + } + return undefined; } // The list of tools known to be used by GitHub Copilot custom agents -export const knownGithubCopilotTools: Record = { - 'shell': localize('githubCopilotTools.shell', 'Execute shell commands'), - 'edit': localize('githubCopilotTools.edit', 'Edit files'), - 'search': localize('githubCopilotTools.search', 'Search in files'), - 'custom-agent': localize('githubCopilotTools.customAgent', 'Call custom agents') -}; +export const knownGithubCopilotTools = [ + SpecedToolAliases.execute, SpecedToolAliases.read, SpecedToolAliases.edit, SpecedToolAliases.search, SpecedToolAliases.agent, +]; + export function isGithubTarget(promptType: PromptsType, target: string | undefined): boolean { return promptType === PromptsType.agent && target === Target.GitHubCopilot; } diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts index a4ed0889775..ef5406557e2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts @@ -10,8 +10,6 @@ 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'; -export const PROMPT_NAME_REGEXP = /^[\p{L}\d_\-\.]+$/u; - export class PromptFileParser { constructor() { } @@ -76,6 +74,13 @@ export namespace PromptHeaderAttributes { export const argumentHint = 'argument-hint'; export const excludeAgent = 'excludeAgent'; export const target = 'target'; + export const infer = 'infer'; + export const license = 'license'; + export const compatibility = 'compatibility'; + export const metadata = 'metadata'; + export const agents = 'agents'; + export const userInvokable = 'user-invokable'; + export const disableModelInvocation = 'disable-model-invocation'; } export namespace GithubPromptHeaderAttributes { @@ -162,11 +167,7 @@ export class PromptHeader { } public get name(): string | undefined { - const name = this.getStringAttribute(PromptHeaderAttributes.name); - if (name && PROMPT_NAME_REGEXP.test(name)) { - return name; - } - return undefined; + return this.getStringAttribute(PromptHeaderAttributes.name); } public get description(): string | undefined { @@ -177,8 +178,8 @@ export class PromptHeader { return this.getStringAttribute(PromptHeaderAttributes.agent) ?? this.getStringAttribute(PromptHeaderAttributes.mode); } - public get model(): string | undefined { - return this.getStringAttribute(PromptHeaderAttributes.model); + public get model(): readonly string[] | undefined { + return this.getStringOrStringArrayAttribute(PromptHeaderAttributes.model); } public get applyTo(): string | undefined { @@ -193,6 +194,14 @@ export class PromptHeader { return this.getStringAttribute(PromptHeaderAttributes.target); } + public get infer(): boolean | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.infer); + if (attribute?.value.type === 'boolean') { + return attribute.value.value; + } + return undefined; + } + public get tools(): string[] | undefined { const toolsAttribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.tools); if (!toolsAttribute) { @@ -227,7 +236,7 @@ export class PromptHeader { return undefined; } if (handoffsAttribute.value.type === 'array') { - // Array format: list of objects: { agent, label, prompt, send? } + // Array format: list of objects: { agent, label, prompt, send?, showContinueOn?, model? } const handoffs: IHandOff[] = []; for (const item of handoffsAttribute.value.items) { if (item.type === 'object') { @@ -235,6 +244,8 @@ export class PromptHeader { let label: string | undefined; let prompt: string | undefined; let send: boolean | undefined; + let showContinueOn: boolean | undefined; + let model: string | undefined; for (const prop of item.properties) { if (prop.key.value === 'agent' && prop.value.type === 'string') { agent = prop.value.value; @@ -244,10 +255,22 @@ export class PromptHeader { prompt = prop.value.value; } else if (prop.key.value === 'send' && prop.value.type === 'boolean') { send = prop.value.value; + } else if (prop.key.value === 'showContinueOn' && prop.value.type === 'boolean') { + showContinueOn = prop.value.value; + } else if (prop.key.value === 'model' && prop.value.type === 'string') { + model = prop.value.value; } } if (agent && label && prompt !== undefined) { - handoffs.push({ agent, label, prompt, send }); + const handoff: IHandOff = { + agent, + label, + prompt, + ...(send !== undefined ? { send } : {}), + ...(showContinueOn !== undefined ? { showContinueOn } : {}), + ...(model !== undefined ? { model } : {}) + }; + handoffs.push(handoff); } } } @@ -255,9 +278,73 @@ export class PromptHeader { } return undefined; } + + private getStringArrayAttribute(key: string): string[] | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); + if (!attribute) { + return undefined; + } + if (attribute.value.type === 'array') { + const result: string[] = []; + for (const item of attribute.value.items) { + if (item.type === 'string' && item.value) { + result.push(item.value); + } + } + return result; + } + return undefined; + } + + private getStringOrStringArrayAttribute(key: string): readonly string[] | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); + if (!attribute) { + return undefined; + } + if (attribute.value.type === 'string') { + return [attribute.value.value]; + } + if (attribute.value.type === 'array') { + const result: string[] = []; + for (const item of attribute.value.items) { + if (item.type === 'string') { + result.push(item.value); + } + } + return result; + } + return undefined; + } + + public get agents(): string[] | undefined { + return this.getStringArrayAttribute(PromptHeaderAttributes.agents); + } + + public get userInvokable(): boolean | undefined { + return this.getBooleanAttribute(PromptHeaderAttributes.userInvokable); + } + + public get disableModelInvocation(): boolean | undefined { + return this.getBooleanAttribute(PromptHeaderAttributes.disableModelInvocation); + } + + private getBooleanAttribute(key: string): boolean | undefined { + const attribute = this._parsedHeader.attributes.find(attr => attr.key === key); + if (attribute?.value.type === 'boolean') { + return attribute.value.value; + } + return undefined; + } } -export interface IHandOff { readonly agent: string; readonly label: string; readonly prompt: string; readonly send?: boolean } +export interface IHandOff { + readonly agent: string; + readonly label: string; + readonly prompt: string; + readonly send?: boolean; + readonly showContinueOn?: boolean; // treated exactly like send (optional boolean) + readonly model?: string; // qualified model name to switch to (e.g., "GPT-5 (copilot)") +} export interface IHeaderAttribute { readonly range: Range; @@ -320,6 +407,9 @@ export class PromptBody { // Match markdown links: [text](link) const linkMatch = line.matchAll(/\[(.*?)\]\((.+?)\)/g); for (const match of linkMatch) { + if (match.index > 0 && line[match.index - 1] === '!') { + continue; // skip image links + } 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); diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts index 9ae26e570af..7da38b26d22 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/promptTypes.ts @@ -11,6 +11,7 @@ import { LanguageSelector } from '../../../../../editor/common/languageSelector. export const PROMPT_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-prompt-snippets'; export const INSTRUCTIONS_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-custom-instructions'; export const AGENT_DOCUMENTATION_URL = 'https://aka.ms/vscode-ghcp-custom-chat-modes'; // todo +export const SKILL_DOCUMENTATION_URL = 'https://aka.ms/vscode-agent-skills'; /** * Language ID for the reusable prompt syntax. @@ -27,13 +28,18 @@ export const INSTRUCTIONS_LANGUAGE_ID = 'instructions'; */ export const AGENT_LANGUAGE_ID = 'chatagent'; +/** + * Language ID for skill syntax. + */ +export const SKILL_LANGUAGE_ID = 'skill'; + /** * Prompt and instructions files language selector. */ -export const ALL_PROMPTS_LANGUAGE_SELECTOR: LanguageSelector = [PROMPT_LANGUAGE_ID, INSTRUCTIONS_LANGUAGE_ID, AGENT_LANGUAGE_ID]; +export const ALL_PROMPTS_LANGUAGE_SELECTOR: LanguageSelector = [PROMPT_LANGUAGE_ID, INSTRUCTIONS_LANGUAGE_ID, AGENT_LANGUAGE_ID, SKILL_LANGUAGE_ID]; /** - * The language id for for a prompts type. + * The language id for a prompts type. */ export function getLanguageIdForPromptsType(type: PromptsType): string { switch (type) { @@ -43,6 +49,8 @@ export function getLanguageIdForPromptsType(type: PromptsType): string { return INSTRUCTIONS_LANGUAGE_ID; case PromptsType.agent: return AGENT_LANGUAGE_ID; + case PromptsType.skill: + return SKILL_LANGUAGE_ID; default: throw new Error(`Unknown prompt type: ${type}`); } @@ -56,6 +64,8 @@ export function getPromptsTypeForLanguageId(languageId: string): PromptsType | u return PromptsType.instructions; case AGENT_LANGUAGE_ID: return PromptsType.agent; + case SKILL_LANGUAGE_ID: + return PromptsType.skill; default: return undefined; } @@ -68,7 +78,8 @@ export function getPromptsTypeForLanguageId(languageId: string): PromptsType | u export enum PromptsType { instructions = 'instructions', prompt = 'prompt', - agent = 'agent' + agent = 'agent', + skill = 'skill' } export function isValidPromptType(type: string): type is PromptsType { return Object.values(PromptsType).includes(type as PromptsType); 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 2efbd97742b..64623874735 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts @@ -14,6 +14,30 @@ import { IChatModeInstructions, IVariableReference } from '../../chatModes.js'; import { PromptsType } from '../promptTypes.js'; import { IHandOff, ParsedPromptFile } from '../promptFileParser.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; +import { IResolvedPromptSourceFolder } from '../config/promptFileLocations.js'; + +/** + * Activation events for prompt file providers. + */ +export const CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT = 'onCustomAgentProvider'; +export const INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT = 'onInstructionsProvider'; +export const PROMPT_FILE_PROVIDER_ACTIVATION_EVENT = 'onPromptFileProvider'; +export const SKILL_PROVIDER_ACTIVATION_EVENT = 'onSkillProvider'; + +/** + * Context for querying prompt files. + */ +export interface IPromptFileContext { } + +/** + * Represents a prompt file resource from an external provider. + */ +export interface IPromptFileResource { + /** + * The URI to the agent or prompt resource file. + */ + readonly uri: URI; +} /** * Provides prompt services. @@ -29,6 +53,14 @@ export enum PromptsStorage { extension = 'extension' } +/** + * The type of source for extension agents. + */ +export enum ExtensionAgentSourceType { + contribution = 'contribution', + provider = 'provider', +} + /** * Represents a prompt path with its type. * This is used for both prompt files and prompt source folders. @@ -65,8 +97,9 @@ export interface IPromptPathBase { export interface IExtensionPromptPath extends IPromptPathBase { readonly storage: PromptsStorage.extension; readonly extension: IExtensionDescription; - readonly name: string; - readonly description: string; + readonly source: ExtensionAgentSourceType; + readonly name?: string; + readonly description?: string; } export interface ILocalPromptPath extends IPromptPathBase { readonly storage: PromptsStorage.local; @@ -78,10 +111,31 @@ export interface IUserPromptPath extends IPromptPathBase { export type IAgentSource = { readonly storage: PromptsStorage.extension; readonly extensionId: ExtensionIdentifier; + readonly type: ExtensionAgentSourceType; } | { readonly storage: PromptsStorage.local | PromptsStorage.user; }; +/** + * The visibility/availability of an agent. + * - 'all': available as custom agent in picker AND can be used as subagent + * - 'user': only available in the custom agent picker + * - 'agent': only usable as subagent by the subagent tool + * - 'hidden': neither in picker nor usable as subagent + */ +export type ICustomAgentVisibility = { + readonly userInvokable: boolean; + readonly agentInvokable: boolean; +}; + +export function isCustomAgentVisibility(obj: unknown): obj is ICustomAgentVisibility { + if (typeof obj !== 'object' || obj === null) { + return false; + } + const v = obj as { userInvokable?: unknown; agentInvokable?: unknown }; + return typeof v.userInvokable === 'boolean' && typeof v.agentInvokable === 'boolean'; +} + export interface ICustomAgent { /** * URI of a custom agent file. @@ -106,7 +160,7 @@ export interface ICustomAgent { /** * Model metadata in the prompt header. */ - readonly model?: string; + readonly model?: readonly string[]; /** * Argument hint metadata in the prompt header that describes what inputs the agent expects or supports. @@ -118,6 +172,11 @@ export interface ICustomAgent { */ readonly target?: string; + /** + * What visibility the agent has (user invokable, subagent invokable). + */ + readonly visibility: ICustomAgentVisibility; + /** * Contents of the custom agent file body and other agent instructions. */ @@ -128,6 +187,12 @@ export interface ICustomAgent { */ readonly handOffs?: readonly IHandOff[]; + /** + * List of subagent names that can be used by the agent. + * If empty, no subagents are available. If ['*'] or undefined, all agents can be used. + */ + readonly agents?: readonly string[]; + /** * Where the agent was loaded from. */ @@ -140,6 +205,57 @@ export interface IAgentInstructions { readonly metadata?: Record; } +export interface IChatPromptSlashCommand { + readonly name: string; + readonly description: string | undefined; + readonly argumentHint: string | undefined; + readonly promptPath: IPromptPath; + readonly parsedPromptFile: ParsedPromptFile; +} + +export interface IAgentSkill { + readonly uri: URI; + readonly storage: PromptsStorage; + readonly name: string; + readonly description: string | undefined; +} + +/** + * Reason why a prompt file was skipped during discovery. + */ +export type PromptFileSkipReason = + | 'missing-name' + | 'missing-description' + | 'name-mismatch' + | 'duplicate-name' + | 'parse-error' + | 'disabled'; + +/** + * Result of discovering a single prompt file. + */ +export interface IPromptFileDiscoveryResult { + readonly uri: URI; + readonly storage: PromptsStorage; + readonly status: 'loaded' | 'skipped'; + readonly name?: string; + readonly skipReason?: PromptFileSkipReason; + /** Error message if parse-error */ + readonly errorMessage?: string; + /** For duplicates, the URI of the file that took precedence */ + readonly duplicateOf?: URI; + /** Extension ID if from extension */ + readonly extensionId?: string; +} + +/** + * Summary of prompt file discovery for a specific type. + */ +export interface IPromptDiscoveryInfo { + readonly type: PromptsType; + readonly files: readonly IPromptFileDiscoveryResult[]; +} + /** * Provides prompt services. */ @@ -165,40 +281,40 @@ export interface IPromptsService extends IDisposable { /** * Get a list of prompt source folders based on the provided prompt type. */ - getSourceFolders(type: PromptsType): readonly IPromptPath[]; + getSourceFolders(type: PromptsType): Promise; /** - * Returns a prompt command if the command name. - * Undefined is returned if the name does not look like a file name of a prompt file. + * Get a list of resolved prompt source folders with full metadata. + * This includes displayPath, isDefault, and storage information. + * Used for diagnostics and config-info displays. */ - asPromptSlashCommand(name: string): IChatPromptSlashCommand | undefined; + getResolvedSourceFolders(type: PromptsType): Promise; /** - * Gets the prompt file for a slash command. + * Validates if the provided command name is a valid prompt slash command. */ - resolvePromptSlashCommand(data: IChatPromptSlashCommand, _token: CancellationToken): Promise; + isValidSlashCommandName(name: string): boolean; /** - * Gets the prompt file for a slash command from cache if available. - * @param command - name of the prompt command without slash + * Gets the prompt file for a slash command. */ - resolvePromptSlashCommandFromCache(command: string): ParsedPromptFile | undefined; + resolvePromptSlashCommand(command: string, token: CancellationToken): Promise; /** - * Event that is triggered when slash command -> ParsedPromptFile cache is updated. - * Event handler can call resolvePromptSlashCommandFromCache in case there is new value populated. + * Event that is triggered when the slash command to ParsedPromptFile cache is updated. + * Event handlers can use {@link resolvePromptSlashCommand} to retrieve the latest data. */ - readonly onDidChangeParsedPromptFilesCache: Event; + readonly onDidChangeSlashCommands: Event; /** * Returns a prompt command if the command name is valid. */ - findPromptSlashCommands(): Promise; + getPromptSlashCommands(token: CancellationToken): Promise; /** * Returns the prompt command name for the given URI. */ - getPromptCommandName(uri: URI): Promise; + getPromptSlashCommandName(uri: URI, token: CancellationToken): Promise; /** * Event that is triggered when the list of custom agents changes. @@ -216,17 +332,11 @@ export interface IPromptsService extends IDisposable { */ parseNew(uri: URI, token: CancellationToken): Promise; - /** - * Returns the prompt file type for the given URI. - * @param resource the URI of the resource - */ - getPromptFileType(resource: URI): PromptsType | undefined; - /** * Internal: register a contributed file. Returns a disposable that removes the contribution. * Not intended for extension authors; used by contribution point handler. */ - registerContributedFile(type: PromptsType, name: string, description: string, uri: URI, extension: IExtensionDescription): IDisposable; + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable; getPromptLocationLabel(promptPath: IPromptPath): string; @@ -262,10 +372,28 @@ export interface IPromptsService extends IDisposable { * Persists the set of disabled prompt file URIs for the given type. */ setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void; -} -export interface IChatPromptSlashCommand { - readonly command: string; - readonly detail: string; - readonly promptPath?: IPromptPath; + /** + * Registers a prompt file provider that can provide prompt files for repositories. + * @param extension The extension registering the provider. + * @param type The type of contribution. + * @param provider The provider implementation with optional change event. + * @returns A disposable that unregisters the provider when disposed. + */ + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { + onDidChangePromptFiles?: Event; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; + }): IDisposable; + + /** + * Gets list of agent skills files. + */ + findAgentSkills(token: CancellationToken): Promise; + + /** + * Gets detailed discovery information for a prompt type. + * This includes all files found and their load/skip status with reasons. + * Used for diagnostics and config-info displays. + */ + getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken): Promise; } 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 32287324baf..161e73495b6 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -3,36 +3,68 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Delayer } from '../../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../../base/common/errors.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 { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; -import { basename } from '../../../../../../base/common/path.js'; -import { dirname, isEqual } from '../../../../../../base/common/resources.js'; +import { basename, dirname, isEqual } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; -import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { type 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 { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { FileOperationError, FileOperationResult, IFileService } from '../../../../../../platform/files/common/files.js'; +import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IFilesConfigurationService } from '../../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { IVariableReference } from '../../chatModes.js'; import { PromptsConfig } from '../config/config.js'; -import { getCleanPromptName, PROMPT_FILE_EXTENSION } from '../config/promptFileLocations.js'; -import { getPromptsTypeForLanguageId, PROMPT_LANGUAGE_ID, PromptsType, getLanguageIdForPromptsType } from '../promptTypes.js'; +import { getCleanPromptName, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; +import { PROMPT_LANGUAGE_ID, PromptsType, getPromptsTypeForLanguageId } from '../promptTypes.js'; import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; -import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IUserPromptPath, PromptsStorage } from './promptsService.js'; +import { IAgentInstructions, IAgentSource, IChatPromptSlashCommand, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, ICustomAgentVisibility } from './promptsService.js'; +import { Delayer } from '../../../../../../base/common/async.js'; +import { Schemas } from '../../../../../../base/common/network.js'; + +/** + * Error thrown when a skill file is missing the required name attribute. + */ +export class SkillMissingNameError extends Error { + constructor(public readonly uri: URI) { + super('Skill file must have a name attribute'); + } +} + +/** + * Error thrown when a skill file is missing the required description attribute. + */ +export class SkillMissingDescriptionError extends Error { + constructor(public readonly uri: URI) { + super('Skill file must have a description attribute'); + } +} + +/** + * Error thrown when a skill's name does not match its parent folder name. + */ +export class SkillNameMismatchError extends Error { + constructor( + public readonly uri: URI, + public readonly skillName: string, + public readonly folderName: string + ) { + super(`Skill name must match folder name: expected "${folderName}" but got "${skillName}"`); + } +} /** * Provides prompt services. @@ -48,20 +80,31 @@ export class PromptsService extends Disposable implements IPromptsService { /** * Cached custom agents. Caching only happens if the `onDidChangeCustomAgents` event is used. */ - private cachedCustomAgents: Promise | undefined; + private readonly cachedCustomAgents: CachedPromise; + + /** + * Cached slash commands. Caching only happens if the `onDidChangeSlashCommands` event is used. + */ + private readonly cachedSlashCommands: CachedPromise; /** * Cache for parsed prompt files keyed by URI. * The number in the returned tuple is textModel.getVersionId(), which is an internal VS Code counter that increments every time the text model's content changes. */ - private parsedPromptFileCache = new ResourceMap<[number, ParsedPromptFile]>(); + private readonly cachedParsedPromptFromModels = new ResourceMap<[number, ParsedPromptFile]>(); /** - * Cache for parsed prompt files keyed by command name. + * Cached file locations commands. Caching only happens if the corresponding `fileLocatorEvents` event is used. */ - private promptFileByCommandCache = new Map | undefined }>(); + private readonly cachedFileLocations: { [key in PromptsType]?: Promise } = {}; + + /** + * Lazily created events that notify listeners when the file locations for a given prompt type change. + * An event is created on demand for each prompt type and can be used by consumers to react to updates + * in the set of prompt files (e.g., when prompt files are added, removed, or modified). + */ + private readonly fileLocatorEvents: { [key in PromptsType]?: Event } = {}; - private onDidChangeParsedPromptFilesCacheEmitter = new Emitter(); /** * Contributed files from extensions keyed by prompt type then name. @@ -70,109 +113,217 @@ export class PromptsService extends Disposable implements IPromptsService { [PromptsType.prompt]: new ResourceMap>(), [PromptsType.instructions]: new ResourceMap>(), [PromptsType.agent]: new ResourceMap>(), + [PromptsType.skill]: new ResourceMap>(), }; - /** - * Lazily created event that is fired when the custom agents change. - */ - private onDidChangeCustomAgentsEmitter: Emitter | undefined; - constructor( @ILogService public readonly logger: ILogService, @ILabelService private readonly labelService: ILabelService, @IModelService private readonly modelService: IModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataProfileService private readonly userDataService: IUserDataProfileService, - @ILanguageService private readonly languageService: ILanguageService, @IConfigurationService private readonly configurationService: IConfigurationService, @IFileService private readonly fileService: IFileService, @IFilesConfigurationService private readonly filesConfigService: IFilesConfigurationService, @IStorageService private readonly storageService: IStorageService, + @IExtensionService private readonly extensionService: IExtensionService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); - this.onDidChangeParsedPromptFilesCacheEmitter = this._register(new Emitter()); - - this.fileLocator = this._register(this.instantiationService.createInstance(PromptFilesLocator)); - - const promptUpdateTracker = this._register(new UpdateTracker(this.fileLocator, PromptsType.prompt, this.modelService)); - this._register(promptUpdateTracker.onDidPromptChange((event) => { - if (event.kind === 'fileSystem') { - this.promptFileByCommandCache.clear(); - } - else { - // Clear cache for prompt files that match the changed URI - const pendingDeletes: string[] = []; - for (const [key, value] of this.promptFileByCommandCache) { - if (isEqual(value.value?.uri, event.uri)) { - pendingDeletes.push(key); - } - } - - for (const key of pendingDeletes) { - this.promptFileByCommandCache.delete(key); - } - } - - this.onDidChangeParsedPromptFilesCacheEmitter.fire(); - })); - + this.fileLocator = this.instantiationService.createInstance(PromptFilesLocator); this._register(this.modelService.onModelRemoved((model) => { - this.parsedPromptFileCache.delete(model.uri); + this.cachedParsedPromptFromModels.delete(model.uri); })); - } - /** - * Emitter for the custom agents change event. - */ - public get onDidChangeCustomAgents(): Event { - if (!this.onDidChangeCustomAgentsEmitter) { - const emitter = this.onDidChangeCustomAgentsEmitter = this._register(new Emitter()); - const updateTracker = this._register(new UpdateTracker(this.fileLocator, PromptsType.agent, this.modelService)); - this._register(updateTracker.onDidPromptChange((event) => { - this.cachedCustomAgents = undefined; // reset cached custom agents - emitter.fire(); - })); - } - return this.onDidChangeCustomAgentsEmitter.event; - } + const modelChangeEvent = this._register(new ModelChangeTracker(this.modelService)).onDidPromptChange; + this.cachedCustomAgents = this._register(new CachedPromise( + (token) => this.computeCustomAgents(token), + () => Event.any(this.getFileLocatorEvent(PromptsType.agent), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.agent)) + )); - public get onDidChangeParsedPromptFilesCache(): Event { - return this.onDidChangeParsedPromptFilesCacheEmitter.event; + this.cachedSlashCommands = this._register(new CachedPromise( + (token) => this.computePromptSlashCommands(token), + () => Event.any(this.getFileLocatorEvent(PromptsType.prompt), Event.filter(modelChangeEvent, e => e.promptType === PromptsType.prompt)) + )); } - public getPromptFileType(uri: URI): PromptsType | undefined { - const model = this.modelService.getModel(uri); - const languageId = model ? model.getLanguageId() : this.languageService.guessLanguageIdByFilepathOrFirstLine(uri); - return languageId ? getPromptsTypeForLanguageId(languageId) : undefined; + private getFileLocatorEvent(type: PromptsType): Event { + let event = this.fileLocatorEvents[type]; + if (!event) { + event = this.fileLocatorEvents[type] = this._register(this.fileLocator.createFilesUpdatedEvent(type)).event; + this._register(event(() => { + this.cachedFileLocations[type] = undefined; + })); + } + return event; } public getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { - const cached = this.parsedPromptFileCache.get(textModel.uri); + const cached = this.cachedParsedPromptFromModels.get(textModel.uri); if (cached && cached[0] === textModel.getVersionId()) { return cached[1]; } const ast = new PromptFileParser().parse(textModel.uri, textModel.getValue()); if (!cached || cached[0] < textModel.getVersionId()) { - this.parsedPromptFileCache.set(textModel.uri, [textModel.getVersionId(), ast]); + this.cachedParsedPromptFromModels.set(textModel.uri, [textModel.getVersionId(), ast]); } return ast; } public async listPromptFiles(type: PromptsType, token: CancellationToken): Promise { + let listPromise = this.cachedFileLocations[type]; + if (!listPromise) { + listPromise = this.computeListPromptFiles(type, token); + if (!this.fileLocatorEvents[type]) { + return listPromise; + } + this.cachedFileLocations[type] = listPromise; + return listPromise; + } + return listPromise; + } + + private async computeListPromptFiles(type: PromptsType, token: CancellationToken): Promise { const prompts = await Promise.all([ this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))), this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))), - this.getExtensionContributions(type) + this.getExtensionPromptFiles(type, token), ]); return [...prompts.flat()]; } + /** + * Registry of prompt file provider instances (custom agents, instructions, prompt files). + * Extensions can register providers via the proposed API. + */ + private readonly promptFileProviders: Array<{ + extension: IExtensionDescription; + type: PromptsType; + onDidChangePromptFiles?: Event; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; + }> = []; + + /** + * Registers a prompt file provider (CustomAgentProvider, InstructionsProvider, or PromptFileProvider). + * This will be called by the extension host bridge when + * an extension registers a provider via vscode.chat.registerCustomAgentProvider(), + * registerInstructionsProvider(), or registerPromptFileProvider(). + */ + public registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { + onDidChangePromptFiles?: Event; + providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise; + }): IDisposable { + const providerEntry = { extension, type, ...provider }; + this.promptFileProviders.push(providerEntry); + + const disposables = new DisposableStore(); + + // Listen to provider change events to rerun computeListPromptFiles + if (provider.onDidChangePromptFiles) { + disposables.add(provider.onDidChangePromptFiles(() => { + if (type === PromptsType.agent) { + this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; + } + })); + } + + // Invalidate cache when providers change + if (type === PromptsType.agent) { + this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; + } + + disposables.add({ + dispose: () => { + const index = this.promptFileProviders.findIndex((p) => p === providerEntry); + if (index >= 0) { + this.promptFileProviders.splice(index, 1); + if (type === PromptsType.agent) { + this.cachedFileLocations[PromptsType.agent] = undefined; + this.cachedCustomAgents.refresh(); + } else if (type === PromptsType.instructions) { + this.cachedFileLocations[PromptsType.instructions] = undefined; + } else if (type === PromptsType.prompt) { + this.cachedFileLocations[PromptsType.prompt] = undefined; + this.cachedSlashCommands.refresh(); + } else if (type === PromptsType.skill) { + this.cachedFileLocations[PromptsType.skill] = undefined; + } + } + } + }); + + return disposables; + } + + /** + * Shared helper to list prompt files from registered providers for a given type. + */ + private async listFromProviders(type: PromptsType, activationEvent: string, token: CancellationToken): Promise { + const result: IExtensionPromptPath[] = []; + + // Activate extensions that might provide files for this type + await this.extensionService.activateByEvent(activationEvent); + + const providers = this.promptFileProviders.filter(p => p.type === type); + if (providers.length === 0) { + return result; + } + + // Collect files from all providers + for (const providerEntry of providers) { + try { + const files = await providerEntry.providePromptFiles({}, token); + if (!files || token.isCancellationRequested) { + continue; + } + + for (const file of files) { + try { + await this.filesConfigService.updateReadonly(file.uri, true); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[listFromProviders] Failed to make file readonly: ${file.uri}`, msg); + } + result.push({ + uri: file.uri, + storage: PromptsStorage.extension, + type, + extension: providerEntry.extension, + source: ExtensionAgentSourceType.provider + } satisfies IExtensionPromptPath); + } + } catch (e) { + this.logger.error(`[listFromProviders] Failed to get ${type} files from provider`, e instanceof Error ? e.message : String(e)); + } + } + + return result; + } + + + public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { switch (storage) { case PromptsStorage.extension: - return this.getExtensionContributions(type); + return this.getExtensionPromptFiles(type, token); case PromptsStorage.local: return this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))); case PromptsStorage.user: @@ -182,151 +333,152 @@ export class PromptsService extends Disposable implements IPromptsService { } } - private async getExtensionContributions(type: PromptsType): Promise { - return Promise.all(this.contributedFiles[type].values()); + private async getExtensionPromptFiles(type: PromptsType, token: CancellationToken): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + const settledResults = await Promise.allSettled(this.contributedFiles[type].values()); + const contributedFiles = settledResults + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map(result => result.value); + + const activationEvent = this.getProviderActivationEvent(type); + const providerFiles = await this.listFromProviders(type, activationEvent, token); + return [...contributedFiles, ...providerFiles]; + } + + private getProviderActivationEvent(type: PromptsType): string { + switch (type) { + case PromptsType.agent: + return CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT; + case PromptsType.instructions: + return INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT; + case PromptsType.prompt: + return PROMPT_FILE_PROVIDER_ACTIVATION_EVENT; + case PromptsType.skill: + return SKILL_PROVIDER_ACTIVATION_EVENT; + } } - public getSourceFolders(type: PromptsType): readonly IPromptPath[] { + public async getSourceFolders(type: PromptsType): Promise { const result: IPromptPath[] = []; if (type === PromptsType.agent) { - const folders = this.fileLocator.getAgentSourceFolder(); + const folders = await this.fileLocator.getAgentSourceFolders(); for (const uri of folders) { result.push({ uri, storage: PromptsStorage.local, type }); } } else { - for (const uri of this.fileLocator.getConfigBasedSourceFolders(type)) { + for (const uri of await this.fileLocator.getConfigBasedSourceFolders(type)) { result.push({ uri, storage: PromptsStorage.local, type }); } } - const userHome = this.userDataService.currentProfile.promptsHome; - result.push({ uri: userHome, storage: PromptsStorage.user, type }); + if (type !== PromptsType.skill) { + // no user source folders for skills + const userHome = this.userDataService.currentProfile.promptsHome; + result.push({ uri: userHome, storage: PromptsStorage.user, type }); + } return result; } - public asPromptSlashCommand(command: string): IChatPromptSlashCommand | undefined { - if (command.match(/^[\p{L}\d_\-\.]+$/u)) { - return { command, detail: localize('prompt.file.detail', 'Prompt file: {0}', command) }; - } - return undefined; + public async getResolvedSourceFolders(type: PromptsType): Promise { + return this.fileLocator.getResolvedSourceFolders(type); } - public async resolvePromptSlashCommand(data: IChatPromptSlashCommand, token: CancellationToken): Promise { - const promptUri = data.promptPath?.uri ?? await this.getPromptPath(data.command); - if (!promptUri) { - return undefined; - } + // slash prompt commands - try { - return await this.parseNew(promptUri, token); - } catch (error) { - this.logger.error(`[resolvePromptSlashCommand] Failed to parse prompt file: ${promptUri}`, error); - return undefined; - } + /** + * Emitter for slash commands change events. + */ + public get onDidChangeSlashCommands(): Event { + return this.cachedSlashCommands.onDidChange; } - private async populatePromptCommandCache(command: string): Promise { - let cache = this.promptFileByCommandCache.get(command); - if (cache && cache.pendingPromise) { - return cache.pendingPromise; - } + public async getPromptSlashCommands(token: CancellationToken): Promise { + return this.cachedSlashCommands.get(token); + } - const newPromise = this.resolvePromptSlashCommand({ command, detail: '' }, CancellationToken.None); - if (cache) { - cache.pendingPromise = newPromise; + private async computePromptSlashCommands(token: CancellationToken): Promise { + const promptFiles = await this.listPromptFiles(PromptsType.prompt, token); + const details = await Promise.all(promptFiles.map(async promptPath => { + try { + const parsedPromptFile = await this.parseNew(promptPath.uri, token); + return this.asChatPromptSlashCommand(parsedPromptFile, promptPath); + } catch (e) { + this.logger.error(`[computePromptSlashCommands] Failed to parse prompt file for slash command: ${promptPath.uri}`, e instanceof Error ? e.message : String(e)); + return undefined; + } + })); + const result = []; + const seen = new ResourceSet(); + for (const detail of details) { + if (detail) { + result.push(detail); + seen.add(detail.promptPath.uri); + } } - else { - cache = { value: undefined, pendingPromise: newPromise }; - this.promptFileByCommandCache.set(command, cache); + for (const model of this.modelService.getModels()) { + if (model.getLanguageId() === PROMPT_LANGUAGE_ID && model.uri.scheme === Schemas.untitled && !seen.has(model.uri)) { + const parsedPromptFile = this.getParsedPromptFile(model); + result.push(this.asChatPromptSlashCommand(parsedPromptFile, { uri: model.uri, storage: PromptsStorage.local, type: PromptsType.prompt })); + } } - - const newValue = await newPromise.finally(() => cache.pendingPromise = undefined); - - // TODO: consider comparing the newValue and the old and only emit change event when there are value changes - cache.value = newValue; - this.onDidChangeParsedPromptFilesCacheEmitter.fire(); - - return newValue; + return result; } - public resolvePromptSlashCommandFromCache(command: string): ParsedPromptFile | undefined { - const cache = this.promptFileByCommandCache.get(command); - const value = cache?.value; - if (value === undefined) { - // kick off a async process to refresh the cache while we returns the current cached value - void this.populatePromptCommandCache(command).catch((error) => { }); - } + public isValidSlashCommandName(command: string): boolean { + return command.match(/^[\p{L}\d_\-\.]+$/u) !== null; + } - return value; + public async resolvePromptSlashCommand(name: string, token: CancellationToken): Promise { + const commands = await this.getPromptSlashCommands(token); + return commands.find(cmd => cmd.name === name); } - private async getPromptDetails(promptPath: IPromptPath): Promise<{ name: string; description?: string }> { - const parsedPromptFile = await this.parseNew(promptPath.uri, CancellationToken.None).catch(() => undefined); + private asChatPromptSlashCommand(parsedPromptFile: ParsedPromptFile, promptPath: IPromptPath): IChatPromptSlashCommand { + let name = parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(promptPath.uri); + name = name.replace(/[^\p{L}\d_\-\.]+/gu, '-'); // replace spaces with dashes return { - name: parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(promptPath.uri), - description: parsedPromptFile?.header?.description ?? promptPath.description + name: name, + description: parsedPromptFile?.header?.description ?? promptPath.description, + argumentHint: parsedPromptFile?.header?.argumentHint, + parsedPromptFile, + promptPath }; } - private async getPromptPath(command: string): Promise { - const promptPaths = await this.listPromptFiles(PromptsType.prompt, CancellationToken.None); - for (const promptPath of promptPaths) { - const details = await this.getPromptDetails(promptPath); - if (details.name === command) { - return promptPath.uri; - } + public async getPromptSlashCommandName(uri: URI, token: CancellationToken): Promise { + const slashCommands = await this.getPromptSlashCommands(token); + const slashCommand = slashCommands.find(c => isEqual(c.promptPath.uri, uri)); + if (!slashCommand) { + return getCleanPromptName(uri); } - const textModel = this.modelService.getModels().find(model => model.getLanguageId() === PROMPT_LANGUAGE_ID && getCommandNameFromURI(model.uri) === command); - if (textModel) { - return textModel.uri; - } - return undefined; + return slashCommand.name; } - public async getPromptCommandName(uri: URI): Promise { - const promptPaths = await this.listPromptFiles(PromptsType.prompt, CancellationToken.None); - let promptPath = promptPaths.find(promptPath => isEqual(promptPath.uri, uri)); - if (!promptPath) { - promptPath = { uri, storage: PromptsStorage.local, type: PromptsType.prompt }; // make up a prompt path - } - const { name } = await this.getPromptDetails(promptPath); - return name; - } + // custom agents - public async findPromptSlashCommands(): Promise { - const promptFiles = await this.listPromptFiles(PromptsType.prompt, CancellationToken.None); - return Promise.all(promptFiles.map(async promptPath => { - const { name } = await this.getPromptDetails(promptPath); - return { - command: name, - detail: localize('prompt.file.detail', 'Prompt file: {0}', this.labelService.getUriLabel(promptPath.uri, { relative: true })), - promptPath - }; - })); + /** + * Emitter for custom agents change events. + */ + public get onDidChangeCustomAgents(): Event { + return this.cachedCustomAgents.onDidChange; } public async getCustomAgents(token: CancellationToken): Promise { - let customAgents = this.cachedCustomAgents; - if (!customAgents) { - customAgents = this.computeCustomAgents(token); - if (this.onDidChangeCustomAgentsEmitter) { - this.cachedCustomAgents = customAgents; - } - } - const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); - return (await customAgents).filter(agent => !disabledAgents.has(agent.uri)); + return this.cachedCustomAgents.get(token); } private async computeCustomAgents(token: CancellationToken): Promise { - const agentFiles = await this.listPromptFiles(PromptsType.agent, token); - - const customAgents = await Promise.all( + let agentFiles = await this.listPromptFiles(PromptsType.agent, token); + const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); + agentFiles = agentFiles.filter(promptPath => !disabledAgents.has(promptPath.uri)); + const customAgentsResults = await Promise.allSettled( agentFiles.map(async (promptPath): Promise => { const uri = promptPath.uri; const ast = await this.parseNew(uri, token); + // eslint-disable-next-line @typescript-eslint/no-explicit-any let metadata: any | undefined; if (ast.header) { const advanced = ast.header.getAttribute(PromptHeaderAttributes.advancedOptions); @@ -360,20 +512,44 @@ export class PromptsService extends Disposable implements IPromptsService { const source: IAgentSource = IAgentSource.fromPromptPath(promptPath); if (!ast.header) { - return { uri, name, agentInstructions, source }; + return { uri, name, agentInstructions, source, visibility: { userInvokable: true, agentInvokable: true } }; } - const { description, model, tools, handOffs, argumentHint, target } = ast.header; - return { uri, name, description, model, tools, handOffs, argumentHint, target, agentInstructions, source }; + const visibility = { + userInvokable: ast.header.userInvokable !== false, + agentInvokable: ast.header.infer === true || ast.header.disableModelInvocation !== true, + } satisfies ICustomAgentVisibility; + + const { description, model, tools, handOffs, argumentHint, target, agents } = ast.header; + return { uri, name, description, model, tools, handOffs, argumentHint, target, visibility, agents, agentInstructions, source }; }) ); + + const customAgents: ICustomAgent[] = []; + for (let i = 0; i < customAgentsResults.length; i++) { + const result = customAgentsResults[i]; + if (result.status === 'fulfilled') { + customAgents.push(result.value); + } else { + const uri = agentFiles[i].uri; + const error = result.reason; + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + this.logger.warn(`[computeCustomAgents] Skipping agent file that does not exist: ${uri}`, error.message); + } else { + this.logger.error(`[computeCustomAgents] Failed to parse agent file: ${uri}`, error); + } + } + } + return customAgents; } + 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(); @@ -381,34 +557,52 @@ export class PromptsService extends Disposable implements IPromptsService { return new PromptFileParser().parse(uri, fileContent.value.toString()); } - public registerContributedFile(type: PromptsType, name: string, description: string, uri: URI, extension: IExtensionDescription) { + public registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name?: string, description?: string) { const bucket = this.contributedFiles[type]; if (bucket.has(uri)) { // keep first registration per extension (handler filters duplicates per extension already) return Disposable.None; } const entryPromise = (async () => { + // For skills, validate that the file follows the required structure + if (type === PromptsType.skill) { + try { + const validated = await this.validateAndSanitizeSkillFile(uri, CancellationToken.None); + name = validated.name; + description = validated.description; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[registerContributedFile] Extension '${extension.identifier.value}' failed to validate skill file: ${uri}`, msg); + throw e; + } + } + try { await this.filesConfigService.updateReadonly(uri, true); } catch (e) { const msg = e instanceof Error ? e.message : String(e); this.logger.error(`[registerContributedFile] Failed to make prompt file readonly: ${uri}`, msg); } - return { uri, name, description, storage: PromptsStorage.extension, type, extension } satisfies IExtensionPromptPath; + return { uri, name, description, storage: PromptsStorage.extension, type, extension, source: ExtensionAgentSourceType.contribution } satisfies IExtensionPromptPath; })(); bucket.set(uri, entryPromise); - const updateAgentsIfRequired = () => { - if (type === PromptsType.agent) { - this.cachedCustomAgents = undefined; - this.onDidChangeCustomAgentsEmitter?.fire(); + const flushCachesIfRequired = () => { + this.cachedFileLocations[type] = undefined; + switch (type) { + case PromptsType.agent: + this.cachedCustomAgents.refresh(); + break; + case PromptsType.prompt: + this.cachedSlashCommands.refresh(); + break; } }; - updateAgentsIfRequired(); + flushCachesIfRequired(); return { dispose: () => { bucket.delete(uri); - updateAgentsIfRequired(); + flushCachesIfRequired(); } }; } @@ -454,7 +648,6 @@ export class PromptsService extends Disposable implements IPromptsService { // --- Enabled Prompt Files ----------------------------------------------------------- - private readonly disabledPromptsStorageKeyPrefix = 'chat.disabledPromptFiles.'; public getDisabledPromptFiles(type: PromptsType): ResourceSet { @@ -483,57 +676,478 @@ export class PromptsService extends Disposable implements IPromptsService { const disabled = Array.from(uris).map(uri => uri.toJSON()); this.storageService.store(this.disabledPromptsStorageKeyPrefix + type, JSON.stringify(disabled), StorageScope.PROFILE, StorageTarget.USER); if (type === PromptsType.agent) { - this.onDidChangeCustomAgentsEmitter?.fire(); + this.cachedCustomAgents.refresh(); } } -} + // Agent skills + + private sanitizeAgentSkillText(text: string): string { + // Remove XML tags + return text.replace(/<[^>]+>/g, ''); + } + + /** + * Validates and sanitizes a skill file. Throws an error if validation fails. + * @returns The sanitized name and description + */ + private async validateAndSanitizeSkillFile(uri: URI, token: CancellationToken): Promise<{ name: string; description: string | undefined }> { + const parsedFile = await this.parseNew(uri, token); + const name = parsedFile.header?.name; + + if (!name) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill file missing name attribute: ${uri}`); + throw new SkillMissingNameError(uri); + } + + const description = parsedFile.header?.description; + if (!description) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill file missing description attribute: ${uri}`); + throw new SkillMissingDescriptionError(uri); + } + + // Sanitize the name first (remove XML tags and truncate) + const sanitizedName = this.truncateAgentSkillName(name, uri); + + // Validate that the sanitized name matches the parent folder name (per agentskills.io specification) + const skillFolderUri = dirname(uri); + const folderName = basename(skillFolderUri); + if (sanitizedName !== folderName) { + this.logger.error(`[validateAndSanitizeSkillFile] Agent skill name "${sanitizedName}" does not match folder name "${folderName}": ${uri}`); + throw new SkillNameMismatchError(uri, sanitizedName, folderName); + } + + const sanitizedDescription = this.truncateAgentSkillDescription(parsedFile.header?.description, uri); + return { name: sanitizedName, description: sanitizedDescription }; + } + + private truncateAgentSkillName(name: string, uri: URI): string { + const MAX_NAME_LENGTH = 64; + const sanitized = this.sanitizeAgentSkillText(name); + if (sanitized !== name) { + this.logger.warn(`[findAgentSkills] Agent skill name contains XML tags, removed: ${uri}`); + } + if (sanitized.length > MAX_NAME_LENGTH) { + this.logger.warn(`[findAgentSkills] Agent skill name exceeds ${MAX_NAME_LENGTH} characters, truncated: ${uri}`); + return sanitized.substring(0, MAX_NAME_LENGTH); + } + return sanitized; + } + + private truncateAgentSkillDescription(description: string | undefined, uri: URI): string | undefined { + if (!description) { + return undefined; + } + const MAX_DESCRIPTION_LENGTH = 1024; + const sanitized = this.sanitizeAgentSkillText(description); + if (sanitized !== description) { + this.logger.warn(`[findAgentSkills] Agent skill description contains XML tags, removed: ${uri}`); + } + if (sanitized.length > MAX_DESCRIPTION_LENGTH) { + this.logger.warn(`[findAgentSkills] Agent skill description exceeds ${MAX_DESCRIPTION_LENGTH} characters, truncated: ${uri}`); + return sanitized.substring(0, MAX_DESCRIPTION_LENGTH); + } + return sanitized; + } + + public async findAgentSkills(token: CancellationToken): Promise { + const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); + if (!useAgentSkills) { + return undefined; + } + + const { files, skillsBySource } = await this.computeSkillDiscoveryInfo(token); + + // Extract loaded skills + const result: IAgentSkill[] = []; + for (const file of files) { + if (file.status === 'loaded' && file.name) { + const sanitizedDescription = this.truncateAgentSkillDescription(file.description, file.uri); + result.push({ uri: file.uri, storage: file.storage, name: file.name, description: sanitizedDescription }); + } + } + + // Count skip reasons for telemetry + let skippedMissingName = 0; + let skippedMissingDescription = 0; + let skippedDuplicateName = 0; + let skippedParseFailed = 0; + let skippedNameMismatch = 0; + for (const file of files) { + if (file.status === 'skipped') { + switch (file.skipReason) { + case 'missing-name': skippedMissingName++; break; + case 'missing-description': skippedMissingDescription++; break; + case 'duplicate-name': skippedDuplicateName++; break; + case 'name-mismatch': skippedNameMismatch++; break; + case 'parse-error': skippedParseFailed++; break; + } + } + } + + // Send telemetry about skill usage + type AgentSkillsFoundEvent = { + totalSkillsFound: number; + claudePersonal: number; + claudeWorkspace: number; + copilotPersonal: number; + githubWorkspace: number; + configPersonal: number; + configWorkspace: number; + extensionContribution: number; + extensionAPI: number; + skippedDuplicateName: number; + skippedMissingName: number; + skippedMissingDescription: number; + skippedNameMismatch: number; + skippedParseFailed: number; + }; + + type AgentSkillsFoundClassification = { + totalSkillsFound: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Total number of agent skills found.' }; + claudePersonal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of Claude personal skills.' }; + claudeWorkspace: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of Claude workspace skills.' }; + copilotPersonal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of Copilot personal skills.' }; + githubWorkspace: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of GitHub workspace skills.' }; + configPersonal: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of custom configured personal skills.' }; + configWorkspace: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of custom configured workspace skills.' }; + extensionContribution: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of extension contributed skills.' }; + extensionAPI: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of extension API provided skills.' }; + skippedDuplicateName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to duplicate names.' }; + skippedMissingName: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to missing name attribute.' }; + skippedMissingDescription: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to missing description attribute.' }; + skippedNameMismatch: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to name not matching folder name.' }; + skippedParseFailed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of skills skipped due to parse failures.' }; + owner: 'pwang347'; + comment: 'Tracks agent skill usage, discovery, and skipped files.'; + }; + + this.telemetryService.publicLog2('agentSkillsFound', { + totalSkillsFound: result.length, + claudePersonal: skillsBySource.get(PromptFileSource.ClaudePersonal) ?? 0, + claudeWorkspace: skillsBySource.get(PromptFileSource.ClaudeWorkspace) ?? 0, + copilotPersonal: skillsBySource.get(PromptFileSource.CopilotPersonal) ?? 0, + githubWorkspace: skillsBySource.get(PromptFileSource.GitHubWorkspace) ?? 0, + configWorkspace: skillsBySource.get(PromptFileSource.ConfigWorkspace) ?? 0, + configPersonal: skillsBySource.get(PromptFileSource.ConfigPersonal) ?? 0, + extensionContribution: skillsBySource.get(PromptFileSource.ExtensionContribution) ?? 0, + extensionAPI: skillsBySource.get(PromptFileSource.ExtensionAPI) ?? 0, + skippedDuplicateName, + skippedMissingName, + skippedMissingDescription, + skippedNameMismatch, + skippedParseFailed + }); + + return result; + } + + public async getPromptDiscoveryInfo(type: PromptsType, token: CancellationToken): Promise { + const files: IPromptFileDiscoveryResult[] = []; + + if (type === PromptsType.skill) { + return this.getSkillDiscoveryInfo(token); + } else if (type === PromptsType.agent) { + return this.getAgentDiscoveryInfo(token); + } else if (type === PromptsType.prompt) { + return this.getPromptSlashCommandDiscoveryInfo(token); + } else if (type === PromptsType.instructions) { + return this.getInstructionsDiscoveryInfo(token); + } + + return { type, files }; + } + + private async getSkillDiscoveryInfo(token: CancellationToken): Promise { + const useAgentSkills = this.configurationService.getValue(PromptsConfig.USE_AGENT_SKILLS); + + if (!useAgentSkills) { + // Skills disabled - list all files as skipped with 'disabled' reason + const allFiles = await this.listPromptFiles(PromptsType.skill, token); + const files: IPromptFileDiscoveryResult[] = allFiles.map(promptPath => ({ + uri: promptPath.uri, + storage: promptPath.storage, + status: 'skipped' as const, + skipReason: 'disabled' as const, + extensionId: promptPath.extension?.identifier?.value + })); + return { type: PromptsType.skill, files }; + } + + const { files } = await this.computeSkillDiscoveryInfo(token); + return { type: PromptsType.skill, files }; + } + + /** + * Shared implementation for skill discovery used by both findAgentSkills and getSkillDiscoveryInfo. + * Returns the discovery results and a map of skill counts by source type for telemetry. + */ + private async computeSkillDiscoveryInfo(token: CancellationToken): Promise<{ + files: (IPromptFileDiscoveryResult & { description?: string; source?: PromptFileSource })[]; + skillsBySource: Map; + }> { + const files: (IPromptFileDiscoveryResult & { description?: string; source?: PromptFileSource })[] = []; + const skillsBySource = new Map(); + const seenNames = new Set(); + const nameToUri = new Map(); + + // Collect all skills with their metadata for sorting + const allSkills: Array = []; + const discoveredSkills = await this.fileLocator.findAgentSkills(token); + const extensionSkills = await this.getExtensionPromptFiles(PromptsType.skill, token); + allSkills.push(...discoveredSkills, ...extensionSkills.map((extPath) => ({ + fileUri: extPath.uri, + storage: extPath.storage, + source: extPath.source === ExtensionAgentSourceType.contribution ? PromptFileSource.ExtensionContribution : PromptFileSource.ExtensionAPI + }))); + + const getPriority = (skill: IResolvedPromptFile | IExtensionPromptPath): number => { + if (skill.storage === PromptsStorage.local) { + return 0; // workspace + } + if (skill.storage === PromptsStorage.user) { + return 1; // personal + } + if (skill.source === PromptFileSource.ExtensionAPI) { + return 2; + } + if (skill.source === PromptFileSource.ExtensionContribution) { + return 3; + } + return 4; + }; + // Stable sort; we should keep order consistent to the order in the user's configuration object + allSkills.sort((a, b) => getPriority(a) - getPriority(b)); + + // Build map of URI to extension ID + const extensionIdByUri = new Map(); + for (const extSkill of extensionSkills) { + extensionIdByUri.set(extSkill.uri.toString(), extSkill.extension.identifier.value); + } + + for (const skill of allSkills) { + const uri = skill.fileUri; + const storage = skill.storage; + const source = skill.source; + const extensionId = extensionIdByUri.get(uri.toString()); + + try { + const parsedFile = await this.parseNew(uri, token); + const name = parsedFile.header?.name; + if (!name) { + this.logger.error(`[computeSkillDiscoveryInfo] Agent skill file missing name attribute: ${uri}`); + files.push({ uri, storage, status: 'skipped', skipReason: 'missing-name', extensionId, source }); + continue; + } + + const sanitizedName = this.truncateAgentSkillName(name, uri); + const skillFolderUri = dirname(uri); + const folderName = basename(skillFolderUri); + if (sanitizedName !== folderName) { + this.logger.error(`[computeSkillDiscoveryInfo] Agent skill name "${sanitizedName}" does not match folder name "${folderName}": ${uri}`); + files.push({ uri, storage, status: 'skipped', skipReason: 'name-mismatch', name: sanitizedName, extensionId, source }); + continue; + } + + if (seenNames.has(sanitizedName)) { + this.logger.warn(`[computeSkillDiscoveryInfo] Skipping duplicate agent skill name: ${sanitizedName} at ${uri}`); + files.push({ uri, storage, status: 'skipped', skipReason: 'duplicate-name', name: sanitizedName, duplicateOf: nameToUri.get(sanitizedName), extensionId, source }); + continue; + } + + const description = parsedFile.header?.description; + if (!description) { + this.logger.error(`[computeSkillDiscoveryInfo] Agent skill file missing description attribute: ${uri}`); + files.push({ uri, storage, status: 'skipped', skipReason: 'missing-description', name: sanitizedName, extensionId, source }); + continue; + } + + seenNames.add(sanitizedName); + nameToUri.set(sanitizedName, uri); + files.push({ uri, storage, status: 'loaded', name: sanitizedName, description, extensionId, source }); + + // Track skill type + skillsBySource.set(source, (skillsBySource.get(source) || 0) + 1); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + this.logger.error(`[computeSkillDiscoveryInfo] Failed to validate Agent skill file: ${uri}`, msg); + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'parse-error', + errorMessage: msg, + extensionId, + source + }); + } + } + + return { files, skillsBySource }; + } + + private async getAgentDiscoveryInfo(token: CancellationToken): Promise { + const files: IPromptFileDiscoveryResult[] = []; + const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); + + const agentFiles = await this.listPromptFiles(PromptsType.agent, token); + for (const promptPath of agentFiles) { + const uri = promptPath.uri; + const storage = promptPath.storage; + const extensionId = promptPath.extension?.identifier?.value; + + if (disabledAgents.has(uri)) { + files.push({ uri, storage, status: 'skipped', skipReason: 'disabled', extensionId }); + continue; + } + + try { + const ast = await this.parseNew(uri, token); + const name = ast.header?.name ?? promptPath.name ?? getCleanPromptName(uri); + files.push({ uri, storage, status: 'loaded', name, extensionId }); + } catch (e) { + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'parse-error', + errorMessage: e instanceof Error ? e.message : String(e), + extensionId + }); + } + } + + + return { type: PromptsType.agent, files }; + } + + private async getPromptSlashCommandDiscoveryInfo(token: CancellationToken): Promise { + const files: IPromptFileDiscoveryResult[] = []; -function getCommandNameFromURI(uri: URI): string { - return basename(uri.fsPath, PROMPT_FILE_EXTENSION); + const promptFiles = await this.listPromptFiles(PromptsType.prompt, token); + for (const promptPath of promptFiles) { + const uri = promptPath.uri; + const storage = promptPath.storage; + const extensionId = promptPath.extension?.identifier?.value; + + try { + const parsedPromptFile = await this.parseNew(uri, token); + const name = parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(uri); + files.push({ uri, storage, status: 'loaded', name, extensionId }); + } catch (e) { + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'parse-error', + errorMessage: e instanceof Error ? e.message : String(e), + extensionId + }); + } + } + + return { type: PromptsType.prompt, files }; + } + + private async getInstructionsDiscoveryInfo(token: CancellationToken): Promise { + const files: IPromptFileDiscoveryResult[] = []; + + const instructionsFiles = await this.listPromptFiles(PromptsType.instructions, token); + for (const promptPath of instructionsFiles) { + const uri = promptPath.uri; + const storage = promptPath.storage; + const extensionId = promptPath.extension?.identifier?.value; + + try { + const parsedPromptFile = await this.parseNew(uri, token); + const name = parsedPromptFile?.header?.name ?? promptPath.name ?? getCleanPromptName(uri); + files.push({ uri, storage, status: 'loaded', name, extensionId }); + } catch (e) { + files.push({ + uri, + storage, + status: 'skipped', + skipReason: 'parse-error', + errorMessage: e instanceof Error ? e.message : String(e), + extensionId + }); + } + } + + return { type: PromptsType.instructions, files }; + } } -export type UpdateKind = 'fileSystem' | 'textModel'; +// helpers -export interface IUpdateEvent { - kind: UpdateKind; - uri?: URI; +class CachedPromise extends Disposable { + private cachedPromise: Promise | undefined = undefined; + private onDidUpdatePromiseEmitter: Emitter | undefined = undefined; + + constructor(private readonly computeFn: (token: CancellationToken) => Promise, private readonly getEvent: () => Event, private readonly delay: number = 0) { + super(); + } + + public get onDidChange(): Event { + if (!this.onDidUpdatePromiseEmitter) { + const emitter = this.onDidUpdatePromiseEmitter = this._register(new Emitter()); + const delayer = this._register(new Delayer(this.delay)); + this._register(this.getEvent()(() => { + this.cachedPromise = undefined; + delayer.trigger(() => emitter.fire()); + })); + } + return this.onDidUpdatePromiseEmitter.event; + } + + public get(token: CancellationToken): Promise { + if (this.cachedPromise !== undefined) { + return this.cachedPromise; + } + const result = this.computeFn(token); + if (!this.onDidUpdatePromiseEmitter) { + return result; // only cache if there is an event listener + } + this.cachedPromise = result; + this.onDidUpdatePromiseEmitter.fire(); + return result; + } + + public refresh(): void { + this.cachedPromise = undefined; + this.onDidUpdatePromiseEmitter?.fire(); + } } -export class UpdateTracker extends Disposable { +interface ModelChangeEvent { + readonly promptType: PromptsType; + readonly uri: URI; +} - private static readonly PROMPT_UPDATE_DELAY_MS = 200; +class ModelChangeTracker extends Disposable { private readonly listeners = new ResourceMap(); - private readonly onDidPromptModelChange: Emitter; + private readonly onDidPromptModelChange: Emitter; - public get onDidPromptChange(): Event { + public get onDidPromptChange(): Event { return this.onDidPromptModelChange.event; } - constructor( - fileLocator: PromptFilesLocator, - promptType: PromptsType, - @IModelService modelService: IModelService, - ) { + constructor(modelService: IModelService) { super(); - this.onDidPromptModelChange = this._register(new Emitter()); - const delayer = this._register(new Delayer(UpdateTracker.PROMPT_UPDATE_DELAY_MS)); - const trigger = (event: IUpdateEvent) => delayer.trigger(() => this.onDidPromptModelChange.fire(event)); - - const filesUpdatedEventRegistration = this._register(fileLocator.createFilesUpdatedEvent(promptType)); - this._register(filesUpdatedEventRegistration.event(() => trigger({ kind: 'fileSystem' }))); - + this.onDidPromptModelChange = this._register(new Emitter()); const onAdd = (model: ITextModel) => { - if (model.getLanguageId() === getLanguageIdForPromptsType(promptType)) { - this.listeners.set(model.uri, model.onDidChangeContent(() => trigger({ kind: 'textModel', uri: model.uri }))); + const promptType = getPromptsTypeForLanguageId(model.getLanguageId()); + if (promptType !== undefined) { + this.listeners.set(model.uri, model.onDidChangeContent(() => this.onDidPromptModelChange.fire({ uri: model.uri, promptType }))); } }; const onRemove = (languageId: string, uri: URI) => { - if (languageId === getLanguageIdForPromptsType(promptType)) { + const promptType = getPromptsTypeForLanguageId(languageId); + if (promptType !== undefined) { this.listeners.get(uri)?.dispose(); this.listeners.delete(uri); - trigger({ kind: 'textModel', uri }); + this.onDidPromptModelChange.fire({ uri, promptType }); } }; this._register(modelService.onModelAdded(model => onAdd(model))); @@ -556,7 +1170,8 @@ namespace IAgentSource { if (promptPath.storage === PromptsStorage.extension) { return { storage: PromptsStorage.extension, - extensionId: promptPath.extension.identifier + extensionId: promptPath.extension.identifier, + type: promptPath.source }; } else { return { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index b346f9f1d45..2d81297751b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -6,12 +6,13 @@ import { URI } from '../../../../../../base/common/uri.js'; import { isAbsolute } from '../../../../../../base/common/path.js'; import { ResourceSet } from '../../../../../../base/common/map.js'; +import * as nls from '../../../../../../nls.js'; import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { getPromptFileLocationsConfigKey, PromptsConfig } from '../config/config.js'; +import { getPromptFileLocationsConfigKey, isTildePath, PromptsConfig } from '../config/config.js'; import { basename, dirname, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION } from '../config/promptFileLocations.js'; +import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, DEFAULT_AGENT_SOURCE_FOLDERS, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; import { PromptsType } from '../promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -21,13 +22,14 @@ import { isCancellationError } from '../../../../../../base/common/errors.js'; import { PromptsStorage } from '../service/promptsService.js'; import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IPathService } from '../../../../../services/path/common/pathService.js'; /** * Utility class to locate prompt files. */ -export class PromptFilesLocator extends Disposable { +export class PromptFilesLocator { constructor( @IFileService private readonly fileService: IFileService, @@ -36,9 +38,9 @@ export class PromptFilesLocator extends Disposable { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ISearchService private readonly searchService: ISearchService, @IUserDataProfileService private readonly userDataService: IUserDataProfileService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IPathService private readonly pathService: IPathService, ) { - super(); } /** @@ -56,8 +58,74 @@ export class PromptFilesLocator extends Disposable { } private async listFilesInUserData(type: PromptsType, token: CancellationToken): Promise { - const files = await this.resolveFilesAtLocation(this.userDataService.currentProfile.promptsHome, token); - return files.filter(file => getPromptFileType(file) === type); + const userStorageFolders = await this.getUserStorageFolders(type); + const paths = new ResourceSet(); + + for (const { uri } of userStorageFolders) { + const files = await this.resolveFilesAtLocation(uri, type, token); + for (const file of files) { + if (getPromptFileType(file) === type) { + paths.add(file); + } + } + if (token.isCancellationRequested) { + return []; + } + } + + return [...paths]; + } + + /** + * Gets all user storage folders for the given prompt type. + * This includes configured tilde paths and the VS Code user data prompts folder. + */ + private async getUserStorageFolders(type: PromptsType): Promise { + const userHome = await this.pathService.userHome(); + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); + const absoluteLocations = this.toAbsoluteLocations(type, configuredLocations, userHome); + + // Filter to only user storage locations + const result = absoluteLocations.filter(loc => loc.storage === PromptsStorage.user); + + // Also include the VS Code user data prompts folder (for all types except skills) + if (type !== PromptsType.skill) { + const userDataPromptsHome = this.userDataService.currentProfile.promptsHome; + return [ + ...result, + { + uri: userDataPromptsHome, + source: PromptFileSource.CopilotPersonal, + storage: PromptsStorage.user, + displayPath: nls.localize('promptsUserDataFolder', "User Data"), + isDefault: true + } + ]; + } + + return result; + } + + /** + * Gets all source folder URIs for a prompt type (both workspace and user home). + * This is used for file watching to detect changes in all relevant locations. + */ + private getSourceFoldersSync(type: PromptsType, userHome: URI): readonly URI[] { + const result: URI[] = []; + const { folders } = this.workspaceService.getWorkspace(); + const defaultFolders = getPromptFileDefaultLocations(type); + + for (const sourceFolder of defaultFolders) { + if (sourceFolder.storage === PromptsStorage.local) { + for (const workspaceFolder of folders) { + result.push(joinPath(workspaceFolder.uri, sourceFolder.path)); + } + } else if (sourceFolder.storage === PromptsStorage.user) { + result.push(joinPath(userHome, sourceFolder.path)); + } + } + + return result; } public createFilesUpdatedEvent(type: PromptsType): { readonly event: Event; dispose: () => void } { @@ -68,6 +136,7 @@ export class PromptFilesLocator extends Disposable { const key = getPromptFileLocationsConfigKey(type); let parentFolders = this.getLocalParentFolders(type); + let allSourceFolders: URI[] = []; const externalFolderWatchers = disposables.add(new DisposableStore()); const updateExternalFolderWatchers = () => { @@ -79,8 +148,20 @@ export class PromptFilesLocator extends Disposable { externalFolderWatchers.add(this.fileService.watch(folder.parent, { recursive, excludes: [] })); } } + // Watch all source folders (including user home if applicable) + for (const folder of allSourceFolders) { + if (!this.workspaceService.getWorkspaceFolder(folder)) { + externalFolderWatchers.add(this.fileService.watch(folder, { recursive: true, excludes: [] })); + } + } }; - updateExternalFolderWatchers(); + + // Initialize source folders (async if type has userHome locations) + this.pathService.userHome().then(userHome => { + allSourceFolders = [...this.getSourceFoldersSync(type, userHome)]; + updateExternalFolderWatchers(); + }); + disposables.add(this.configService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(key)) { parentFolders = this.getLocalParentFolders(type); @@ -97,14 +178,19 @@ export class PromptFilesLocator extends Disposable { eventEmitter.fire(); return; } + if (allSourceFolders.some(folder => e.affects(folder))) { + eventEmitter.fire(); + return; + } })); disposables.add(this.fileService.watch(userDataFolder)); return { event: eventEmitter.event, dispose: () => disposables.dispose() }; } - public getAgentSourceFolder(): readonly URI[] { - return this.toAbsoluteLocations([AGENTS_SOURCE_FOLDER]); + public async getAgentSourceFolders(): Promise { + const userHome = await this.pathService.userHome(); + return this.toAbsoluteLocations(PromptsType.agent, DEFAULT_AGENT_SOURCE_FOLDERS, userHome).map(l => l.uri); } /** @@ -119,9 +205,15 @@ export class PromptFilesLocator extends Disposable { * * @returns List of possible unambiguous prompt file folders. */ - public getConfigBasedSourceFolders(type: PromptsType): readonly URI[] { + public async getConfigBasedSourceFolders(type: PromptsType): Promise { + const userHome = await this.pathService.userHome(); const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); - const absoluteLocations = this.toAbsoluteLocations(configuredLocations); + const absoluteLocations = this.toAbsoluteLocations(type, configuredLocations, userHome).map(l => l.uri); + + // For anything that doesn't support glob patterns, we can return + if (type !== PromptsType.prompt && type !== PromptsType.instructions) { + return absoluteLocations; + } // locations in the settings can contain glob patterns so we need // to process them to get "clean" paths; the goal here is to have @@ -158,6 +250,53 @@ export class PromptFilesLocator extends Disposable { return [...result]; } + /** + * Gets all resolved source folders for the given prompt type with metadata. + * This method merges configured locations with default locations and resolves them + * to absolute paths, including displayPath and isDefault information. + * + * @param type The type of prompt files. + * @returns List of resolved source folders with metadata. + */ + public async getResolvedSourceFolders(type: PromptsType): Promise { + const localFolders = await this.getLocalStorageFolders(type); + const userFolders = await this.getUserStorageFolders(type); + return this.dedupeSourceFolders([...localFolders, ...userFolders]); + } + + /** + * Gets all local (workspace) storage folders for the given prompt type. + * This merges default folders with configured locations. + */ + private async getLocalStorageFolders(type: PromptsType): Promise { + const userHome = await this.pathService.userHome(); + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); + const defaultFolders = getPromptFileDefaultLocations(type); + + // Merge default folders with configured locations, avoiding duplicates + const allFolders = [ + ...defaultFolders, + ...configuredLocations.filter(loc => !defaultFolders.some(df => df.path === loc.path)) + ]; + + return this.toAbsoluteLocations(type, allFolders, userHome, defaultFolders); + } + + /** + * Deduplicates source folders by URI. + */ + private dedupeSourceFolders(folders: readonly IResolvedPromptSourceFolder[]): IResolvedPromptSourceFolder[] { + const seen = new ResourceSet(); + const result: IResolvedPromptSourceFolder[] = []; + for (const folder of folders) { + if (!seen.has(folder.uri)) { + seen.add(folder.uri); + result.push(folder); + } + } + return result; + } + /** * Finds all existent prompt files in the configured local source folders. * @@ -170,7 +309,7 @@ export class PromptFilesLocator extends Disposable { for (const { parent, filePattern } of this.getLocalParentFolders(type)) { const files = (filePattern === undefined) - ? await this.resolveFilesAtLocation(parent, token) // if the location does not contain a glob pattern, resolve the location directly + ? await this.resolveFilesAtLocation(parent, type, token) // if the location does not contain a glob pattern, resolve the location directly : await this.searchFilesInLocation(parent, filePattern, token); for (const file of files) { if (getPromptFileType(file) === type) { @@ -188,23 +327,67 @@ export class PromptFilesLocator extends Disposable { private getLocalParentFolders(type: PromptsType): readonly { parent: URI; filePattern?: string }[] { const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, type); if (type === PromptsType.agent) { - configuredLocations.push(AGENTS_SOURCE_FOLDER); + configuredLocations.push(...DEFAULT_AGENT_SOURCE_FOLDERS); } - const absoluteLocations = this.toAbsoluteLocations(configuredLocations); - return absoluteLocations.map(firstNonGlobParentAndPattern); + + const absoluteLocations = this.toAbsoluteLocations(type, configuredLocations, undefined); + return absoluteLocations.map((location) => firstNonGlobParentAndPattern(location.uri)); } /** - * Converts locations defined in `settings` to absolute filesystem path URIs. + * Converts locations defined in `settings` to absolute filesystem path URIs with metadata. * This conversion is needed because locations in settings can be relative, * hence we need to resolve them based on the current workspace folders. + * If userHome is provided, paths starting with `~` will be expanded. Otherwise these paths are ignored. + * Preserves the type and location properties from the source folder definitions. */ - private toAbsoluteLocations(configuredLocations: readonly string[]): readonly URI[] { - const result = new ResourceSet(); + private toAbsoluteLocations(type: PromptsType, configuredLocations: readonly IPromptSourceFolder[], userHome: URI | undefined, defaultLocations?: readonly IPromptSourceFolder[]): readonly IResolvedPromptSourceFolder[] { + const result: IResolvedPromptSourceFolder[] = []; + const seen = new ResourceSet(); const { folders } = this.workspaceService.getWorkspace(); - for (const configuredLocation of configuredLocations) { + // Create a set of default paths for quick lookup + const defaultPaths = new Set(defaultLocations?.map(loc => loc.path)); + + // Filter and validate skill paths before resolving + const validLocations = configuredLocations.filter(sourceFolder => { + // TODO: deprecate glob patterns for prompts and instructions in the future + if (type === PromptsType.instructions || type === PromptsType.prompt) { + const path = sourceFolder.path; + if (hasGlobPattern(path)) { + if (type === PromptsType.prompt) { + this.logService.warn(`[Deprecated] Glob patterns (* and **) in prompt file locations are deprecated: "${path}". Consider using explicit paths instead.`); + } else if (type === PromptsType.instructions) { + this.logService.info(`Glob patterns (* and **) detected in instruction file location: "${path}". Consider using explicit paths for better performance.`); + } + } + return true; + } + const configuredLocation = sourceFolder.path; + if (!isValidPromptFolderPath(configuredLocation)) { + this.logService.warn(`Skipping invalid path (glob patterns and absolute paths not supported): ${configuredLocation}`); + return false; + } + return true; + }); + + for (const sourceFolder of validLocations) { + const configuredLocation = sourceFolder.path; + const isDefault = defaultPaths?.has(configuredLocation); try { + // Handle tilde paths when userHome is provided + if (isTildePath(configuredLocation)) { + // If userHome is not provided, we cannot resolve tilde paths so we skip this entry + if (userHome) { + const uri = joinPath(userHome, configuredLocation.substring(2)); + if (!seen.has(uri)) { + seen.add(uri); + result.push({ uri, source: sourceFolder.source, storage: sourceFolder.storage, displayPath: configuredLocation, isDefault }); + } + } + continue; + } + if (isAbsolute(configuredLocation)) { let uri = URI.file(configuredLocation); const remoteAuthority = this.environmentService.remoteAuthority; @@ -213,11 +396,17 @@ export class PromptFilesLocator extends Disposable { // we need to convert it to a file URI with the remote authority uri = uri.with({ scheme: Schemas.vscodeRemote, authority: remoteAuthority }); } - result.add(uri); + if (!seen.has(uri)) { + seen.add(uri); + result.push({ uri, source: sourceFolder.source, storage: sourceFolder.storage, displayPath: configuredLocation, isDefault }); + } } else { for (const workspaceFolder of folders) { const absolutePath = joinPath(workspaceFolder.uri, configuredLocation); - result.add(absolutePath); + if (!seen.has(absolutePath)) { + seen.add(absolutePath); + result.push({ uri: absolutePath, source: sourceFolder.source, storage: sourceFolder.storage, displayPath: configuredLocation, isDefault }); + } } } } catch (error) { @@ -225,13 +414,16 @@ export class PromptFilesLocator extends Disposable { } } - return [...result]; + return result; } /** * Uses the file service to resolve the provided location and return either the file at the location of files in the directory. */ - private async resolveFilesAtLocation(location: URI, token: CancellationToken): Promise { + private async resolveFilesAtLocation(location: URI, type: PromptsType, token: CancellationToken): Promise { + if (type === PromptsType.skill) { + return this.findAgentSkillsInFolder(location, token); + } try { const info = await this.fileService.resolve(location); if (info.isFile) { @@ -251,9 +443,16 @@ export class PromptFilesLocator extends Disposable { } /** - * Uses the search service to find all files at the provided location + * Uses the search service to find all files at the provided location. + * Requires a FileSearchProvider to be available for the folder's scheme. */ private async searchFilesInLocation(folder: URI, filePattern: string | undefined, token: CancellationToken): Promise { + // Check if a FileSearchProvider is available for this scheme + if (!this.searchService.schemeHasFileSearchProvider(folder.scheme)) { + this.logService.warn(`[PromptFilesLocator] No FileSearchProvider available for scheme '${folder.scheme}'. Cannot search for pattern '${filePattern}' in ${folder.toString()}`); + return []; + } + const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); const workspaceRoot = this.workspaceService.getWorkspaceFolder(folder); @@ -264,6 +463,7 @@ export class PromptFilesLocator extends Disposable { type: QueryType.File, shouldGlobMatchFilePattern: true, excludePattern: workspaceRoot ? getExcludePattern(workspaceRoot.uri) : undefined, + ignoreGlobCase: true, sortByScore: true, filePattern }; @@ -287,8 +487,13 @@ export class PromptFilesLocator extends Disposable { const { folders } = this.workspaceService.getWorkspace(); for (const folder of folders) { const file = joinPath(folder.uri, `.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); - if (await this.fileService.exists(file)) { - result.push(file); + try { + const stat = await this.fileService.stat(file); + if (stat.isFile) { + result.push(file); + } + } catch (error) { + this.logService.trace(`[PromptFilesLocator] Skipping copilot-instructions.md at ${file.toString()}: ${error}`); } } return result; @@ -303,29 +508,69 @@ export class PromptFilesLocator extends Disposable { } private async findAgentMDsInFolder(folder: URI, token: CancellationToken): Promise { - const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); - const getExcludePattern = (folder: URI) => getExcludes(this.configService.getValue({ resource: folder })) || {}; - const searchOptions: IFileQuery = { - folderQueries: [{ folder, disregardIgnoreFiles }], - type: QueryType.File, - shouldGlobMatchFilePattern: true, - excludePattern: getExcludePattern(folder), - filePattern: '**/AGENTS.md', - }; + // Check if a FileSearchProvider is available for this scheme + if (this.searchService.schemeHasFileSearchProvider(folder.scheme)) { + // Use the search service if a FileSearchProvider is available + const disregardIgnoreFiles = this.configService.getValue('explorer.excludeGitIgnore'); + const getExcludePattern = (folder: URI) => getExcludes(this.configService.getValue({ resource: folder })) || {}; + const searchOptions: IFileQuery = { + folderQueries: [{ folder, disregardIgnoreFiles }], + type: QueryType.File, + shouldGlobMatchFilePattern: true, + excludePattern: getExcludePattern(folder), + filePattern: '**/AGENTS.md', + ignoreGlobCase: true, + }; - try { - const searchResult = await this.searchService.fileSearch(searchOptions, token); + try { + const searchResult = await this.searchService.fileSearch(searchOptions, token); + if (token.isCancellationRequested) { + return []; + } + return searchResult.results.map(r => r.resource); + } catch (e) { + if (!isCancellationError(e)) { + throw e; + } + } + return []; + } else { + // Fallback to recursive traversal using file service + return this.findAgentMDsUsingFileService(folder, token); + } + } + + /** + * Recursively traverses a folder using the file service to find AGENTS.md files. + * This is used as a fallback when no FileSearchProvider is available for the scheme. + */ + private async findAgentMDsUsingFileService(folder: URI, token: CancellationToken): Promise { + const result: URI[] = []; + const agentsMdFileName = 'agents.md'; + + const traverse = async (uri: URI): Promise => { if (token.isCancellationRequested) { - return []; + return; } - return searchResult.results.map(r => r.resource); - } catch (e) { - if (!isCancellationError(e)) { - throw e; + + try { + const stat = await this.fileService.resolve(uri); + if (stat.isFile && stat.name.toLowerCase() === agentsMdFileName) { + result.push(stat.resource); + } else if (stat.isDirectory && stat.children) { + // Recursively traverse subdirectories + for (const child of stat.children) { + await traverse(child.resource); + } + } + } catch (error) { + // Ignore errors for individual files/folders (e.g., permission denied) + this.logService.trace(`[PromptFilesLocator] Error traversing ${uri.toString()}: ${error}`); } - } - return []; + }; + await traverse(folder); + return result; } /** @@ -359,8 +604,73 @@ export class PromptFilesLocator extends Disposable { } return undefined; } + + private async findAgentSkillsInFolder(uri: URI, token: CancellationToken): Promise { + try { + const result: URI[] = []; + const stat = await this.fileService.resolve(uri); + if (stat.isDirectory && stat.children) { + // Recursively traverse subdirectories + for (const child of stat.children) { + try { + if (token.isCancellationRequested) { + return []; + } + if (child.isDirectory) { + const skillFile = joinPath(child.resource, SKILL_FILENAME); + const skillStat = await this.fileService.resolve(skillFile); + if (skillStat.isFile) { + result.push(skillStat.resource); + } + } + } catch (error) { + // Ignore errors for individual files/folders (e.g., permission denied) + } + } + } + return result; + } catch (e) { + if (!isCancellationError(e)) { + this.logService.trace(`[PromptFilesLocator] Error searching for skills in ${uri.toString()}: ${e}`); + } + return []; + } + } + + /** + * Searches for skills in all configured locations. + */ + public async findAgentSkills(token: CancellationToken): Promise { + const userHome = await this.pathService.userHome(); + const configuredLocations = PromptsConfig.promptSourceFolders(this.configService, PromptsType.skill); + const absoluteLocations = this.toAbsoluteLocations(PromptsType.skill, configuredLocations, userHome); + const allResults: IResolvedPromptFile[] = []; + + for (const { uri, source, storage } of absoluteLocations) { + if (token.isCancellationRequested) { + return []; + } + const results = await this.findAgentSkillsInFolder(uri, token); + allResults.push(...results.map(uri => ({ fileUri: uri, source, storage }))); + } + + return allResults; + } +} + + +/** + * Checks if the provided path contains a glob pattern (* or **). + * Used to detect deprecated glob usage in prompt file locations. + * + * @param path - path to check + * @returns `true` if the path contains `*` or `**`, `false` otherwise + */ +export function hasGlobPattern(path: string): boolean { + return path.includes('*'); } + /** * Checks if the provided `pattern` could be a valid glob pattern. */ @@ -468,3 +778,37 @@ function firstNonGlobParentAndPattern(location: URI): { parent: URI; filePattern filePattern: segments.slice(i).join('/') }; } + + +/** + * Regex pattern string for validating paths for all prompt files. + * Paths only support: + * - Relative paths: someFolder, ./someFolder + * - User home paths: ~/folder (only forward slash, not backslash for cross-platform sharing) + * - Parent relative paths for monorepos: ../folder + * + * NOT supported: + * - Absolute paths (portability issue) + * - Glob patterns with * or ** (performance issue) + * - Backslashes (paths should be shareable in repos across platforms) + * - Tilde without forward slash (e.g., ~abc, ~\folder) + * - Empty or whitespace-only paths + * + * The regex validates: + * - Not a Windows absolute path (e.g., C:\, C:/) + * - Not starting with / (Unix absolute path) + * - No backslashes anywhere (use forward slashes only) + * - If starts with ~, must be followed by / + * - No glob pattern characters: * ? [ ] { } + * - At least one non-whitespace character + */ +export const VALID_PROMPT_FOLDER_PATTERN = '^(?![A-Za-z]:[\\\\/])(?!/)(?!~(?!/))(?!.*\\\\)(?!.*[*?\\[\\]{}]).*\\S.*$'; +const VALID_PROMPT_FOLDER_REGEX = new RegExp(VALID_PROMPT_FOLDER_PATTERN); + +/** + * Validates if a path is allowed for simplified path configurations. + * Only forward slashes are supported to ensure paths are shareable across platforms. + */ +export function isValidPromptFolderPath(path: string): boolean { + return VALID_PROMPT_FOLDER_REGEX.test(path); +} diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptsServiceUtils.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptsServiceUtils.ts new file mode 100644 index 00000000000..f458870f629 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptsServiceUtils.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { IProductService } from '../../../../../../platform/product/common/productService.js'; + +/** + * Checks if a prompt file is organization-provided. + * Organization-provided prompt files come from the built-in chat extension + * and are located under a `/github/` path. + * + * @param uri The URI of the prompt file + * @param extensionId The extension identifier that provides the prompt file + * @param productService The product service to get the built-in chat extension ID + * @returns `true` if the prompt file is organization-provided, `false` otherwise + */ +export function isOrganizationPromptFile(uri: URI, extensionId: ExtensionIdentifier, productService: IProductService): boolean { + const chatExtensionId = productService.defaultChatAgent?.chatExtensionId; + if (!chatExtensionId) { + return false; + } + const isFromBuiltinChatExtension = ExtensionIdentifier.equals(extensionId, chatExtensionId); + const pathContainsGithub = uri.path.includes('/github/'); + return isFromBuiltinChatExtension && pathContainsGithub; +} diff --git a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts b/src/vs/workbench/contrib/chat/common/requestParser/chatParserTypes.ts similarity index 89% rename from src/vs/workbench/contrib/chat/common/chatParserTypes.ts rename to src/vs/workbench/contrib/chat/common/requestParser/chatParserTypes.ts index fefa954edf1..8040aeeba59 100644 --- a/src/vs/workbench/contrib/chat/common/chatParserTypes.ts +++ b/src/vs/workbench/contrib/chat/common/requestParser/chatParserTypes.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { revive } from '../../../../base/common/marshalling.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; -import { IChatSlashData } from './chatSlashCommands.js'; -import { IChatRequestProblemsVariable, IChatRequestVariableValue } from './chatVariables.js'; -import { ChatAgentLocation } from './constants.js'; -import { IToolData } from './languageModelToolsService.js'; -import { IChatPromptSlashCommand } from './promptSyntax/service/promptsService.js'; -import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from './chatVariableEntries.js'; +import { revive } from '../../../../../base/common/marshalling.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { IOffsetRange, OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { IRange, Range } from '../../../../../editor/common/core/range.js'; +import { IChatAgentCommand, IChatAgentData, IChatAgentService, reviveSerializedAgent } from '../participants/chatAgents.js'; +import { IChatSlashData } from '../participants/chatSlashCommands.js'; +import { IChatRequestProblemsVariable, IChatRequestVariableValue } from '../attachments/chatVariables.js'; +import { ChatAgentLocation } from '../constants.js'; +import { IToolData } from '../tools/languageModelToolsService.js'; +import { IChatRequestToolEntry, IChatRequestToolSetEntry, IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../attachments/chatVariableEntries.js'; +import { arrayEquals } from '../../../../../base/common/equals.js'; // These are in a separate file to avoid circular dependencies with the dependencies of the parser @@ -22,6 +22,17 @@ export interface IParsedChatRequest { readonly text: string; } +export namespace IParsedChatRequest { + export function equals(a: IParsedChatRequest, b: IParsedChatRequest): boolean { + return a.text === b.text && arrayEquals(a.parts, b.parts, (p1, p2) => + p1.kind === p2.kind && + OffsetRange.equals(p1.range, p2.range) && + Range.equalsRange(p1.editorRange, p2.editorRange) && + p1.text === p2.text + ); + } +} + export interface IParsedChatRequestPart { readonly kind: string; // for serialization readonly range: IOffsetRange; @@ -172,14 +183,14 @@ export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { export class ChatRequestSlashPromptPart implements IParsedChatRequestPart { static readonly Kind = 'prompt'; readonly kind = ChatRequestSlashPromptPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashPromptCommand: IChatPromptSlashCommand) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly name: string) { } get text(): string { - return `${chatSubcommandLeader}${this.slashPromptCommand.command}`; + return `${chatSubcommandLeader}${this.name}`; } get promptText(): string { - return `${chatSubcommandLeader}${this.slashPromptCommand.command}`; + return `${chatSubcommandLeader}${this.name}`; } } @@ -269,7 +280,7 @@ export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsed return new ChatRequestSlashPromptPart( new OffsetRange(part.range.start, part.range.endExclusive), part.editorRange, - (part as ChatRequestSlashPromptPart).slashPromptCommand + (part as ChatRequestSlashPromptPart).name ); } else if (part.kind === ChatRequestDynamicVariablePart.Kind) { return new ChatRequestDynamicVariablePart( diff --git a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts b/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts similarity index 90% rename from src/vs/workbench/contrib/chat/common/chatRequestParser.ts rename to src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts index c47116a4c8b..fa07524a29e 100644 --- a/src/vs/workbench/contrib/chat/common/chatRequestParser.ts +++ b/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../base/common/uri.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js'; -import { IChatAgentData, IChatAgentService } from './chatAgents.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IPosition, Position } from '../../../../../editor/common/core/position.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; +import { IChatVariablesService, IDynamicVariable } from '../attachments/chatVariables.js'; +import { ChatAgentLocation, ChatModeKind } from '../constants.js'; +import { IChatAgentData, IChatAgentService } from '../participants/chatAgents.js'; +import { IChatSlashCommandService } from '../participants/chatSlashCommands.js'; +import { IPromptsService } from '../promptSyntax/service/promptsService.js'; +import { IToolData, IToolSet, isToolSet } from '../tools/languageModelToolsService.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js'; -import { IChatSlashCommandService } from './chatSlashCommands.js'; -import { IChatVariablesService, IDynamicVariable } from './chatVariables.js'; -import { ChatAgentLocation, ChatModeKind } from './constants.js'; -import { IToolData, ToolSet } from './languageModelToolsService.js'; -import { IPromptsService } from './promptSyntax/service/promptsService.js'; const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) @@ -39,10 +39,10 @@ export class ChatRequestParser { const parts: IParsedChatRequestPart[] = []; const references = this.variableService.getDynamicVariables(sessionResource); // must access this list before any async calls const toolsByName = new Map(); - const toolSetsByName = new Map(); + const toolSetsByName = new Map(); for (const [entry, enabled] of this.variableService.getSelectedToolAndToolSets(sessionResource)) { if (enabled) { - if (entry instanceof ToolSet) { + if (isToolSet(entry)) { toolSetsByName.set(entry.referenceName, entry); } else { toolsByName.set(entry.toolReferenceName ?? entry.displayName, entry); @@ -160,7 +160,7 @@ export class ChatRequestParser { return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); } - private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray, toolsByName: ReadonlyMap, toolSetsByName: ReadonlyMap): ChatRequestToolPart | ChatRequestToolSetPart | undefined { + private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray, toolsByName: ReadonlyMap, toolSetsByName: ReadonlyMap): ChatRequestToolPart | ChatRequestToolSetPart | undefined { const nextVariableMatch = message.match(variableReg); if (!nextVariableMatch) { return; @@ -231,10 +231,10 @@ export class ChatRequestParser { } } - // if there's no agent, check if it's a prompt command - const promptCommand = this.promptsService.asPromptSlashCommand(command); - if (promptCommand) { - return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, promptCommand); + // if there's no agent, asume it is a prompt slash command + const isPromptCommand = this.promptsService.isValidSlashCommandName(command); + if (isPromptCommand) { + return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, command); } } return; diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts new file mode 100644 index 00000000000..b6af28672fe --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatExternalPathConfirmation.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; +import { dirname, extUriBiasedIgnorePathCase } from '../../../../../../base/common/resources.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { ConfirmedReason, ToolConfirmKind } from '../../chatService/chatService.js'; +import { + ILanguageModelToolConfirmationActions, + ILanguageModelToolConfirmationContribution, + ILanguageModelToolConfirmationRef +} from '../languageModelToolsConfirmationService.js'; + +export interface IExternalPathInfo { + path: string; + isDirectory: boolean; +} + +/** + * Confirmation contribution for read_file and list_dir tools that allows users to approve + * accessing paths outside the workspace, with an option to allow all access + * from a containing folder for the current chat session. + */ +export class ChatExternalPathConfirmationContribution implements ILanguageModelToolConfirmationContribution { + readonly canUseDefaultApprovals = false; + + private readonly _sessionFolderAllowlist = new ResourceMap(); + + constructor( + private readonly _getPathInfo: (ref: ILanguageModelToolConfirmationRef) => IExternalPathInfo | undefined, + ) { } + + getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { + const pathInfo = this._getPathInfo(ref); + if (!pathInfo || !ref.chatSessionResource) { + return undefined; + } + + const allowedFolders = this._sessionFolderAllowlist.get(ref.chatSessionResource); + if (!allowedFolders || allowedFolders.size === 0) { + return undefined; + } + + // Parse the file path to a URI + let pathUri: URI; + try { + pathUri = URI.file(pathInfo.path); + } catch { + return undefined; + } + + // Check if path is under any allowed folder + for (const folderUri of allowedFolders) { + if (extUriBiasedIgnorePathCase.isEqualOrParent(pathUri, folderUri)) { + return { type: ToolConfirmKind.UserAction }; + } + } + + return undefined; + } + + getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { + const pathInfo = this._getPathInfo(ref); + if (!pathInfo || !ref.chatSessionResource) { + return []; + } + + // Parse the path to a URI + let pathUri: URI; + try { + pathUri = URI.file(pathInfo.path); + } catch { + return []; + } + + // For directories, use the path itself; for files, use the parent directory + const folderUri = pathInfo.isDirectory ? pathUri : dirname(pathUri); + const sessionResource = ref.chatSessionResource; + + return [ + { + label: localize('allowFolderSession', 'Allow this folder in this session'), + detail: localize('allowFolderSessionDetail', 'Allow reading files from this folder without further confirmation in this chat session'), + select: async () => { + let folders = this._sessionFolderAllowlist.get(sessionResource); + if (!folders) { + folders = new ResourceSet(); + this._sessionFolderAllowlist.set(sessionResource, folders); + } + folders.add(folderUri); + return true; + } + } + ]; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingConfirmation.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingConfirmation.ts new file mode 100644 index 00000000000..fb070032878 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingConfirmation.ts @@ -0,0 +1,338 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { localize } from '../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IQuickInputButton, IQuickInputService, IQuickTreeItem } from '../../../../../../platform/quickinput/common/quickInput.js'; +import { IPreferencesService } from '../../../../../services/preferences/common/preferences.js'; +import { ConfirmedReason, ToolConfirmKind } from '../../chatService/chatService.js'; +import { ChatConfiguration } from '../../constants.js'; +import { + ILanguageModelToolConfirmationActions, + ILanguageModelToolConfirmationContribution, + ILanguageModelToolConfirmationContributionQuickTreeItem, + ILanguageModelToolConfirmationRef +} from '../languageModelToolsConfirmationService.js'; +import { extractUrlPatterns, getPatternLabel, isUrlApproved, IUrlApprovalSettings } from './chatUrlFetchingPatterns.js'; + +const trashButton: IQuickInputButton = { + iconClass: ThemeIcon.asClassName(Codicon.trash), + tooltip: localize('delete', "Delete") +}; + +export class ChatUrlFetchingConfirmationContribution implements ILanguageModelToolConfirmationContribution { + readonly canUseDefaultApprovals = false; + + constructor( + private readonly _getURLS: (parameters: unknown) => string[] | undefined, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IPreferencesService private readonly _preferencesService: IPreferencesService + ) { } + + getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { + return this._checkApproval(ref, true); + } + + getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { + return this._checkApproval(ref, false); + } + + private _checkApproval(ref: ILanguageModelToolConfirmationRef, checkRequest: boolean): ConfirmedReason | undefined { + const urls = this._getURLS(ref.parameters); + if (!urls || urls.length === 0) { + return undefined; + } + + const approvedUrls = this._getApprovedUrls(); + + // Check if all URLs are approved + const allApproved = urls.every(url => { + try { + const uri = URI.parse(url); + return isUrlApproved(uri, approvedUrls, checkRequest); + } catch { + return false; + } + }); + + if (allApproved) { + return { + type: ToolConfirmKind.Setting, + id: ChatConfiguration.AutoApprovedUrls + }; + } + + return undefined; + } + + getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { + return this._getConfirmActions(ref, true); + } + + getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { + return this._getConfirmActions(ref, false); + } + + private _getConfirmActions(ref: ILanguageModelToolConfirmationRef, forRequest: boolean): ILanguageModelToolConfirmationActions[] { + const urls = this._getURLS(ref.parameters); + if (!urls || urls.length === 0) { + return []; + } + + //remove query strings + const urlsWithoutQuery = urls.map(u => u.split('?')[0]); + + const actions: ILanguageModelToolConfirmationActions[] = []; + + // Get unique URLs (may have duplicates) + const uniqueUrls = Array.from(new Set(urlsWithoutQuery)).map(u => URI.parse(u)); + + // For each URL, get its patterns + const urlPatterns = new ResourceMap(uniqueUrls.map(u => [u, extractUrlPatterns(u)] as const)); + + // If only one URL, show quick actions for specific patterns + if (urlPatterns.size === 1) { + const uri = uniqueUrls[0]; + const patterns = urlPatterns.get(uri)!; + + // Show top 2 most relevant patterns as quick actions + const topPatterns = patterns.slice(0, 2); + for (const pattern of topPatterns) { + const patternLabel = getPatternLabel(uri, pattern); + actions.push({ + label: forRequest + ? localize('approveRequestTo', "Allow requests to {0}", patternLabel) + : localize('approveResponseFrom', "Allow responses from {0}", patternLabel), + select: async () => { + await this._approvePattern(pattern, forRequest, !forRequest); + return true; + } + }); + } + + // "More options" action + actions.push({ + label: localize('moreOptions', "Allow requests to..."), + select: async () => { + const result = await this._showMoreOptions(ref, [{ uri, patterns }], forRequest); + return result; + } + }); + } else { + // Multiple URLs - show "More options" only + actions.push({ + label: localize('moreOptionsMultiple', "Configure URL Approvals..."), + select: async () => { + await this._showMoreOptions(ref, [...urlPatterns].map(([uri, patterns]) => ({ uri, patterns })), forRequest); + return true; + } + }); + } + + return actions; + } + + private async _showMoreOptions(ref: ILanguageModelToolConfirmationRef, urls: { uri: URI; patterns: string[] }[], forRequest: boolean): Promise { + interface IPatternTreeItem extends IQuickTreeItem { + pattern: string; + approvalType?: 'request' | 'response'; + children?: IPatternTreeItem[]; + } + + return new Promise((resolve) => { + const disposables = new DisposableStore(); + const quickTree = disposables.add(this._quickInputService.createQuickTree()); + quickTree.ignoreFocusOut = true; + quickTree.sortByLabel = false; + quickTree.placeholder = localize('selectApproval', "Select URL pattern to approve"); + + const treeItems: IPatternTreeItem[] = []; + const approvedUrls = this._getApprovedUrls(); + const dedupedPatterns = new Set(); + + for (const { uri, patterns } of urls) { + for (const pattern of patterns.slice().sort((a, b) => b.length - a.length)) { + if (dedupedPatterns.has(pattern)) { + continue; + } + dedupedPatterns.add(pattern); + const settings = approvedUrls[pattern]; + const requestChecked = typeof settings === 'boolean' ? settings : (settings?.approveRequest ?? false); + const responseChecked = typeof settings === 'boolean' ? settings : (settings?.approveResponse ?? false); + + treeItems.push({ + label: getPatternLabel(uri, pattern), + pattern, + checked: requestChecked && responseChecked ? true : (!requestChecked && !responseChecked ? false : 'mixed'), + collapsed: true, + children: [ + { + label: localize('allowRequestsCheckbox', "Make requests without confirmation"), + pattern, + approvalType: 'request', + checked: requestChecked + }, + { + label: localize('allowResponsesCheckbox', "Allow responses without confirmation"), + pattern, + approvalType: 'response', + checked: responseChecked + } + ], + }); + } + } + + quickTree.setItemTree(treeItems); + + const updateApprovals = () => { + const current = { ...this._getApprovedUrls() }; + for (const item of quickTree.itemTree) { + // root-level items + + const allowPre = item.children?.find(c => c.approvalType === 'request')?.checked; + const allowPost = item.children?.find(c => c.approvalType === 'response')?.checked; + + if (allowPost && allowPre) { + current[item.pattern] = true; + } else if (!allowPost && !allowPre) { + delete current[item.pattern]; + } else { + current[item.pattern] = { + approveRequest: !!allowPre || undefined, + approveResponse: !!allowPost || undefined, + }; + } + } + + return this._configurationService.updateValue(ChatConfiguration.AutoApprovedUrls, current); + }; + + disposables.add(quickTree.onDidAccept(async () => { + quickTree.busy = true; + await updateApprovals(); + resolve(!!this._checkApproval(ref, forRequest)); + quickTree.hide(); + })); + + disposables.add(quickTree.onDidHide(() => { + updateApprovals(); + disposables.dispose(); + resolve(false); + })); + + quickTree.show(); + }); + } + + private async _approvePattern(pattern: string, approveRequest: boolean, approveResponse: boolean): Promise { + const approvedUrls = { ...this._getApprovedUrls() }; + + // Merge with existing settings for this pattern + const existingSettings = approvedUrls[pattern]; + let existingRequest = false; + let existingResponse = false; + if (typeof existingSettings === 'boolean') { + existingRequest = existingSettings; + existingResponse = existingSettings; + } else if (existingSettings) { + existingRequest = existingSettings.approveRequest ?? false; + existingResponse = existingSettings.approveResponse ?? false; + } + + const mergedRequest = approveRequest || existingRequest; + const mergedResponse = approveResponse || existingResponse; + + // Create the approval settings + let value: boolean | IUrlApprovalSettings; + if (mergedRequest === mergedResponse) { + value = mergedRequest; + } else { + value = { approveRequest: mergedRequest, approveResponse: mergedResponse }; + } + + approvedUrls[pattern] = value; + + await this._configurationService.updateValue( + ChatConfiguration.AutoApprovedUrls, + approvedUrls + ); + } + + getManageActions(): ILanguageModelToolConfirmationContributionQuickTreeItem[] { + const approvedUrls = { ...this._getApprovedUrls() }; + const items: ILanguageModelToolConfirmationContributionQuickTreeItem[] = []; + + for (const [pattern, settings] of Object.entries(approvedUrls)) { + const label = pattern; + let description: string; + + if (typeof settings === 'boolean') { + description = settings + ? localize('approveAll', "Approve all") + : localize('denyAll', "Deny all"); + } else { + const parts: string[] = []; + if (settings.approveRequest) { + parts.push(localize('requests', "requests")); + } + if (settings.approveResponse) { + parts.push(localize('responses', "responses")); + } + description = parts.length > 0 + ? localize('approves', "Approves {0}", parts.join(', ')) + : localize('noApprovals', "No approvals"); + } + + const item: ILanguageModelToolConfirmationContributionQuickTreeItem = { + label, + description, + buttons: [trashButton], + checked: true, + onDidChangeChecked: (checked) => { + if (checked) { + approvedUrls[pattern] = settings; + } else { + delete approvedUrls[pattern]; + } + + this._configurationService.updateValue(ChatConfiguration.AutoApprovedUrls, approvedUrls); + } + }; + + items.push(item); + } + + items.push({ + pickable: false, + label: localize('moreOptionsManage', "More Options..."), + description: localize('openSettings', "Open settings"), + onDidOpen: () => { + this._preferencesService.openUserSettings({ query: ChatConfiguration.AutoApprovedUrls }); + } + }); + + return items; + } + + async reset(): Promise { + await this._configurationService.updateValue( + ChatConfiguration.AutoApprovedUrls, + {} + ); + } + + private _getApprovedUrls(): Readonly> { + return this._configurationService.getValue>( + ChatConfiguration.AutoApprovedUrls + ) || {}; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingPatterns.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingPatterns.ts new file mode 100644 index 00000000000..4112a6da011 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/chatUrlFetchingPatterns.ts @@ -0,0 +1,159 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { normalizeURL } from '../../../../../../platform/url/common/trustedDomains.js'; +import { testUrlMatchesGlob } from '../../../../../../platform/url/common/urlGlob.js'; + +/** + * Approval settings for a URL pattern + */ +export interface IUrlApprovalSettings { + approveRequest?: boolean; + approveResponse?: boolean; +} + +/** + * Extracts domain patterns from a URL for use in approval actions + * @param url The URL to extract patterns from + * @returns An array of patterns in order of specificity (most specific first) + */ +export function extractUrlPatterns(url: URI): string[] { + const normalizedStr = normalizeURL(url); + const normalized = URI.parse(normalizedStr); + const patterns = new Set(); + + // Full URL (most specific) + const fullUrl = normalized.toString(true); + patterns.add(fullUrl); + + // Domain-only pattern (without trailing slash) + const domainOnly = normalized.with({ path: '', query: '', fragment: '' }).toString(true); + patterns.add(domainOnly); + + // Wildcard subdomain pattern (*.example.com) + const authority = normalized.authority; + const domainParts = authority.split('.'); + + // Only add wildcard subdomain if there are at least 2 parts and it's not an IP + const isIPv4 = domainParts.length === 4 && domainParts.every((segment: string) => + Number.isInteger(+segment)); + const isIPv6 = authority.includes(':') && authority.match(/^(\[)?[0-9a-fA-F:]+(\])?(?::\d+)?$/); + const isIP = isIPv4 || isIPv6; + + // Only emit subdomain patterns if there are actually subdomains (more than 2 parts) + if (!isIP && domainParts.length > 2) { + // Create patterns by replacing each subdomain segment with * + // For example, foo.bar.example.com -> *.bar.example.com, *.example.com + for (let i = 0; i < domainParts.length - 2; i++) { + const wildcardAuthority = '*.' + domainParts.slice(i + 1).join('.'); + const wildcardPattern = normalized.with({ + authority: wildcardAuthority, + path: '', + query: '', + fragment: '' + }).toString(true); + patterns.add(wildcardPattern); + } + } + + // Path patterns (if there's a non-trivial path) + const pathSegments = normalized.path.split('/').filter((s: string) => s.length > 0); + if (pathSegments.length > 0) { + // Add patterns for each path level with wildcard + for (let i = pathSegments.length - 1; i >= 0; i--) { + const pathPattern = pathSegments.slice(0, i).join('/'); + const urlWithPathPattern = normalized.with({ + path: (i > 0 ? '/' : '') + pathPattern, + query: '', + fragment: '' + }).toString(true); + patterns.add(urlWithPathPattern); + } + } + + return [...patterns].map(p => p.replace(/\/+$/, '')); +} + +/** + * Generates user-friendly labels for URL patterns to show in quick pick + * @param url The original URL + * @param pattern The pattern to generate a label for + * @returns A user-friendly label describing what the pattern matches (without protocol) + */ +export function getPatternLabel(url: URI, pattern: string): string { + let displayPattern = pattern; + + if (displayPattern.startsWith('https://')) { + displayPattern = displayPattern.substring(8); + } else if (displayPattern.startsWith('http://')) { + displayPattern = displayPattern.substring(7); + } + + return displayPattern.replace(/\/+$/, ''); // Remove trailing slashes +} + +/** + * Checks if a URL matches any approved pattern + * @param url The URL to check + * @param approvedUrls Map of approved URL patterns to their settings + * @param checkRequest Whether to check request approval (true) or response approval (false) + * @returns true if the URL is approved for the specified action + */ +export function isUrlApproved( + url: URI, + approvedUrls: Record, + checkRequest: boolean +): boolean { + const normalizedUrlStr = normalizeURL(url); + const normalizedUrl = URI.parse(normalizedUrlStr); + + for (const [pattern, settings] of Object.entries(approvedUrls)) { + // Check if URL matches this pattern + if (testUrlMatchesGlob(normalizedUrl, pattern)) { + // Handle boolean settings + if (typeof settings === 'boolean') { + return settings; + } + + // Handle granular settings + if (checkRequest && settings.approveRequest !== undefined) { + return settings.approveRequest; + } + + if (!checkRequest && settings.approveResponse !== undefined) { + return settings.approveResponse; + } + } + } + + return false; +} + +/** + * Gets the most specific matching pattern for a URL + * @param url The URL to find a matching pattern for + * @param approvedUrls Map of approved URL patterns + * @returns The most specific matching pattern, or undefined if none match + */ +export function getMatchingPattern( + url: URI, + approvedUrls: Record +): string | undefined { + const normalizedUrlStr = normalizeURL(url); + const normalizedUrl = URI.parse(normalizedUrlStr); + const patterns = extractUrlPatterns(url); + + // Check patterns in order of specificity (most specific first) + for (const pattern of patterns) { + for (const approvedPattern of Object.keys(approvedUrls)) { + if (testUrlMatchesGlob(normalizedUrl, approvedPattern) && testUrlMatchesGlob(URI.parse(pattern), approvedPattern)) { + return approvedPattern; + } + } + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/chat/common/tools/confirmationTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts similarity index 93% rename from src/vs/workbench/contrib/chat/common/tools/confirmationTool.ts rename to src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts index be0ea9ce410..7548a6b36e2 100644 --- a/src/vs/workbench/contrib/chat/common/tools/confirmationTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/confirmationTool.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IChatTerminalToolInvocationData } from '../chatService.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IChatTerminalToolInvocationData } from '../../chatService/chatService.js'; import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; export const ConfirmationToolId = 'vscode_get_confirmation'; diff --git a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts similarity index 81% rename from src/vs/workbench/contrib/chat/common/tools/editFileTool.ts rename to src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts index 8e1e3669b4c..f83c3c4e7a8 100644 --- a/src/vs/workbench/contrib/chat/common/tools/editFileTool.ts +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/editFileTool.ts @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../../base/common/observable.js'; -import { URI, UriComponents } from '../../../../../base/common/uri.js'; -import { CellUri } from '../../../notebook/common/notebookCommon.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ICodeMapperService } from '../../common/chatCodeMapperService.js'; -import { ChatModel } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../../common/languageModelToolsService.js'; -import { LocalChatSessionUri } from '../chatUri.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../../../base/common/observable.js'; +import { URI, UriComponents } from '../../../../../../base/common/uri.js'; +import { CellUri } from '../../../../notebook/common/notebookCommon.js'; +import { INotebookService } from '../../../../notebook/common/notebookService.js'; +import { ICodeMapperService } from '../../editing/chatCodeMapperService.js'; +import { ChatModel } from '../../model/chatModel.js'; +import { IChatService } from '../../chatService/chatService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolInvocationPresentation, ToolProgress } from '../languageModelToolsService.js'; export const ExtensionEditToolId = 'vscode_editFile'; export const InternalEditToolId = 'vscode_editFile_internal'; @@ -48,7 +47,7 @@ export class EditTool implements IToolImpl { const fileUri = URI.revive(parameters.uri); const uri = CellUri.parse(fileUri)?.notebook || fileUri; - const model = this.chatService.getSession(LocalChatSessionUri.forSession(invocation.context?.sessionId)) as ChatModel; + const model = this.chatService.getSession(invocation.context.sessionResource) as ChatModel; const request = model.getRequests().at(-1)!; model.acceptResponseProgress(request, { @@ -89,7 +88,7 @@ export class EditTool implements IToolImpl { location: 'tool', chatRequestId: invocation.chatRequestId, chatRequestModel: invocation.modelId, - chatSessionId: invocation.context.sessionId, + chatSessionResource: invocation.context.sessionResource, }, { textEdit: (target, edits) => { model.acceptResponseProgress(request, { kind: 'textEdit', uri: target, edits }); diff --git a/src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts similarity index 100% rename from src/vs/workbench/contrib/chat/common/tools/manageTodoListTool.ts rename to src/vs/workbench/contrib/chat/common/tools/builtinTools/manageTodoListTool.ts diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts new file mode 100644 index 00000000000..c02b27cdb9c --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/runSubagentTool.ts @@ -0,0 +1,315 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; +import { IJSONSchema, IJSONSchemaMap } from '../../../../../../base/common/jsonSchema.js'; +import { Disposable, DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { localize } from '../../../../../../nls.js'; +import { IConfigurationChangeEvent, IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../../../platform/log/common/log.js'; +import { IChatAgentRequest, IChatAgentService } from '../../participants/chatAgents.js'; +import { ChatModel, IChatRequestModeInstructions } from '../../model/chatModel.js'; +import { ChatMode, IChatMode, IChatModeService } from '../../chatModes.js'; +import { IChatProgress, IChatService } from '../../chatService/chatService.js'; +import { ChatRequestVariableSet } from '../../attachments/chatVariableEntries.js'; +import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../constants.js'; +import { ILanguageModelsService } from '../../languageModels.js'; +import { + CountTokensCallback, + ILanguageModelToolsService, + IPreparedToolInvocation, + IToolData, + IToolImpl, + IToolInvocation, + IToolInvocationPreparationContext, + IToolResult, + isToolSet, + ToolDataSource, + ToolProgress, + VSCodeToolReference, +} from '../languageModelToolsService.js'; +import { ComputeAutomaticInstructions } from '../../promptSyntax/computeAutomaticInstructions.js'; +import { ManageTodoListToolToolId } from './manageTodoListTool.js'; +import { createToolSimpleTextResult } from './toolHelpers.js'; + +const BaseModelDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. This tool is good at researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use this agent to perform the search for you. + +- Agents do not run async or in the background, you will wait for the agent\'s result. +- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. +- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. +- The agent's outputs should generally be trusted +- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent`; + +export interface IRunSubagentToolInputParams { + prompt: string; + description: string; + agentName?: string; +} + +export class RunSubagentTool extends Disposable implements IToolImpl { + + static readonly Id = 'runSubagent'; + + readonly onDidUpdateToolData: Event; + + constructor( + @IChatAgentService private readonly chatAgentService: IChatAgentService, + @IChatService private readonly chatService: IChatService, + @IChatModeService private readonly chatModeService: IChatModeService, + @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILogService private readonly logService: ILogService, + @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + this.onDidUpdateToolData = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ChatConfiguration.SubagentToolCustomAgents)); + } + + getToolData(): IToolData { + let modelDescription = BaseModelDescription; + const inputSchema: IJSONSchema & { properties: IJSONSchemaMap } = { + type: 'object', + properties: { + prompt: { + type: 'string', + description: 'A detailed description of the task for the agent to perform' + }, + description: { + type: 'string', + description: 'A short (3-5 word) description of the task' + } + }, + required: ['prompt', 'description'] + }; + + if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { + inputSchema.properties.agentName = { + type: 'string', + description: 'Optional name of a specific agent to invoke. If not provided, uses the current agent.' + }; + modelDescription += `\n- If the user asks for a certain agent, you MUST provide that EXACT agent name (case-sensitive) to invoke that specific agent.`; + } + const runSubagentToolData: IToolData = { + id: RunSubagentTool.Id, + toolReferenceName: VSCodeToolReference.runSubagent, + icon: ThemeIcon.fromId(Codicon.organization.id), + displayName: localize('tool.runSubagent.displayName', 'Run Subagent'), + userDescription: localize('tool.runSubagent.userDescription', 'Run a task within an isolated subagent context to enable efficient organization of tasks and context window management.'), + modelDescription: modelDescription, + source: ToolDataSource.Internal, + inputSchema: inputSchema + }; + return runSubagentToolData; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { + const args = invocation.parameters as IRunSubagentToolInputParams; + + this.logService.debug(`RunSubagentTool: Invoking with prompt: ${args.prompt.substring(0, 100)}...`); + + if (!invocation.context) { + throw new Error('toolInvocationToken is required for this tool'); + } + + // Get the chat model and request for writing progress + const model = this.chatService.getSession(invocation.context.sessionResource) as ChatModel | undefined; + if (!model) { + throw new Error('Chat model not found for session'); + } + + const request = model.getRequests().at(-1)!; + + const store = new DisposableStore(); + + try { + // Get the default agent + const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Agent); + if (!defaultAgent) { + return createToolSimpleTextResult('Error: No default agent available'); + } + + // Resolve mode-specific configuration if subagentId is provided + let modeModelId = invocation.modelId; + let modeTools = invocation.userSelectedTools; + let modeInstructions: IChatRequestModeInstructions | undefined; + let mode: IChatMode | undefined; + + if (args.agentName) { + mode = this.chatModeService.findModeByName(args.agentName); + if (mode) { + // Use mode-specific model if available + const modeModelQualifiedNames = mode.model?.get(); + if (modeModelQualifiedNames) { + // Find the actual model identifier from the qualified name(s) + for (const qualifiedName of modeModelQualifiedNames) { + const lmByQualifiedName = this.languageModelsService.lookupLanguageModelByQualifiedName(qualifiedName); + for (const fullId of this.languageModelsService.getLanguageModelIds()) { + const lmById = this.languageModelsService.lookupLanguageModel(fullId); + if (lmById && lmById?.id === lmByQualifiedName?.id) { + modeModelId = fullId; + break; + } + } + } + } + + // Use mode-specific tools if available + const modeCustomTools = mode.customTools?.get(); + if (modeCustomTools) { + // Convert the mode's custom tools (array of qualified names) to UserSelectedTools format + const enablementMap = this.languageModelToolsService.toToolAndToolSetEnablementMap(modeCustomTools, mode.target?.get(), undefined); + // Convert enablement map to UserSelectedTools (Record) + modeTools = {}; + for (const [tool, enabled] of enablementMap) { + if (!isToolSet(tool)) { + modeTools[tool.id] = enabled; + } + } + } + + const instructions = mode.modeInstructions?.get(); + modeInstructions = instructions && { + name: mode.name.get(), + content: instructions.content, + toolReferences: this.toolsService.toToolReferences(instructions.toolReferences), + metadata: instructions.metadata, + }; + } else { + this.logService.warn(`RunSubagentTool: Agent '${args.agentName}' not found, using current configuration`); + } + } + + // Track whether we should collect markdown (after the last tool invocation) + const markdownParts: string[] = []; + + // Generate a stable subAgentInvocationId for routing edits to this subagent's content part + const subAgentInvocationId = invocation.callId ?? `subagent-${generateUuid()}`; + + let inEdit = false; + const progressCallback = (parts: IChatProgress[]) => { + for (const part of parts) { + // Write certain parts immediately to the model + if (part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { + if (part.kind === 'codeblockUri' && !inEdit) { + inEdit = true; + model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('```\n') }); + } + // Attach subAgentInvocationId to codeblockUri parts so they can be routed to the subagent content part + if (part.kind === 'codeblockUri') { + model.acceptResponseProgress(request, { ...part, subAgentInvocationId }); + } else { + model.acceptResponseProgress(request, part); + } + } else if (part.kind === 'markdownContent') { + if (inEdit) { + model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n```\n\n') }); + inEdit = false; + } + + // Collect markdown content for the tool result + markdownParts.push(part.content.value); + } + } + }; + + if (modeTools) { + modeTools[RunSubagentTool.Id] = false; + modeTools[ManageTodoListToolToolId] = false; + modeTools['copilot_askQuestions'] = false; + } + + const variableSet = new ChatRequestVariableSet(); + const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, mode ?? ChatMode.Agent, modeTools, undefined); // agents can not call subagents + await computer.collect(variableSet, token); + + // Build the agent request + const agentRequest: IChatAgentRequest = { + sessionResource: invocation.context.sessionResource, + requestId: invocation.callId ?? `subagent-${Date.now()}`, + agentId: defaultAgent.id, + message: args.prompt, + variables: { variables: variableSet.asArray() }, + location: ChatAgentLocation.Chat, + subAgentInvocationId: invocation.callId, + subAgentName: args.agentName ?? 'subagent', + userSelectedModelId: modeModelId, + userSelectedTools: modeTools, + modeInstructions, + }; + + // Subscribe to tool invocations to clear markdown parts when a tool is invoked + store.add(this.languageModelToolsService.onDidInvokeTool(e => { + if (e.subagentInvocationId === subAgentInvocationId) { + markdownParts.length = 0; + } + })); + + // Invoke the agent + const result = await this.chatAgentService.invokeAgent( + defaultAgent.id, + agentRequest, + progressCallback, + [], + token + ); + + // Check for errors + if (result.errorDetails) { + return createToolSimpleTextResult(`Agent error: ${result.errorDetails.message}`); + } + + // This is a hack due to the fact that edits are represented as empty codeblocks with URIs. That needs to be cleaned up, + // in the meantime, just strip an empty codeblock left behind. + const resultText = markdownParts.join('').replace(/^\n*```\n+```\n*/g, '').trim() || 'Agent completed with no output'; + + // Store result in toolSpecificData for serialization + if (invocation.toolSpecificData?.kind === 'subagent') { + invocation.toolSpecificData.result = resultText; + } + + // Return result with toolMetadata containing subAgentInvocationId for trajectory tracking + return { + content: [{ + kind: 'text', + value: resultText + }], + toolMetadata: { + subAgentInvocationId, + description: args.description, + agentName: agentRequest.subAgentName, + } + }; + + } catch (error) { + const errorMessage = `Error invoking subagent: ${error instanceof Error ? error.message : 'Unknown error'}`; + this.logService.error(errorMessage, error); + return createToolSimpleTextResult(errorMessage); + } finally { + store.dispose(); + } + } + + async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + const args = context.parameters as IRunSubagentToolInputParams; + + return { + invocationMessage: args.description, + toolSpecificData: { + kind: 'subagent', + description: args.description, + agentName: args.agentName, + prompt: args.prompt, + }, + }; + } +} diff --git a/src/vs/workbench/contrib/chat/common/tools/toolHelpers.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/toolHelpers.ts similarity index 100% rename from src/vs/workbench/contrib/chat/common/tools/toolHelpers.ts rename to src/vs/workbench/contrib/chat/common/tools/builtinTools/toolHelpers.ts diff --git a/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.ts new file mode 100644 index 00000000000..ad914109af1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/builtinTools/tools.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 { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../../../common/contributions.js'; +import { ILanguageModelToolsService } from '../languageModelToolsService.js'; +import { ConfirmationTool, ConfirmationToolData } from './confirmationTool.js'; +import { EditTool, EditToolData } from './editFileTool.js'; +import { createManageTodoListToolData, ManageTodoListTool } from './manageTodoListTool.js'; +import { RunSubagentTool } from './runSubagentTool.js'; + +export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.builtinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const editTool = instantiationService.createInstance(EditTool); + this._register(toolsService.registerTool(EditToolData, editTool)); + + const todoToolData = createManageTodoListToolData(); + const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool)); + this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); + + // Register the confirmation tool + const confirmationTool = instantiationService.createInstance(ConfirmationTool); + this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); + + const runSubagentTool = this._register(instantiationService.createInstance(RunSubagentTool)); + + let runSubagentRegistration: IDisposable | undefined; + let toolSetRegistration: IDisposable | undefined; + const registerRunSubagentTool = () => { + runSubagentRegistration?.dispose(); + toolSetRegistration?.dispose(); + toolsService.flushToolUpdates(); + const runSubagentToolData = runSubagentTool.getToolData(); + runSubagentRegistration = toolsService.registerTool(runSubagentToolData, runSubagentTool); + toolSetRegistration = toolsService.agentToolSet.addTool(runSubagentToolData); + }; + registerRunSubagentTool(); + this._register(runSubagentTool.onDidUpdateToolData(registerRunSubagentTool)); + this._register({ + dispose: () => { + runSubagentRegistration?.dispose(); + toolSetRegistration?.dispose(); + } + }); + + + } +} + +export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; diff --git a/src/vs/workbench/contrib/chat/common/chatTodoListService.ts b/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts similarity index 85% rename from src/vs/workbench/contrib/chat/common/chatTodoListService.ts rename to src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts index 716f4309a49..6b7c0af31a1 100644 --- a/src/vs/workbench/contrib/chat/common/chatTodoListService.ts +++ b/src/vs/workbench/contrib/chat/common/tools/chatTodoListService.ts @@ -3,18 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { Memento } from '../../../common/memento.js'; -import { chatSessionResourceToId } from './chatUri.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { Memento } from '../../../../common/memento.js'; +import { chatSessionResourceToId } from '../model/chatUri.js'; export interface IChatTodo { id: number; title: string; - description?: string; status: 'not-started' | 'in-progress' | 'completed'; } diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts new file mode 100644 index 00000000000..96ea253b754 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsConfirmationService.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IQuickInputButton, IQuickTreeItem } from '../../../../../platform/quickinput/common/quickInput.js'; +import { ConfirmedReason } from '../chatService/chatService.js'; +import { IToolData, ToolDataSource } from './languageModelToolsService.js'; + +export interface ILanguageModelToolConfirmationActions { + /** Label for the action */ + label: string; + /** Action detail (e.g. tooltip) */ + detail?: string; + /** Show a separator before this action */ + divider?: boolean; + /** Selects this action. Resolves true if the action should be confirmed after selection */ + select(): Promise; +} + +export interface ILanguageModelToolConfirmationRef { + toolId: string; + source: ToolDataSource; + parameters: unknown; + chatSessionResource?: URI; +} + +export interface ILanguageModelToolConfirmationActionProducer { + getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined; + getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined; + + /** Gets the selectable actions to take to memorize confirmation changes */ + getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[]; + getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[]; +} + +export interface ILanguageModelToolConfirmationContributionQuickTreeItem extends IQuickTreeItem { + onDidTriggerItemButton?(button: IQuickInputButton): void; + onDidChangeChecked?(checked: boolean): void; + onDidOpen?(): void; +} + +/** + * Type that can be registered to provide more specific confirmation + * actions for a specific tool. + */ +export type ILanguageModelToolConfirmationContribution = Partial & { + /** + * Gets items to be shown in the `manageConfirmationPreferences` quick tree. + * These are added under the tool's category. + */ + getManageActions?(): ILanguageModelToolConfirmationContributionQuickTreeItem[]; + + /** + * Defaults to true. If false, the "Always Allow" options will not be shown + * and _only_ your custom manage actions will be shown. + */ + canUseDefaultApprovals?: boolean; + + /** + * Reset all confirmation settings for this tool. + */ + reset?(): void; +}; + +/** + * Handles language model tool confirmation. + * + * - By default, all tools can have their confirmation preferences saved within + * a session, workspace, or globally. + * - Tools with ToolDataSource from an extension or MCP can have that entire + * source's preference saved within a session, workspace, or globally. + * - Contributable confirmations may also be registered for specific behaviors. + * + * Note: this interface MUST NOT depend in the ILanguageModelToolsService. + * The ILanguageModelToolsService depends on this service instead in order to + * call getPreConfirmAction/getPostConfirmAction. + */ +export interface ILanguageModelToolsConfirmationService extends ILanguageModelToolConfirmationActionProducer { + readonly _serviceBrand: undefined; + + /** Opens an IQuickTree to let the user manage their preferences. */ + manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void; + + /** + * Registers a contribution that provides more specific confirmation logic + * for a tool, in addition to the default confirmation handling. + */ + registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable; + + /** Resets all tool and server confirmation preferences */ + resetToolAutoConfirmation(): void; +} + +export const ILanguageModelToolsConfirmationService = createDecorator('ILanguageModelToolsConfirmationService'); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts index b580f5e4cc0..96fb13cb669 100644 --- a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsContribution.ts @@ -6,7 +6,7 @@ import { isFalsyOrEmpty } from '../../../../../base/common/arrays.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { Disposable, DisposableMap, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; import { transaction } from '../../../../../base/common/observable.js'; import { joinPath } from '../../../../../base/common/resources.js'; import { isFalsyOrWhitespace } from '../../../../../base/common/strings.js'; @@ -21,7 +21,7 @@ import { IWorkbenchContribution } from '../../../../common/contributions.js'; import { Extensions, IExtensionFeaturesRegistry, IExtensionFeatureTableRenderer, IRenderedData, IRowData, ITableData } from '../../../../services/extensionManagement/common/extensionFeatures.js'; import { isProposedApiEnabled } from '../../../../services/extensions/common/extensions.js'; import * as extensionsRegistry from '../../../../services/extensions/common/extensionsRegistry.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../languageModelToolsService.js'; +import { ILanguageModelToolsService, IToolData, IToolSet, ToolDataSource, ToolSet } from './languageModelToolsService.js'; import { toolsParametersSchemaSchemaId } from './languageModelToolsParametersSchema.js'; export interface IRawToolContribution { @@ -29,6 +29,7 @@ export interface IRawToolContribution { displayName: string; modelDescription: string; toolReferenceName?: string; + legacyToolReferenceFullNames?: string[]; icon?: string | { light: string; dark: string }; when?: string; tags?: string[]; @@ -86,6 +87,7 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r description: localize('toolUserDescription', "A description of this tool that may be shown to the user."), type: 'string' }, + // eslint-disable-next-line local/code-no-localized-model-description modelDescription: { description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."), type: 'string' @@ -140,6 +142,7 @@ export interface IRawToolSetContribution { * @deprecated */ referenceName?: string; + legacyFullNames?: string[]; description: string; icon?: string; tools: string[]; @@ -193,7 +196,7 @@ function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { return `${extensionIdentifier.value}/${toolName}`; } -function toToolSetKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { +export function toToolSetKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { return `toolset:${extensionIdentifier.value}/${toolName}`; } @@ -234,6 +237,11 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT register tool with tags starting with "vscode_" or "copilot_"`); } + if (rawTool.legacyToolReferenceFullNames && !isProposedApiEnabled(extension.description, 'chatParticipantPrivate')) { + extension.collector.error(`Extension '${extension.description.identifier.value}' CANNOT use 'legacyToolReferenceFullNames' without the 'chatParticipantPrivate' API proposal enabled`); + continue; + } + const rawIcon = rawTool.icon; let icon: IToolData['icon'] | undefined; if (typeof rawIcon === 'string') { @@ -307,16 +315,21 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri continue; } + if (toolSet.legacyFullNames && !isProposedApiEnabled(extension.description, 'contribLanguageModelToolSets')) { + extension.collector.error(`Tool set '${toolSet.name}' CANNOT use 'legacyFullNames' without the 'contribLanguageModelToolSets' API proposal enabled`); + continue; + } + if (isFalsyOrEmpty(toolSet.tools)) { extension.collector.error(`Tool set '${toolSet.name}' CANNOT have an empty tools array`); continue; } const tools: IToolData[] = []; - const toolSets: ToolSet[] = []; + const toolSets: IToolSet[] = []; for (const toolName of toolSet.tools) { - const toolObj = languageModelToolsService.getToolByName(toolName, true); + const toolObj = languageModelToolsService.getToolByName(toolName); if (toolObj) { tools.push(toolObj); continue; @@ -335,16 +348,27 @@ export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContri } const store = new DisposableStore(); - - const obj = languageModelToolsService.createToolSet( - source, - toToolSetKey(extension.description.identifier, toolSet.name), - toolSet.referenceName ?? toolSet.name, - { icon: toolSet.icon ? ThemeIcon.fromString(toolSet.icon) : undefined, description: toolSet.description } - ); + const referenceName = toolSet.referenceName ?? toolSet.name; + const existingToolSet = languageModelToolsService.getToolSetByName(referenceName); + const mergeExisting = isBuiltinTool && existingToolSet?.source === ToolDataSource.Internal; + + let obj: ToolSet & IDisposable; + // Allow built-in tool to update the tool set if it already exists + if (mergeExisting) { + obj = existingToolSet as ToolSet & IDisposable; + } else { + obj = languageModelToolsService.createToolSet( + source, + toToolSetKey(extension.description.identifier, toolSet.name), + referenceName, + { icon: toolSet.icon ? ThemeIcon.fromString(toolSet.icon) : undefined, description: toolSet.description, legacyFullNames: toolSet.legacyFullNames } + ); + } transaction(tx => { - store.add(obj); + if (!mergeExisting) { + store.add(obj); + } tools.forEach(tool => store.add(obj.addTool(tool, tx))); toolSets.forEach(toolSet => store.add(obj.addToolSet(toolSet, tx))); }); diff --git a/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts new file mode 100644 index 00000000000..c3eb419ca68 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/tools/languageModelToolsService.ts @@ -0,0 +1,608 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Separator } from '../../../../../base/common/actions.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../base/common/event.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { derived, IObservable, IReader, ITransaction, ObservableSet } from '../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../base/common/themables.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Location } from '../../../../../editor/common/languages.js'; +import { localize } from '../../../../../nls.js'; +import { ContextKeyExpression, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; +import { ByteSize } from '../../../../../platform/files/common/files.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IProgress } from '../../../../../platform/progress/common/progress.js'; +import { ChatRequestToolReferenceEntry } from '../attachments/chatVariableEntries.js'; +import { IVariableReference } from '../chatModes.js'; +import { IChatExtensionsContent, IChatSubagentToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolInvocation, type IChatTerminalToolInvocationData } from '../chatService/chatService.js'; +import { ILanguageModelChatMetadata, LanguageModelPartAudience } from '../languageModels.js'; +import { UserSelectedTools } from '../participants/chatAgents.js'; +import { PromptElementJSON, stringifyPromptElementJSON } from './promptTsxTypes.js'; + +/** + * Selector for matching language models by vendor, family, version, or id. + * Used to filter tools to specific models or model families. + */ +export interface ILanguageModelChatSelector { + readonly vendor?: string; + readonly family?: string; + readonly version?: string; + readonly id?: string; +} + +export interface IToolData { + readonly id: string; + readonly source: ToolDataSource; + readonly toolReferenceName?: string; + readonly legacyToolReferenceFullNames?: readonly string[]; + readonly icon?: { dark: URI; light?: URI } | ThemeIcon; + readonly when?: ContextKeyExpression; + readonly tags?: readonly string[]; + readonly displayName: string; + readonly userDescription?: string; + readonly modelDescription: string; + readonly inputSchema?: IJSONSchema; + readonly canBeReferencedInPrompt?: boolean; + /** + * True if the tool runs in the (possibly remote) workspace, false if it runs + * on the host, undefined if known. + */ + readonly runsInWorkspace?: boolean; + readonly alwaysDisplayInputOutput?: boolean; + /** True if this tool might ask for pre-approval */ + readonly canRequestPreApproval?: boolean; + /** True if this tool might ask for post-approval */ + readonly canRequestPostApproval?: boolean; + /** + * Model selectors that this tool is available for. + * If defined, the tool is only available when the selected model matches one of the selectors. + */ + readonly models?: readonly ILanguageModelChatSelector[]; +} + +/** + * Check if a tool matches the given model metadata based on the tool's `models` selectors. + * If the tool has no `models` defined, it matches all models. + * If model is undefined, model-specific filtering is skipped (tool is included). + */ +export function toolMatchesModel(toolData: IToolData, model: ILanguageModelChatMetadata | undefined): boolean { + // If no model selectors are defined, the tool is available for all models + if (!toolData.models || toolData.models.length === 0) { + return true; + } + // If model is undefined, skip model-specific filtering + if (!model) { + return true; + } + // Check if any selector matches the model (OR logic) + return toolData.models.some(selector => + (!selector.id || selector.id === model.id) && + (!selector.vendor || selector.vendor === model.vendor) && + (!selector.family || selector.family === model.family) && + (!selector.version || selector.version === model.version) + ); +} + +export interface IToolProgressStep { + readonly message: string | IMarkdownString | undefined; + /** 0-1 progress of the tool call */ + readonly progress?: number; +} + +export type ToolProgress = IProgress; + +export type ToolDataSource = + | { + type: 'extension'; + label: string; + extensionId: ExtensionIdentifier; + } + | { + type: 'mcp'; + label: string; + serverLabel: string | undefined; + instructions: string | undefined; + collectionId: string; + definitionId: string; + } + | { + type: 'user'; + label: string; + file: URI; + } + | { + type: 'internal'; + label: string; + } | { + type: 'external'; + label: string; + }; + +export namespace ToolDataSource { + + export const Internal: ToolDataSource = { type: 'internal', label: 'Built-In' }; + + /** External tools may not be contributed or invoked, but may be invoked externally and described in an IChatToolInvocationSerialized */ + export const External: ToolDataSource = { type: 'external', label: 'External' }; + + export function toKey(source: ToolDataSource): string { + switch (source.type) { + case 'extension': return `extension:${source.extensionId.value}`; + case 'mcp': return `mcp:${source.collectionId}:${source.definitionId}`; + case 'user': return `user:${source.file.toString()}`; + case 'internal': return 'internal'; + case 'external': return 'external'; + } + } + + export function equals(a: ToolDataSource, b: ToolDataSource): boolean { + return toKey(a) === toKey(b); + } + + export function classify(source: ToolDataSource): { readonly ordinal: number; readonly label: string } { + if (source.type === 'internal') { + return { ordinal: 1, label: localize('builtin', 'Built-In') }; + } else if (source.type === 'mcp') { + return { ordinal: 2, label: source.label }; + } else if (source.type === 'user') { + return { ordinal: 0, label: localize('user', 'User Defined') }; + } else { + return { ordinal: 3, label: source.label }; + } + } +} + +export interface IToolInvocation { + callId: string; + toolId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: Record; + tokenBudget?: number; + context: IToolInvocationContext | undefined; + chatRequestId?: string; + chatInteractionId?: string; + /** + * Optional tool call ID from the chat stream, used to correlate with pending streaming tool calls. + */ + chatStreamToolCallId?: 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 + */ + subAgentInvocationId?: string; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData; + modelId?: string; + userSelectedTools?: UserSelectedTools; +} + +export interface IToolInvocationContext { + /** @deprecated Use {@link sessionResource} instead */ + readonly sessionId: string; + readonly sessionResource: URI; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isToolInvocationContext(obj: any): obj is IToolInvocationContext { + return typeof obj === 'object' && typeof obj.sessionId === 'string' && URI.isUri(obj.sessionResource); +} + +export interface IToolInvocationPreparationContext { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parameters: any; + chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ + chatSessionId?: string; + chatSessionResource: URI | undefined; + chatInteractionId?: string; +} + +export type ToolInputOutputBase = { + /** Mimetype of the value, optional */ + mimeType?: string; + /** URI of the resource on the MCP server. */ + uri?: URI; + /** If true, this part came in as a resource reference rather than direct data. */ + asResource?: boolean; + /** Audience of the data part */ + audience?: LanguageModelPartAudience[]; +}; + +export type ToolInputOutputEmbedded = ToolInputOutputBase & { + type: 'embed'; + value: string; + /** If true, value is text. If false or not given, value is base64 */ + isText?: boolean; +}; + +export type ToolInputOutputReference = ToolInputOutputBase & { type: 'ref'; uri: URI }; + +export interface IToolResultInputOutputDetails { + readonly input: string; + readonly output: (ToolInputOutputEmbedded | ToolInputOutputReference)[]; + readonly isError?: boolean; + /** Raw MCP tool result for MCP App UI rendering */ + readonly mcpOutput?: unknown; +} + +export interface IToolResultOutputDetails { + readonly output: { type: 'data'; mimeType: string; value: VSBuffer }; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isToolResultInputOutputDetails(obj: any): obj is IToolResultInputOutputDetails { + return typeof obj === 'object' && typeof obj?.input === 'string' && (typeof obj?.output === 'string' || Array.isArray(obj?.output)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isToolResultOutputDetails(obj: any): obj is IToolResultOutputDetails { + return typeof obj === 'object' && typeof obj?.output === 'object' && typeof obj?.output?.mimeType === 'string' && obj?.output?.type === 'data'; +} + +export interface IToolResult { + content: (IToolResultPromptTsxPart | IToolResultTextPart | IToolResultDataPart)[]; + toolResultMessage?: string | IMarkdownString; + toolResultDetails?: Array | IToolResultInputOutputDetails | IToolResultOutputDetails; + toolResultError?: string; + toolMetadata?: unknown; + /** Whether to ask the user to confirm these tool results. Overrides {@link IToolConfirmationMessages.confirmResults}. */ + confirmResults?: boolean; +} + +export function toolContentToA11yString(part: IToolResult['content']) { + return part.map(p => { + switch (p.kind) { + case 'promptTsx': + return stringifyPromptTsxPart(p); + case 'text': + return p.value; + case 'data': + return localize('toolResultDataPartA11y', "{0} of {1} binary data", ByteSize.formatSize(p.value.data.byteLength), p.value.mimeType || 'unknown'); + } + }).join(', '); +} + +export function toolResultHasBuffers(result: IToolResult): boolean { + return result.content.some(part => part.kind === 'data'); +} + +export interface IToolResultPromptTsxPart { + kind: 'promptTsx'; + value: unknown; +} + +export function stringifyPromptTsxPart(part: IToolResultPromptTsxPart): string { + return stringifyPromptElementJSON(part.value as PromptElementJSON); +} + +export interface IToolResultTextPart { + kind: 'text'; + value: string; + audience?: LanguageModelPartAudience[]; + title?: string; +} + +export interface IToolResultDataPart { + kind: 'data'; + value: { + mimeType: string; + data: VSBuffer; + }; + audience?: LanguageModelPartAudience[]; + title?: string; +} + +export interface IToolConfirmationMessages { + /** Title for the confirmation. If set, the user will be asked to confirm execution of the tool */ + title?: string | IMarkdownString; + /** MUST be set if `title` is also set */ + message?: string | IMarkdownString; + disclaimer?: string | IMarkdownString; + allowAutoConfirm?: boolean; + terminalCustomActions?: ToolConfirmationAction[]; + /** If true, confirmation will be requested after the tool executes and before results are sent to the model */ + confirmResults?: boolean; + /** If title is not set (no confirmation needed), this reason will be shown to explain why confirmation was not needed */ + confirmationNotNeededReason?: string | IMarkdownString; +} + +export interface IToolConfirmationAction { + label: string; + disabled?: boolean; + tooltip?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any; +} + +export type ToolConfirmationAction = IToolConfirmationAction | Separator; + +export enum ToolInvocationPresentation { + Hidden = 'hidden', + HiddenAfterComplete = 'hiddenAfterComplete' +} + +export interface IToolInvocationStreamContext { + toolCallId: string; + rawInput: unknown; + chatRequestId?: string; + /** @deprecated Use {@link chatSessionResource} instead */ + chatSessionId?: string; + chatSessionResource?: URI; + chatInteractionId?: string; +} + +export interface IStreamedToolInvocation { + invocationMessage?: string | IMarkdownString; +} + +export interface IPreparedToolInvocation { + invocationMessage?: string | IMarkdownString; + pastTenseMessage?: string | IMarkdownString; + originMessage?: string | IMarkdownString; + confirmationMessages?: IToolConfirmationMessages; + presentation?: ToolInvocationPresentation; + toolSpecificData?: IChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatTodoListContent | IChatSubagentToolInvocationData; +} + +export interface IToolImpl { + invoke(invocation: IToolInvocation, countTokens: CountTokensCallback, progress: ToolProgress, token: CancellationToken): Promise; + prepareToolInvocation?(context: IToolInvocationPreparationContext, token: CancellationToken): Promise; + handleToolStream?(context: IToolInvocationStreamContext, token: CancellationToken): Promise; +} + +export interface IToolSet { + readonly id: string; + readonly referenceName: string; + readonly icon: ThemeIcon; + readonly source: ToolDataSource; + readonly description?: string; + readonly legacyFullNames?: string[]; + + getTools(r?: IReader): Iterable; +} + +export type IToolAndToolSetEnablementMap = ReadonlyMap; + +export function isToolSet(obj: IToolData | IToolSet | undefined): obj is IToolSet { + return !!obj && (obj as IToolSet).getTools !== undefined; +} + +export class ToolSet implements IToolSet { + + protected readonly _tools = new ObservableSet(); + + protected readonly _toolSets = new ObservableSet(); + + /** + * A homogenous tool set only contains tools from the same source as the tool set itself + */ + readonly isHomogenous: IObservable; + + constructor( + readonly id: string, + readonly referenceName: string, + readonly icon: ThemeIcon, + readonly source: ToolDataSource, + readonly description: string | undefined, + readonly legacyFullNames: string[] | undefined, + private readonly _contextKeyService: IContextKeyService, + ) { + + this.isHomogenous = derived(r => { + return !Iterable.some(this._tools.observable.read(r), tool => !ToolDataSource.equals(tool.source, this.source)) + && !Iterable.some(this._toolSets.observable.read(r), toolSet => !ToolDataSource.equals(toolSet.source, this.source)); + }); + } + + addTool(data: IToolData, tx?: ITransaction): IDisposable { + this._tools.add(data, tx); + return toDisposable(() => { + this._tools.delete(data); + }); + } + + addToolSet(toolSet: IToolSet, tx?: ITransaction): IDisposable { + if (toolSet === this) { + return Disposable.None; + } + this._toolSets.add(toolSet, tx); + return toDisposable(() => { + this._toolSets.delete(toolSet); + }); + } + + getTools(r?: IReader): Iterable { + return Iterable.concat( + Iterable.filter(this._tools.observable.read(r), toolData => this._contextKeyService.contextMatchesRules(toolData.when)), + ...Iterable.map(this._toolSets.observable.read(r), toolSet => toolSet.getTools(r)) + ); + } +} + +export class ToolSetForModel { + public get id() { + return this._toolSet.id; + } + + public get referenceName() { + return this._toolSet.referenceName; + } + + public get icon() { + return this._toolSet.icon; + } + + public get source() { + return this._toolSet.source; + } + + public get description() { + return this._toolSet.description; + } + + public get legacyFullNames() { + return this._toolSet.legacyFullNames; + } + + constructor( + private readonly _toolSet: IToolSet, + private readonly model: ILanguageModelChatMetadata | undefined, + ) { } + + public getTools(r?: IReader): Iterable { + return Iterable.filter(this._toolSet.getTools(r), toolData => toolMatchesModel(toolData, this.model)); + } +} + + +export interface IBeginToolCallOptions { + toolCallId: string; + toolId: string; + chatRequestId?: string; + sessionResource?: URI; + subagentInvocationId?: string; +} + +export interface IToolInvokedEvent { + readonly toolId: string; + readonly sessionResource: URI | undefined; + readonly requestId: string | undefined; + readonly subagentInvocationId: string | undefined; +} + +export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); + +export type CountTokensCallback = (input: string, token: CancellationToken) => Promise; + +export interface ILanguageModelToolsService { + _serviceBrand: undefined; + readonly vscodeToolSet: ToolSet; + readonly executeToolSet: ToolSet; + readonly readToolSet: ToolSet; + readonly agentToolSet: ToolSet; + readonly onDidChangeTools: Event; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ readonly sessionResource: URI; readonly toolData: IToolData }>; + readonly onDidInvokeTool: Event; + registerToolData(toolData: IToolData): IDisposable; + registerToolImplementation(id: string, tool: IToolImpl): IDisposable; + registerTool(toolData: IToolData, tool: IToolImpl): IDisposable; + + /** + * Get all tools currently enabled (matching `when` clauses and model). + * @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped. + */ + getTools(model: ILanguageModelChatMetadata | undefined): Iterable; + + /** + * Creats an observable of enabled tools in the context. Note the observable + * should be created and reused, not created per reader, for example: + * + * ``` + * const toolsObs = toolsService.observeTools(model); + * autorun(reader => { + * const tools = toolsObs.read(reader); + * ... + * }); + * ``` + * @param model The language model metadata to filter tools by. If undefined, model-specific filtering is skipped. + */ + observeTools(model: ILanguageModelChatMetadata | undefined): IObservable; + + /** + * Get all registered tools regardless of enablement state. + * Use this for configuration UIs, completions, etc. where all tools should be visible. + */ + getAllToolsIncludingDisabled(): Iterable; + + /** + * Get a tool by its ID. Does not check when clauses. + */ + getTool(id: string): IToolData | undefined; + + /** + * Get a tool by its reference name. Does not check when clauses. + */ + getToolByName(name: string): IToolData | undefined; + + /** + * Begin a tool call in the streaming phase. + * Creates a ChatToolInvocation in the Streaming state and appends it to the chat. + * Returns the invocation so it can be looked up later when invokeTool is called. + */ + beginToolCall(options: IBeginToolCallOptions): IChatToolInvocation | undefined; + + /** + * Update the streaming state of a pending tool call. + * Calls the tool's handleToolStream method to get a custom invocation message. + */ + updateToolStream(toolCallId: string, partialInput: unknown, token: CancellationToken): Promise; + + invokeTool(invocation: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise; + cancelToolCallsForRequest(requestId: string): void; + /** Flush any pending tool updates to the extension hosts. */ + flushToolUpdates(): void; + + readonly toolSets: IObservable>; + getToolSetsForModel(model: ILanguageModelChatMetadata | undefined, reader?: IReader): Iterable; + getToolSet(id: string): IToolSet | undefined; + getToolSetByName(name: string): IToolSet | undefined; + createToolSet(source: ToolDataSource, id: string, referenceName: string, options?: { icon?: ThemeIcon; description?: string; legacyFullNames?: string[] }): ToolSet & IDisposable; + + // tool names in prompt and agent files ('full reference names') + getFullReferenceNames(): Iterable; + getFullReferenceName(tool: IToolData, toolSet?: IToolSet): string; + getToolByFullReferenceName(fullReferenceName: string): IToolData | IToolSet | undefined; + getDeprecatedFullReferenceNames(): Map>; + + /** + * Gets the enablement maps based on the given set of references. + * @param fullReferenceNames The full reference names of the tools and tool sets to enable. + * @param target Optional target to filter tools by. + * @param model Optional language model metadata to filter tools by. + * If undefined is passed, all tools will be returned, even if normally disabled. + */ + toToolAndToolSetEnablementMap( + fullReferenceNames: readonly string[], + target: string | undefined, + model: ILanguageModelChatMetadata | undefined, + ): IToolAndToolSetEnablementMap; + + toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[]; + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[]; +} + + +export function createToolInputUri(toolCallId: string): URI { + return URI.from({ scheme: Schemas.inMemory, path: `/lm/tool/${toolCallId}/tool_input.json` }); +} + +export function createToolSchemaUri(toolOrId: IToolData | string): URI { + if (typeof toolOrId !== 'string') { + toolOrId = toolOrId.id; + } + return URI.from({ scheme: Schemas.vscode, authority: 'schemas', path: `/lm/tool/${toolOrId}` }); +} + +export namespace SpecedToolAliases { + export const execute = 'execute'; + export const edit = 'edit'; + export const search = 'search'; + export const agent = 'agent'; + export const read = 'read'; + export const web = 'web'; + export const todo = 'todo'; +} + +export namespace VSCodeToolReference { + export const runSubagent = 'runSubagent'; + export const vscode = 'vscode'; + +} diff --git a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts b/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts deleted file mode 100644 index 93b6aa3d858..00000000000 --- a/src/vs/workbench/contrib/chat/common/tools/runSubagentTool.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { localize } from '../../../../../nls.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IChatAgentRequest, IChatAgentService } from '../chatAgents.js'; -import { ChatModel, IChatRequestModeInstructions } from '../chatModel.js'; -import { IChatModeService } from '../chatModes.js'; -import { IChatProgress, IChatService } from '../chatService.js'; -import { LocalChatSessionUri } from '../chatUri.js'; -import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../constants.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../languageModels.js'; -import { - CountTokensCallback, - ILanguageModelToolsService, - IPreparedToolInvocation, - IToolData, - IToolImpl, - IToolInvocation, - IToolInvocationPreparationContext, - IToolResult, - ToolDataSource, - ToolProgress, - ToolSet, - VSCodeToolReference -} from '../languageModelToolsService.js'; -import { ManageTodoListToolToolId } from './manageTodoListTool.js'; -import { createToolSimpleTextResult } from './toolHelpers.js'; - -export const RunSubagentToolId = 'runSubagent'; - -const BaseModelDescription = `Launch a new agent to handle complex, multi-step tasks autonomously. This tool is good at researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use this agent to perform the search for you. - -- Agents do not run async or in the background, you will wait for the agent\'s result. -- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result. -- Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you. -- The agent's outputs should generally be trusted -- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user\'s intent`; - -interface IRunSubagentToolInputParams { - prompt: string; - description: string; - subagentType?: string; -} - -export class RunSubagentTool extends Disposable implements IToolImpl { - - constructor( - @IChatAgentService private readonly chatAgentService: IChatAgentService, - @IChatService private readonly chatService: IChatService, - @IChatModeService private readonly chatModeService: IChatModeService, - @ILanguageModelToolsService private readonly languageModelToolsService: ILanguageModelToolsService, - @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, - @ILogService private readonly logService: ILogService, - @ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService, - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - super(); - } - - getToolData(): IToolData { - const runSubagentToolData: IToolData = { - id: RunSubagentToolId, - toolReferenceName: VSCodeToolReference.runSubagent, - canBeReferencedInPrompt: true, - icon: ThemeIcon.fromId(Codicon.organization.id), - displayName: localize('tool.runSubagent.displayName', 'Run Subagent'), - userDescription: localize('tool.runSubagent.userDescription', 'Runs a task within an isolated subagent context. Enables efficient organization of tasks and context window management.'), - modelDescription: BaseModelDescription, - source: ToolDataSource.Internal, - inputSchema: { - type: 'object', - properties: { - prompt: { - type: 'string', - description: 'A detailed description of the task for the agent to perform' - }, - description: { - type: 'string', - description: 'A short (3-5 word) description of the task' - } - }, - required: ['prompt', 'description'] - } - }; - - if (this.configurationService.getValue(ChatConfiguration.SubagentToolCustomAgents)) { - runSubagentToolData.inputSchema!.properties!['subagentType'] = { - type: 'string', - description: 'Optional ID of a specific agent to invoke. If not provided, uses the current agent.' - }; - runSubagentToolData.modelDescription += `\n- If the user asks for a certain agent by name, you MUST provide that EXACT subagentType (case-sensitive) to invoke that specific agent.`; - } - - - - return runSubagentToolData; - } - - async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { - const args = invocation.parameters as IRunSubagentToolInputParams; - - this.logService.debug(`RunSubagentTool: Invoking with prompt: ${args.prompt.substring(0, 100)}...`); - - if (!invocation.context) { - throw new Error('toolInvocationToken is required for this tool'); - } - - // Get the chat model and request for writing progress - const model = this.chatService.getSession(LocalChatSessionUri.forSession(invocation.context.sessionId)) as ChatModel | undefined; - if (!model) { - throw new Error('Chat model not found for session'); - } - - const request = model.getRequests().at(-1)!; - - try { - // Get the default agent - const defaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Agent); - if (!defaultAgent) { - return createToolSimpleTextResult('Error: No default agent available'); - } - - // Resolve mode-specific configuration if subagentId is provided - let modeModelId = invocation.modelId; - let modeTools = invocation.userSelectedTools; - let modeInstructions: IChatRequestModeInstructions | undefined; - - if (args.subagentType) { - const mode = this.chatModeService.findModeByName(args.subagentType); - if (mode) { - // Use mode-specific model if available - const modeModelQualifiedName = mode.model?.get(); - if (modeModelQualifiedName) { - // Find the actual model identifier from the qualified name - const modelIds = this.languageModelsService.getLanguageModelIds(); - for (const modelId of modelIds) { - const metadata = this.languageModelsService.lookupLanguageModel(modelId); - if (metadata && ILanguageModelChatMetadata.matchesQualifiedName(modeModelQualifiedName, metadata)) { - modeModelId = modelId; - break; - } - } - } - - // Use mode-specific tools if available - const modeCustomTools = mode.customTools?.get(); - if (modeCustomTools) { - // Convert the mode's custom tools (array of qualified names) to UserSelectedTools format - const enablementMap = this.languageModelToolsService.toToolAndToolSetEnablementMap(modeCustomTools, mode.target?.get()); - // Convert enablement map to UserSelectedTools (Record) - modeTools = {}; - for (const [tool, enabled] of enablementMap) { - if (!(tool instanceof ToolSet)) { - modeTools[tool.id] = enabled; - } - } - } - - const instructions = mode.modeInstructions?.get(); - modeInstructions = instructions && { - name: mode.name.get(), - content: instructions.content, - toolReferences: this.toolsService.toToolReferences(instructions.toolReferences), - metadata: instructions.metadata, - }; - } else { - this.logService.warn(`RunSubagentTool: Agent '${args.subagentType}' not found, using current configuration`); - } - } - - // Track whether we should collect markdown (after the last prepare tool invocation) - const markdownParts: string[] = []; - - let inEdit = false; - const progressCallback = (parts: IChatProgress[]) => { - for (const part of parts) { - // Write certain parts immediately to the model - if (part.kind === 'prepareToolInvocation' || part.kind === 'textEdit' || part.kind === 'notebookEdit' || part.kind === 'codeblockUri') { - if (part.kind === 'codeblockUri' && !inEdit) { - inEdit = true; - model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('```\n'), fromSubagent: true }); - } - model.acceptResponseProgress(request, part); - - // When we see a prepare tool invocation, reset markdown collection - if (part.kind === 'prepareToolInvocation') { - markdownParts.length = 0; // Clear previously collected markdown - } - } else if (part.kind === 'markdownContent') { - if (inEdit) { - model.acceptResponseProgress(request, { kind: 'markdownContent', content: new MarkdownString('\n```\n\n'), fromSubagent: true }); - inEdit = false; - } - - // Collect markdown content for the tool result - markdownParts.push(part.content.value); - } - } - }; - - if (modeTools) { - modeTools[RunSubagentToolId] = false; - modeTools[ManageTodoListToolToolId] = false; - } - - // Build the agent request - const agentRequest: IChatAgentRequest = { - sessionId: invocation.context.sessionId, - requestId: invocation.callId ?? `subagent-${Date.now()}`, - agentId: defaultAgent.id, - message: args.prompt, - variables: { variables: [] }, - location: ChatAgentLocation.Chat, - isSubagent: true, - userSelectedModelId: modeModelId, - userSelectedTools: modeTools, - modeInstructions, - }; - - // Invoke the agent - const result = await this.chatAgentService.invokeAgent( - defaultAgent.id, - agentRequest, - progressCallback, - [], - token - ); - - // Check for errors - if (result.errorDetails) { - return createToolSimpleTextResult(`Agent error: ${result.errorDetails.message}`); - } - - return createToolSimpleTextResult(markdownParts.join('') || 'Agent completed with no output'); - - } catch (error) { - const errorMessage = `Error invoking subagent: ${error instanceof Error ? error.message : 'Unknown error'}`; - this.logService.error(errorMessage, error); - return createToolSimpleTextResult(errorMessage); - } - } - - async prepareToolInvocation(context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { - const args = context.parameters as IRunSubagentToolInputParams; - - return { - invocationMessage: args.description, - }; - } -} diff --git a/src/vs/workbench/contrib/chat/common/tools/tools.ts b/src/vs/workbench/contrib/chat/common/tools/tools.ts deleted file mode 100644 index a8c747a46b2..00000000000 --- a/src/vs/workbench/contrib/chat/common/tools/tools.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 { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; -import { ConfirmationTool, ConfirmationToolData } from './confirmationTool.js'; -import { EditTool, EditToolData } from './editFileTool.js'; -import { createManageTodoListToolData, ManageTodoListTool, TodoListToolDescriptionFieldSettingId, TodoListToolWriteOnlySettingId } from './manageTodoListTool.js'; -import { RunSubagentTool } from './runSubagentTool.js'; - -export class BuiltinToolsContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'chat.builtinTools'; - - constructor( - @ILanguageModelToolsService toolsService: ILanguageModelToolsService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService, - ) { - super(); - - const editTool = instantiationService.createInstance(EditTool); - this._register(toolsService.registerTool(EditToolData, editTool)); - - // Check if write-only mode is enabled for the todo tool - const writeOnlyMode = this.configurationService.getValue(TodoListToolWriteOnlySettingId) === true; - const includeDescription = this.configurationService.getValue(TodoListToolDescriptionFieldSettingId) !== false; - const todoToolData = createManageTodoListToolData(writeOnlyMode, includeDescription); - const manageTodoListTool = this._register(instantiationService.createInstance(ManageTodoListTool, writeOnlyMode, includeDescription)); - this._register(toolsService.registerTool(todoToolData, manageTodoListTool)); - - // Register the confirmation tool - const confirmationTool = instantiationService.createInstance(ConfirmationTool); - this._register(toolsService.registerTool(ConfirmationToolData, confirmationTool)); - - const runSubagentTool = instantiationService.createInstance(RunSubagentTool); - this._register(toolsService.registerTool(runSubagentTool.getToolData(), runSubagentTool)); - } -} - -export const InternalFetchWebPageToolId = 'vscode_fetchWebPage_internal'; diff --git a/src/vs/workbench/contrib/chat/common/voiceChatService.ts b/src/vs/workbench/contrib/chat/common/voiceChatService.ts index bcf91b3a5b5..39cc65a7f18 100644 --- a/src/vs/workbench/contrib/chat/common/voiceChatService.ts +++ b/src/vs/workbench/contrib/chat/common/voiceChatService.ts @@ -10,9 +10,9 @@ import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.j import { rtrim } from '../../../../base/common/strings.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IChatAgentService } from './chatAgents.js'; -import { IChatModel } from './chatModel.js'; -import { chatAgentLeader, chatSubcommandLeader } from './chatParserTypes.js'; +import { IChatAgentService } from './participants/chatAgents.js'; +import { IChatModel } from './model/chatModel.js'; +import { chatAgentLeader, chatSubcommandLeader } from './requestParser/chatParserTypes.js'; import { ISpeechService, ISpeechToTextEvent, SpeechToTextStatus } from '../../speech/common/speechService.js'; export const IVoiceChatService = createDecorator('voiceChatService'); diff --git a/src/vs/workbench/contrib/chat/common/widget/annotations.ts b/src/vs/workbench/contrib/chat/common/widget/annotations.ts new file mode 100644 index 00000000000..c22c60ca430 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/widget/annotations.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { findLastIdx } from '../../../../../base/common/arraysFind.js'; +import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { basename } from '../../../../../base/common/resources.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { IRange } from '../../../../../editor/common/core/range.js'; +import { isLocation } from '../../../../../editor/common/languages.js'; +import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from '../model/chatModel.js'; +import { IChatAgentVulnerabilityDetails } from '../chatService/chatService.js'; + +export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI + +export function annotateSpecialMarkdownContent(response: Iterable): IChatProgressRenderableResponseContent[] { + let refIdPool = 0; + + const result: IChatProgressRenderableResponseContent[] = []; + for (const item of response) { + const previousItemIndex = findLastIdx(result, p => p.kind !== 'textEditGroup' && p.kind !== 'undoStop'); + const previousItem = result[previousItemIndex]; + if (item.kind === 'inlineReference') { + let label: string | undefined = item.name; + if (!label) { + if (URI.isUri(item.inlineReference)) { + label = basename(item.inlineReference); + } else if (isLocation(item.inlineReference)) { + label = basename(item.inlineReference.uri); + } else { + label = item.inlineReference.name; + } + } + + const refId = refIdPool++; + const printUri = URI.parse(contentRefUrl).with({ path: String(refId) }); + const markdownText = `[${label}](${printUri.toString()})`; + + const annotationMetadata = { [refId]: item }; + + if (previousItem?.kind === 'markdownContent') { + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[previousItemIndex] = { ...previousItem, content: merged, inlineReferences: { ...annotationMetadata, ...(previousItem.inlineReferences || {}) } }; + } else { + result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' }); + } + } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) { + const merged = appendMarkdownString(previousItem.content, item.content); + result[previousItemIndex] = { ...previousItem, content: merged }; + } else if (item.kind === 'markdownVuln') { + const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); + const markdownText = `${item.content.value}`; + if (previousItem?.kind === 'markdownContent') { + // Since this is inside a codeblock, it needs to be merged into the previous markdown content. + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[previousItemIndex] = { ...previousItem, content: merged }; + } else { + result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); + } + } else if (item.kind === 'codeblockUri') { + if (previousItem?.kind === 'markdownContent') { + const isEditText = item.isEdit ? ` isEdit` : ''; + const subAgentText = item.subAgentInvocationId ? ` subAgentInvocationId="${encodeURIComponent(item.subAgentInvocationId)}"` : ''; + const markdownText = `${item.uri.toString()}`; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + // delete the previous and append to ensure that we don't reorder the edit before the undo stop containing it + result.splice(previousItemIndex, 1); + result.push({ ...previousItem, content: merged }); + } + } else { + result.push(item); + } + } + + return result; +} + +export interface IMarkdownVulnerability { + readonly title: string; + readonly description: string; + readonly range: IRange; +} +export function extractCodeblockUrisFromText(text: string): { uri: URI; isEdit?: boolean; subAgentInvocationId?: string; textWithoutResult: string } | undefined { + const match = /([\s\S]*?)<\/vscode_codeblock_uri>/ms.exec(text); + if (match) { + const [all, isEdit, , encodedSubAgentId, uriString] = match; + if (uriString) { + const result = URI.parse(uriString); + const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + all.length); + let subAgentInvocationId: string | undefined; + if (encodedSubAgentId) { + try { + subAgentInvocationId = decodeURIComponent(encodedSubAgentId); + } catch { + subAgentInvocationId = encodedSubAgentId; + } + } + return { uri: result, textWithoutResult, isEdit: !!isEdit, subAgentInvocationId }; + } + } + return undefined; +} + +export function extractSubAgentInvocationIdFromText(text: string): string | undefined { + const match = /]* subAgentInvocationId="([^"]*)"/ms.exec(text); + if (match) { + try { + return decodeURIComponent(match[1]); + } catch { + return match[1]; + } + } + return undefined; +} + +export function hasCodeblockUriTag(text: string): boolean { + return text.includes('(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) { + const [full, details, content] = match; + const start = match.index; + const textBefore = newText.substring(0, start); + const linesBefore = textBefore.split('\n').length - 1; + const linesInside = content.split('\n').length - 1; + + const previousNewlineIdx = textBefore.lastIndexOf('\n'); + const startColumn = start - (previousNewlineIdx + 1) + 1; + const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n'); + const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1; + + try { + const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details)); + vulnDetails.forEach(({ title, description }) => vulnerabilities.push({ + title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } + })); + } catch (err) { + // Something went wrong with encoding this text, just ignore it + } + newText = newText.substring(0, start) + content + newText.substring(start + full.length); + } + + return { newText, vulnerabilities }; +} diff --git a/src/vs/workbench/contrib/chat/common/chatColors.ts b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts similarity index 89% rename from src/vs/workbench/contrib/chat/common/chatColors.ts rename to src/vs/workbench/contrib/chat/common/widget/chatColors.ts index 43eca32a105..ad98943587c 100644 --- a/src/vs/workbench/contrib/chat/common/chatColors.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatColors.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Color, RGBA } from '../../../../base/common/color.js'; -import { localize } from '../../../../nls.js'; -import { badgeBackground, badgeForeground, contrastBorder, editorBackground, editorSelectionBackground, editorWidgetBackground, foreground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { Color, RGBA } from '../../../../../base/common/color.js'; +import { localize } from '../../../../../nls.js'; +import { badgeBackground, badgeForeground, contrastBorder, editorBackground, editorSelectionBackground, editorWidgetBackground, foreground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js'; export const chatRequestBorder = registerColor( 'chat.requestBorder', @@ -73,3 +73,8 @@ export const chatLinesRemovedForeground = registerColor( 'chat.linesRemovedForeground', { dark: '#FC6A6A', light: '#BC2F32', hcDark: '#F48771', hcLight: '#B5200D' }, localize('chat.linesRemovedForeground', 'Foreground color of lines removed in chat code block pill.'), true); + +export const chatThinkingShimmer = registerColor( + 'chat.thinkingShimmer', + { dark: '#ffffff', light: '#000000', hcDark: '#ffffff', hcLight: '#000000' }, + localize('chat.thinkingShimmer', 'Shimmer highlight for thinking/working labels.'), true); diff --git a/src/vs/workbench/contrib/chat/common/widget/chatLayoutService.ts b/src/vs/workbench/contrib/chat/common/widget/chatLayoutService.ts new file mode 100644 index 00000000000..4fbe693af01 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/widget/chatLayoutService.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IObservable } from '../../../../../base/common/observable.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; + +export const IChatLayoutService = createDecorator('chatLayoutService'); + +export interface IChatLayoutService { + readonly _serviceBrand: undefined; + + readonly fontFamily: IObservable; + readonly fontSize: IObservable; +} diff --git a/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts similarity index 84% rename from src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts rename to src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts index 98aad3f975e..d1d069424dd 100644 --- a/src/vs/workbench/contrib/chat/common/chatResponseResourceFileSystemProvider.ts +++ b/src/vs/workbench/contrib/chat/common/widget/chatResponseResourceFileSystemProvider.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { decodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; -import { Event } from '../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { newWriteableStream, ReadableStreamEvents } from '../../../../base/common/stream.js'; -import { URI } from '../../../../base/common/uri.js'; -import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat } from '../../../../platform/files/common/files.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ChatResponseResource } from './chatModel.js'; -import { IChatService, IChatToolInvocation, IChatToolInvocationSerialized } from './chatService.js'; -import { LocalChatSessionUri } from './chatUri.js'; -import { isToolResultInputOutputDetails } from './languageModelToolsService.js'; +import { decodeBase64, VSBuffer } from '../../../../../base/common/buffer.js'; +import { Event } from '../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { newWriteableStream, ReadableStreamEvents } from '../../../../../base/common/stream.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { createFileSystemProviderError, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IStat } from '../../../../../platform/files/common/files.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ChatResponseResource } from '../model/chatModel.js'; +import { IChatService, IChatToolInvocation, IChatToolInvocationSerialized } from '../chatService/chatService.js'; +import { isToolResultInputOutputDetails } from '../tools/languageModelToolsService.js'; export class ChatResponseResourceFileSystemProvider extends Disposable implements IWorkbenchContribution, @@ -90,8 +89,8 @@ export class ChatResponseResourceFileSystemProvider extends Disposable implement if (!parsed) { throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); } - const { sessionId, toolCallId, index } = parsed; - const session = this.chatService.getSession(LocalChatSessionUri.forSession(sessionId)); + const { sessionResource, toolCallId, index } = parsed; + const session = this.chatService.getSession(sessionResource); if (!session) { throw createFileSystemProviderError(`File not found`, FileSystemProviderErrorCode.FileNotFound); } diff --git a/src/vs/workbench/contrib/chat/common/widget/chatWidgetHistoryService.ts b/src/vs/workbench/contrib/chat/common/widget/chatWidgetHistoryService.ts new file mode 100644 index 00000000000..9f511624920 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/widget/chatWidgetHistoryService.ts @@ -0,0 +1,264 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { equals as arraysEqual } from '../../../../../base/common/arrays.js'; +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; +import { Memento } from '../../../../common/memento.js'; +import { IChatModelInputState } from '../model/chatModel.js'; +import { CHAT_PROVIDER_ID } from '../participants/chatParticipantContribTypes.js'; +import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; +import { ChatAgentLocation, ChatModeKind } from '../constants.js'; + +interface IChatHistoryEntry { + text: string; + state?: IChatInputState; +} + +/** The collected input state for chat history entries */ +interface IChatInputState { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + chatContextAttachments?: ReadonlyArray; + + /** + * This should be a mode id (ChatMode | string). + * { id: string } is the old IChatMode. This is deprecated but may still be in persisted data. + */ + chatMode?: ChatModeKind | string | { id: string }; +} + +export const IChatWidgetHistoryService = createDecorator('IChatWidgetHistoryService'); +export interface IChatWidgetHistoryService { + _serviceBrand: undefined; + + readonly onDidChangeHistory: Event; + + clearHistory(): void; + getHistory(location: ChatAgentLocation): readonly IChatModelInputState[]; + append(location: ChatAgentLocation, history: IChatModelInputState): void; +} + +interface IChatHistory { + history?: { [providerId: string]: IChatModelInputState[] }; +} + +export type ChatHistoryChange = { kind: 'append'; entry: IChatModelInputState } | { kind: 'clear' }; + +export const ChatInputHistoryMaxEntries = 40; + +export class ChatWidgetHistoryService extends Disposable implements IChatWidgetHistoryService { + _serviceBrand: undefined; + + private memento: Memento; + private viewState: IChatHistory; + + private readonly _onDidChangeHistory = this._register(new Emitter()); + private changed = false; + readonly onDidChangeHistory = this._onDidChangeHistory.event; + + constructor( + @IStorageService storageService: IStorageService + ) { + super(); + + this.memento = new Memento('interactive-session', storageService); + const loadedState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); + this.viewState = loadedState; + + this._register(storageService.onWillSaveState(() => { + if (this.changed) { + this.memento.saveMemento(); + this.changed = false; + } + })); + } + + getHistory(location: ChatAgentLocation): IChatModelInputState[] { + const key = this.getKey(location); + const history = this.viewState.history?.[key] ?? []; + return history.map(entry => this.migrateHistoryEntry(entry)); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private migrateHistoryEntry(entry: any): IChatModelInputState { + // If it's already in the new format (has 'inputText' property), return as-is + if (entry.inputText !== undefined) { + return entry as IChatModelInputState; + } + + // Otherwise, it's an old IChatHistoryEntry with 'text' and 'state' properties + const oldEntry = entry as IChatHistoryEntry; + const oldState = oldEntry.state ?? {}; + + // Migrate chatMode to the new mode structure + let modeId: string; + let modeKind: ChatModeKind | undefined; + if (oldState.chatMode) { + if (typeof oldState.chatMode === 'string') { + modeId = oldState.chatMode; + modeKind = Object.values(ChatModeKind).includes(oldState.chatMode as ChatModeKind) + ? oldState.chatMode as ChatModeKind + : undefined; + } else if (typeof oldState.chatMode === 'object' && oldState.chatMode !== null) { + // Old format: { id: string } + const oldMode = oldState.chatMode as { id?: string }; + modeId = oldMode.id ?? ChatModeKind.Ask; + modeKind = oldMode.id && Object.values(ChatModeKind).includes(oldMode.id as ChatModeKind) + ? oldMode.id as ChatModeKind + : undefined; + } else { + modeId = ChatModeKind.Ask; + modeKind = ChatModeKind.Ask; + } + } else { + modeId = ChatModeKind.Ask; + modeKind = ChatModeKind.Ask; + } + + return { + inputText: oldEntry.text ?? '', + attachments: oldState.chatContextAttachments ?? [], + mode: { + id: modeId, + kind: modeKind + }, + contrib: oldEntry.state || {}, + selectedModel: undefined, + selections: [] + }; + } + + private getKey(location: ChatAgentLocation): string { + // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. + return location === ChatAgentLocation.Chat ? CHAT_PROVIDER_ID : location; + } + + append(location: ChatAgentLocation, history: IChatModelInputState): void { + this.viewState.history ??= {}; + + const key = this.getKey(location); + this.viewState.history[key] = this.getHistory(location).concat(history).slice(-ChatInputHistoryMaxEntries); + this.changed = true; + this._onDidChangeHistory.fire({ kind: 'append', entry: history }); + } + + clearHistory(): void { + this.viewState.history = {}; + this.changed = true; + this._onDidChangeHistory.fire({ kind: 'clear' }); + } +} + +export class ChatHistoryNavigator extends Disposable { + /** + * Index of our point in history. Goes 1 past the length of `_history` + */ + private _currentIndex: number; + private _history: readonly IChatModelInputState[]; + private _overlay: (IChatModelInputState | undefined)[] = []; + + public get values() { + return this.chatWidgetHistoryService.getHistory(this.location); + } + + constructor( + private readonly location: ChatAgentLocation, + @IChatWidgetHistoryService private readonly chatWidgetHistoryService: IChatWidgetHistoryService + ) { + super(); + this._history = this.chatWidgetHistoryService.getHistory(this.location); + this._currentIndex = this._history.length; + + this._register(this.chatWidgetHistoryService.onDidChangeHistory(e => { + if (e.kind === 'append') { + const prevLength = this._history.length; + this._history = this.chatWidgetHistoryService.getHistory(this.location); + const newLength = this._history.length; + + // If this append operation adjusted all history entries back, move our index back too + // if we weren't pointing to the end of the history. + if (prevLength === newLength) { + this._overlay.shift(); + if (this._currentIndex < this._history.length) { + this._currentIndex = Math.max(this._currentIndex - 1, 0); + } + } else if (this._currentIndex === prevLength) { + this._currentIndex = newLength; + } + } else if (e.kind === 'clear') { + this._history = []; + this._currentIndex = 0; + this._overlay = []; + } + })); + } + + public isAtEnd() { + return this._currentIndex === Math.max(this._history.length, this._overlay.length); + } + + public isAtStart() { + return this._currentIndex === 0; + } + + /** + * Replaces a history entry at the current index in this view of the history. + * Allows editing of old history entries while preventing accidental navigation + * from losing the edits. + */ + public overlay(entry: IChatModelInputState) { + this._overlay[this._currentIndex] = entry; + } + + public resetCursor() { + this._currentIndex = this._history.length; + } + + public previous() { + this._currentIndex = Math.max(this._currentIndex - 1, 0); + return this.current(); + } + + public next() { + this._currentIndex = Math.min(this._currentIndex + 1, this._history.length); + return this.current(); + } + + public current() { + return this._overlay[this._currentIndex] ?? this._history[this._currentIndex]; + } + + /** + * Appends a new entry to the navigator. Resets the state back to the end + * and clears any overlayed entries. + */ + public append(entry: IChatModelInputState) { + this._overlay = []; + this._currentIndex = this._history.length; + + if (!entriesEqual(this._history.at(-1), entry)) { + this.chatWidgetHistoryService.append(this.location, entry); + } + } +} + +function entriesEqual(a: IChatModelInputState | undefined, b: IChatModelInputState | undefined): boolean { + if (!a || !b) { + return false; + } + + if (a.inputText !== b.inputText) { + return false; + } + + if (!arraysEqual(a.attachments, b.attachments, (x, y) => x.id === y.id)) { + return false; + } + + return true; +} diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts similarity index 87% rename from src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts rename to src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts index 470c6554bc9..5236ff30add 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/widget/codeBlockModelCollection.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { encodeBase64, VSBuffer } from '../../../../base/common/buffer.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, IReference } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js'; -import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { encodeBase64, VSBuffer } from '../../../../../base/common/buffer.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { Disposable, IReference } from '../../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../../base/common/network.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { Range } from '../../../../../editor/common/core/range.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js'; +import { EndOfLinePreference, ITextModel } from '../../../../../editor/common/model.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; -import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js'; +import { isChatContentVariableReference } from '../chatService/chatService.js'; +import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from '../model/chatViewModel.js'; interface CodeBlockContent { @@ -28,6 +29,7 @@ export interface CodeBlockEntry { readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI; readonly isEdit?: boolean; + readonly subAgentInvocationId?: string; } export class CodeBlockModelCollection extends Disposable { @@ -38,6 +40,7 @@ export class CodeBlockModelCollection extends Disposable { inLanguageId: string | undefined; codemapperUri?: URI; isEdit?: boolean; + subAgentInvocationId?: string; }>(); /** @@ -84,6 +87,7 @@ export class CodeBlockModelCollection extends Disposable { vulns: entry.vulns, codemapperUri: entry.codemapperUri, isEdit: entry.isEdit, + subAgentInvocationId: entry.subAgentInvocationId, }; } @@ -194,6 +198,7 @@ export class CodeBlockModelCollection extends Disposable { if (entry) { entry.codemapperUri = codeblockUri.uri; entry.isEdit = codeblockUri.isEdit; + entry.subAgentInvocationId = codeblockUri.subAgentInvocationId; } newText = codeblockUri.textWithoutResult; @@ -240,7 +245,7 @@ export class CodeBlockModelCollection extends Disposable { return; } - const uriOrLocation = 'variableName' in ref.reference ? + const uriOrLocation = isChatContentVariableReference(ref.reference) ? ref.reference.value : ref.reference; if (!uriOrLocation) { diff --git a/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts b/src/vs/workbench/contrib/chat/common/widget/input/modelPickerWidget.ts similarity index 90% rename from src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts rename to src/vs/workbench/contrib/chat/common/widget/input/modelPickerWidget.ts index 779c011f797..5d5f437c1db 100644 --- a/src/vs/workbench/contrib/chat/common/modelPicker/modelPickerWidget.ts +++ b/src/vs/workbench/contrib/chat/common/widget/input/modelPickerWidget.ts @@ -3,6 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../../nls.js'; +import { localize } from '../../../../../../nls.js'; export const DEFAULT_MODEL_PICKER_CATEGORY = { label: localize('chat.modelPicker.other', "Other Models"), order: Number.MAX_SAFE_INTEGER }; diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/chatDeveloperActions.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/chatDeveloperActions.ts index 72b00481cda..ed1be679048 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/actions/chatDeveloperActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/chatDeveloperActions.ts @@ -8,8 +8,8 @@ import { localize2 } from '../../../../../nls.js'; import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { INativeHostService } from '../../../../../platform/native/common/native.js'; -import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatService } from '../../common/chatService.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatService } from '../../common/chatService/chatService.js'; export function registerChatDeveloperActions() { registerAction2(OpenChatStorageFolderAction); diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts new file mode 100644 index 00000000000..e16e8af4d8b --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/chatExportZip.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { joinPath } from '../../../../../base/common/resources.js'; +import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { INativeHostService } from '../../../../../platform/native/common/native.js'; +import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; +import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; +import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; +import { IChatWidgetService } from '../../browser/chat.js'; +import { captureRepoInfo } from '../../browser/chatRepoInfo.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { ChatConfiguration } from '../../common/constants.js'; +import { ISCMService } from '../../../scm/common/scm.js'; + +export function registerChatExportZipAction() { + registerAction2(class ExportChatAsZipAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.exportAsZip', + category: CHAT_CATEGORY, + title: localize2('chat.exportAsZip.label', "Export Chat as Zip..."), + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatEntitlementContextKeys.Entitlement.internal), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const widgetService = accessor.get(IChatWidgetService); + const fileDialogService = accessor.get(IFileDialogService); + const chatService = accessor.get(IChatService); + const nativeHostService = accessor.get(INativeHostService); + const notificationService = accessor.get(INotificationService); + const scmService = accessor.get(ISCMService); + const fileService = accessor.get(IFileService); + const configurationService = accessor.get(IConfigurationService); + + const repoInfoEnabled = configurationService.getValue(ChatConfiguration.RepoInfoEnabled) ?? true; + + const widget = widgetService.lastFocusedWidget; + if (!widget || !widget.viewModel) { + return; + } + + const defaultUri = joinPath(await fileDialogService.defaultFilePath(), 'chat.zip'); + const result = await fileDialogService.showSaveDialog({ + defaultUri, + filters: [{ name: 'Zip Archive', extensions: ['zip'] }] + }); + + if (!result) { + return; + } + + const model = chatService.getSession(widget.viewModel.sessionResource); + if (!model) { + return; + } + + const files: { path: string; contents: string }[] = [ + { + path: 'chat.json', + contents: JSON.stringify(model.toExport(), undefined, 2) + } + ]; + + const hasMessages = model.getRequests().length > 0; + + if (hasMessages) { + if (model.repoData) { + files.push({ + path: 'chat.repo.begin.json', + contents: JSON.stringify(model.repoData, undefined, 2) + }); + } + + if (repoInfoEnabled) { + const currentRepoData = await captureRepoInfo(scmService, fileService); + if (currentRepoData) { + files.push({ + path: 'chat.repo.end.json', + contents: JSON.stringify(currentRepoData, undefined, 2) + }); + } + + if (!model.repoData && !currentRepoData) { + notificationService.notify({ + severity: Severity.Warning, + message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") + }); + } + } + } else { + if (repoInfoEnabled) { + const currentRepoData = await captureRepoInfo(scmService, fileService); + if (currentRepoData) { + files.push({ + path: 'chat.repo.begin.json', + contents: JSON.stringify(currentRepoData, undefined, 2) + }); + } else { + notificationService.notify({ + severity: Severity.Warning, + message: localize('chatExportZip.noRepoData', "Exported chat without repository context. No Git repository was detected.") + }); + } + } + } + + try { + await nativeHostService.createZipFile(result, files); + } catch (error) { + notificationService.notify({ + severity: Severity.Error, + message: localize('chatExportZip.error', "Failed to export chat as zip: {0}", error instanceof Error ? error.message : String(error)) + }); + } + } + }); +} diff --git a/src/vs/workbench/contrib/chat/electron-browser/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-browser/actions/voiceChatActions.ts index 9f55ce34e57..bf4af290a4b 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/actions/voiceChatActions.ts @@ -31,12 +31,11 @@ import { isHighContrast } from '../../../../../platform/theme/common/theme.js'; import { registerThemingParticipant } from '../../../../../platform/theme/common/themeService.js'; import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ACTIVITY_BAR_BADGE_BACKGROUND } from '../../../../common/theme.js'; +import { ACTIVITY_BAR_FOREGROUND } from '../../../../common/theme.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IHostService } from '../../../../services/host/browser/host.js'; import { IWorkbenchLayoutService, Parts } from '../../../../services/layout/browser/layoutService.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; import { AccessibilityVoiceSettingId, SpeechTimeoutDefault, accessibilityConfigurationNodeBase } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { CTX_INLINE_CHAT_FOCUSED, MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; @@ -46,12 +45,12 @@ import { SearchContext } from '../../../search/common/constants.js'; import { TextToSpeechInProgress as GlobalTextToSpeechInProgress, HasSpeechProvider, ISpeechService, KeywordRecognitionStatus, SpeechToTextInProgress, SpeechToTextStatus, TextToSpeechStatus } from '../../../speech/common/speechService.js'; import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; import { IChatExecuteActionContext } from '../../browser/actions/chatExecuteActions.js'; -import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../../browser/chat.js'; -import { IChatAgentService } from '../../common/chatAgents.js'; -import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatResponseModel } from '../../common/chatModel.js'; -import { KEYWORD_ACTIVIATION_SETTING_ID } from '../../common/chatService.js'; -import { ChatResponseViewModel, IChatResponseViewModel, isResponseVM } from '../../common/chatViewModel.js'; +import { IChatWidget, IChatWidgetService, IQuickChatService } from '../../browser/chat.js'; +import { IChatAgentService } from '../../common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; +import { IChatResponseModel } from '../../common/model/chatModel.js'; +import { KEYWORD_ACTIVIATION_SETTING_ID } from '../../common/chatService/chatService.js'; +import { ChatResponseViewModel, IChatResponseViewModel, isResponseVM } from '../../common/model/chatViewModel.js'; import { ChatAgentLocation } from '../../common/constants.js'; import { VoiceChatInProgress as GlobalVoiceChatInProgress, IVoiceChatService } from '../../common/voiceChatService.js'; import './media/voiceChatActions.css'; @@ -64,7 +63,6 @@ const VoiceChatSessionContexts: VoiceChatSessionContext[] = ['view', 'inline', ' // Global Context Keys (set on global context key service) const CanVoiceChat = ContextKeyExpr.and(ChatContextKeys.enabled, HasSpeechProvider); const FocusInChatInput = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, ChatContextKeys.inChatInput); -const AnyChatRequestInProgress = ChatContextKeys.requestInProgress; // Scoped Context Keys (set on per-chat-context scoped context key service) const ScopedVoiceChatGettingReady = new RawContextKey('scopedVoiceChatGettingReady', false, { type: 'boolean', description: localize('scopedVoiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat. This key is only defined scoped, per chat context.") }); @@ -103,7 +101,6 @@ class VoiceChatSessionControllerFactory { const quickChatService = accessor.get(IQuickChatService); const layoutService = accessor.get(IWorkbenchLayoutService); const editorService = accessor.get(IEditorService); - const viewsService = accessor.get(IViewsService); switch (context) { case 'focused': { @@ -111,7 +108,7 @@ class VoiceChatSessionControllerFactory { return controller ?? VoiceChatSessionControllerFactory.create(accessor, 'view'); // fallback to 'view' } case 'view': { - const chatWidget = await showChatView(viewsService, layoutService); + const chatWidget = await chatWidgetService.revealWidget(); if (chatWidget) { return VoiceChatSessionControllerFactory.doCreateForChatWidget('view', chatWidget); } @@ -434,10 +431,7 @@ export class VoiceChatInChatViewAction extends VoiceChatWithHoldModeAction { id: VoiceChatInChatViewAction.ID, title: localize2('workbench.action.chat.voiceChatInView.label', "Voice Chat in Chat View"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and( - CanVoiceChat, - ChatContextKeys.requestInProgress.negate() // disable when a chat request is in progress - ), + precondition: CanVoiceChat, f1: true }, 'view'); } @@ -475,8 +469,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { const instantiationService = accessor.get(IInstantiationService); const keybindingService = accessor.get(IKeybindingService); - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); + const widgetService = accessor.get(IChatWidgetService); const holdMode = keybindingService.enableKeybindingHoldMode(HoldToVoiceChatInChatViewAction.ID); @@ -489,7 +482,7 @@ export class HoldToVoiceChatInChatViewAction extends Action2 { } }, VOICE_KEY_HOLD_THRESHOLD); - (await showChatView(viewsService, layoutService))?.focusInput(); + (await widgetService.revealWidget())?.focusInput(); await holdMode; handle.dispose(); @@ -512,7 +505,6 @@ export class InlineVoiceChatAction extends VoiceChatWithHoldModeAction { precondition: ContextKeyExpr.and( CanVoiceChat, ActiveEditorContext, - ChatContextKeys.requestInProgress.negate() // disable when a chat request is in progress ), f1: true }, 'inline'); @@ -528,10 +520,7 @@ export class QuickVoiceChatAction extends VoiceChatWithHoldModeAction { id: QuickVoiceChatAction.ID, title: localize2('workbench.action.chat.quickVoiceChat.label', "Quick Voice Chat"), category: CHAT_CATEGORY, - precondition: ContextKeyExpr.and( - CanVoiceChat, - ChatContextKeys.requestInProgress.negate() // disable when a chat request is in progress - ), + precondition: CanVoiceChat, f1: true }, 'quick'); } @@ -577,7 +566,6 @@ export class StartVoiceChatAction extends Action2 { precondition: ContextKeyExpr.and( CanVoiceChat, ScopedVoiceChatGettingReady.negate(), // disable when voice chat is getting ready - AnyChatRequestInProgress?.negate(), // disable when any chat request is in progress SpeechToTextInProgress.negate() // disable when speech to text is in progress ), menu: primaryVoiceActionMenu(ContextKeyExpr.and( @@ -688,8 +676,7 @@ class ChatSynthesizerSessionController { const contextKeyService = accessor.get(IContextKeyService); let chatWidget = chatWidgetService.getWidgetBySessionResource(response.session.sessionResource); if (chatWidget?.location === ChatAgentLocation.EditorInline) { - // workaround for https://github.com/microsoft/vscode/issues/212785 - chatWidget = chatWidgetService.lastFocusedWidget; + chatWidget = chatWidgetService.lastFocusedWidget; // workaround for https://github.com/microsoft/vscode/issues/212785 } return { @@ -871,8 +858,8 @@ export class ReadChatResponseAloud extends Action2 { when: ContextKeyExpr.and( CanVoiceChat, ChatContextKeys.isResponse, // only for responses - ScopedChatSynthesisInProgress.negate(), // but not when already in progress - ChatContextKeys.responseIsFiltered.negate(), // and not when response is filtered + ScopedChatSynthesisInProgress.negate(), // but not when already in progress + ChatContextKeys.responseIsFiltered.negate(), // and not when response is filtered ), group: 'navigation', order: -10 // first @@ -881,7 +868,7 @@ export class ReadChatResponseAloud extends Action2 { when: ContextKeyExpr.and( CanVoiceChat, ChatContextKeys.isResponse, // only for responses - ScopedChatSynthesisInProgress.negate(), // but not when already in progress + ScopedChatSynthesisInProgress.negate(), // but not when already in progress ChatContextKeys.responseIsFiltered.negate() // and not when response is filtered ), group: 'navigation', @@ -980,7 +967,7 @@ export class StopReadChatItemAloud extends Action2 { { id: MenuId.ChatMessageFooter, when: ContextKeyExpr.and( - ScopedChatSynthesisInProgress, // only when in progress + ScopedChatSynthesisInProgress, // only when in progress ChatContextKeys.isResponse, // only for responses ChatContextKeys.responseIsFiltered.negate() // but not when response is filtered ), @@ -990,7 +977,7 @@ export class StopReadChatItemAloud extends Action2 { { id: MENU_INLINE_CHAT_WIDGET_SECONDARY, when: ContextKeyExpr.and( - ScopedChatSynthesisInProgress, // only when in progress + ScopedChatSynthesisInProgress, // only when in progress ChatContextKeys.isResponse, // only for responses ChatContextKeys.responseIsFiltered.negate() // but not when response is filtered ), @@ -1256,7 +1243,7 @@ registerThemingParticipant((theme, collector) => { let activeRecordingColor: Color | undefined; let activeRecordingDimmedColor: Color | undefined; if (!isHighContrast(theme.type)) { - activeRecordingColor = theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND) ?? theme.getColor(focusBorder); + activeRecordingColor = theme.getColor(ACTIVITY_BAR_FOREGROUND) ?? theme.getColor(focusBorder); activeRecordingDimmedColor = activeRecordingColor?.transparent(0.38); } else { activeRecordingColor = theme.getColor(contrastBorder); diff --git a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts new file mode 100644 index 00000000000..274ca60b475 --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../../nls.js'; +import { Action2 } from '../../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; +import { INativeEnvironmentService } from '../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { INativeHostService } from '../../../../../platform/native/common/native.js'; +import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; +import { IWorkbenchModeService } from '../../../../services/layout/common/workbenchModeService.js'; +import { IsAgentSessionsWorkspaceContext, WorkbenchModeContext } from '../../../../common/contextkeys.js'; +import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; +import { ProductQualityContext } from '../../../../../platform/contextkey/common/contextkeys.js'; + +export class OpenAgentSessionsWindowAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.openAgentSessionsWindow', + title: localize2('openAgentSessionsWindow', "Open Agent Sessions Window"), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and(ProductQualityContext.notEqualsTo('stable'), ChatEntitlementContextKeys.Setup.hidden.negate()), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const environmentService = accessor.get(INativeEnvironmentService); + const nativeHostService = accessor.get(INativeHostService); + const fileService = accessor.get(IFileService); + + // Create workspace file if it doesn't exist + const workspaceUri = environmentService.agentSessionsWorkspace; + if (!workspaceUri) { + throw new Error('Agent Sessions workspace is not configured'); + } + + const workspaceExists = await fileService.exists(workspaceUri); + if (!workspaceExists) { + const emptyWorkspaceContent = JSON.stringify({ folders: [] }, null, '\t'); + await fileService.writeFile(workspaceUri, VSBuffer.fromString(emptyWorkspaceContent)); + } + + await nativeHostService.openWindow([{ workspaceUri }], { forceNewWindow: true }); + } +} + +export class SwitchToAgentSessionsModeAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.switchToAgentSessionsMode', + title: localize2('switchToAgentSessionsMode', "Switch to Agent Sessions Mode"), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + ProductQualityContext.notEqualsTo('stable'), + ChatEntitlementContextKeys.Setup.hidden.negate(), + IsAgentSessionsWorkspaceContext.toNegated(), + WorkbenchModeContext.notEqualsTo('agent-sessions') + ), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const workbenchModeService = accessor.get(IWorkbenchModeService); + await workbenchModeService.setWorkbenchMode('agent-sessions'); + } +} + +export class SwitchToNormalModeAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.switchToNormalMode', + title: localize2('switchToNormalMode', "Switch to Default Mode"), + category: CHAT_CATEGORY, + precondition: ContextKeyExpr.and( + ProductQualityContext.notEqualsTo('stable'), + ChatEntitlementContextKeys.Setup.hidden.negate(), + IsAgentSessionsWorkspaceContext.toNegated(), + WorkbenchModeContext.notEqualsTo('') + ), + f1: true, + }); + } + + async run(accessor: ServicesAccessor) { + const workbenchModeService = accessor.get(IWorkbenchModeService); + await workbenchModeService.setWorkbenchMode(undefined); + } +} diff --git a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts similarity index 75% rename from src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts rename to src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts index 7fc8b70b6f9..d59f63292d7 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/tools/fetchPageTool.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/fetchPageTool.ts @@ -6,6 +6,8 @@ import { assertNever } from '../../../../../base/common/assert.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; +import { Iterable } from '../../../../../base/common/iterator.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; import { extname } from '../../../../../base/common/path.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; @@ -13,15 +15,16 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { IWebContentExtractorService, WebContentExtractResult } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { detectEncodingFromBuffer } from '../../../../services/textfile/common/encoding.js'; import { ITrustedDomainService } from '../../../url/browser/trustedDomainService.js'; +import { IChatService } from '../../common/chatService/chatService.js'; import { ChatImageMimeType } from '../../common/languageModels.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultDataPart, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/languageModelToolsService.js'; -import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultDataPart, IToolResultTextPart, ToolDataSource, ToolProgress } from '../../common/tools/languageModelToolsService.js'; +import { InternalFetchWebPageToolId } from '../../common/tools/builtinTools/tools.js'; export const FetchWebPageToolData: IToolData = { id: InternalFetchWebPageToolId, displayName: 'Fetch Web Page', canBeReferencedInPrompt: false, - modelDescription: localize('fetchWebPage.modelDescription', 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'), + modelDescription: 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.', source: ToolDataSource.Internal, canRequestPostApproval: true, canRequestPreApproval: true, @@ -40,6 +43,10 @@ export const FetchWebPageToolData: IToolData = { } }; +export interface IFetchWebPageToolParams { + urls?: string[]; +} + type ResultType = string | { type: 'tooldata'; value: IToolResultDataPart } | { type: 'extracted'; value: WebContentExtractResult } | undefined; export class FetchWebPageTool implements IToolImpl { @@ -47,11 +54,12 @@ export class FetchWebPageTool implements IToolImpl { constructor( @IWebContentExtractorService private readonly _readerModeService: IWebContentExtractorService, @IFileService private readonly _fileService: IFileService, - @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService + @ITrustedDomainService private readonly _trustedDomainService: ITrustedDomainService, + @IChatService private readonly _chatService: IChatService, ) { } async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { - const urls = (invocation.parameters as { urls?: string[] }).urls || []; + const urls = (invocation.parameters as IFetchWebPageToolParams).urls || []; const { webUris, fileUris, invalidUris } = this._parseUris(urls); const allValidUris = [...webUris.values(), ...fileUris.values()]; @@ -62,7 +70,11 @@ export class FetchWebPageTool implements IToolImpl { } // Get contents from web URIs - const webContents = webUris.size > 0 ? await this._readerModeService.extract([...webUris.values()]) : []; + let webContents: WebContentExtractResult[] = []; + if (webUris.size > 0) { + const trustedDomains = this._trustedDomainService.trustedDomains; + webContents = await this._readerModeService.extract([...webUris.values()], { trustedDomains }); + } // Get contents from file URIs const fileContents: (string | { type: 'tooldata'; value: IToolResultDataPart } | undefined)[] = []; @@ -135,7 +147,7 @@ export class FetchWebPageTool implements IToolImpl { const actuallyValidUris = [...webUris.values(), ...successfulFileUris]; return { - content: this._getPromptPartsForResults(results), + content: this._getPromptPartsForResults(urls, results), toolResultDetails: actuallyValidUris, confirmResults, }; @@ -158,7 +170,7 @@ export class FetchWebPageTool implements IToolImpl { } const invalid = [...Array.from(invalidUris), ...additionalInvalidUrls]; - const urlsNeedingConfirmation = [...webUris.values(), ...validFileUris]; + const urlsNeedingConfirmation = new ResourceSet([...webUris.values(), ...validFileUris]); const pastTenseMessage = invalid.length ? invalid.length > 1 @@ -166,7 +178,7 @@ export class FetchWebPageTool implements IToolImpl { ? new MarkdownString( localize( 'fetchWebPage.pastTenseMessage.plural', - 'Fetched {0} resources, but the following were invalid URLs:\n\n{1}\n\n', urlsNeedingConfirmation.length, invalid.map(url => `- ${url}`).join('\n') + 'Fetched {0} resources, but the following were invalid URLs:\n\n{1}\n\n', urlsNeedingConfirmation.size, invalid.map(url => `- ${url}`).join('\n') )) // If there is only one invalid URL, show it : new MarkdownString( @@ -178,11 +190,11 @@ export class FetchWebPageTool implements IToolImpl { : new MarkdownString(); const invocationMessage = new MarkdownString(); - if (urlsNeedingConfirmation.length > 1) { - pastTenseMessage.appendMarkdown(localize('fetchWebPage.pastTenseMessageResult.plural', 'Fetched {0} resources', urlsNeedingConfirmation.length)); - invocationMessage.appendMarkdown(localize('fetchWebPage.invocationMessage.plural', 'Fetching {0} resources', urlsNeedingConfirmation.length)); - } else if (urlsNeedingConfirmation.length === 1) { - const url = urlsNeedingConfirmation[0].toString(); + if (urlsNeedingConfirmation.size > 1) { + pastTenseMessage.appendMarkdown(localize('fetchWebPage.pastTenseMessageResult.plural', 'Fetched {0} resources', urlsNeedingConfirmation.size)); + invocationMessage.appendMarkdown(localize('fetchWebPage.invocationMessage.plural', 'Fetching {0} resources', urlsNeedingConfirmation.size)); + } else if (urlsNeedingConfirmation.size === 1) { + const url = Iterable.first(urlsNeedingConfirmation)!.toString(true); // If the URL is too long or it's a file url, show it as a link... otherwise, show it as plain text if (url.length > 400 || validFileUris.length === 1) { pastTenseMessage.appendMarkdown(localize({ @@ -205,22 +217,40 @@ export class FetchWebPageTool implements IToolImpl { } } + let confirmationNotNeededReason: string | undefined; + if (context.chatSessionResource) { + const model = this._chatService.getSession(context.chatSessionResource); + const userMessages = model?.getRequests().map(r => r.message.text.toLowerCase()); + let urlsMentionedInPrompt = false; + for (const uri of urlsNeedingConfirmation) { + // Normalize to lowercase and remove any trailing slash + const toToCheck = uri.toString(true).toLowerCase().replace(/\/$/, ''); + if (userMessages?.some(m => m.includes(toToCheck))) { + urlsNeedingConfirmation.delete(uri); + urlsMentionedInPrompt = true; + } + } + if (urlsMentionedInPrompt && urlsNeedingConfirmation.size === 0) { + confirmationNotNeededReason = localize('fetchWebPage.urlMentionedInPrompt', 'Auto approved because URL was in prompt'); + } + } + const result: IPreparedToolInvocation = { invocationMessage, pastTenseMessage }; - const allDomainsTrusted = urlsNeedingConfirmation.every(u => this._trustedDomainService.isValid(u)); + const allDomainsTrusted = Iterable.every(urlsNeedingConfirmation, u => this._trustedDomainService.isValid(u)); let confirmationTitle: string | undefined; let confirmationMessage: string | MarkdownString | undefined; - if (urlsNeedingConfirmation.length && !allDomainsTrusted) { - if (urlsNeedingConfirmation.length === 1) { + if (urlsNeedingConfirmation.size && !allDomainsTrusted) { + if (urlsNeedingConfirmation.size === 1) { confirmationTitle = localize('fetchWebPage.confirmationTitle.singular', 'Fetch web page?'); confirmationMessage = new MarkdownString( - urlsNeedingConfirmation[0].toString(), + Iterable.first(urlsNeedingConfirmation)!.toString(true), { supportThemeIcons: true } ); } else { confirmationTitle = localize('fetchWebPage.confirmationTitle.plural', 'Fetch web pages?'); confirmationMessage = new MarkdownString( - urlsNeedingConfirmation.map(uri => `- ${uri.toString()}`).join('\n'), + [...urlsNeedingConfirmation].map(uri => `- ${uri.toString(true)}`).join('\n'), { supportThemeIcons: true } ); } @@ -228,9 +258,10 @@ export class FetchWebPageTool implements IToolImpl { result.confirmationMessages = { title: confirmationTitle, message: confirmationMessage, - confirmResults: urlsNeedingConfirmation.length > 0, + confirmResults: urlsNeedingConfirmation.size > 0, allowAutoConfirm: true, - disclaimer: new MarkdownString('$(info) ' + localize('fetchWebPage.confirmationMessage.plural', 'Web content may contain malicious code or attempt prompt injection attacks.'), { supportThemeIcons: true }) + disclaimer: new MarkdownString('$(info) ' + localize('fetchWebPage.confirmationMessage.plural', 'Web content may contain malicious code or attempt prompt injection attacks.'), { supportThemeIcons: true }), + confirmationNotNeededReason }; return result; } @@ -257,28 +288,31 @@ export class FetchWebPageTool implements IToolImpl { return { webUris, fileUris, invalidUris }; } - private _getPromptPartsForResults(results: ResultType[]): (IToolResultTextPart | IToolResultDataPart)[] { - return results.map(value => { + private _getPromptPartsForResults(urls: string[], results: ResultType[]): (IToolResultTextPart | IToolResultDataPart)[] { + return results.map((value, i) => { + const title = results.length > 1 ? localize('fetchWebPage.fetchedFrom', 'Fetched from {0}', urls[i]) : undefined; if (!value) { return { kind: 'text', + title, value: localize('fetchWebPage.invalidUrl', 'Invalid URL') }; } else if (typeof value === 'string') { return { kind: 'text', + title, value: value }; } else if (value.type === 'tooldata') { - return value.value; + return { ...value.value, title }; } else if (value.type === 'extracted') { switch (value.value.status) { case 'ok': - return { kind: 'text', value: value.value.result }; + return { kind: 'text', title, value: value.value.result }; case 'redirect': - return { kind: 'text', value: `The webpage has redirected to "${value.value.toURI.toString(true)}". Use the ${InternalFetchWebPageToolId} again to get its contents.` }; + return { kind: 'text', title, value: `The webpage has redirected to "${value.value.toURI.toString(true)}". Use the ${InternalFetchWebPageToolId} again to get its contents.` }; case 'error': - return { kind: 'text', value: `An error occurred retrieving the fetch result: ${value.value.error}` }; + return { kind: 'text', title, value: `An error occurred retrieving the fetch result: ${value.value.error}` }; default: assertNever(value.value); } diff --git a/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts new file mode 100644 index 00000000000..b87ac4675e9 --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-browser/builtInTools/tools.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../../common/contributions.js'; +import { ChatExternalPathConfirmationContribution } from '../../common/tools/builtinTools/chatExternalPathConfirmation.js'; +import { ChatUrlFetchingConfirmationContribution } from '../../common/tools/builtinTools/chatUrlFetchingConfirmation.js'; +import { ILanguageModelToolsConfirmationService } from '../../common/tools/languageModelToolsConfirmationService.js'; +import { ILanguageModelToolsService } from '../../common/tools/languageModelToolsService.js'; +import { InternalFetchWebPageToolId } from '../../common/tools/builtinTools/tools.js'; +import { FetchWebPageTool, FetchWebPageToolData, IFetchWebPageToolParams } from './fetchPageTool.js'; + +export class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution { + + static readonly ID = 'chat.nativeBuiltinTools'; + + constructor( + @ILanguageModelToolsService toolsService: ILanguageModelToolsService, + @IInstantiationService instantiationService: IInstantiationService, + @ILanguageModelToolsConfirmationService confirmationService: ILanguageModelToolsConfirmationService, + ) { + super(); + + const editTool = instantiationService.createInstance(FetchWebPageTool); + this._register(toolsService.registerTool(FetchWebPageToolData, editTool)); + + this._register(confirmationService.registerConfirmationContribution( + InternalFetchWebPageToolId, + instantiationService.createInstance( + ChatUrlFetchingConfirmationContribution, + params => (params as IFetchWebPageToolParams).urls + ) + )); + + // Register external path confirmation contribution for read_file and list_dir + // They share the same allowlist so approving a folder for reading files also allows listing that directory + const externalPathConfirmation = new ChatExternalPathConfirmationContribution( + (ref) => { + const params = ref.parameters as { filePath?: string; path?: string }; + // read_file uses filePath (it's a file), list_dir uses path (it's a directory) + if (params?.filePath) { + return { path: params.filePath, isDirectory: false }; + } + if (params?.path) { + return { path: params.path, isDirectory: true }; + } + return undefined; + } + ); + + this._register(confirmationService.registerConfirmationContribution( + 'copilot_readFile', + externalPathConfirmation + )); + + this._register(confirmationService.registerConfirmationContribution( + 'copilot_listDirectory', + externalPathConfirmation + )); + } +} diff --git a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts index 0459a36db92..3abfc386a4c 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/chat.contribution.ts @@ -14,41 +14,28 @@ import { registerAction2 } from '../../../../platform/actions/common/actions.js' import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; -import { IWorkbenchContribution, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; +import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { ViewContainerLocation } from '../../../common/views.js'; import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ACTION_ID_NEW_CHAT, CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../browser/actions/chatActions.js'; -import { showChatView } from '../browser/chat.js'; -import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { IChatService } from '../common/chatService.js'; +import { IChatWidgetService } from '../browser/chat.js'; +import { AgentSessionProviders } from '../browser/agentSessions/agentSessions.js'; +import { isSessionInProgressStatus } from '../browser/agentSessions/agentSessionsModel.js'; +import { IAgentSessionsService } from '../browser/agentSessions/agentSessionsService.js'; +import { ChatContextKeys } from '../common/actions/chatContextKeys.js'; import { ChatModeKind } from '../common/constants.js'; -import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; +import { IChatService } from '../common/chatService/chatService.js'; import { registerChatDeveloperActions } from './actions/chatDeveloperActions.js'; +import { registerChatExportZipAction } from './actions/chatExportZip.js'; import { HoldToVoiceChatInChatViewAction, InlineVoiceChatAction, KeywordActivationContribution, QuickVoiceChatAction, ReadChatResponseAloud, StartVoiceChatAction, StopListeningAction, StopListeningAndSubmitAction, StopReadAloud, StopReadChatItemAloud, VoiceChatInChatViewAction } from './actions/voiceChatActions.js'; -import { FetchWebPageTool, FetchWebPageToolData } from './tools/fetchPageTool.js'; - -class NativeBuiltinToolsContribution extends Disposable implements IWorkbenchContribution { - - static readonly ID = 'chat.nativeBuiltinTools'; - - constructor( - @ILanguageModelToolsService toolsService: ILanguageModelToolsService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - const editTool = instantiationService.createInstance(FetchWebPageTool); - this._register(toolsService.registerTool(FetchWebPageToolData, editTool)); - } -} +import { NativeBuiltinToolsContribution } from './builtInTools/tools.js'; +import { OpenAgentSessionsWindowAction, SwitchToAgentSessionsModeAction, SwitchToNormalModeAction } from './agentSessions/agentSessionsActions.js'; class ChatCommandLineHandler extends Disposable { @@ -58,7 +45,6 @@ class ChatCommandLineHandler extends Disposable { @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IViewsService private readonly viewsService: IViewsService, @ILogService private readonly logService: ILogService, @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, @IContextKeyService private readonly contextKeyService: IContextKeyService @@ -96,8 +82,6 @@ class ChatCommandLineHandler extends Disposable { attachFiles: args['add-file']?.map(file => URI.file(resolve(file))), // use `resolve` to deal with relative paths properly }; - const chatWidget = await showChatView(this.viewsService, this.layoutService); - if (args.maximize) { const location = this.contextKeyService.getContextKeyValue(ChatContextKeys.panelLocation.key); if (location === ViewContainerLocation.AuxiliaryBar) { @@ -107,7 +91,6 @@ class ChatCommandLineHandler extends Disposable { } } - await chatWidget?.waitForReady(); await this.commandService.executeCommand(ACTION_ID_NEW_CHAT); await this.commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts); } @@ -140,12 +123,12 @@ class ChatLifecycleHandler extends Disposable { constructor( @ILifecycleService lifecycleService: ILifecycleService, - @IChatService private readonly chatService: IChatService, + @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @IDialogService private readonly dialogService: IDialogService, - @IViewsService private readonly viewsService: IViewsService, + @IChatWidgetService private readonly widgetService: IChatWidgetService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionService extensionService: IExtensionService, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, ) { super(); @@ -154,13 +137,22 @@ class ChatLifecycleHandler extends Disposable { })); this._register(extensionService.onWillStop(e => { - e.veto(this.chatService.requestInProgressObs.get(), localize('chatRequestInProgress', "A chat request is in progress.")); + e.veto(this.hasNonCloudSessionInProgress(), localize('chatRequestInProgress', "A chat request is in progress.")); })); } + private hasNonCloudSessionInProgress(): boolean { + return this.agentSessionsService.model.sessions.some(session => + isSessionInProgressStatus(session.status) && session.providerType !== AgentSessionProviders.Cloud + ); + } + private shouldVetoShutdown(reason: ShutdownReason): boolean | Promise { - const running = this.chatService.requestInProgressObs.read(undefined); - if (!running) { + if (this.environmentService.enableSmokeTestDriver) { + return false; + } + + if (!this.hasNonCloudSessionInProgress()) { return false; } @@ -173,7 +165,7 @@ class ChatLifecycleHandler extends Disposable { private async doShouldVetoShutdown(reason: ShutdownReason): Promise { - showChatView(this.viewsService, this.layoutService); + this.widgetService.revealWidget(); let message: string; let detail: string; @@ -202,6 +194,9 @@ class ChatLifecycleHandler extends Disposable { } } +registerAction2(OpenAgentSessionsWindowAction); +registerAction2(SwitchToAgentSessionsModeAction); +registerAction2(SwitchToNormalModeAction); registerAction2(StartVoiceChatAction); registerAction2(VoiceChatInChatViewAction); @@ -217,6 +212,7 @@ registerAction2(StopReadChatItemAloud); registerAction2(StopReadAloud); registerChatDeveloperActions(); +registerChatExportZipAction(); registerWorkbenchContribution2(KeywordActivationContribution.ID, KeywordActivationContribution, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(NativeBuiltinToolsContribution.ID, NativeBuiltinToolsContribution, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts new file mode 100644 index 00000000000..728883ddfe1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { Location } from '../../../../../../editor/common/languages.js'; +import { getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js'; +import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData } from '../../../common/chatService/chatService.js'; + +suite('ChatResponseAccessibleView', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('getToolSpecificDataDescription', () => { + test('returns empty string for undefined', () => { + assert.strictEqual(getToolSpecificDataDescription(undefined), ''); + }); + + test('returns command line for terminal data', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci', + userEdited: 'npm install --save-dev' + }, + language: 'bash' + }; + // Should prefer userEdited over toolEdited over original + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install --save-dev'); + }); + + test('returns tool edited command for terminal data without user edit', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install', + toolEdited: 'npm ci' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm ci'); + }); + + test('returns original command for terminal data without edits', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { + original: 'npm install' + }, + language: 'bash' + }; + assert.strictEqual(getToolSpecificDataDescription(terminalData), 'npm install'); + }); + + test('returns description for subagent data', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'TestAgent', + description: 'Running analysis', + prompt: 'Analyze the code' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.ok(result.includes('TestAgent')); + assert.ok(result.includes('Running analysis')); + assert.ok(result.includes('Analyze the code')); + }); + + test('handles subagent with only description', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + description: 'Running analysis' + }; + const result = getToolSpecificDataDescription(subagentData); + assert.strictEqual(result, 'Running analysis'); + }); + + test('returns extensions list for extensions data', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: ['eslint', 'prettier', 'typescript'] + }; + const result = getToolSpecificDataDescription(extensionsData); + assert.ok(result.includes('eslint')); + assert.ok(result.includes('prettier')); + assert.ok(result.includes('typescript')); + }); + + test('returns empty for empty extensions array', () => { + const extensionsData: IChatExtensionsContent = { + kind: 'extensions', + extensions: [] + }; + assert.strictEqual(getToolSpecificDataDescription(extensionsData), ''); + }); + + test('returns todo list description for todoList data', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + todoList: [ + { id: '1', title: 'Task 1', status: 'in-progress' }, + { id: '2', title: 'Task 2', status: 'completed' } + ] + }; + const result = getToolSpecificDataDescription(todoData); + assert.ok(result.includes('2 items')); + assert.ok(result.includes('Task 1')); + assert.ok(result.includes('in-progress')); + assert.ok(result.includes('Task 2')); + assert.ok(result.includes('completed')); + }); + + test('returns empty for empty todo list', () => { + const todoData: IChatTodoListContent = { + kind: 'todoList', + todoList: [] + }; + assert.strictEqual(getToolSpecificDataDescription(todoData), ''); + }); + + test('returns PR info for pullRequest data', () => { + const prData: IChatPullRequestContent = { + kind: 'pullRequest', + uri: URI.file('/test'), + title: 'Add new feature', + description: 'This PR adds a great feature', + author: 'testuser', + linkTag: '#123' + }; + const result = getToolSpecificDataDescription(prData); + assert.ok(result.includes('Add new feature')); + assert.ok(result.includes('testuser')); + }); + + test('returns raw input for input data (string)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: 'some input string' + }; + assert.strictEqual(getToolSpecificDataDescription(inputData), 'some input string'); + }); + + test('returns JSON stringified for input data (object)', () => { + const inputData: IChatToolInputInvocationData = { + kind: 'input', + rawInput: { key: 'value', nested: { data: 123 } } + }; + const result = getToolSpecificDataDescription(inputData); + assert.ok(result.includes('key')); + assert.ok(result.includes('value')); + }); + }); + + suite('getResultDetailsDescription', () => { + test('returns empty object for undefined', () => { + assert.deepStrictEqual(getResultDetailsDescription(undefined), {}); + }); + + test('returns files for URI array', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getResultDetailsDescription(uris); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + assert.ok(result.files![0].includes('file1.ts')); + assert.ok(result.files![1].includes('file2.ts')); + }); + + test('returns files for Location array', () => { + const locations: Location[] = [ + { uri: URI.file('/path/to/file1.ts'), range: new Range(1, 1, 10, 1) }, + { uri: URI.file('/path/to/file2.ts'), range: new Range(5, 1, 15, 1) } + ]; + const result = getResultDetailsDescription(locations); + assert.ok(result.files); + assert.strictEqual(result.files!.length, 2); + }); + + test('returns input and isError for IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: false + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.input, 'create_file path=/test/file.ts'); + assert.strictEqual(result.isError, false); + }); + + test('returns isError true for errored IToolResultInputOutputDetails', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getResultDetailsDescription(details); + assert.strictEqual(result.isError, true); + }); + }); + + suite('getToolInvocationA11yDescription', () => { + test('returns invocation message when not complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + false + ); + assert.strictEqual(result, 'Creating file'); + }); + + test('returns past tense message when complete', () => { + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + undefined, + true + ); + assert.strictEqual(result, 'Created file'); + }); + + test('includes tool-specific data description', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + undefined, + true + ); + assert.ok(result.includes('Ran command')); + assert.ok(result.includes('npm test')); + }); + + test('includes files from result details when complete', () => { + const uris = [ + URI.file('/path/to/file1.ts'), + URI.file('/path/to/file2.ts') + ]; + const result = getToolInvocationA11yDescription( + 'Creating files', + 'Created files', + undefined, + uris, + true + ); + assert.ok(result.includes('Created files')); + assert.ok(result.includes('file1.ts')); + assert.ok(result.includes('file2.ts')); + }); + + test('includes error status when result has error', () => { + const details = { + input: 'create_file path=/test/file.ts', + output: [], + isError: true + }; + const result = getToolInvocationA11yDescription( + 'Creating file', + 'Created file', + undefined, + details, + true + ); + assert.ok(result.includes('Errored')); + }); + + test('does not show input when tool-specific data is provided', () => { + const terminalData: IChatTerminalToolInvocationData = { + kind: 'terminal', + commandLine: { original: 'npm test' }, + language: 'bash' + }; + const details = { + input: 'some redundant input', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Running command', + 'Ran command', + terminalData, + details, + true + ); + // Should have tool-specific data but not the "Input:" label + assert.ok(result.includes('npm test')); + assert.ok(!result.includes('Input:')); + }); + + test('shows input when no tool-specific data', () => { + const details = { + input: 'apply_patch file=/test/file.ts', + output: [], + isError: false + }; + const result = getToolInvocationA11yDescription( + 'Applying patch', + 'Applied patch', + undefined, + details, + true + ); + assert.ok(result.includes('Applied patch')); + assert.ok(result.includes('Input:')); + assert.ok(result.includes('apply_patch')); + }); + + test('handles all parts together', () => { + const subagentData: IChatSubagentToolInvocationData = { + kind: 'subagent', + agentName: 'CodeReviewer', + description: 'Reviewing code changes' + }; + const uris = [URI.file('/src/test.ts')]; + const result = getToolInvocationA11yDescription( + 'Starting code review', + 'Completed code review', + subagentData, + uris, + true + ); + assert.ok(result.includes('Completed code review')); + assert.ok(result.includes('CodeReviewer')); + assert.ok(result.includes('Reviewing code changes')); + assert.ok(result.includes('test.ts')); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts new file mode 100644 index 00000000000..105de058437 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/actions/chatCustomizationDiagnosticsAction.test.ts @@ -0,0 +1,524 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Schemas } from '../../../../../../base/common/network.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { formatStatusOutput, IFileStatusInfo, IPathInfo, ITypeStatusInfo } from '../../../browser/actions/chatCustomizationDiagnosticsAction.js'; +import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; +import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; + +suite('formatStatusOutput', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + const emptySpecialFiles = { + agentsMd: { enabled: false, files: [] }, + copilotInstructions: { enabled: false, files: [] } + }; + + function createPath(displayPath: string, exists: boolean, storage: PromptsStorage = PromptsStorage.local, isDefault = true): IPathInfo { + return { + uri: URI.file(`/workspace/${displayPath.replace(/^~\//, 'home/')}`), + exists, + storage, + scanOrder: 1, + displayPath, + isDefault + }; + } + + function createFile(name: string, status: 'loaded' | 'skipped' | 'overwritten', parentPath: string, storage: PromptsStorage = PromptsStorage.local, options?: { reason?: string; extensionId?: string; overwrittenBy?: string }): IFileStatusInfo { + return { + uri: URI.file(`/workspace/${parentPath}/${name}`), + status, + name, + storage, + reason: options?.reason, + extensionId: options?.extensionId, + overwrittenBy: options?.overwrittenBy + }; + } + + + + /** + * Returns the fsPath of a file URI for use in test expectations. + * Normalizes to forward slashes for cross-platform consistency in markdown links. + */ + function filePath(relativePath: string): string { + return URI.file(`/workspace/${relativePath}`).fsPath.replace(/\\/g, '/'); + } + + /** + * Builds expected output from lines array to avoid hygiene issues with template literal indentation. + */ + function lines(...parts: string[]): string { + return parts.join('\n'); + } + + // Tree prefixes + // allow-any-unicode-next-line + const TREE_BRANCH = '├─'; + // allow-any-unicode-next-line + const TREE_END = '└─'; + // allow-any-unicode-next-line + const ICON_ERROR = '❌'; + // allow-any-unicode-next-line + const ICON_WARN = '⚠️'; + + test('agents with loaded files', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [createPath('.github/agents', true)], + files: [ + createFile('code-reviewer.agent.md', 'loaded', '.github/agents'), + createFile('test-helper.agent.md', 'loaded', '.github/agents') + ], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '*2 files loaded*', + '', + '.github/agents
', + `${TREE_BRANCH} [\`code-reviewer.agent.md\`](${filePath('.github/agents/code-reviewer.agent.md')})
`, + `${TREE_END} [\`test-helper.agent.md\`](${filePath('.github/agents/test-helper.agent.md')})
`, + '' + )); + }); + + test('agents with loaded and skipped files', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [createPath('.github/agents', true)], + files: [ + createFile('good-agent.agent.md', 'loaded', '.github/agents'), + createFile('broken-agent.agent.md', 'skipped', '.github/agents', PromptsStorage.local, { reason: 'Missing name attribute' }) + ], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '*1 file loaded, 1 skipped*', + '', + '.github/agents
', + `${TREE_BRANCH} [\`good-agent.agent.md\`](${filePath('.github/agents/good-agent.agent.md')})
`, + `${TREE_END} ${ICON_ERROR} [\`broken-agent.agent.md\`](${filePath('.github/agents/broken-agent.agent.md')}) - *Missing name attribute*
`, + '' + )); + }); + + test('agents with overwritten files', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [ + createPath('.github/agents', true), + createPath('~/.copilot/agents', true, PromptsStorage.user) + ], + files: [ + createFile('my-agent.agent.md', 'loaded', '.github/agents'), + createFile('my-agent.agent.md', 'overwritten', 'home/.copilot/agents', PromptsStorage.user, { overwrittenBy: 'my-agent.agent.md' }) + ], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '*1 file loaded, 1 skipped*', + '', + '.github/agents
', + `${TREE_END} [\`my-agent.agent.md\`](${filePath('.github/agents/my-agent.agent.md')})
`, + '~/.copilot/agents
', + `${TREE_END} ${ICON_WARN} [\`my-agent.agent.md\`](${filePath('home/.copilot/agents/my-agent.agent.md')}) - *Overwritten by higher priority file*
`, + '' + )); + }); + + test('disabled skills shows setting hint', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.skill, + paths: [], + files: [], + enabled: false + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Skills**', + '*Skills are disabled. Enable them by setting `chat.useAgentSkills` to `true` in your settings.*', + '' + )); + }); + + test('skills with loaded files uses "skills loaded"', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.skill, + paths: [createPath('.github/skills', true)], + files: [ + createFile('search', 'loaded', '.github/skills'), + createFile('refactor', 'loaded', '.github/skills') + ], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Skills**
', + '*2 skills loaded*', + '', + '.github/skills
', + `${TREE_BRANCH} [\`search\`](${filePath('.github/skills/search')})
`, + `${TREE_END} [\`refactor\`](${filePath('.github/skills/refactor')})
`, + '' + )); + }); + + test('instructions with copilot-instructions.md enabled', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.instructions, + paths: [createPath('.github/instructions', true)], + files: [ + createFile('testing.instructions.md', 'loaded', '.github/instructions') + ], + enabled: true + }]; + + const specialFiles = { + agentsMd: { enabled: false, files: [] }, + copilotInstructions: { enabled: true, files: [URI.file('/workspace/.github/copilot-instructions.md')] } + }; + + const output = formatStatusOutput(statusInfos, specialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Instructions**
', + '*2 files loaded*', + '', + '.github/instructions
', + `${TREE_END} [\`testing.instructions.md\`](${filePath('.github/instructions/testing.instructions.md')})
`, + 'AGENTS.md -
', + 'copilot-instructions.md
', + `${TREE_END} [\`copilot-instructions.md\`](${filePath('.github/copilot-instructions.md')})
`, + '' + )); + }); + + test('instructions with AGENTS.md enabled', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.instructions, + paths: [createPath('.github/instructions', true)], + files: [], + enabled: true + }]; + + const specialFiles = { + agentsMd: { enabled: true, files: [URI.file('/workspace/AGENTS.md'), URI.file('/workspace/docs/AGENTS.md')] }, + copilotInstructions: { enabled: false, files: [] } + }; + + const output = formatStatusOutput(statusInfos, specialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Instructions**
', + '*2 files loaded*', + '', + '.github/instructions
', + 'AGENTS.md
', + `${TREE_BRANCH} [\`AGENTS.md\`](${filePath('AGENTS.md')})
`, + `${TREE_END} [\`AGENTS.md\`](${filePath('docs/AGENTS.md')})
`, + 'copilot-instructions.md -
', + '' + )); + }); + + test('custom folder that does not exist shows error', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [ + createPath('.github/agents', true), + createPath('custom/agents', false, PromptsStorage.local, false) + ], + files: [], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '', + '.github/agents
', + `${ICON_ERROR} custom/agents - *Folder does not exist*
`, + '' + )); + }); + + test('default folder that does not exist shows no error', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [ + createPath('.github/agents', false, PromptsStorage.local, true) + ], + files: [], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '', + '.github/agents
', + '' + )); + }); + + test('extension files grouped separately', () => { + const extFile = createFile('ext-agent.agent.md', 'loaded', 'extensions/my-publisher.my-extension/agents', PromptsStorage.extension, { extensionId: 'my-publisher.my-extension' }); + + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [createPath('.github/agents', true)], + files: [ + createFile('local-agent.agent.md', 'loaded', '.github/agents'), + extFile + ], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '*2 files loaded*', + '', + '.github/agents
', + `${TREE_END} [\`local-agent.agent.md\`](${filePath('.github/agents/local-agent.agent.md')})
`, + 'Extension: my-publisher.my-extension
', + `${TREE_END} [\`ext-agent.agent.md\`](${filePath('extensions/my-publisher.my-extension/agents/ext-agent.agent.md')})
`, + '' + )); + }); + + test('prompts with no files shows message', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.prompt, + paths: [], + files: [], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Prompt Files**
', + '', + '*No files loaded*', + '' + )); + }); + + test('full output with all four types', () => { + const statusInfos: ITypeStatusInfo[] = [ + { + type: PromptsType.agent, + paths: [createPath('.github/agents', true)], + files: [createFile('helper.agent.md', 'loaded', '.github/agents')], + enabled: true + }, + { + type: PromptsType.instructions, + paths: [createPath('.github/instructions', true)], + files: [createFile('code-style.instructions.md', 'loaded', '.github/instructions')], + enabled: true + }, + { + type: PromptsType.prompt, + paths: [createPath('.github/prompts', true)], + files: [createFile('fix-bug.prompt.md', 'loaded', '.github/prompts')], + enabled: true + }, + { + type: PromptsType.skill, + paths: [createPath('.github/skills', true)], + files: [createFile('search', 'loaded', '.github/skills')], + enabled: true + } + ]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + assert.strictEqual(output, lines( + '## Chat Customization Diagnostics', + '*WARNING: This file may contain sensitive information.*', + '', + '**Custom Agents**
', + '*1 file loaded*', + '', + '.github/agents
', + `${TREE_END} [\`helper.agent.md\`](${filePath('.github/agents/helper.agent.md')})
`, + '', + '**Instructions**
', + '*1 file loaded*', + '', + '.github/instructions
', + `${TREE_END} [\`code-style.instructions.md\`](${filePath('.github/instructions/code-style.instructions.md')})
`, + 'AGENTS.md -
', + 'copilot-instructions.md -
', + '', + '**Prompt Files**
', + '*1 file loaded*', + '', + '.github/prompts
', + `${TREE_END} [\`fix-bug.prompt.md\`](${filePath('.github/prompts/fix-bug.prompt.md')})
`, + '', + '**Skills**
', + '*1 skill loaded*', + '', + '.github/skills
', + `${TREE_END} [\`search\`](${filePath('.github/skills/search')})
`, + '' + )); + }); + + test('paths with spaces are URL encoded in markdown links', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [{ + uri: URI.file('/workspace/my folder/agents'), + exists: true, + storage: PromptsStorage.local, + scanOrder: 1, + displayPath: 'my folder/agents', + isDefault: false + }], + files: [{ + uri: URI.file('/workspace/my folder/agents/my agent.agent.md'), + status: 'loaded', + name: 'my agent.agent.md', + storage: PromptsStorage.local + }], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + // Verify that spaces in paths are URL encoded (%20) + assert.ok(output.includes('my%20folder/agents/my%20agent.agent.md'), 'Path should have URL-encoded spaces'); + assert.ok(output.includes('[`my agent.agent.md`]'), 'Display name should not be encoded'); + }); + + test('paths with special characters are URL encoded in markdown links', () => { + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.prompt, + paths: [{ + uri: URI.file('/workspace/docs & notes/prompts'), + exists: true, + storage: PromptsStorage.local, + scanOrder: 1, + displayPath: 'docs & notes/prompts', + isDefault: false + }], + files: [{ + uri: URI.file('/workspace/docs & notes/prompts/test[1].prompt.md'), + status: 'loaded', + name: 'test[1].prompt.md', + storage: PromptsStorage.local + }], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, []); + + // Verify that special characters in paths are URL encoded + assert.ok(output.includes('docs%20%26%20notes'), 'Ampersand should be URL-encoded'); + assert.ok(output.includes('test%5B1%5D.prompt.md'), 'Brackets should be URL-encoded'); + }); + + test('vscode-userdata scheme URIs are converted to file scheme for relative paths', () => { + // Create a workspace folder + const workspaceFolderUri = URI.file('/Users/test/workspace'); + const workspaceFolder = { + uri: workspaceFolderUri, + name: 'workspace', + index: 0, + toResource: (relativePath: string) => URI.joinPath(workspaceFolderUri, relativePath) + }; + + // Create a vscode-userdata URI that maps to a path under the workspace + const userDataUri = URI.file('/Users/test/workspace/.github/agents/my-agent.agent.md').with({ scheme: Schemas.vscodeUserData }); + + const statusInfos: ITypeStatusInfo[] = [{ + type: PromptsType.agent, + paths: [{ + uri: URI.file('/Users/test/workspace/.github/agents'), + exists: true, + storage: PromptsStorage.local, + scanOrder: 1, + displayPath: '.github/agents', + isDefault: true + }], + files: [{ + uri: userDataUri, + status: 'loaded', + name: 'my-agent.agent.md', + storage: PromptsStorage.local + }], + enabled: true + }]; + + const output = formatStatusOutput(statusInfos, emptySpecialFiles, [workspaceFolder]); + + // The vscode-userdata URI should be converted to file scheme internally, + // allowing relative path computation against workspace folders + assert.ok(output.includes('.github/agents/my-agent.agent.md'), 'Should use relative path from workspace folder'); + // Should not contain the full absolute path + assert.ok(!output.includes('/Users/test/workspace/.github'), 'Should not contain absolute path when relative path is available'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/actions/chatTitleActions.test.ts b/src/vs/workbench/contrib/chat/test/browser/actions/chatTitleActions.test.ts new file mode 100644 index 00000000000..5765508c902 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/actions/chatTitleActions.test.ts @@ -0,0 +1,268 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { mock } from '../../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IDialogService, IConfirmation, IConfirmationResult } from '../../../../../../platform/dialogs/common/dialogs.js'; +import { ServiceIdentifier } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IChatWidgetService, IChatWidget, IChatAccessibilityService } from '../../../browser/chat.js'; +import { IChatEditingSession, IModifiedFileEntry } from '../../../common/editing/chatEditingService.js'; +import { IChatService } from '../../../common/chatService/chatService.js'; +import { IChatModel, IChatRequestModel } from '../../../common/model/chatModel.js'; +import { IChatResponseViewModel } from '../../../common/model/chatViewModel.js'; +import { ChatModeKind } from '../../../common/constants.js'; +import { CommandsRegistry } from '../../../../../../platform/commands/common/commands.js'; +import { registerChatTitleActions } from '../../../browser/actions/chatTitleActions.js'; +import { MockChatWidgetService } from '../widget/mockChatWidget.js'; +import { MockChatService } from '../../common/chatService/mockChatService.js'; + +suite('RetryChatAction', () => { + const store = new DisposableStore(); + let instantiationService: TestInstantiationService; + + // Register actions once for all tests + let actionsRegistered = false; + function ensureActionsRegistered(): void { + if (!actionsRegistered) { + registerChatTitleActions(); + actionsRegistered = true; + } + } + + setup(() => { + instantiationService = store.add(new TestInstantiationService()); + ensureActionsRegistered(); + }); + + teardown(() => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createMockResponseVM(sessionResource: URI, requestId: string): IChatResponseViewModel { + return { + sessionResource, + requestId, + setVote: () => { }, // Required by isResponseVM check + } as unknown as IChatResponseViewModel; + } + + function createMockRequest(id: string): IChatRequestModel { + return { + id, + attempt: 0, + } as IChatRequestModel; + } + + function createMockEditingSession(entriesModifiedByRequest: IModifiedFileEntry[]): IChatEditingSession { + return { + entries: observableValue('entries', entriesModifiedByRequest), + restoreSnapshot: async (_requestId: string, _undoIndex: number | undefined) => { }, + } as unknown as IChatEditingSession; + } + + function createMockWidget(mode: ChatModeKind, editingSession: IChatEditingSession | undefined, lastResponseItem?: IChatResponseViewModel): Partial { + return { + input: { + currentModeKind: mode, + currentLanguageModel: 'test-model', + } as IChatWidget['input'], + viewModel: { + model: { + editingSession, + }, + getItems: () => lastResponseItem ? [lastResponseItem] : [], + } as unknown as IChatWidget['viewModel'], + getModeRequestOptions: () => ({}), + }; + } + + test('retry action should not throw when using accessor synchronously', async () => { + const sessionResource = URI.parse('test://session'); + const requestId = 'test-request-1'; + const mockRequest = createMockRequest(requestId); + const mockResponse = createMockResponseVM(sessionResource, requestId); + + const editingSession = createMockEditingSession([]); + const mockWidget = createMockWidget(ChatModeKind.Edit, editingSession, mockResponse); + + // Mock chat model + const mockChatModel: Partial = { + getRequests: () => [mockRequest as IChatRequestModel], + }; + + // Create MockChatWidgetService with widget lookup override + const mockChatWidgetService = new class extends MockChatWidgetService { + override getWidgetBySessionResource(_resource: URI) { + return mockWidget as IChatWidget; + } + }; + + let resendCalled = false; + const mockChatService = new class extends MockChatService { + override getSession(_sessionResource: URI) { + return mockChatModel as IChatModel; + } + override async resendRequest(_request: IChatRequestModel, _options?: unknown) { + resendCalled = true; + } + }; + + const mockConfigService = new TestConfigurationService(); + await mockConfigService.setUserConfiguration('chat.editing.confirmEditRequestRetry', false); + + const mockDialogService = new class extends mock() { + override async confirm(_confirmation: IConfirmation): Promise { + return { confirmed: true }; + } + }; + + let acceptRequestCalled = false; + const mockChatAccessibilityService = new class extends mock() { + override acceptRequest(_resource: URI) { + acceptRequestCalled = true; + } + }; + + // Use set() instead of stub() for more direct service registration + instantiationService.set(IChatWidgetService, mockChatWidgetService); + instantiationService.set(IChatService, mockChatService); + instantiationService.set(IConfigurationService, mockConfigService); + instantiationService.set(IDialogService, mockDialogService); + instantiationService.set(IChatAccessibilityService, mockChatAccessibilityService); + + // Get the action handler + const commandHandler = CommandsRegistry.getCommand('workbench.action.chat.retry')?.handler; + assert.ok(commandHandler, 'Command handler should be registered'); + + // Run the action with the instantiation service acting as accessor + await commandHandler(instantiationService, mockResponse); + + assert.ok(resendCalled, 'resendRequest should have been called'); + assert.ok(acceptRequestCalled, 'acceptRequest should have been called'); + }); + + test('retry action should work with confirmation dialog (accessor used after await)', async () => { + const sessionResource = URI.parse('test://session'); + const requestId = 'test-request-1'; + const mockRequest = createMockRequest(requestId); + const mockResponse = createMockResponseVM(sessionResource, requestId); + + // Create an entry that was modified by this request to trigger confirmation + const modifiedEntry: IModifiedFileEntry = { + modifiedURI: URI.parse('test://file.ts'), + lastModifyingRequestId: requestId, + } as IModifiedFileEntry; + + const editingSession = createMockEditingSession([modifiedEntry]); + const mockWidget = createMockWidget(ChatModeKind.Edit, editingSession, mockResponse); + + // Mock chat model + const mockChatModel: Partial = { + getRequests: () => [mockRequest as IChatRequestModel], + }; + + // Create MockChatWidgetService with widget lookup override + const mockChatWidgetService = new class extends MockChatWidgetService { + override getWidgetBySessionResource(_resource: URI) { + return mockWidget as IChatWidget; + } + }; + + let resendCalled = false; + const mockChatService = new class extends MockChatService { + override getSession(_sessionResource: URI) { + return mockChatModel as IChatModel; + } + override async resendRequest(_request: IChatRequestModel, _options?: unknown) { + resendCalled = true; + } + }; + + // Enable confirmation dialog - this will trigger an await + const mockConfigService = new TestConfigurationService(); + await mockConfigService.setUserConfiguration('chat.editing.confirmEditRequestRetry', true); + + let dialogShown = false; + const mockDialogService = new class extends mock() { + override async confirm(_confirmation: IConfirmation): Promise { + dialogShown = true; + // Simulate async delay that would happen in real dialog + await new Promise(resolve => setTimeout(resolve, 10)); + return { confirmed: true, checkboxChecked: false }; + } + }; + + let acceptRequestCalled = false; + const mockChatAccessibilityService = new class extends mock() { + override acceptRequest(_resource: URI) { + acceptRequestCalled = true; + } + }; + + // Use set() for more direct service registration + instantiationService.set(IChatWidgetService, mockChatWidgetService); + instantiationService.set(IChatService, mockChatService); + instantiationService.set(IConfigurationService, mockConfigService); + instantiationService.set(IDialogService, mockDialogService); + instantiationService.set(IChatAccessibilityService, mockChatAccessibilityService); + + // Get the action handler + const commandHandler = CommandsRegistry.getCommand('workbench.action.chat.retry')?.handler; + assert.ok(commandHandler, 'Command handler should be registered'); + + // Create a strict accessor that throws when used after dispose + // This simulates the behavior of the real ServicesAccessor which becomes + // invalid after the synchronous portion of the action handler + let disposed = false; + const strictAccessor = { + get(id: ServiceIdentifier): T { + if (disposed) { + throw new Error(`Accessor was used after being disposed. Tried to get service: ${id.toString()}`); + } + return instantiationService.get(id); + } + }; + + // Create a wrapper that disposes the accessor after the first await + // by wrapping the dialog service + const originalConfirm = mockDialogService.confirm.bind(mockDialogService); + mockDialogService.confirm = async (confirmation: IConfirmation): Promise => { + const result = await originalConfirm(confirmation); + // Mark accessor as disposed after the await, simulating real behavior + disposed = true; + return result; + }; + + // Run the action - this should throw if accessor is used after the confirm await + let threwError = false; + let errorMessage = ''; + try { + await commandHandler(strictAccessor, mockResponse); + } catch (e) { + threwError = true; + errorMessage = (e as Error).message; + } + + assert.ok(dialogShown, 'Dialog should have been shown'); + + // The bug is that accessor.get(IChatAccessibilityService) is called after the await + // This test should fail until the bug is fixed + if (threwError) { + assert.fail(`Action threw an error because accessor was used after await: ${errorMessage}`); + } + + assert.ok(resendCalled, 'resendRequest should have been called'); + assert.ok(acceptRequestCalled, 'acceptRequest should have been called'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts deleted file mode 100644 index e077e6eec1a..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/agentSessionViewModel.test.ts +++ /dev/null @@ -1,936 +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 { Event } from '../../../../../base/common/event.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { AgentSessionsViewModel, IAgentSessionViewModel, isAgentSession, isAgentSessionsViewModel, isLocalAgentSessionItem } from '../../browser/agentSessions/agentSessionViewModel.js'; -import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, localChatSessionType } from '../../common/chatSessionsService.js'; -import { LocalChatSessionUri } from '../../common/chatUri.js'; -import { MockChatSessionsService } from '../common/mockChatSessionsService.js'; -import { TestLifecycleService } from '../../../../test/browser/workbenchTestServices.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; - -suite('AgentSessionsViewModel', () => { - - const disposables = new DisposableStore(); - let mockChatSessionsService: MockChatSessionsService; - let mockLifecycleService: TestLifecycleService; - let viewModel: AgentSessionsViewModel; - - setup(() => { - mockChatSessionsService = new MockChatSessionsService(); - mockLifecycleService = disposables.add(new TestLifecycleService()); - }); - - teardown(() => { - disposables.clear(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('should initialize with empty sessions', () => { - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - assert.strictEqual(viewModel.sessions.length, 0); - }); - - test('should resolve sessions from providers', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session 1', - description: 'Description 1', - timing: { startTime: Date.now() } - }, - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Test Session 2', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 2); - assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); - assert.strictEqual(viewModel.sessions[0].label, 'Test Session 1'); - assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); - assert.strictEqual(viewModel.sessions[1].label, 'Test Session 2'); - }); - }); - - test('should resolve sessions from multiple providers', async () => { - return runWithFakedTimers({}, async () => { - const provider1: IChatSessionItemProvider = { - chatSessionType: 'type-1', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Session 1', - timing: { startTime: Date.now() } - } - ] - }; - - const provider2: IChatSessionItemProvider = { - chatSessionType: 'type-2', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Session 2', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); - - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 2); - assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); - assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); - }); - }); - - test('should fire onWillResolve and onDidResolve events', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - let willResolveFired = false; - let didResolveFired = false; - - disposables.add(viewModel.onWillResolve(() => { - willResolveFired = true; - assert.strictEqual(didResolveFired, false, 'onDidResolve should not fire before onWillResolve completes'); - })); - - disposables.add(viewModel.onDidResolve(() => { - didResolveFired = true; - assert.strictEqual(willResolveFired, true, 'onWillResolve should fire before onDidResolve'); - })); - - await viewModel.resolve(undefined); - - assert.strictEqual(willResolveFired, true, 'onWillResolve should have fired'); - assert.strictEqual(didResolveFired, true, 'onDidResolve should have fired'); - }); - }); - - test('should fire onDidChangeSessions event after resolving', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - let sessionsChangedFired = false; - disposables.add(viewModel.onDidChangeSessions(() => { - sessionsChangedFired = true; - })); - - await viewModel.resolve(undefined); - - assert.strictEqual(sessionsChangedFired, true, 'onDidChangeSessions should have fired'); - }); - }); - - test('should handle session with all properties', async () => { - return runWithFakedTimers({}, async () => { - const startTime = Date.now(); - const endTime = startTime + 1000; - - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - description: new MarkdownString('**Bold** description'), - status: ChatSessionStatus.Completed, - tooltip: 'Session tooltip', - iconPath: ThemeIcon.fromId('check'), - timing: { startTime, endTime }, - statistics: { insertions: 10, deletions: 5 } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - const session = viewModel.sessions[0]; - assert.strictEqual(session.resource.toString(), 'test://session-1'); - assert.strictEqual(session.label, 'Test Session'); - assert.ok(session.description instanceof MarkdownString); - if (session.description instanceof MarkdownString) { - assert.strictEqual(session.description.value, '**Bold** description'); - } - assert.strictEqual(session.status, ChatSessionStatus.Completed); - assert.strictEqual(session.timing.startTime, startTime); - assert.strictEqual(session.timing.endTime, endTime); - assert.deepStrictEqual(session.statistics, { insertions: 10, deletions: 5 }); - }); - }); - - test('should add default description for sessions without description', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - assert.ok(typeof viewModel.sessions[0].description === 'string'); - }); - }); - - test('should filter out special session IDs', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'show-history', - resource: URI.parse('test://show-history'), - label: 'Show History', - timing: { startTime: Date.now() } - }, - { - id: 'workbench.panel.chat.view.copilot', - resource: URI.parse('test://copilot'), - label: 'Copilot', - timing: { startTime: Date.now() } - }, - { - id: 'valid-session', - resource: URI.parse('test://valid'), - label: 'Valid Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://valid'); - }); - }); - - test('should handle resolve with specific provider', async () => { - return runWithFakedTimers({}, async () => { - const provider1: IChatSessionItemProvider = { - chatSessionType: 'type-1', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Session 1', - timing: { startTime: Date.now() } - } - ] - }; - - const provider2: IChatSessionItemProvider = { - chatSessionType: 'type-2', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Session 2', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); - - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - // First resolve all - await viewModel.resolve(undefined); - assert.strictEqual(viewModel.sessions.length, 2); - - // Now resolve only type-1 - await viewModel.resolve('type-1'); - // Should still have both sessions, but only type-1 was re-resolved - assert.strictEqual(viewModel.sessions.length, 2); - }); - }); - - test('should handle resolve with multiple specific providers', async () => { - return runWithFakedTimers({}, async () => { - const provider1: IChatSessionItemProvider = { - chatSessionType: 'type-1', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Session 1', - timing: { startTime: Date.now() } - } - ] - }; - - const provider2: IChatSessionItemProvider = { - chatSessionType: 'type-2', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Session 2', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); - - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(['type-1', 'type-2']); - - assert.strictEqual(viewModel.sessions.length, 2); - }); - }); - - test('should respond to onDidChangeItemsProviders event', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); - - // Trigger event - this should automatically call resolve - mockChatSessionsService.fireDidChangeItemsProviders(provider); - - // Wait for the sessions to be resolved - await sessionsChangedPromise; - - assert.strictEqual(viewModel.sessions.length, 1); - }); - }); - - test('should respond to onDidChangeAvailability event', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); - - // Trigger event - this should automatically call resolve - mockChatSessionsService.fireDidChangeAvailability(); - - // Wait for the sessions to be resolved - await sessionsChangedPromise; - - assert.strictEqual(viewModel.sessions.length, 1); - }); - }); - - test('should respond to onDidChangeSessionItems event', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); - - // Trigger event - this should automatically call resolve - mockChatSessionsService.fireDidChangeSessionItems('test-type'); - - // Wait for the sessions to be resolved - await sessionsChangedPromise; - - assert.strictEqual(viewModel.sessions.length, 1); - }); - }); - - test('should maintain provider reference in session view model', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].provider, provider); - assert.strictEqual(viewModel.sessions[0].provider.chatSessionType, 'test-type'); - }); - }); - - test('should handle empty provider results', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 0); - }); - }); - - test('should handle sessions with different statuses', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-failed', - resource: URI.parse('test://session-failed'), - label: 'Failed Session', - status: ChatSessionStatus.Failed, - timing: { startTime: Date.now() } - }, - { - id: 'session-completed', - resource: URI.parse('test://session-completed'), - label: 'Completed Session', - status: ChatSessionStatus.Completed, - timing: { startTime: Date.now() } - }, - { - id: 'session-inprogress', - resource: URI.parse('test://session-inprogress'), - label: 'In Progress Session', - status: ChatSessionStatus.InProgress, - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 3); - assert.strictEqual(viewModel.sessions[0].status, ChatSessionStatus.Failed); - assert.strictEqual(viewModel.sessions[1].status, ChatSessionStatus.Completed); - assert.strictEqual(viewModel.sessions[2].status, ChatSessionStatus.InProgress); - }); - }); - - test('should replace sessions on re-resolve', async () => { - return runWithFakedTimers({}, async () => { - let sessionCount = 1; - - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - const sessions: IChatSessionItem[] = []; - for (let i = 0; i < sessionCount; i++) { - sessions.push({ - id: `session-${i}`, - resource: URI.parse(`test://session-${i}`), - label: `Session ${i}`, - timing: { startTime: Date.now() } - }); - } - return sessions; - } - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - assert.strictEqual(viewModel.sessions.length, 1); - - sessionCount = 3; - await viewModel.resolve(undefined); - assert.strictEqual(viewModel.sessions.length, 3); - }); - }); - - test('should handle local agent session type specially', async () => { - return runWithFakedTimers({}, async () => { - const provider: IChatSessionItemProvider = { - chatSessionType: localChatSessionType, - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'local-session', - resource: LocalChatSessionUri.forSession('local-session'), - label: 'Local Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].provider.chatSessionType, localChatSessionType); - }); - }); - - test('should correctly construct resource URIs for sessions', async () => { - return runWithFakedTimers({}, async () => { - const resource = URI.parse('custom://my-session/path'); - - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [ - { - id: 'session-1', - resource: resource, - label: 'Test Session', - timing: { startTime: Date.now() } - } - ] - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - await viewModel.resolve(undefined); - - assert.strictEqual(viewModel.sessions.length, 1); - assert.strictEqual(viewModel.sessions[0].resource.toString(), resource.toString()); - }); - }); - - test('should throttle multiple rapid resolve calls', async () => { - return runWithFakedTimers({}, async () => { - let providerCallCount = 0; - - const provider: IChatSessionItemProvider = { - chatSessionType: 'test-type', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - providerCallCount++; - return [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Test Session', - timing: { startTime: Date.now() } - } - ]; - } - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider); - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - // Make multiple rapid resolve calls - const resolvePromises = [ - viewModel.resolve(undefined), - viewModel.resolve(undefined), - viewModel.resolve(undefined) - ]; - - await Promise.all(resolvePromises); - - // Should only call provider once due to throttling - assert.strictEqual(providerCallCount, 1); - assert.strictEqual(viewModel.sessions.length, 1); - }); - }); - - test('should preserve sessions from non-resolved providers', async () => { - return runWithFakedTimers({}, async () => { - let provider1CallCount = 0; - let provider2CallCount = 0; - - const provider1: IChatSessionItemProvider = { - chatSessionType: 'type-1', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - provider1CallCount++; - return [ - { - id: 'session-1', - resource: URI.parse('test://session-1'), - label: `Session 1 (call ${provider1CallCount})`, - timing: { startTime: Date.now() } - } - ]; - } - }; - - const provider2: IChatSessionItemProvider = { - chatSessionType: 'type-2', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - provider2CallCount++; - return [ - { - id: 'session-2', - resource: URI.parse('test://session-2'), - label: `Session 2 (call ${provider2CallCount})`, - timing: { startTime: Date.now() } - } - ]; - } - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); - - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - // First resolve all - await viewModel.resolve(undefined); - assert.strictEqual(viewModel.sessions.length, 2); - assert.strictEqual(provider1CallCount, 1); - assert.strictEqual(provider2CallCount, 1); - const originalSession1Label = viewModel.sessions[0].label; - - // Now resolve only type-2 - await viewModel.resolve('type-2'); - - // Should still have both sessions - assert.strictEqual(viewModel.sessions.length, 2); - // Provider 1 should not be called again - assert.strictEqual(provider1CallCount, 1); - // Provider 2 should be called again - assert.strictEqual(provider2CallCount, 2); - // Session 1 should be preserved with original label - assert.strictEqual(viewModel.sessions.find(s => s.resource.toString() === 'test://session-1')?.label, originalSession1Label); - }); - }); - - test('should accumulate providers when resolve is called with different provider types', async () => { - return runWithFakedTimers({}, async () => { - let resolveCount = 0; - const resolvedProviders: (string | undefined)[] = []; - - const provider1: IChatSessionItemProvider = { - chatSessionType: 'type-1', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - resolveCount++; - resolvedProviders.push('type-1'); - return [{ - id: 'session-1', - resource: URI.parse('test://session-1'), - label: 'Session 1', - timing: { startTime: Date.now() } - }]; - } - }; - - const provider2: IChatSessionItemProvider = { - chatSessionType: 'type-2', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => { - resolveCount++; - resolvedProviders.push('type-2'); - return [{ - id: 'session-2', - resource: URI.parse('test://session-2'), - label: 'Session 2', - timing: { startTime: Date.now() } - }]; - } - }; - - mockChatSessionsService.registerChatSessionItemProvider(provider1); - mockChatSessionsService.registerChatSessionItemProvider(provider2); - - viewModel = disposables.add(new AgentSessionsViewModel( - mockChatSessionsService, - mockLifecycleService - )); - - // Call resolve with different types rapidly - they should accumulate - const promise1 = viewModel.resolve('type-1'); - const promise2 = viewModel.resolve(['type-2']); - - await Promise.all([promise1, promise2]); - - // Both providers should be resolved - assert.strictEqual(viewModel.sessions.length, 2); - }); - }); -}); - -suite('AgentSessionsViewModel - Helper Functions', () => { - const disposables = new DisposableStore(); - - teardown(() => { - disposables.clear(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('isLocalAgentSessionItem should identify local sessions', () => { - const localSession: IAgentSessionViewModel = { - provider: { - chatSessionType: localChatSessionType, - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, - providerLabel: 'Local', - icon: Codicon.chatSparkle, - resource: URI.parse('test://local-1'), - label: 'Local', - description: 'test', - timing: { startTime: Date.now() } - }; - - const remoteSession: IAgentSessionViewModel = { - provider: { - chatSessionType: 'remote', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, - providerLabel: 'Local', - icon: Codicon.chatSparkle, - resource: URI.parse('test://remote-1'), - label: 'Remote', - description: 'test', - timing: { startTime: Date.now() } - }; - - assert.strictEqual(isLocalAgentSessionItem(localSession), true); - assert.strictEqual(isLocalAgentSessionItem(remoteSession), false); - }); - - test('isAgentSession should identify session view models', () => { - const session: IAgentSessionViewModel = { - provider: { - chatSessionType: 'test', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, - providerLabel: 'Local', - icon: Codicon.chatSparkle, - resource: URI.parse('test://test-1'), - label: 'Test', - description: 'test', - timing: { startTime: Date.now() } - }; - - // Test with a session object - assert.strictEqual(isAgentSession(session), true); - - // Test with a sessions container - pass as session to see it returns false - const sessionOrContainer: IAgentSessionViewModel = session; - assert.strictEqual(isAgentSession(sessionOrContainer), true); - }); - - test('isAgentSessionsViewModel should identify sessions view models', () => { - const session: IAgentSessionViewModel = { - provider: { - chatSessionType: 'test', - onDidChangeChatSessionItems: Event.None, - provideChatSessionItems: async () => [] - }, - providerLabel: 'Local', - icon: Codicon.chatSparkle, - resource: URI.parse('test://test-1'), - label: 'Test', - description: 'test', - timing: { startTime: Date.now() } - }; - - // Test with actual view model - const actualViewModel = new AgentSessionsViewModel(new MockChatSessionsService(), disposables.add(new TestLifecycleService())); - disposables.add(actualViewModel); - assert.strictEqual(isAgentSessionsViewModel(actualViewModel), true); - - // Test with session object - assert.strictEqual(isAgentSessionsViewModel(session), false); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts new file mode 100644 index 00000000000..ab75922685a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionViewModel.test.ts @@ -0,0 +1,2181 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentSessionsModel, IAgentSession, isAgentSession, isAgentSessionsModel, isLocalAgentSessionItem } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { AgentSessionsFilter } from '../../../browser/agentSessions/agentSessionsFilter.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionItemProvider, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; +import { MockChatSessionsService } from '../../common/mockChatSessionsService.js'; +import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { MenuId } from '../../../../../../platform/actions/common/actions.js'; +import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js'; +import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../../browser/agentSessions/agentSessions.js'; + +suite('AgentSessions', () => { + + suite('AgentSessionsViewModel', () => { + + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let mockLifecycleService: TestLifecycleService; + let viewModel: AgentSessionsModel; + let instantiationService: TestInstantiationService; + + function createViewModel(): AgentSessionsModel { + return disposables.add(instantiationService.createInstance( + AgentSessionsModel, + )); + } + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + mockLifecycleService = disposables.add(new TestLifecycleService()); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, mockLifecycleService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should initialize with empty sessions', () => { + viewModel = createViewModel(); + + assert.strictEqual(viewModel.sessions.length, 0); + }); + + test('should resolve sessions from providers', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1', { + label: 'Test Session 1' + }), + makeSimpleSessionItem('session-2', { + label: 'Test Session 2' + }) + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 2); + assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); + assert.strictEqual(viewModel.sessions[0].label, 'Test Session 1'); + assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); + assert.strictEqual(viewModel.sessions[1].label, 'Test Session 2'); + }); + }); + + test('should resolve sessions from multiple providers', async () => { + return runWithFakedTimers({}, async () => { + const provider1: IChatSessionItemProvider = { + chatSessionType: 'type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + const provider2: IChatSessionItemProvider = { + chatSessionType: 'type-2', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-2'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider1); + mockChatSessionsService.registerChatSessionItemProvider(provider2); + + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 2); + assert.strictEqual(viewModel.sessions[0].resource.toString(), 'test://session-1'); + assert.strictEqual(viewModel.sessions[1].resource.toString(), 'test://session-2'); + }); + }); + + test('should fire onWillResolve and onDidResolve events', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + let willResolveFired = false; + let didResolveFired = false; + + disposables.add(viewModel.onWillResolve(() => { + willResolveFired = true; + assert.strictEqual(didResolveFired, false, 'onDidResolve should not fire before onWillResolve completes'); + })); + + disposables.add(viewModel.onDidResolve(() => { + didResolveFired = true; + assert.strictEqual(willResolveFired, true, 'onWillResolve should fire before onDidResolve'); + })); + + await viewModel.resolve(undefined); + + assert.strictEqual(willResolveFired, true, 'onWillResolve should have fired'); + assert.strictEqual(didResolveFired, true, 'onDidResolve should have fired'); + }); + }); + + test('should fire onDidChangeSessions event after resolving', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + let sessionsChangedFired = false; + disposables.add(viewModel.onDidChangeSessions(() => { + sessionsChangedFired = true; + })); + + await viewModel.resolve(undefined); + + assert.strictEqual(sessionsChangedFired, true, 'onDidChangeSessions should have fired'); + }); + }); + + test('should handle session with all properties', async () => { + return runWithFakedTimers({}, async () => { + const created = Date.now(); + const lastRequestEnded = created + 1000; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Test Session', + description: new MarkdownString('**Bold** description'), + status: ChatSessionStatus.Completed, + tooltip: 'Session tooltip', + iconPath: ThemeIcon.fromId('check'), + timing: { created, lastRequestStarted: created, lastRequestEnded }, + changes: { files: 1, insertions: 10, deletions: 5 } + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 1); + const session = viewModel.sessions[0]; + assert.strictEqual(session.resource.toString(), 'test://session-1'); + assert.strictEqual(session.label, 'Test Session'); + assert.ok(session.description instanceof MarkdownString); + if (session.description instanceof MarkdownString) { + assert.strictEqual(session.description.value, '**Bold** description'); + } + assert.strictEqual(session.status, ChatSessionStatus.Completed); + assert.strictEqual(session.timing.created, created); + assert.strictEqual(session.timing.lastRequestEnded, lastRequestEnded); + assert.deepStrictEqual(session.changes, { files: 1, insertions: 10, deletions: 5 }); + }); + }); + + test('should handle resolve with specific provider', async () => { + return runWithFakedTimers({}, async () => { + const provider1: IChatSessionItemProvider = { + chatSessionType: 'type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + const provider2: IChatSessionItemProvider = { + chatSessionType: 'type-2', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-2'), + ] + }; + + disposables.add(mockChatSessionsService.registerChatSessionItemProvider(provider1)); + disposables.add(mockChatSessionsService.registerChatSessionItemProvider(provider2)); + + viewModel = createViewModel(); + + // First resolve all + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 2); + + // Now resolve only type-1 + await viewModel.resolve('type-1'); + // Only type-1 sessions remain since non-resolved providers are cleared + assert.strictEqual(viewModel.sessions.length, 1); + }); + }); + + test('should handle resolve with multiple specific providers', async () => { + return runWithFakedTimers({}, async () => { + const provider1: IChatSessionItemProvider = { + chatSessionType: 'type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + const provider2: IChatSessionItemProvider = { + chatSessionType: 'type-2', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-2'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider1); + mockChatSessionsService.registerChatSessionItemProvider(provider2); + + viewModel = createViewModel(); + + await viewModel.resolve(['type-1', 'type-2']); + + assert.strictEqual(viewModel.sessions.length, 2); + }); + }); + + test('should respond to onDidChangeItemsProviders event', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); + + // Trigger event - this should automatically call resolve + mockChatSessionsService.fireDidChangeItemsProviders(provider); + + // Wait for the sessions to be resolved + await sessionsChangedPromise; + + assert.strictEqual(viewModel.sessions.length, 1); + }); + }); + + test('should respond to onDidChangeAvailability event', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); + + // Trigger event - this should automatically call resolve + mockChatSessionsService.fireDidChangeAvailability(); + + // Wait for the sessions to be resolved + await sessionsChangedPromise; + + assert.strictEqual(viewModel.sessions.length, 1); + }); + }); + + test('should respond to onDidChangeSessionItems event', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + const sessionsChangedPromise = Event.toPromise(viewModel.onDidChangeSessions); + + // Trigger event - this should automatically call resolve + mockChatSessionsService.fireDidChangeSessionItems('test-type'); + + // Wait for the sessions to be resolved + await sessionsChangedPromise; + + assert.strictEqual(viewModel.sessions.length, 1); + }); + }); + + test('should maintain provider reference in session view model', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 1); + assert.strictEqual(viewModel.sessions[0].providerType, 'test-type'); + }); + }); + + test('should handle empty provider results', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 0); + }); + }); + + test('should handle sessions with different statuses', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + id: 'session-failed', + resource: URI.parse('test://session-failed'), + label: 'Failed Session', + status: ChatSessionStatus.Failed, + timing: makeNewSessionTiming() + }, + { + id: 'session-completed', + resource: URI.parse('test://session-completed'), + label: 'Completed Session', + status: ChatSessionStatus.Completed, + timing: makeNewSessionTiming() + }, + { + id: 'session-inprogress', + resource: URI.parse('test://session-inprogress'), + label: 'In Progress Session', + status: ChatSessionStatus.InProgress, + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 3); + assert.strictEqual(viewModel.sessions[0].status, ChatSessionStatus.Failed); + assert.strictEqual(viewModel.sessions[1].status, ChatSessionStatus.Completed); + assert.strictEqual(viewModel.sessions[2].status, ChatSessionStatus.InProgress); + }); + }); + + test('should replace sessions on re-resolve', async () => { + return runWithFakedTimers({}, async () => { + let sessionCount = 1; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + const sessions: IChatSessionItem[] = []; + for (let i = 0; i < sessionCount; i++) { + sessions.push(makeSimpleSessionItem(`session-${i + 1}`)); + } + return sessions; + } + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 1); + + sessionCount = 3; + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 3); + }); + }); + + test('should handle local agent session type specially', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: localChatSessionType, + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + id: 'local-session', + resource: LocalChatSessionUri.forSession('local-session'), + label: 'Local Session', + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 1); + assert.strictEqual(viewModel.sessions[0].providerType, localChatSessionType); + }); + }); + + test('should correctly construct resource URIs for sessions', async () => { + return runWithFakedTimers({}, async () => { + const resource = URI.parse('custom://my-session/path'); + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: resource, + label: 'Test Session', + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + await viewModel.resolve(undefined); + + assert.strictEqual(viewModel.sessions.length, 1); + assert.strictEqual(viewModel.sessions[0].resource.toString(), resource.toString()); + }); + }); + + test('should throttle multiple rapid resolve calls', async () => { + return runWithFakedTimers({}, async () => { + let providerCallCount = 0; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + providerCallCount++; + return [ + makeSimpleSessionItem('session-1'), + ]; + } + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = createViewModel(); + + // Make multiple rapid resolve calls + const resolvePromises = [ + viewModel.resolve(undefined), + viewModel.resolve(undefined), + viewModel.resolve(undefined) + ]; + + await Promise.all(resolvePromises); + + // Should only call provider once due to throttling + assert.strictEqual(providerCallCount, 1); + assert.strictEqual(viewModel.sessions.length, 1); + }); + }); + + test('should not preserve sessions from non-resolved providers', async () => { + return runWithFakedTimers({}, async () => { + let provider1CallCount = 0; + let provider2CallCount = 0; + + const provider1: IChatSessionItemProvider = { + chatSessionType: 'type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + provider1CallCount++; + return [ + { + resource: URI.parse('test://session-1'), + label: `Session 1 (call ${provider1CallCount})`, + timing: makeNewSessionTiming() + } + ]; + } + }; + + const provider2: IChatSessionItemProvider = { + chatSessionType: 'type-2', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + provider2CallCount++; + return [ + { + resource: URI.parse('test://session-2'), + label: `Session 2 (call ${provider2CallCount})`, + timing: makeNewSessionTiming() + } + ]; + } + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider1); + mockChatSessionsService.registerChatSessionItemProvider(provider2); + + viewModel = createViewModel(); + + // First resolve all + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 2); + assert.strictEqual(provider1CallCount, 1); + assert.strictEqual(provider2CallCount, 1); + + // Now resolve only type-2 + await viewModel.resolve('type-2'); + + // Should still have only one session + assert.strictEqual(viewModel.sessions.length, 1); + // Provider 1 should not be called again + assert.strictEqual(provider1CallCount, 1); + // Provider 2 should be called again + assert.strictEqual(provider2CallCount, 2); + }); + }); + + test('should accumulate providers when resolve is called with different provider types', async () => { + return runWithFakedTimers({}, async () => { + let resolveCount = 0; + const resolvedProviders: (string | undefined)[] = []; + + const provider1: IChatSessionItemProvider = { + chatSessionType: 'type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + resolveCount++; + resolvedProviders.push('type-1'); + return [makeSimpleSessionItem('session-1'),]; + } + }; + + const provider2: IChatSessionItemProvider = { + chatSessionType: 'type-2', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + resolveCount++; + resolvedProviders.push('type-2'); + return [{ + resource: URI.parse('test://session-2'), + label: 'Session 2', + timing: makeNewSessionTiming() + }]; + } + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider1); + mockChatSessionsService.registerChatSessionItemProvider(provider2); + + viewModel = createViewModel(); + + // Call resolve with different types rapidly - they should accumulate + const promise1 = viewModel.resolve('type-1'); + const promise2 = viewModel.resolve(['type-2']); + + await Promise.all([promise1, promise2]); + + // Both providers should be resolved + assert.strictEqual(viewModel.sessions.length, 2); + }); + }); + }); + + suite('AgentSessionsViewModel - Helper Functions', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('isLocalAgentSessionItem should identify local sessions', () => { + const localSession: IAgentSession = { + providerType: localChatSessionType, + providerLabel: 'Local', + icon: Codicon.chatSparkle, + resource: URI.parse('test://local-1'), + label: 'Local', + description: 'test', + timing: makeNewSessionTiming(), + status: ChatSessionStatus.Completed, + isArchived: () => false, + setArchived: archived => { }, + isRead: () => false, + setRead: read => { } + }; + + const remoteSession: IAgentSession = { + providerType: 'remote', + providerLabel: 'Remote', + icon: Codicon.chatSparkle, + resource: URI.parse('test://remote-1'), + label: 'Remote', + description: 'test', + timing: makeNewSessionTiming(), + status: ChatSessionStatus.Completed, + isArchived: () => false, + setArchived: archived => { }, + isRead: () => false, + setRead: read => { } + }; + + assert.strictEqual(isLocalAgentSessionItem(localSession), true); + assert.strictEqual(isLocalAgentSessionItem(remoteSession), false); + }); + + test('isAgentSession should identify session view models', () => { + const session: IAgentSession = { + providerType: 'test', + providerLabel: 'Local', + icon: Codicon.chatSparkle, + resource: URI.parse('test://test-1'), + label: 'Test', + description: 'test', + timing: makeNewSessionTiming(), + status: ChatSessionStatus.Completed, + isArchived: () => false, + setArchived: archived => { }, + isRead: () => false, + setRead: read => { } + }; + + // Test with a session object + assert.strictEqual(isAgentSession(session), true); + + // Test with a sessions container - pass as session to see it returns false + const sessionOrContainer: IAgentSession = session; + assert.strictEqual(isAgentSession(sessionOrContainer), true); + }); + + test('isAgentSessionsViewModel should identify sessions view models', () => { + const session: IAgentSession = { + providerType: 'test', + providerLabel: 'Local', + icon: Codicon.chatSparkle, + resource: URI.parse('test://test-1'), + label: 'Test', + description: 'test', + timing: makeNewSessionTiming(), + status: ChatSessionStatus.Completed, + isArchived: () => false, + setArchived: archived => { }, + isRead: () => false, + setRead: read => { } + }; + + // Test with actual view model + const instantiationService = workbenchInstantiationService(undefined, disposables); + const lifecycleService = disposables.add(new TestLifecycleService()); + instantiationService.stub(IChatSessionsService, new MockChatSessionsService()); + instantiationService.stub(ILifecycleService, lifecycleService); + const actualViewModel = disposables.add(instantiationService.createInstance( + AgentSessionsModel, + )); + assert.strictEqual(isAgentSessionsModel(actualViewModel), true); + + // Test with session object + assert.strictEqual(isAgentSessionsModel(session), false); + }); + }); + + suite('AgentSessionsFilter', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + + function createSession(overrides: Partial = {}): IAgentSession { + return { + providerType: 'test-type', + providerLabel: 'Test Provider', + icon: Codicon.chatSparkle, + resource: URI.parse('test://session'), + label: 'Test Session', + timing: makeNewSessionTiming(), + status: ChatSessionStatus.Completed, + isArchived: () => false, + setArchived: () => { }, + isRead: () => false, + setRead: read => { }, + ...overrides + }; + } + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should initialize with default excludes', () => { + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + // Default: archived sessions should NOT be excluded (archived: false by default) + const archivedSession = createSession({ + isArchived: () => true + }); + const activeSession = createSession({ + isArchived: () => false + }); + + assert.strictEqual(filter.exclude(archivedSession), false); + assert.strictEqual(filter.exclude(activeSession), false); + }); + + test('should filter out sessions from excluded provider', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session1 = createSession({ + providerType: 'type-1', + resource: URI.parse('test://session-1') + }); + + const session2 = createSession({ + providerType: 'type-2', + resource: URI.parse('test://session-2') + }); + + // Initially, no sessions should be filtered by provider + assert.strictEqual(filter.exclude(session1), false); + assert.strictEqual(filter.exclude(session2), false); + + // Exclude type-1 by setting it in storage + const excludes = { + providers: ['type-1'], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // After excluding type-1, session1 should be filtered but not session2 + assert.strictEqual(filter.exclude(session1), true); + assert.strictEqual(filter.exclude(session2), false); + }); + + test('should filter out multiple excluded providers', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session1 = createSession({ providerType: 'type-1' }); + const session2 = createSession({ providerType: 'type-2' }); + const session3 = createSession({ providerType: 'type-3' }); + + // Exclude type-1 and type-2 + const excludes = { + providers: ['type-1', 'type-2'], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + assert.strictEqual(filter.exclude(session1), true); + assert.strictEqual(filter.exclude(session2), true); + assert.strictEqual(filter.exclude(session3), false); + }); + + test('should filter not out archived sessions', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const archivedSession = createSession({ + resource: URI.parse('test://archived-session'), + isArchived: () => true + }); + + const activeSession = createSession({ + resource: URI.parse('test://active-session'), + isArchived: () => false + }); + + // By default, archived sessions should NOT be filtered (archived: false in default excludes) + assert.strictEqual(filter.exclude(archivedSession), false); + assert.strictEqual(filter.exclude(activeSession), false); + + // Exclude archived by setting archived to true in storage + const excludes = { + providers: [], + states: [], + archived: true + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // After excluding archived, only archived session should be filtered + assert.strictEqual(filter.exclude(archivedSession), false); + assert.strictEqual(filter.exclude(activeSession), false); + }); + + test('should filter out sessions with excluded status', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const failedSession = createSession({ + resource: URI.parse('test://failed-session'), + status: ChatSessionStatus.Failed + }); + + const completedSession = createSession({ + resource: URI.parse('test://completed-session'), + status: ChatSessionStatus.Completed + }); + + const inProgressSession = createSession({ + resource: URI.parse('test://inprogress-session'), + status: ChatSessionStatus.InProgress + }); + + // Initially, no sessions should be filtered by status (archived is default exclude) + assert.strictEqual(filter.exclude(failedSession), false); + assert.strictEqual(filter.exclude(completedSession), false); + assert.strictEqual(filter.exclude(inProgressSession), false); + + // Exclude failed status by setting it in storage + const excludes = { + providers: [], + states: [ChatSessionStatus.Failed], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // After excluding failed status, only failedSession should be filtered + assert.strictEqual(filter.exclude(failedSession), true); + assert.strictEqual(filter.exclude(completedSession), false); + assert.strictEqual(filter.exclude(inProgressSession), false); + }); + + test('should filter out multiple excluded statuses', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const failedSession = createSession({ status: ChatSessionStatus.Failed }); + const completedSession = createSession({ status: ChatSessionStatus.Completed }); + const inProgressSession = createSession({ status: ChatSessionStatus.InProgress }); + + // Exclude failed and in-progress + const excludes = { + providers: [], + states: [ChatSessionStatus.Failed, ChatSessionStatus.InProgress], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + assert.strictEqual(filter.exclude(failedSession), true); + assert.strictEqual(filter.exclude(completedSession), false); + assert.strictEqual(filter.exclude(inProgressSession), true); + }); + + test('should combine multiple filter conditions', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session1 = createSession({ + providerType: 'type-1', + status: ChatSessionStatus.Failed, + isArchived: () => true + }); + + const session2 = createSession({ + providerType: 'type-2', + status: ChatSessionStatus.Completed, + isArchived: () => false + }); + + // Exclude type-1, failed status, and archived + const excludes = { + providers: ['type-1'], + states: [ChatSessionStatus.Failed], + archived: true + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // session1 should be excluded for multiple reasons + assert.strictEqual(filter.exclude(session1), true); + // session2 should not be excluded + assert.strictEqual(filter.exclude(session2), false); + }); + + test('should emit onDidChange when excludes are updated', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + let changeEventFired = false; + disposables.add(filter.onDidChange(() => { + changeEventFired = true; + })); + + // Update excludes + const excludes = { + providers: ['type-1'], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + assert.strictEqual(changeEventFired, true); + }); + + test('should handle storage updates from other windows', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session = createSession({ providerType: 'type-1' }); + + // Initially not excluded + assert.strictEqual(filter.exclude(session), false); + + // Simulate storage update from another window + const excludes = { + providers: ['type-1'], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // Should now be excluded + assert.strictEqual(filter.exclude(session), true); + }); + + test('should register provider filter actions', () => { + const provider1: IChatSessionItemProvider = { + chatSessionType: 'custom-type-1', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider1); + + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + // Filter should work with custom provider + const session = createSession({ providerType: 'custom-type-1' }); + assert.strictEqual(filter.exclude(session), false); + }); + + test('should handle providers registered after filter creation', () => { + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const provider: IChatSessionItemProvider = { + chatSessionType: 'new-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [] + }; + + // Register provider after filter creation + mockChatSessionsService.registerChatSessionItemProvider(provider); + mockChatSessionsService.fireDidChangeItemsProviders(provider); + + // Filter should work with new provider + const session = createSession({ providerType: 'new-type' }); + assert.strictEqual(filter.exclude(session), false); + }); + + test('should not exclude when all filters are disabled', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session = createSession({ + providerType: 'type-1', + status: ChatSessionStatus.Failed, + isArchived: () => true + }); + + // Disable all filters + const excludes = { + providers: [], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // Nothing should be excluded + assert.strictEqual(filter.exclude(session), false); + }); + + test('should handle empty provider list in storage', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session = createSession({ providerType: 'type-1' }); + + // Set empty provider list + const excludes = { + providers: [], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + assert.strictEqual(filter.exclude(session), false); + }); + + test('should handle different MenuId contexts', () => { + const storageService = instantiationService.get(IStorageService); + + // Create two filters with different menu IDs + const filter1 = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const filter2 = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewItemContext } + )); + + const session = createSession({ providerType: 'type-1' }); + + // Set excludes only for ViewTitle + const excludes = { + providers: ['type-1'], + states: [], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // filter1 should exclude the session + assert.strictEqual(filter1.exclude(session), true); + // filter2 should not exclude the session (different storage key) + assert.strictEqual(filter2.exclude(session), false); + }); + + test('should handle malformed storage data gracefully', () => { + const storageService = instantiationService.get(IStorageService); + + // Store malformed JSON + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, 'invalid json', StorageScope.PROFILE, StorageTarget.USER); + + // Filter should still be created with default excludes + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const archivedSession = createSession({ isArchived: () => true }); + // Default behavior: archived should NOT be excluded + assert.strictEqual(filter.exclude(archivedSession), false); + }); + + test('should prioritize archived check first', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const session = createSession({ + providerType: 'type-1', + status: ChatSessionStatus.Completed, + isArchived: () => true + }); + + // Set excludes for provider and status, but include archived + const excludes = { + providers: ['type-1'], + states: [ChatSessionStatus.Completed], + archived: true + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + // Should be excluded due to archived (checked first) + assert.strictEqual(filter.exclude(session), true); + }); + + test('should handle all three status types correctly', () => { + const storageService = instantiationService.get(IStorageService); + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + const completedSession = createSession({ status: ChatSessionStatus.Completed }); + const inProgressSession = createSession({ status: ChatSessionStatus.InProgress }); + const failedSession = createSession({ status: ChatSessionStatus.Failed }); + + // Exclude all statuses + const excludes = { + providers: [], + states: [ChatSessionStatus.Completed, ChatSessionStatus.InProgress, ChatSessionStatus.Failed], + archived: false + }; + storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER); + + assert.strictEqual(filter.exclude(completedSession), true); + assert.strictEqual(filter.exclude(inProgressSession), true); + assert.strictEqual(filter.exclude(failedSession), true); + }); + }); + + suite('AgentSessionsViewModel - Session Archiving', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + let viewModel: AgentSessionsModel; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should archive and unarchive sessions', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isArchived(), false); + + // Archive the session + session.setArchived(true); + assert.strictEqual(session.isArchived(), true); + + // Unarchive the session + session.setArchived(false); + assert.strictEqual(session.isArchived(), false); + }); + }); + + test('should fire onDidChangeSessions when archiving', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + let changeEventFired = false; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventFired = true; + })); + + session.setArchived(true); + assert.strictEqual(changeEventFired, true); + }); + }); + + test('should not fire onDidChangeSessions when archiving with same value', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + session.setArchived(true); + + let changeEventFired = false; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventFired = true; + })); + + // Try to archive again with same value + session.setArchived(true); + assert.strictEqual(changeEventFired, false); + }); + }); + + test('should preserve archived state from provider', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Test Session', + archived: true, + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isArchived(), true); + }); + }); + + test('should override provider archived state with user preference', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Test Session', + archived: true, + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isArchived(), true); + + // User unarchives + session.setArchived(false); + assert.strictEqual(session.isArchived(), false); + + // Re-resolve should preserve user preference + await viewModel.resolve(undefined); + const sessionAfterResolve = viewModel.sessions[0]; + assert.strictEqual(sessionAfterResolve.isArchived(), false); + }); + }); + }); + + suite('AgentSessionsViewModel - Session Read State', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + let viewModel: AgentSessionsModel; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + const storageService = instantiationService.get(IStorageService); + storageService.store('agentSessions.readDateBaseline', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should mark session as read and unread', async () => { + return runWithFakedTimers({}, async () => { + const futureSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Session 1', + timing: futureSessionTiming, + }, + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + + // Mark as read + session.setRead(true); + assert.strictEqual(session.isRead(), true); + + // Mark as unread + session.setRead(false); + assert.strictEqual(session.isRead(), false); + }); + }); + + test('should fire onDidChangeSessions when marking as read', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + session.setRead(false); // ensure it's unread first + + let changeEventFired = false; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventFired = true; + })); + + session.setRead(true); + assert.strictEqual(changeEventFired, true); + }); + }); + + test('should not fire onDidChangeSessions when marking as read with same value', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + session.setRead(true); + + let changeEventFired = false; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventFired = true; + })); + + // Try to mark as read again with same value + session.setRead(true); + assert.strictEqual(changeEventFired, false); + }); + }); + + test('should preserve read state after re-resolve', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + session.setRead(true); + assert.strictEqual(session.isRead(), true); + + // Re-resolve should preserve read state + await viewModel.resolve(undefined); + const sessionAfterResolve = viewModel.sessions[0]; + assert.strictEqual(sessionAfterResolve.isRead(), true); + }); + }); + + test('should consider sessions before initial date as read by default', async () => { + return runWithFakedTimers({}, async () => { + // Without migration, all sessions are unread by default + const oldSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 10 /* November */, 1), + lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), + lastRequestEnded: Date.UTC(2025, 10 /* November */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://old-session'), + label: 'Old Session', + timing: oldSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Sessions are unread by default (migration already happened in setup) + assert.strictEqual(session.isRead(), false); + }); + }); + + test('should consider sessions after initial date as unread by default', async () => { + return runWithFakedTimers({}, async () => { + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Sessions after the initial date should be considered unread + assert.strictEqual(session.isRead(), false); + }); + }); + + test('should use endTime for read state comparison when available', async () => { + return runWithFakedTimers({}, async () => { + // Session with startTime before initial date but endTime after + const sessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 10 /* November */, 1), + lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 1), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-with-endtime'), + label: 'Session With EndTime', + timing: sessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Should use lastRequestEnded (December 10) which is after the initial date + assert.strictEqual(session.isRead(), false); + }); + }); + + test('should use startTime for read state comparison when endTime is not available', async () => { + return runWithFakedTimers({}, async () => { + // Session with only startTime + const sessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 10 /* November */, 1), + lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), + lastRequestEnded: undefined, + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-no-endtime'), + label: 'Session Without EndTime', + timing: sessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Sessions are unread by default + assert.strictEqual(session.isRead(), false); + }); + }); + + test('should treat archived sessions as read', async () => { + return runWithFakedTimers({}, async () => { + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Session after the initial date should be unread by default + assert.strictEqual(session.isRead(), false); + assert.strictEqual(session.isArchived(), false); + + // Archive the session + session.setArchived(true); + + // Archived sessions should always be considered read + assert.strictEqual(session.isArchived(), true); + assert.strictEqual(session.isRead(), true); + }); + }); + + test('should mark session as read when archiving', async () => { + return runWithFakedTimers({}, async () => { + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isRead(), false); + + // Archive the session + session.setArchived(true); + + // Should be read after archiving (archived sessions are always read) + assert.strictEqual(session.isRead(), true); + + // Unarchive the session + session.setArchived(false); + + // After unarchiving, the read state depends on the stored read date vs session timing. + // When archiving marked the session as read, the read date was set to the test's + // faked Date.now() which may be earlier than the session's lastRequestEnded, + // so the session may appear unread again based on the time comparison. + assert.strictEqual(session.isArchived(), false); + }); + }); + + test('should fire onDidChangeSessions when archiving an unread session', async () => { + return runWithFakedTimers({}, async () => { + const newSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2026, 1 /* February */, 1), + lastRequestStarted: Date.UTC(2026, 1 /* February */, 1), + lastRequestEnded: Date.UTC(2026, 1 /* February */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://new-session'), + label: 'New Session', + timing: newSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.isRead(), false); + + let changeEventCount = 0; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventCount++; + })); + + // Archive the session (which also marks as read) + session.setArchived(true); + + // Fires twice: once for setting read state, once for setting archived state + assert.strictEqual(changeEventCount, 2); + }); + }); + + test('should not fire onDidChangeSessions when archiving an already read session', async () => { + return runWithFakedTimers({}, async () => { + // Session with timing + const oldSessionTiming: IChatSessionItem['timing'] = { + created: Date.UTC(2025, 10 /* November */, 1), + lastRequestStarted: Date.UTC(2025, 10 /* November */, 1), + lastRequestEnded: Date.UTC(2025, 10 /* November */, 2), + }; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://old-session'), + label: 'Old Session', + timing: oldSessionTiming, + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + // Mark session as read first + session.setRead(true); + assert.strictEqual(session.isRead(), true); + + let changeEventCount = 0; + disposables.add(viewModel.onDidChangeSessions(() => { + changeEventCount++; + })); + + // Archive the session + session.setArchived(true); + + // Should fire only once for archived state change since session is already read + assert.strictEqual(changeEventCount, 1); + }); + }); + }); + + suite('AgentSessionsViewModel - State Tracking', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + let viewModel: AgentSessionsModel; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should track status transitions', async () => { + return runWithFakedTimers({}, async () => { + let sessionStatus = ChatSessionStatus.InProgress; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Test Session', + status: sessionStatus, + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions[0].status, ChatSessionStatus.InProgress); + + // Change status + sessionStatus = ChatSessionStatus.Completed; + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions[0].status, ChatSessionStatus.Completed); + }); + }); + + test('should clean up state tracking for removed sessions', async () => { + return runWithFakedTimers({}, async () => { + let includeSessions = true; + + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => { + if (includeSessions) { + return [ + makeSimpleSessionItem('session-1'), + ]; + } + return []; + } + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 1); + + // Remove sessions + includeSessions = false; + await viewModel.resolve(undefined); + assert.strictEqual(viewModel.sessions.length, 0); + }); + }); + }); + + suite('AgentSessionsViewModel - Provider Icons and Names', () => { + const disposables = new DisposableStore(); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should return correct name for Local provider', () => { + const name = getAgentSessionProviderName(AgentSessionProviders.Local); + assert.ok(name.length > 0); + }); + + test('should return correct name for Background provider', () => { + const name = getAgentSessionProviderName(AgentSessionProviders.Background); + assert.ok(name.length > 0); + }); + + test('should return correct name for Cloud provider', () => { + const name = getAgentSessionProviderName(AgentSessionProviders.Cloud); + assert.ok(name.length > 0); + }); + + test('should return correct icon for Local provider', () => { + const icon = getAgentSessionProviderIcon(AgentSessionProviders.Local); + assert.strictEqual(icon.id, Codicon.vm.id); + }); + + test('should return correct icon for Background provider', () => { + const icon = getAgentSessionProviderIcon(AgentSessionProviders.Background); + assert.strictEqual(icon.id, Codicon.worktree.id); + }); + + test('should return correct icon for Cloud provider', () => { + const icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud); + assert.strictEqual(icon.id, Codicon.cloud.id); + }); + + test('should handle Local provider type in model', async () => { + return runWithFakedTimers({}, async () => { + const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + const mockChatSessionsService = new MockChatSessionsService(); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + + const provider: IChatSessionItemProvider = { + chatSessionType: AgentSessionProviders.Local, + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + const viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.providerType, AgentSessionProviders.Local); + assert.strictEqual(session.icon.id, Codicon.vm.id); + assert.strictEqual(session.providerLabel, getAgentSessionProviderName(AgentSessionProviders.Local)); + }); + }); + + test('should handle Background provider type in model', async () => { + return runWithFakedTimers({}, async () => { + const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + const mockChatSessionsService = new MockChatSessionsService(); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + + const provider: IChatSessionItemProvider = { + chatSessionType: AgentSessionProviders.Background, + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + const viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.providerType, AgentSessionProviders.Background); + assert.strictEqual(session.icon.id, Codicon.worktree.id); + assert.strictEqual(session.providerLabel, getAgentSessionProviderName(AgentSessionProviders.Background)); + }); + }); + + test('should handle Cloud provider type in model', async () => { + return runWithFakedTimers({}, async () => { + const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + const mockChatSessionsService = new MockChatSessionsService(); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + + const provider: IChatSessionItemProvider = { + chatSessionType: AgentSessionProviders.Cloud, + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + const viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.providerType, AgentSessionProviders.Cloud); + assert.strictEqual(session.icon.id, Codicon.cloud.id); + assert.strictEqual(session.providerLabel, getAgentSessionProviderName(AgentSessionProviders.Cloud)); + }); + }); + + test('should use custom icon from session item', async () => { + return runWithFakedTimers({}, async () => { + const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + const mockChatSessionsService = new MockChatSessionsService(); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + + const customIcon = ThemeIcon.fromId('beaker'); + const provider: IChatSessionItemProvider = { + chatSessionType: 'custom-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + { + resource: URI.parse('test://session-1'), + label: 'Test Session', + iconPath: customIcon, + timing: makeNewSessionTiming() + } + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + const viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.icon.id, customIcon.id); + }); + }); + + test('should use default icon for custom provider without iconPath', async () => { + return runWithFakedTimers({}, async () => { + const instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + const mockChatSessionsService = new MockChatSessionsService(); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService())); + + const provider: IChatSessionItemProvider = { + chatSessionType: 'custom-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + const viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + await viewModel.resolve(undefined); + + const session = viewModel.sessions[0]; + assert.strictEqual(session.icon.id, Codicon.terminal.id); + }); + }); + }); + + suite('AgentSessionsViewModel - Cancellation and Lifecycle', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let mockLifecycleService: TestLifecycleService; + let instantiationService: TestInstantiationService; + let viewModel: AgentSessionsModel; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + mockLifecycleService = disposables.add(new TestLifecycleService()); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + instantiationService.stub(ILifecycleService, mockLifecycleService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should not resolve if lifecycle will shutdown', async () => { + return runWithFakedTimers({}, async () => { + const provider: IChatSessionItemProvider = { + chatSessionType: 'test-type', + onDidChangeChatSessionItems: Event.None, + provideChatSessionItems: async () => [ + makeSimpleSessionItem('session-1'), + ] + }; + + mockChatSessionsService.registerChatSessionItemProvider(provider); + viewModel = disposables.add(instantiationService.createInstance(AgentSessionsModel)); + + // Set willShutdown to true + mockLifecycleService.willShutdown = true; + + await viewModel.resolve(undefined); + + // Should not resolve sessions + assert.strictEqual(viewModel.sessions.length, 0); + }); + }); + }); + + suite('AgentSessionsFilter - Dynamic Provider Registration', () => { + const disposables = new DisposableStore(); + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + + setup(() => { + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('should respond to onDidChangeAvailability', () => { + const filter = disposables.add(instantiationService.createInstance( + AgentSessionsFilter, + { filterMenuId: MenuId.ViewTitle } + )); + + disposables.add(filter.onDidChange(() => { + // Event handler registered to verify filter responds to availability changes + })); + + // Trigger availability change + mockChatSessionsService.fireDidChangeAvailability(); + + // Filter should update its actions (internally) + // We can't directly test action registration but we verified event handling + }); + }); + +}); // End of Agent Sessions suite + +function makeSimpleSessionItem(id: string, overrides?: Partial): IChatSessionItem { + return { + resource: URI.parse(`test://${id}`), + label: `Session ${id}`, + timing: makeNewSessionTiming(), + ...overrides + }; +} + +function makeNewSessionTiming(options?: { + created?: number; + lastRequestStarted?: number | undefined; + lastRequestEnded?: number | undefined; +}): IChatSessionItem['timing'] { + const now = Date.now(); + return { + created: options?.created ?? now, + lastRequestStarted: options?.lastRequestStarted, + lastRequestEnded: options?.lastRequestEnded, + }; +} diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts new file mode 100644 index 00000000000..336955f5bb0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsAccessibilityProvider.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentSessionsAccessibilityProvider } from '../../../browser/agentSessions/agentSessionsViewer.js'; +import { AgentSessionSection, IAgentSession, IAgentSessionSection } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { ChatSessionStatus } from '../../../common/chatSessionsService.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; + +suite('AgentSessionsAccessibilityProvider', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + let accessibilityProvider: AgentSessionsAccessibilityProvider; + + function createMockSession(overrides: Partial<{ + id: string; + label: string; + providerLabel: string; + status: ChatSessionStatus; + }> = {}): IAgentSession { + const now = Date.now(); + return { + providerType: 'test', + providerLabel: overrides.providerLabel ?? 'Test', + resource: URI.parse(`test://session/${overrides.id ?? 'default'}`), + status: overrides.status ?? ChatSessionStatus.Completed, + label: overrides.label ?? `Session ${overrides.id ?? 'default'}`, + icon: Codicon.terminal, + timing: { + created: now, + lastRequestEnded: undefined, + lastRequestStarted: undefined, + }, + changes: undefined, + isArchived: () => false, + setArchived: () => { }, + isRead: () => true, + setRead: () => { }, + }; + } + + function createMockSection(section: AgentSessionSection = AgentSessionSection.Today): IAgentSessionSection { + return { + section, + label: 'Today', + sessions: [] + }; + } + + setup(() => { + accessibilityProvider = new AgentSessionsAccessibilityProvider(); + }); + + test('getWidgetRole returns list', () => { + assert.strictEqual(accessibilityProvider.getWidgetRole(), 'list'); + }); + + test('getRole returns listitem for session', () => { + const session = createMockSession(); + assert.strictEqual(accessibilityProvider.getRole(session), 'listitem'); + }); + + test('getRole returns listitem for section', () => { + const section = createMockSection(); + assert.strictEqual(accessibilityProvider.getRole(section), 'listitem'); + }); + + test('getWidgetAriaLabel returns correct label', () => { + assert.strictEqual(accessibilityProvider.getWidgetAriaLabel(), 'Agent Sessions'); + }); + + test('getAriaLabel returns correct label for session', () => { + const session = createMockSession({ + id: 'test-session', + label: 'Test Session Title', + providerLabel: 'Agent' + }); + + const ariaLabel = accessibilityProvider.getAriaLabel(session); + + assert.ok(ariaLabel); + assert.ok(ariaLabel.includes('Test Session Title'), 'Aria label should include the session title'); + assert.ok(ariaLabel.includes('Agent'), 'Aria label should include the provider label'); + }); + + test('getAriaLabel returns correct label for section', () => { + const section = createMockSection(); + const ariaLabel = accessibilityProvider.getAriaLabel(section); + + assert.ok(ariaLabel); + assert.ok(ariaLabel.includes('sessions section'), 'Aria label should indicate it is a section'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts new file mode 100644 index 00000000000..63db5f0632a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsDataSource.test.ts @@ -0,0 +1,450 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { AgentSessionsDataSource, AgentSessionListItem, IAgentSessionsFilter, sessionDateFromNow } from '../../../browser/agentSessions/agentSessionsViewer.js'; +import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSessionSection } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { ChatSessionStatus, isSessionInProgressStatus } from '../../../common/chatSessionsService.js'; +import { ITreeSorter } from '../../../../../../base/browser/ui/tree/tree.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { AgentSessionsGrouping } from '../../../browser/agentSessions/agentSessionsFilter.js'; +import { getAgentSessionTime } from '../../../browser/agentSessions/agentSessions.js'; +import { IChatSessionTiming } from '../../../common/chatService/chatService.js'; + +suite('getAgentSessionTime', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('returns lastRequestEnded when available', () => { + const timing: IChatSessionTiming = { + created: 1000, + lastRequestStarted: 2000, + lastRequestEnded: 3000, + }; + assert.strictEqual(getAgentSessionTime(timing), 3000); + }); + + test('returns lastRequestStarted when lastRequestEnded is undefined', () => { + const timing: IChatSessionTiming = { + created: 1000, + lastRequestStarted: 2000, + lastRequestEnded: undefined, + }; + assert.strictEqual(getAgentSessionTime(timing), 2000); + }); + + test('returns created when both lastRequestEnded and lastRequestStarted are undefined', () => { + const timing: IChatSessionTiming = { + created: 1000, + lastRequestStarted: undefined, + lastRequestEnded: undefined, + }; + assert.strictEqual(getAgentSessionTime(timing), 1000); + }); +}); + +suite('sessionDateFromNow', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const ONE_DAY = 24 * 60 * 60 * 1000; + + test('returns "1 day ago" for yesterday', () => { + const now = Date.now(); + const startOfToday = new Date(now).setHours(0, 0, 0, 0); + // Time in the middle of yesterday + const yesterday = startOfToday - ONE_DAY / 2; + assert.strictEqual(sessionDateFromNow(yesterday), '1 day ago'); + }); + + test('returns "2 days ago" for two days ago', () => { + const now = Date.now(); + const startOfToday = new Date(now).setHours(0, 0, 0, 0); + const startOfYesterday = startOfToday - ONE_DAY; + // Time in the middle of two days ago + const twoDaysAgo = startOfYesterday - ONE_DAY / 2; + assert.strictEqual(sessionDateFromNow(twoDaysAgo), '2 days ago'); + }); + + test('returns fromNow result for today', () => { + const now = Date.now(); + // A time from today (5 minutes ago) + const fiveMinutesAgo = now - 5 * 60 * 1000; + const result = sessionDateFromNow(fiveMinutesAgo); + // Should return a time ago string, not "1 day ago" or "2 days ago" + assert.ok(result.includes('min') || result.includes('sec') || result === 'now', `Expected minutes/seconds ago or now, got: ${result}`); + }); + + test('returns fromNow result for three or more days ago', () => { + const now = Date.now(); + const startOfToday = new Date(now).setHours(0, 0, 0, 0); + // Time 5 days ago + const fiveDaysAgo = startOfToday - 5 * ONE_DAY; + const result = sessionDateFromNow(fiveDaysAgo); + // Should return "5 days ago" from fromNow, not our special handling + assert.ok(result.includes('day'), `Expected days ago, got: ${result}`); + assert.ok(!result.includes('1 day') && !result.includes('2 days'), `Should not be 1 or 2 days ago, got: ${result}`); + }); +}); + +suite('AgentSessionsDataSource', () => { + + ensureNoDisposablesAreLeakedInTestSuite(); + + const ONE_DAY = 24 * 60 * 60 * 1000; + const WEEK_THRESHOLD = 7 * ONE_DAY; // 7 days + + function createMockSession(overrides: Partial<{ + id: string; + status: ChatSessionStatus; + isArchived: boolean; + isRead: boolean; + hasChanges: boolean; + startTime: number; + endTime: number; + }> = {}): IAgentSession { + const now = Date.now(); + return { + providerType: 'test', + providerLabel: 'Test', + resource: URI.parse(`test://session/${overrides.id ?? 'default'}`), + status: overrides.status ?? ChatSessionStatus.Completed, + label: `Session ${overrides.id ?? 'default'}`, + icon: Codicon.terminal, + timing: { + created: overrides.startTime ?? now, + lastRequestEnded: undefined, + lastRequestStarted: undefined, + }, + changes: overrides.hasChanges ? { files: 1, insertions: 10, deletions: 5 } : undefined, + isArchived: () => overrides.isArchived ?? false, + setArchived: () => { }, + isRead: () => overrides.isRead ?? true, + setRead: () => { }, + }; + } + + function createMockModel(sessions: IAgentSession[]): IAgentSessionsModel { + return { + sessions, + resolved: true, + getSession: () => undefined, + onWillResolve: Event.None, + onDidResolve: Event.None, + onDidChangeSessions: Event.None, + onDidChangeSessionArchivedState: Event.None, + resolve: async () => { }, + }; + } + + function createMockFilter(options: { + groupBy?: AgentSessionsGrouping; + exclude?: (session: IAgentSession) => boolean; + }): IAgentSessionsFilter { + return { + onDidChange: Event.None, + groupResults: () => options.groupBy, + exclude: options.exclude ?? (() => false), + getExcludes: () => ({ providers: [], states: [], archived: false, read: false }) + }; + } + + function createMockSorter(): ITreeSorter { + return { + compare: (a, b) => { + // Sort by end time, most recent first + const aTime = getAgentSessionTime(a.timing); + const bTime = getAgentSessionTime(b.timing); + return bTime - aTime; + } + }; + } + + function getSectionsFromResult(result: Iterable): IAgentSessionSection[] { + return Array.from(result).filter((item): item is IAgentSessionSection => isAgentSessionSection(item)); + } + + suite('groupSessionsIntoSections', () => { + + test('returns flat list when groupResults is false', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', startTime: now, endTime: now }), + createMockSession({ id: '2', startTime: now - ONE_DAY, endTime: now - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: undefined }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + + // Should be a flat list without sections + assert.strictEqual(result.length, 2); + assert.strictEqual(getSectionsFromResult(result).length, 0); + }); + + test('groups active sessions first with header', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.InProgress, startTime: now - ONE_DAY }), + createMockSession({ id: '3', status: ChatSessionStatus.NeedsInput, startTime: now - 2 * ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + + // First item should be the In Progress section header + const firstItem = result[0]; + assert.ok(isAgentSessionSection(firstItem), 'First item should be a section header'); + assert.strictEqual((firstItem as IAgentSessionSection).section, AgentSessionSection.InProgress); + // Verify the sessions in the section have active status + const activeSessions = (firstItem as IAgentSessionSection).sessions; + assert.ok(activeSessions.every(s => isSessionInProgressStatus(s.status) || s.status === ChatSessionStatus.NeedsInput)); + }); + + test('adds Today header when there are active sessions', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.InProgress, startTime: now - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + // Now all sections have headers, so we expect In Progress and Today sections + assert.strictEqual(sections.length, 2); + assert.strictEqual(sections[0].section, AgentSessionSection.InProgress); + assert.strictEqual(sections[1].section, AgentSessionSection.Today); + }); + + test('adds Today header when there are no active sessions', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.Completed, startTime: now - ONE_DAY, endTime: now - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + // Now all sections have headers, so Today section should be present + assert.strictEqual(sections.filter(s => s.section === AgentSessionSection.Today).length, 1); + }); + + test('adds Older header for sessions older than week threshold', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.Completed, startTime: now - WEEK_THRESHOLD - ONE_DAY, endTime: now - WEEK_THRESHOLD - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + assert.strictEqual(sections.filter(s => s.section === AgentSessionSection.Older).length, 1); + }); + + test('adds Archived header for archived sessions', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.Completed, isArchived: true, startTime: now - ONE_DAY, endTime: now - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + assert.strictEqual(sections.filter(s => s.section === AgentSessionSection.Archived).length, 1); + }); + + test('archived sessions come after older sessions', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, isArchived: true, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.Completed, startTime: now - WEEK_THRESHOLD - ONE_DAY, endTime: now - WEEK_THRESHOLD - ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + + const olderIndex = result.findIndex(item => isAgentSessionSection(item) && item.section === AgentSessionSection.Older); + const archivedIndex = result.findIndex(item => isAgentSessionSection(item) && item.section === AgentSessionSection.Archived); + + assert.ok(olderIndex < archivedIndex, 'Older section should come before Archived section'); + }); + + test('archived in-progress sessions appear in Archived section not In Progress', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: 'archived-active', status: ChatSessionStatus.InProgress, isArchived: true, startTime: now }), + createMockSession({ id: 'active', status: ChatSessionStatus.InProgress, startTime: now }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + // Verify there is both an In Progress and Archived section + const inProgressSection = sections.find(s => s.section === AgentSessionSection.InProgress); + const archivedSection = sections.find(s => s.section === AgentSessionSection.Archived); + + assert.ok(inProgressSection, 'In Progress section should exist'); + assert.ok(archivedSection, 'Archived section should exist'); + + // The archived session should NOT appear in In Progress + assert.strictEqual(inProgressSection.sessions.length, 1); + assert.strictEqual(inProgressSection.sessions[0].label, 'Session active'); + + // The archived session should appear in Archived even though it's in progress + assert.strictEqual(archivedSection.sessions.length, 1); + assert.strictEqual(archivedSection.sessions[0].label, 'Session archived-active'); + }); + + test('correct order: active, today, week, older, archived', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: 'archived', status: ChatSessionStatus.Completed, isArchived: true, startTime: now, endTime: now }), + createMockSession({ id: 'today', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: 'week', status: ChatSessionStatus.Completed, startTime: now - 3 * ONE_DAY, endTime: now - 3 * ONE_DAY }), + createMockSession({ id: 'old', status: ChatSessionStatus.Completed, startTime: now - WEEK_THRESHOLD - ONE_DAY, endTime: now - WEEK_THRESHOLD - ONE_DAY }), + createMockSession({ id: 'active', status: ChatSessionStatus.InProgress, startTime: now }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + + // All sections now have headers + // In Progress section + assert.ok(isAgentSessionSection(result[0])); + assert.strictEqual((result[0] as IAgentSessionSection).section, AgentSessionSection.InProgress); + assert.strictEqual((result[0] as IAgentSessionSection).sessions[0].label, 'Session active'); + + // Today section + assert.ok(isAgentSessionSection(result[1])); + assert.strictEqual((result[1] as IAgentSessionSection).section, AgentSessionSection.Today); + assert.strictEqual((result[1] as IAgentSessionSection).sessions[0].label, 'Session today'); + + // Week section + assert.ok(isAgentSessionSection(result[2])); + assert.strictEqual((result[2] as IAgentSessionSection).section, AgentSessionSection.Week); + assert.strictEqual((result[2] as IAgentSessionSection).sessions[0].label, 'Session week'); + + // Older section + assert.ok(isAgentSessionSection(result[3])); + assert.strictEqual((result[3] as IAgentSessionSection).section, AgentSessionSection.Older); + assert.strictEqual((result[3] as IAgentSessionSection).sessions[0].label, 'Session old'); + + // Archived section + assert.ok(isAgentSessionSection(result[4])); + assert.strictEqual((result[4] as IAgentSessionSection).section, AgentSessionSection.Archived); + assert.strictEqual((result[4] as IAgentSessionSection).sessions[0].label, 'Session archived'); + }); + + test('empty sessions returns empty result', () => { + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel([]); + const result = Array.from(dataSource.getChildren(mockModel)); + + assert.strictEqual(result.length, 0); + }); + + test('only today sessions produces a Today section header', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: '1', status: ChatSessionStatus.Completed, startTime: now, endTime: now }), + createMockSession({ id: '2', status: ChatSessionStatus.Completed, startTime: now - 1000, endTime: now - 1000 }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + const sections = getSectionsFromResult(result); + + // All sections now have headers, so a Today section should be present + assert.strictEqual(sections.length, 1); + assert.strictEqual(sections[0].section, AgentSessionSection.Today); + assert.strictEqual(sections[0].sessions.length, 2); + }); + + test('sessions are sorted within each group', () => { + const now = Date.now(); + const sessions = [ + createMockSession({ id: 'old1', status: ChatSessionStatus.Completed, startTime: now - WEEK_THRESHOLD - 2 * ONE_DAY, endTime: now - WEEK_THRESHOLD - 2 * ONE_DAY }), + createMockSession({ id: 'old2', status: ChatSessionStatus.Completed, startTime: now - WEEK_THRESHOLD - ONE_DAY, endTime: now - WEEK_THRESHOLD - ONE_DAY }), + createMockSession({ id: 'week1', status: ChatSessionStatus.Completed, startTime: now - 3 * ONE_DAY, endTime: now - 3 * ONE_DAY }), + createMockSession({ id: 'week2', status: ChatSessionStatus.Completed, startTime: now - 2 * ONE_DAY, endTime: now - 2 * ONE_DAY }), + ]; + + const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); + const sorter = createMockSorter(); + const dataSource = new AgentSessionsDataSource(filter, sorter); + + const mockModel = createMockModel(sessions); + const result = Array.from(dataSource.getChildren(mockModel)); + + // All sections now have headers + // Week section should be first and contain sorted sessions + const weekSection = result.find((item): item is IAgentSessionSection => isAgentSessionSection(item) && item.section === AgentSessionSection.Week); + assert.ok(weekSection); + assert.strictEqual(weekSection.sessions[0].label, 'Session week2'); + assert.strictEqual(weekSection.sessions[1].label, 'Session week1'); + + // Older section with sorted sessions + const olderSection = result.find((item): item is IAgentSessionSection => isAgentSessionSection(item) && item.section === AgentSessionSection.Older); + assert.ok(olderSection); + assert.strictEqual(olderSection.sessions[0].label, 'Session old2'); + assert.strictEqual(olderSection.sessions[1].label, 'Session old1'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts new file mode 100644 index 00000000000..3fbe3886f43 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/localAgentSessionsProvider.test.ts @@ -0,0 +1,835 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ISettableObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { LocalAgentsSessionsProvider } from '../../../browser/agentSessions/localAgentSessionsProvider.js'; +import { ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { IChatModel, IChatRequestModel, IChatResponseModel } from '../../../common/model/chatModel.js'; +import { IChatDetail, IChatService, IChatSessionStartOptions, ResponseModelState } from '../../../common/chatService/chatService.js'; +import { ChatSessionStatus, IChatSessionItem, IChatSessionsService, localChatSessionType } from '../../../common/chatSessionsService.js'; +import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; +import { MockChatSessionsService } from '../../common/mockChatSessionsService.js'; + +function createTestTiming(options?: { + created?: number; + lastRequestStarted?: number | undefined; + lastRequestEnded?: number | undefined; +}): IChatSessionItem['timing'] { + const now = Date.now(); + return { + created: options?.created ?? now, + lastRequestStarted: options?.lastRequestStarted, + lastRequestEnded: options?.lastRequestEnded, + }; +} + +class MockChatService implements IChatService { + private readonly _chatModels: ISettableObservable> = observableValue('chatModels', []); + readonly chatModels = this._chatModels; + requestInProgressObs = observableValue('name', false); + edits2Enabled: boolean = false; + _serviceBrand: undefined; + editingSessions = []; + transferredSessionResource = undefined; + readonly onDidSubmitRequest = Event.None; + readonly onDidCreateModel = Event.None; + + private sessions = new Map(); + private liveSessionItems: IChatDetail[] = []; + private historySessionItems: IChatDetail[] = []; + + private readonly _onDidDisposeSession = new Emitter<{ sessionResource: URI[]; reason: 'cleared' }>(); + readonly onDidDisposeSession = this._onDidDisposeSession.event; + + fireDidDisposeSession(sessionResource: URI[]): void { + this._onDidDisposeSession.fire({ sessionResource, reason: 'cleared' }); + } + + setSaveModelsEnabled(enabled: boolean): void { + + } + + setLiveSessionItems(items: IChatDetail[]): void { + this.liveSessionItems = items; + } + + setHistorySessionItems(items: IChatDetail[]): void { + this.historySessionItems = items; + } + + addSession(sessionResource: URI, session: IChatModel): void { + this.sessions.set(sessionResource.toString(), session); + // Update the chatModels observable + this._chatModels.set([...this.sessions.values()], undefined); + } + + removeSession(sessionResource: URI): void { + this.sessions.delete(sessionResource.toString()); + // Update the chatModels observable + this._chatModels.set([...this.sessions.values()], undefined); + } + + isEnabled(_location: ChatAgentLocation): boolean { + return true; + } + + hasSessions(): boolean { + return this.sessions.size > 0; + } + + getProviderInfos() { + return []; + } + + startSession(_location: ChatAgentLocation, _options?: IChatSessionStartOptions): any { + throw new Error('Method not implemented.'); + } + + getSession(sessionResource: URI): IChatModel | undefined { + return this.sessions.get(sessionResource.toString()); + } + + getOrRestoreSession(_sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } + + getSessionTitle(_sessionResource: URI): string | undefined { + return undefined; + } + + loadSessionFromContent(_data: any): any { + throw new Error('Method not implemented.'); + } + + loadSessionForResource(_resource: URI, _position: ChatAgentLocation, _token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } + + getActiveSessionReference(_sessionResource: URI): any { + return undefined; + } + + setTitle(_sessionResource: URI, _title: string): void { } + + appendProgress(_request: IChatRequestModel, _progress: any): void { } + + sendRequest(_sessionResource: URI, _message: string): Promise { + throw new Error('Method not implemented.'); + } + + resendRequest(_request: IChatRequestModel, _options?: any): Promise { + throw new Error('Method not implemented.'); + } + + adoptRequest(_sessionResource: URI, _request: IChatRequestModel): Promise { + throw new Error('Method not implemented.'); + } + + removeRequest(_sessionResource: URI, _requestId: string): Promise { + throw new Error('Method not implemented.'); + } + + cancelCurrentRequestForSession(_sessionResource: URI): void { } + + addCompleteRequest(): void { } + + async getLocalSessionHistory(): Promise { + return this.historySessionItems; + } + + async clearAllHistoryEntries(): Promise { } + + async removeHistoryEntry(_resource: URI): Promise { } + + readonly onDidPerformUserAction = Event.None; + + notifyUserAction(_event: any): void { } + + readonly onDidReceiveQuestionCarouselAnswer = Event.None; + + notifyQuestionCarouselAnswer(_requestId: string, _resolveId: string, _answers: Record | undefined): void { } + + async transferChatSession(): Promise { } + + setChatSessionTitle(): void { } + + isEditingLocation(_location: ChatAgentLocation): boolean { + return false; + } + + getChatStorageFolder(): URI { + return URI.file('/tmp'); + } + + logChatIndex(): void { } + + activateDefaultAgent(_location: ChatAgentLocation): Promise { + return Promise.resolve(); + } + + getChatSessionFromInternalUri(_sessionResource: URI): any { + return undefined; + } + + async getLiveSessionItems(): Promise { + return this.liveSessionItems; + } + + async getHistorySessionItems(): Promise { + return this.historySessionItems; + } + + waitForModelDisposals(): Promise { + return Promise.resolve(); + } + + getMetadataForSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } +} + +function createMockChatModel(options: { + sessionResource: URI; + hasRequests?: boolean; + requestInProgress?: boolean; + timestamp?: number; + lastResponseComplete?: boolean; + lastResponseCanceled?: boolean; + lastResponseHasError?: boolean; + lastResponseTimestamp?: number; + lastResponseCompletedAt?: number; + customTitle?: string; + editingSession?: { + entries: Array<{ + state: ModifiedFileEntryState; + linesAdded: number; + linesRemoved: number; + modifiedURI: URI; + }>; + }; +}): IChatModel { + const requests: IChatRequestModel[] = []; + + if (options.hasRequests !== false) { + const mockResponse: Partial = { + isComplete: options.lastResponseComplete ?? true, + isCanceled: options.lastResponseCanceled ?? false, + result: options.lastResponseHasError ? { errorDetails: { message: 'error' } } : undefined, + timestamp: options.lastResponseTimestamp ?? Date.now(), + completedAt: options.lastResponseCompletedAt, + response: { + value: [], + getMarkdown: () => '', + toString: () => options.customTitle ? '' : 'Test response content' + } + }; + + requests.push({ + id: 'request-1', + response: mockResponse as IChatResponseModel + } as IChatRequestModel); + } + + const editingSessionEntries = options.editingSession?.entries.map(entry => ({ + state: observableValue('state', entry.state), + linesAdded: observableValue('linesAdded', entry.linesAdded), + linesRemoved: observableValue('linesRemoved', entry.linesRemoved), + modifiedURI: entry.modifiedURI + })); + + const mockEditingSession = options.editingSession ? { + entries: observableValue('entries', editingSessionEntries ?? []) + } : undefined; + + const _onDidChange = new Emitter<{ kind: string } | undefined>(); + + return { + sessionResource: options.sessionResource, + hasRequests: options.hasRequests !== false, + timestamp: options.timestamp ?? Date.now(), + requestInProgress: observableValue('requestInProgress', options.requestInProgress ?? false), + getRequests: () => requests, + onDidChange: _onDidChange.event, + editingSession: mockEditingSession, + setCustomTitle: (_title: string) => { + _onDidChange.fire({ kind: 'setCustomTitle' }); + } + } as unknown as IChatModel; +} + +suite('LocalAgentsSessionsProvider', () => { + const disposables = new DisposableStore(); + let mockChatService: MockChatService; + let mockChatSessionsService: MockChatSessionsService; + let instantiationService: TestInstantiationService; + + setup(() => { + mockChatService = new MockChatService(); + mockChatSessionsService = new MockChatSessionsService(); + instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables)); + instantiationService.stub(IChatService, mockChatService); + instantiationService.stub(IChatSessionsService, mockChatSessionsService); + }); + + teardown(() => { + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + function createProvider(): LocalAgentsSessionsProvider { + return disposables.add(instantiationService.createInstance(LocalAgentsSessionsProvider)); + } + + test('should have correct session type', () => { + const provider = createProvider(); + assert.strictEqual(provider.chatSessionType, localChatSessionType); + }); + + test('should register itself with chat sessions service', async () => { + const provider = createProvider(); + + const providerResults = await mockChatSessionsService.getChatSessionItems(undefined, CancellationToken.None); + assert.strictEqual(providerResults.length, 1); + assert.strictEqual(providerResults[0].chatSessionType, provider.chatSessionType); + }); + + test('should provide empty sessions when no live or history sessions', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + mockChatService.setLiveSessionItems([]); + mockChatService.setHistorySessionItems([]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 0); + }); + }); + + test('should provide live session items', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('test-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + timestamp: Date.now() + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Test Session', + lastMessageDate: Date.now(), + isActive: true, + timing: createTestTiming(), + lastResponseState: ResponseModelState.Complete + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].label, 'Test Session'); + assert.strictEqual(sessions[0].resource.toString(), sessionResource.toString()); + }); + }); + + test('should provide history session items', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('history-session'); + + mockChatService.setLiveSessionItems([]); + mockChatService.setHistorySessionItems([{ + sessionResource, + title: 'History Session', + lastMessageDate: Date.now() - 10000, + isActive: false, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].label, 'History Session'); + }); + }); + + test('should not duplicate sessions in history and live', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('duplicate-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Live Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + mockChatService.setHistorySessionItems([{ + sessionResource, + title: 'History Session', + lastMessageDate: Date.now() - 10000, + isActive: false, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].label, 'Live Session'); + }); + }); + + suite('Session Status', () => { + test('should return InProgress status when request in progress', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('in-progress-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: true + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'In Progress Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].status, ChatSessionStatus.InProgress); + }); + }); + + test('should return Completed status when last response is complete', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('completed-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: false, + lastResponseComplete: true, + lastResponseCanceled: false, + lastResponseHasError: false + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Completed Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming(), + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].status, ChatSessionStatus.Completed); + }); + }); + + test('should return Success status when last response was canceled', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('canceled-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: false, + lastResponseComplete: false, + lastResponseCanceled: true + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Canceled Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming(), + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].status, ChatSessionStatus.Completed); + }); + }); + + test('should return Failed status when last response has error', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('error-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: false, + lastResponseComplete: true, + lastResponseHasError: true + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Error Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming(), + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].status, ChatSessionStatus.Failed); + }); + }); + }); + + suite('Session Statistics', () => { + test('should return statistics for sessions with modified entries', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('stats-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + editingSession: { + entries: [ + { + state: ModifiedFileEntryState.Modified, + linesAdded: 10, + linesRemoved: 5, + modifiedURI: URI.file('/test/file1.ts') + }, + { + state: ModifiedFileEntryState.Modified, + linesAdded: 20, + linesRemoved: 3, + modifiedURI: URI.file('/test/file2.ts') + } + ] + } + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Stats Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming(), + stats: { + added: 30, + removed: 8, + fileCount: 2 + } + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.ok(sessions[0].changes); + const changes = sessions[0].changes as { files: number; insertions: number; deletions: number }; + assert.strictEqual(changes.files, 2); + assert.strictEqual(changes.insertions, 30); + assert.strictEqual(changes.deletions, 8); + }); + }); + + test('should not return statistics for sessions without modified entries', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('no-stats-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + editingSession: { + entries: [ + { + state: ModifiedFileEntryState.Accepted, + linesAdded: 10, + linesRemoved: 5, + modifiedURI: URI.file('/test/file1.ts') + } + ] + } + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'No Stats Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].changes, undefined); + }); + }); + }); + + suite('Session Timing', () => { + test('should use model timestamp for created when model exists', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('timing-session'); + const modelTimestamp = Date.now() - 5000; + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + timestamp: modelTimestamp + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Timing Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming({ created: modelTimestamp }) + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].timing.created, modelTimestamp); + }); + }); + + test('should use lastMessageDate for created when model does not exist', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('history-timing'); + const lastMessageDate = Date.now() - 10000; + + mockChatService.setLiveSessionItems([]); + mockChatService.setHistorySessionItems([{ + sessionResource, + title: 'History Timing Session', + lastMessageDate, + isActive: false, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming({ created: lastMessageDate }) + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].timing.created, lastMessageDate); + }); + }); + + test('should set lastRequestEnded from last response completedAt', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('endtime-session'); + const completedAt = Date.now() - 1000; + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + lastResponseComplete: true, + lastResponseCompletedAt: completedAt + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'EndTime Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming({ lastRequestEnded: completedAt }) + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].timing.lastRequestEnded, completedAt); + }); + }); + }); + + suite('Session Icon', () => { + test('should use Codicon.chatSparkle as icon', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('icon-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true + }); + + mockChatService.addSession(sessionResource, mockModel); + mockChatService.setLiveSessionItems([{ + sessionResource, + title: 'Icon Session', + lastMessageDate: Date.now(), + isActive: true, + lastResponseState: ResponseModelState.Complete, + timing: createTestTiming() + }]); + + const sessions = await provider.provideChatSessionItems(CancellationToken.None); + assert.strictEqual(sessions.length, 1); + assert.strictEqual(sessions[0].iconPath, Codicon.chatSparkle); + }); + }); + }); + + suite('Events', () => { + test('should fire onDidChangeChatSessionItems when model progress changes', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('progress-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: true + }); + + // Add the session first + mockChatService.addSession(sessionResource, mockModel); + + let changeEventCount = 0; + disposables.add(provider.onDidChangeChatSessionItems(() => { + changeEventCount++; + })); + + // Simulate progress change by triggering the progress listener + mockChatSessionsService.triggerProgressEvent(); + + assert.strictEqual(changeEventCount, 1); + }); + }); + + test('should fire onDidChangeChatSessionItems when model request status changes', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('status-change-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true, + requestInProgress: false + }); + + // Add the session first + mockChatService.addSession(sessionResource, mockModel); + + let changeEventCount = 0; + disposables.add(provider.onDidChangeChatSessionItems(() => { + changeEventCount++; + })); + + // Simulate progress change by triggering the progress listener + mockChatSessionsService.triggerProgressEvent(); + + assert.strictEqual(changeEventCount, 1); + }); + }); + + test('should clean up model listeners when model is removed via chatModels observable', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + const sessionResource = LocalChatSessionUri.forSession('cleanup-session'); + const mockModel = createMockChatModel({ + sessionResource, + hasRequests: true + }); + + // Add the session first + mockChatService.addSession(sessionResource, mockModel); + + // Now remove the session - the observable should trigger cleanup + mockChatService.removeSession(sessionResource); + + // Verify the listener was cleaned up by triggering a title change + // The onDidChangeChatSessionItems from registerModelListeners cleanup should fire once + // but after that, title changes should NOT fire onDidChangeChatSessionItems + let changeEventCount = 0; + disposables.add(provider.onDidChangeChatSessionItems(() => { + changeEventCount++; + })); + + (mockModel as unknown as { setCustomTitle: (title: string) => void }).setCustomTitle('New Title'); + + assert.strictEqual(changeEventCount, 0, 'onDidChangeChatSessionItems should NOT fire after model is removed'); + }); + }); + + test('should fire onDidChange when session items change for local type', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + let changeEventFired = false; + disposables.add(provider.onDidChange(() => { + changeEventFired = true; + })); + + mockChatSessionsService.notifySessionItemsChanged(localChatSessionType); + + assert.strictEqual(changeEventFired, true); + }); + }); + + test('should not fire onDidChange when session items change for other types', async () => { + return runWithFakedTimers({}, async () => { + const provider = createProvider(); + + let changeEventFired = false; + disposables.add(provider.onDidChange(() => { + changeEventFired = true; + })); + + mockChatSessionsService.notifySessionItemsChanged('other-type'); + + assert.strictEqual(changeEventFired, false); + }); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingCheckpointTimeline.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts similarity index 90% rename from src/vs/workbench/contrib/chat/test/browser/chatEditingCheckpointTimeline.test.ts rename to src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts index 28c9069aaff..57478466f4f 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingCheckpointTimeline.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingCheckpointTimeline.test.ts @@ -4,20 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { transaction } from '../../../../../base/common/observable.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { upcastPartial } from '../../../../../base/test/common/mock.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingCheckpointTimelineImpl, IChatEditingTimelineFsDelegate } from '../../browser/chatEditing/chatEditingCheckpointTimelineImpl.js'; -import { FileOperation, FileOperationType } from '../../browser/chatEditing/chatEditingOperations.js'; -import { IModifiedEntryTelemetryInfo } from '../../common/chatEditingService.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { transaction } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { upcastPartial } from '../../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { SyncDescriptor } from '../../../../../../platform/instantiation/common/descriptors.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { INotebookService } from '../../../../notebook/common/notebookService.js'; +import { ChatEditingCheckpointTimelineImpl, IChatEditingTimelineFsDelegate } from '../../../browser/chatEditing/chatEditingCheckpointTimelineImpl.js'; +import { FileOperation, FileOperationType } from '../../../browser/chatEditing/chatEditingOperations.js'; +import { IModifiedEntryTelemetryInfo } from '../../../common/editing/chatEditingService.js'; suite('ChatEditingCheckpointTimeline', function () { @@ -29,7 +29,7 @@ suite('ChatEditingCheckpointTimeline', function () { const DEFAULT_TELEMETRY_INFO: IModifiedEntryTelemetryInfo = upcastPartial({ agentId: 'testAgent', command: undefined, - sessionId: 'test-session', + sessionResource: URI.parse('chat://test-session'), requestId: 'test-request', result: undefined, modelId: undefined, @@ -105,7 +105,7 @@ suite('ChatEditingCheckpointTimeline', function () { collection.set(INotebookService, new SyncDescriptor(TestNotebookService)); const insta = store.add(workbenchInstantiationService(undefined, store).createChild(collection)); - timeline = insta.createInstance(ChatEditingCheckpointTimelineImpl, 'test-session', fileDelegate); + timeline = insta.createInstance(ChatEditingCheckpointTimelineImpl, URI.parse('chat://test-session'), fileDelegate); }); teardown(() => { @@ -507,7 +507,7 @@ suite('ChatEditingCheckpointTimeline', function () { const newTimeline = insta.createInstance( ChatEditingCheckpointTimelineImpl, - 'test-session-2', + URI.parse('chat://test-session-2'), fileDelegate ); @@ -874,6 +874,91 @@ suite('ChatEditingCheckpointTimeline', function () { assert.strictEqual(timeline.hasFileBaseline(uri, 'req2'), false); }); + test('hasFileBaseline returns true for files with create operations', function () { + const uri = URI.parse('file:///created.txt'); + + // Initially, no baseline + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), false); + + // Record a create operation without recording an explicit baseline + timeline.recordFileOperation(createFileCreateOperation( + uri, + 'req1', + timeline.incrementEpoch(), + 'created content' + )); + + // hasFileBaseline should now return true because of the create operation + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), true); + assert.strictEqual(timeline.hasFileBaseline(uri, 'req2'), false); + }); + + test('hasFileBaseline distinguishes between different request IDs for create operations', function () { + const uri = URI.parse('file:///created.txt'); + + // Record a create operation for req1 + timeline.recordFileOperation(createFileCreateOperation( + uri, + 'req1', + timeline.incrementEpoch(), + 'content from req1' + )); + + // hasFileBaseline should only return true for req1 + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), true); + assert.strictEqual(timeline.hasFileBaseline(uri, 'req2'), false); + assert.strictEqual(timeline.hasFileBaseline(uri, 'req3'), false); + }); + + test('hasFileBaseline returns true when both baseline and create operation exist', function () { + const uri = URI.parse('file:///test.txt'); + + // Record both a baseline and a create operation + timeline.recordFileBaseline(upcastPartial({ + uri, + requestId: 'req1', + content: 'baseline content', + epoch: timeline.incrementEpoch(), + telemetryInfo: DEFAULT_TELEMETRY_INFO + })); + + timeline.recordFileOperation(createFileCreateOperation( + uri, + 'req1', + timeline.incrementEpoch(), + 'created content' + )); + + // Should return true (checking either source) + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), true); + }); + + test('hasFileBaseline with create operation followed by edit', function () { + const uri = URI.parse('file:///created-and-edited.txt'); + + // Record a create operation + timeline.recordFileOperation(createFileCreateOperation( + uri, + 'req1', + timeline.incrementEpoch(), + 'initial content' + )); + + // hasFileBaseline should return true + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), true); + + // Record an edit operation on the created file + timeline.recordFileOperation(createTextEditOperation( + uri, + 'req1', + timeline.incrementEpoch(), + [{ range: new Range(1, 1, 1, 16), text: 'edited content' }] + )); + + // hasFileBaseline should still return true + assert.strictEqual(timeline.hasFileBaseline(uri, 'req1'), true); + }); + test('multiple text edits to same file are properly replayed', async function () { const uri = URI.parse('file:///test.txt'); @@ -1170,5 +1255,3 @@ class TestNotebookService { getNotebookTextModel() { return undefined; } hasSupportedNotebooks() { return false; } } - - diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingModifiedNotebookEntry.test.ts similarity index 99% rename from src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts rename to src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingModifiedNotebookEntry.test.ts index 4a9b629ae7b..997d7387915 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingModifiedNotebookEntry.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingModifiedNotebookEntry.test.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements, adjustCellDiffForKeepingADeletedCell, adjustCellDiffForKeepingAnInsertedCell, adjustCellDiffForRevertingADeletedCell, adjustCellDiffForRevertingAnInsertedCell } from '../../browser/chatEditing/notebook/helpers.js'; -import { ICellDiffInfo } from '../../browser/chatEditing/notebook/notebookCellChanges.js'; -import { nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; -import { ObservablePromise, observableValue } from '../../../../../base/common/observable.js'; -import { CellEditType, CellKind, ICell, ICellEditOperation, NotebookCellsChangeType } from '../../../notebook/common/notebookCommon.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { hash } from '../../../../../base/common/hash.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { adjustCellDiffAndOriginalModelBasedOnCellAddDelete, adjustCellDiffAndOriginalModelBasedOnCellMovements, adjustCellDiffForKeepingADeletedCell, adjustCellDiffForKeepingAnInsertedCell, adjustCellDiffForRevertingADeletedCell, adjustCellDiffForRevertingAnInsertedCell } from '../../../browser/chatEditing/notebook/helpers.js'; +import { ICellDiffInfo } from '../../../browser/chatEditing/notebook/notebookCellChanges.js'; +import { nullDocumentDiff } from '../../../../../../editor/common/diff/documentDiffProvider.js'; +import { ObservablePromise, observableValue } from '../../../../../../base/common/observable.js'; +import { CellEditType, CellKind, ICell, ICellEditOperation, NotebookCellsChangeType } from '../../../../notebook/common/notebookCommon.js'; +import { ITextModel } from '../../../../../../editor/common/model.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { hash } from '../../../../../../base/common/hash.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; suite('ChatEditingModifiedNotebookEntry', function () { suite('Keep Inserted Cell', function () { diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts new file mode 100644 index 00000000000..50d5b38401e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingService.test.ts @@ -0,0 +1,340 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { waitForState } from '../../../../../../base/common/observable.js'; +import { isEqual } from '../../../../../../base/common/resources.js'; +import { assertType } from '../../../../../../base/common/types.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { mock } from '../../../../../../base/test/common/mock.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { EditOperation } from '../../../../../../editor/common/core/editOperation.js'; +import { Position } from '../../../../../../editor/common/core/position.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { TextEdit } from '../../../../../../editor/common/languages.js'; +import { IEditorWorkerService } from '../../../../../../editor/common/services/editorWorker.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; +import { SyncDescriptor } from '../../../../../../platform/instantiation/common/descriptors.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; +import { NullWorkbenchAssignmentService } from '../../../../../services/assignment/test/common/nullAssignmentService.js'; +import { nullExtensionDescription } from '../../../../../services/extensions/common/extensions.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TestWorkerService } from '../../../../inlineChat/test/browser/testWorkerService.js'; +import { IMcpService } from '../../../../mcp/common/mcpTypes.js'; +import { TestMcpService } from '../../../../mcp/test/common/testMcpService.js'; +import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService } from '../../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; +import { NotebookTextModel } from '../../../../notebook/common/model/notebookTextModel.js'; +import { INotebookService } from '../../../../notebook/common/notebookService.js'; +import { ChatEditingService } from '../../../browser/chatEditing/chatEditingServiceImpl.js'; +import { ChatSessionsService } from '../../../browser/chatSessions/chatSessions.contribution.js'; +import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { ChatModel } from '../../../common/model/chatModel.js'; +import { IChatService } from '../../../common/chatService/chatService.js'; +import { ChatService } from '../../../common/chatService/chatServiceImpl.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js'; +import { ChatTransferService, IChatTransferService } from '../../../common/model/chatTransferService.js'; +import { IChatVariablesService } from '../../../common/attachments/chatVariables.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { ILanguageModelsService } from '../../../common/languageModels.js'; +import { NullLanguageModelsService } from '../../common/languageModels.js'; +import { MockChatVariablesService } from '../../common/mockChatVariables.js'; + +function getAgentData(id: string): IChatAgentData { + return { + name: id, + id: id, + extensionId: nullExtensionDescription.identifier, + extensionVersion: undefined, + extensionPublisherId: '', + publisherDisplayName: '', + extensionDisplayName: '', + locations: [ChatAgentLocation.Chat], + modes: [ChatModeKind.Ask], + metadata: {}, + slashCommands: [], + disambiguation: [], + }; +} + +suite('ChatEditingService', function () { + + const store = new DisposableStore(); + let editingService: ChatEditingService; + let chatService: IChatService; + let textModelService: ITextModelService; + + setup(function () { + const collection = new ServiceCollection(); + collection.set(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); + collection.set(IChatAgentService, new SyncDescriptor(ChatAgentService)); + collection.set(IChatVariablesService, new MockChatVariablesService()); + collection.set(IChatSlashCommandService, new class extends mock() { }); + collection.set(IChatTransferService, new SyncDescriptor(ChatTransferService)); + collection.set(IChatSessionsService, new SyncDescriptor(ChatSessionsService)); + collection.set(IChatEditingService, new SyncDescriptor(ChatEditingService)); + collection.set(IEditorWorkerService, new SyncDescriptor(TestWorkerService)); + collection.set(IChatService, new SyncDescriptor(ChatService)); + collection.set(IMcpService, new TestMcpService()); + collection.set(ILanguageModelsService, new SyncDescriptor(NullLanguageModelsService)); + collection.set(IMultiDiffSourceResolverService, new class extends mock() { + override registerResolver(_resolver: IMultiDiffSourceResolver): IDisposable { + return Disposable.None; + } + }); + collection.set(INotebookService, new class extends mock() { + override getNotebookTextModel(_uri: URI): NotebookTextModel | undefined { + return undefined; + } + override hasSupportedNotebooks(_resource: URI): boolean { + return false; + } + }); + const insta = store.add(store.add(workbenchInstantiationService(undefined, store)).createChild(collection)); + store.add(insta.get(IEditorWorkerService) as TestWorkerService); + const value = insta.get(IChatEditingService); + assert.ok(value instanceof ChatEditingService); + editingService = value; + + chatService = insta.get(IChatService); + + store.add(insta.get(IChatSessionsService) as ChatSessionsService); // Needs to be disposed in between test runs to clear extensionPoint contribution + store.add(chatService as ChatService); + chatService.setSaveModelsEnabled(false); + + const chatAgentService = insta.get(IChatAgentService); + + const agent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + return {}; + }, + }; + store.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true })); + store.add(chatAgentService.registerAgentImplementation('testAgent', agent)); + + textModelService = insta.get(ITextModelService); + + const modelService = insta.get(IModelService); + + store.add(textModelService.registerTextModelContentProvider('test', { + async provideTextContent(resource) { + return store.add(modelService.createModel(resource.path.repeat(10), null, resource, false)); + }, + })); + }); + + teardown(async () => { + store.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('create session', async function () { + assert.ok(editingService); + + const modelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const model = modelRef.object as ChatModel; + const session = editingService.createEditingSession(model, true); + + assert.strictEqual(session.chatSessionResource.toString(), model.sessionResource.toString()); + assert.strictEqual(session.isGlobalEditingSession, true); + + await assertThrowsAsync(async () => { + // DUPE not allowed + editingService.createEditingSession(model); + }); + + session.dispose(); + modelRef.dispose(); + }); + + test('create session, file entry & isCurrentlyBeingModifiedBy', async function () { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); + + const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const model = modelRef.object as ChatModel; + const session = model.editingSession; + if (!session) { + assert.fail('session not created'); + } + + const chatRequest = model?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }], done: false }); + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); + + const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri)))); + + assert.ok(isEqual(entry.modifiedURI, uri)); + + await waitForState(entry.isCurrentlyBeingModifiedBy.map(value => value === chatRequest.response)); + assert.ok(entry.isCurrentlyBeingModifiedBy.get()?.responseModel === chatRequest.response); + + const unset = waitForState(entry.isCurrentlyBeingModifiedBy.map(res => res === undefined)); + + chatRequest.response.complete(); + + await unset; + + await entry.reject(); + }); + + async function idleAfterEdit(session: IChatEditingSession, model: ChatModel, uri: URI, edits: TextEdit[]) { + const isStreaming = waitForState(session.state.map(s => s === ChatEditingSessionState.StreamingEdits), Boolean); + + const chatRequest = model.addRequest({ text: '', parts: [] }, { variables: [] }, 0); + assertType(chatRequest.response); + + chatRequest.response.updateContent({ kind: 'textEdit', uri, edits, done: true }); + + const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri)))); + + assert.ok(isEqual(entry.modifiedURI, uri)); + + chatRequest.response.complete(); + + await isStreaming; + + const isIdle = waitForState(session.state.map(s => s === ChatEditingSessionState.Idle), Boolean); + await isIdle; + + return entry; + } + + test('mirror typing outside -> accept', async function () { + return runWithFakedTimers({}, async () => { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'abc\n' }); + + const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const model = modelRef.object as ChatModel; + const session = model.editingSession; + assertType(session, 'session not created'); + + const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); + const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; + const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; + + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); + + assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); + assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); + + modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null); + + assert.ok(modified.getValue().includes('USER_TYPE')); + assert.ok(original.getValue().includes('USER_TYPE')); + + await entry.accept(); + assert.strictEqual(modified.getValue(), original.getValue()); + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted); + + assert.ok(modified.getValue().includes('FarBoo')); + assert.ok(original.getValue().includes('FarBoo')); + }); + }); + + test('mirror typing outside -> reject', async function () { + return runWithFakedTimers({}, async () => { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'abc\n' }); + + const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const model = modelRef.object as ChatModel; + const session = model.editingSession; + assertType(session, 'session not created'); + + const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); + const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; + const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; + + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); + + assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); + assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); + + modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null); + + assert.ok(modified.getValue().includes('USER_TYPE')); + assert.ok(original.getValue().includes('USER_TYPE')); + + await entry.reject(); + assert.strictEqual(modified.getValue(), original.getValue()); + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Rejected); + + assert.ok(!modified.getValue().includes('FarBoo')); + assert.ok(!original.getValue().includes('FarBoo')); + }); + }); + + test('NO mirror typing inside -> accept', async function () { + return runWithFakedTimers({}, async () => { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'abc\n' }); + + const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const model = modelRef.object as ChatModel; + const session = model.editingSession; + assertType(session, 'session not created'); + + const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); + const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; + const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; + + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); + + assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); + assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); + + modified.pushEditOperations(null, [EditOperation.replace(new Range(1, 2, 1, 7), 'ooBar')], () => null); + + assert.ok(modified.getValue().includes('FooBar')); + assert.ok(!original.getValue().includes('FooBar')); // typed in the AI edits, DO NOT transpose + + await entry.accept(); + assert.strictEqual(modified.getValue(), original.getValue()); + assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted); + + assert.ok(modified.getValue().includes('FooBar')); + assert.ok(original.getValue().includes('FooBar')); + }); + }); + + test('ChatEditingService merges text edits it shouldn\'t merge, #272679', async function () { + return runWithFakedTimers({}, async () => { + assert.ok(editingService); + + const uri = URI.from({ scheme: 'test', path: 'abc' }); + + const modified = store.add(await textModelService.createModelReference(uri)).object.textEditorModel; + + const modelRef = store.add(chatService.startSession(ChatAgentLocation.Chat)); + const model = modelRef.object as ChatModel; + const session = model.editingSession; + assertType(session, 'session not created'); + + modified.setValue(''); + await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'a' }, { range: new Range(1, 1, 1, 1), text: 'b' }]); + assert.strictEqual(modified.getValue(), 'ab'); + + modified.setValue(''); + await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'a' }]); + await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'b' }]); + assert.strictEqual(modified.getValue(), 'ba'); + }); + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingSessionStorage.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingSessionStorage.test.ts new file mode 100644 index 00000000000..ac5b0aeb0be --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatEditing/chatEditingSessionStorage.test.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { cloneAndChange } from '../../../../../../base/common/objects.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../../base/common/uuid.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { FileService } from '../../../../../../platform/files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import { TestEnvironmentService } from '../../../../../test/browser/workbenchTestServices.js'; +import { ChatEditingSessionStorage, IChatEditingSessionStop, StoredSessionState } from '../../../browser/chatEditing/chatEditingSessionStorage.js'; +import { ChatEditingSnapshotTextModelContentProvider } from '../../../browser/chatEditing/chatEditingTextModelContentProviders.js'; +import { ISnapshotEntry, ModifiedFileEntryState } from '../../../common/editing/chatEditingService.js'; +import { hasKey } from '../../../../../../base/common/types.js'; + +suite('ChatEditingSessionStorage', () => { + const ds = ensureNoDisposablesAreLeakedInTestSuite(); + const sessionResource = URI.parse('chat://test-session'); + let fs: FileService; + let storage: TestChatEditingSessionStorage; + + class TestChatEditingSessionStorage extends ChatEditingSessionStorage { + public get storageLocation() { + return super._getStorageLocation(); + } + } + + setup(() => { + fs = ds.add(new FileService(new NullLogService())); + ds.add(fs.registerProvider(TestEnvironmentService.workspaceStorageHome.scheme, ds.add(new InMemoryFileSystemProvider()))); + + storage = new TestChatEditingSessionStorage( + sessionResource, + fs, + TestEnvironmentService, + new NullLogService(), + // eslint-disable-next-line local/code-no-any-casts + { getWorkspace: () => ({ id: 'workspaceId' }) } as any, + ); + }); + + function makeStop(requestId: string | undefined, before: string, after: string): IChatEditingSessionStop { + const stopId = generateUuid(); + const resource = URI.file('/foo.js'); + return { + stopId, + entries: new ResourceMap([ + [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionResource, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionResource: sessionResource, modelId: undefined, modeId: undefined, applyCodeBlockSuggestionId: undefined, feature: undefined } } satisfies ISnapshotEntry], + ]), + }; + } + + function generateState(): StoredSessionState { + const initialFileContents = new ResourceMap(); + for (let i = 0; i < 10; i++) { initialFileContents.set(URI.file(`/foo${i}.js`), `fileContents${Math.floor(i / 2)}`); } + + return { + initialFileContents, + recentSnapshot: makeStop(undefined, 'd', 'e'), + timeline: undefined, + }; + } + + test('state is empty initially', async () => { + const s = await storage.restoreState(); + assert.strictEqual(s, undefined); + }); + + test('round trips state', async () => { + const original = generateState(); + await storage.storeState(original); + + const changer = (x: any) => { + if (typeof x === 'object' && x && hasKey(x, { isDeleted: true }) && x.isDeleted === undefined) { + delete x.isDeleted; + } + return URI.isUri(x) ? x.toString() : x instanceof Map ? cloneAndChange([...x.values()], changer) : undefined; + }; + + const restored = await storage.restoreState(); + assert.deepStrictEqual(cloneAndChange(restored, changer), cloneAndChange(original, changer)); + }); + + test('clears state', async () => { + await storage.storeState(generateState()); + await storage.clearState(); + const s = await storage.restoreState(); + assert.strictEqual(s, undefined); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts deleted file mode 100644 index 132834f17fb..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingService.test.ts +++ /dev/null @@ -1,335 +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 { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { waitForState } from '../../../../../base/common/observable.js'; -import { isEqual } from '../../../../../base/common/resources.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { assertThrowsAsync, ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { Position } from '../../../../../editor/common/core/position.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { TestWorkerService } from '../../../inlineChat/test/browser/testWorkerService.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService } from '../../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; -import { NotebookTextModel } from '../../../notebook/common/model/notebookTextModel.js'; -import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingService } from '../../browser/chatEditing/chatEditingServiceImpl.js'; -import { ChatSessionsService } from '../../browser/chatSessions.contribution.js'; -import { ChatAgentService, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/chatEditingService.js'; -import { ChatModel } from '../../common/chatModel.js'; -import { IChatService } from '../../common/chatService.js'; -import { ChatService } from '../../common/chatServiceImpl.js'; -import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { ChatTransferService, IChatTransferService } from '../../common/chatTransferService.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; -import { ILanguageModelsService } from '../../common/languageModels.js'; -import { NullLanguageModelsService } from '../common/languageModels.js'; -import { MockChatVariablesService } from '../common/mockChatVariables.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; - -function getAgentData(id: string): IChatAgentData { - return { - name: id, - id: id, - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - extensionPublisherId: '', - publisherDisplayName: '', - extensionDisplayName: '', - locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }; -} - -suite('ChatEditingService', function () { - - const store = new DisposableStore(); - let editingService: ChatEditingService; - let chatService: IChatService; - let textModelService: ITextModelService; - - setup(function () { - const collection = new ServiceCollection(); - collection.set(IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()); - collection.set(IChatAgentService, new SyncDescriptor(ChatAgentService)); - collection.set(IChatVariablesService, new MockChatVariablesService()); - collection.set(IChatSlashCommandService, new class extends mock() { }); - collection.set(IChatTransferService, new SyncDescriptor(ChatTransferService)); - collection.set(IChatSessionsService, new SyncDescriptor(ChatSessionsService)); - collection.set(IChatEditingService, new SyncDescriptor(ChatEditingService)); - collection.set(IEditorWorkerService, new SyncDescriptor(TestWorkerService)); - collection.set(IChatService, new SyncDescriptor(ChatService)); - collection.set(IMcpService, new TestMcpService()); - collection.set(ILanguageModelsService, new SyncDescriptor(NullLanguageModelsService)); - collection.set(IMultiDiffSourceResolverService, new class extends mock() { - override registerResolver(_resolver: IMultiDiffSourceResolver): IDisposable { - return Disposable.None; - } - }); - collection.set(INotebookService, new class extends mock() { - override getNotebookTextModel(_uri: URI): NotebookTextModel | undefined { - return undefined; - } - override hasSupportedNotebooks(_resource: URI): boolean { - return false; - } - }); - const insta = store.add(store.add(workbenchInstantiationService(undefined, store)).createChild(collection)); - store.add(insta.get(IEditorWorkerService) as TestWorkerService); - const value = insta.get(IChatEditingService); - assert.ok(value instanceof ChatEditingService); - editingService = value; - - chatService = insta.get(IChatService); - - store.add(insta.get(IChatSessionsService) as ChatSessionsService); // Needs to be disposed in between test runs to clear extensionPoint contribution - - const chatAgentService = insta.get(IChatAgentService); - - const agent: IChatAgentImplementation = { - async invoke(request, progress, history, token) { - return {}; - }, - }; - store.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true })); - store.add(chatAgentService.registerAgentImplementation('testAgent', agent)); - - textModelService = insta.get(ITextModelService); - - const modelService = insta.get(IModelService); - - store.add(textModelService.registerTextModelContentProvider('test', { - async provideTextContent(resource) { - return store.add(modelService.createModel(resource.path.repeat(10), null, resource, false)); - }, - })); - }); - - teardown(() => { - store.clear(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('create session', async function () { - assert.ok(editingService); - - const model = chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None); - const session = await editingService.createEditingSession(model, true); - - assert.strictEqual(session.chatSessionId, model.sessionId); - assert.strictEqual(session.isGlobalEditingSession, true); - - await assertThrowsAsync(async () => { - // DUPE not allowed - await editingService.createEditingSession(model); - }); - - session.dispose(); - model.dispose(); - }); - - test('create session, file entry & isCurrentlyBeingModifiedBy', async function () { - assert.ok(editingService); - - const uri = URI.from({ scheme: 'test', path: 'HelloWorld' }); - - const model = chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None); - const session = await model.editingSessionObs?.promise; - if (!session) { - assert.fail('session not created'); - } - - const chatRequest = model?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); - assertType(chatRequest.response); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false }); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }], done: false }); - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); - - const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri)))); - - assert.ok(isEqual(entry.modifiedURI, uri)); - - await waitForState(entry.isCurrentlyBeingModifiedBy.map(value => value === chatRequest.response)); - assert.ok(entry.isCurrentlyBeingModifiedBy.get()?.responseModel === chatRequest.response); - - const unset = waitForState(entry.isCurrentlyBeingModifiedBy.map(res => res === undefined)); - - chatRequest.response.complete(); - - await unset; - - await entry.reject(); - - model.dispose(); - }); - - async function idleAfterEdit(session: IChatEditingSession, model: ChatModel, uri: URI, edits: TextEdit[]) { - const isStreaming = waitForState(session.state.map(s => s === ChatEditingSessionState.StreamingEdits), Boolean); - - const chatRequest = model.addRequest({ text: '', parts: [] }, { variables: [] }, 0); - assertType(chatRequest.response); - - chatRequest.response.updateContent({ kind: 'textEdit', uri, edits, done: true }); - - const entry = await waitForState(session.entries.map(value => value.find(a => isEqual(a.modifiedURI, uri)))); - - assert.ok(isEqual(entry.modifiedURI, uri)); - - chatRequest.response.complete(); - - await isStreaming; - - const isIdle = waitForState(session.state.map(s => s === ChatEditingSessionState.Idle), Boolean); - await isIdle; - - return entry; - } - - test('mirror typing outside -> accept', async function () { - return runWithFakedTimers({}, async () => { - assert.ok(editingService); - - const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - - const model = store.add(chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - const session = await model.editingSessionObs?.promise; - assertType(session, 'session not created'); - - const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); - const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; - const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; - - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); - - assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); - assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); - - modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null); - - assert.ok(modified.getValue().includes('USER_TYPE')); - assert.ok(original.getValue().includes('USER_TYPE')); - - await entry.accept(); - assert.strictEqual(modified.getValue(), original.getValue()); - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted); - - assert.ok(modified.getValue().includes('FarBoo')); - assert.ok(original.getValue().includes('FarBoo')); - }); - }); - - test('mirror typing outside -> reject', async function () { - return runWithFakedTimers({}, async () => { - assert.ok(editingService); - - const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - - const model = store.add(chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - const session = await model.editingSessionObs?.promise; - assertType(session, 'session not created'); - - const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); - const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; - const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; - - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); - - assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); - assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); - - modified.pushEditOperations(null, [EditOperation.insert(new Position(3, 1), 'USER_TYPE\n')], () => null); - - assert.ok(modified.getValue().includes('USER_TYPE')); - assert.ok(original.getValue().includes('USER_TYPE')); - - await entry.reject(); - assert.strictEqual(modified.getValue(), original.getValue()); - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Rejected); - - assert.ok(!modified.getValue().includes('FarBoo')); - assert.ok(!original.getValue().includes('FarBoo')); - }); - }); - - test('NO mirror typing inside -> accept', async function () { - return runWithFakedTimers({}, async () => { - assert.ok(editingService); - - const uri = URI.from({ scheme: 'test', path: 'abc\n' }); - - const model = store.add(chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - const session = await model.editingSessionObs?.promise; - assertType(session, 'session not created'); - - const entry = await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'FarBoo\n' }]); - const original = store.add(await textModelService.createModelReference(entry.originalURI)).object.textEditorModel; - const modified = store.add(await textModelService.createModelReference(entry.modifiedURI)).object.textEditorModel; - - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Modified); - - assert.strictEqual(original.getValue(), 'abc\n'.repeat(10)); - assert.strictEqual(modified.getValue(), 'FarBoo\n' + 'abc\n'.repeat(10)); - - modified.pushEditOperations(null, [EditOperation.replace(new Range(1, 2, 1, 7), 'ooBar')], () => null); - - assert.ok(modified.getValue().includes('FooBar')); - assert.ok(!original.getValue().includes('FooBar')); // typed in the AI edits, DO NOT transpose - - await entry.accept(); - assert.strictEqual(modified.getValue(), original.getValue()); - assert.strictEqual(entry.state.get(), ModifiedFileEntryState.Accepted); - - assert.ok(modified.getValue().includes('FooBar')); - assert.ok(original.getValue().includes('FooBar')); - }); - }); - - test('ChatEditingService merges text edits it shouldn\'t merge, #272679', async function () { - return runWithFakedTimers({}, async () => { - assert.ok(editingService); - - const uri = URI.from({ scheme: 'test', path: 'abc' }); - - const modified = store.add(await textModelService.createModelReference(uri)).object.textEditorModel; - - const model = store.add(chatService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - const session = await model.editingSessionObs?.promise; - assertType(session, 'session not created'); - - modified.setValue(''); - await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'a' }, { range: new Range(1, 1, 1, 1), text: 'b' }]); - assert.strictEqual(modified.getValue(), 'ab'); - - modified.setValue(''); - await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'a' }]); - await idleAfterEdit(session, model, uri, [{ range: new Range(1, 1, 1, 1), text: 'b' }]); - assert.strictEqual(modified.getValue(), 'ba'); - }); - }); - -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts deleted file mode 100644 index 1ff93e0ba3b..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/chatEditingSessionStorage.test.ts +++ /dev/null @@ -1,91 +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 * as assert from 'assert'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { cloneAndChange } from '../../../../../base/common/objects.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { FileService } from '../../../../../platform/files/common/fileService.js'; -import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; -import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { TestEnvironmentService } from '../../../../test/browser/workbenchTestServices.js'; -import { ChatEditingSessionStorage, IChatEditingSessionStop, StoredSessionState } from '../../browser/chatEditing/chatEditingSessionStorage.js'; -import { ChatEditingSnapshotTextModelContentProvider } from '../../browser/chatEditing/chatEditingTextModelContentProviders.js'; -import { ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js'; - -suite('ChatEditingSessionStorage', () => { - const ds = ensureNoDisposablesAreLeakedInTestSuite(); - const sessionId = generateUuid(); - let fs: FileService; - let storage: TestChatEditingSessionStorage; - - class TestChatEditingSessionStorage extends ChatEditingSessionStorage { - public get storageLocation() { - return super._getStorageLocation(); - } - } - - setup(() => { - fs = ds.add(new FileService(new NullLogService())); - ds.add(fs.registerProvider(TestEnvironmentService.workspaceStorageHome.scheme, ds.add(new InMemoryFileSystemProvider()))); - - storage = new TestChatEditingSessionStorage( - sessionId, - fs, - TestEnvironmentService, - new NullLogService(), - // eslint-disable-next-line local/code-no-any-casts - { getWorkspace: () => ({ id: 'workspaceId' }) } as any, - ); - }); - - function makeStop(requestId: string | undefined, before: string, after: string): IChatEditingSessionStop { - const stopId = generateUuid(); - const resource = URI.file('/foo.js'); - return { - stopId, - entries: new ResourceMap([ - [resource, { resource, languageId: 'javascript', snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(sessionId, requestId, stopId, resource.path), original: `contents${before}}`, current: `contents${after}`, state: ModifiedFileEntryState.Modified, telemetryInfo: { agentId: 'agentId', command: 'cmd', requestId: generateUuid(), result: undefined, sessionId, modelId: undefined, modeId: undefined, applyCodeBlockSuggestionId: undefined, feature: undefined } } satisfies ISnapshotEntry], - ]), - }; - } - - function generateState(): StoredSessionState { - const initialFileContents = new ResourceMap(); - for (let i = 0; i < 10; i++) { initialFileContents.set(URI.file(`/foo${i}.js`), `fileContents${Math.floor(i / 2)}`); } - - return { - initialFileContents, - recentSnapshot: makeStop(undefined, 'd', 'e'), - timeline: undefined, - }; - } - - test('state is empty initially', async () => { - const s = await storage.restoreState(); - assert.strictEqual(s, undefined); - }); - - test('round trips state', async () => { - const original = generateState(); - await storage.storeState(original); - - const changer = (x: any) => { - return URI.isUri(x) ? x.toString() : x instanceof Map ? cloneAndChange([...x.values()], changer) : undefined; - }; - - const restored = await storage.restoreState(); - assert.deepStrictEqual(cloneAndChange(restored, changer), cloneAndChange(original, changer)); - }); - - test('clears state', async () => { - await storage.storeState(generateState()); - await storage.clearState(); - const s = await storage.restoreState(); - assert.strictEqual(s, undefined); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts new file mode 100644 index 00000000000..322f8893f18 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatManagement/chatModelsViewModel.test.ts @@ -0,0 +1,1094 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from '../../../../../../base/common/event.js'; +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatSelector, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel, ILanguageModelProviderDescriptor } from '../../../common/languageModels.js'; +import { ChatModelGroup, ChatModelsViewModel, ILanguageModelEntry, ILanguageModelProviderEntry, isLanguageModelProviderEntry, isLanguageModelGroupEntry, ILanguageModelGroupEntry } from '../../../browser/chatManagement/chatModelsViewModel.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { IStringDictionary } from '../../../../../../base/common/collections.js'; +import { ILanguageModelsProviderGroup } from '../../../common/languageModelsConfiguration.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; + +class MockLanguageModelsService implements ILanguageModelsService { + _serviceBrand: undefined; + + private vendors: IUserFriendlyLanguageModel[] = []; + private models = new Map(); + private modelsByVendor = new Map(); + private modelGroups = new Map(); + + private readonly _onDidChangeLanguageModels = new Emitter(); + readonly onDidChangeLanguageModels = this._onDidChangeLanguageModels.event; + + private readonly _onDidChangeLanguageModelVendors = new Emitter(); + readonly onDidChangeLanguageModelVendors = this._onDidChangeLanguageModelVendors.event; + + addVendor(vendor: IUserFriendlyLanguageModel): void { + this.vendors.push(vendor); + this.modelsByVendor.set(vendor.vendor, []); + this.modelGroups.set(vendor.vendor, []); + } + + addModel(vendorId: string, identifier: string, metadata: ILanguageModelChatMetadata): void { + this.models.set(identifier, metadata); + const models = this.modelsByVendor.get(vendorId) || []; + models.push(identifier); + this.modelsByVendor.set(vendorId, models); + + // Add to model groups - create a single default group per vendor + const groups = this.modelGroups.get(vendorId) || []; + if (groups.length === 0) { + groups.push({ + group: { + vendor: vendorId, + name: this.vendors.find(v => v.vendor === vendorId)?.displayName || 'Default' + }, + modelIdentifiers: [] + }); + } + groups[0].modelIdentifiers.push(identifier); + this.modelGroups.set(vendorId, groups); + } + + registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable { + throw new Error('Method not implemented.'); + } + + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + throw new Error('Method not implemented.'); + } + + updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { + const metadata = this.models.get(modelIdentifier); + if (metadata) { + this.models.set(modelIdentifier, { ...metadata, isUserSelectable: showInModelPicker }); + } + } + + getVendors(): ILanguageModelProviderDescriptor[] { + return this.vendors.map(v => ({ ...v, isDefault: v.vendor === 'copilot' })); + } + + getLanguageModelIds(): string[] { + return Array.from(this.models.keys()); + } + + lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined { + return this.models.get(identifier); + } + + lookupLanguageModelByQualifiedName(referenceName: string): ILanguageModelChatMetadata | undefined { + for (const metadata of this.models.values()) { + if (ILanguageModelChatMetadata.matchesQualifiedName(referenceName, metadata)) { + return metadata; + } + } + return undefined; + } + + getLanguageModels(): ILanguageModelChatMetadataAndIdentifier[] { + const result: ILanguageModelChatMetadataAndIdentifier[] = []; + for (const [identifier, metadata] of this.models.entries()) { + result.push({ identifier, metadata }); + } + return result; + } + + setContributedSessionModels(): void { + } + + clearContributedSessionModels(): void { + } + + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { + if (selector.vendor) { + return this.modelsByVendor.get(selector.vendor) || []; + } + return Array.from(this.models.keys()); + } + + sendChatRequest(): Promise { + throw new Error('Method not implemented.'); + } + + computeTokenLength(): Promise { + throw new Error('Method not implemented.'); + } + + async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise { + } + + async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { + } + + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { + return this.modelGroups.get(vendor) || []; + } + + async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise { + } + + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { } +} + +suite('ChatModelsViewModel', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let languageModelsService: MockLanguageModelsService; + let viewModel: ChatModelsViewModel; + + setup(async () => { + languageModelsService = new MockLanguageModelsService(); + + // Setup test data + languageModelsService.addVendor({ + vendor: 'copilot', + displayName: 'GitHub Copilot', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + languageModelsService.addVendor({ + vendor: 'openai', + displayName: 'OpenAI', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + languageModelsService.addModel('copilot', 'copilot-gpt-4', { + extension: new ExtensionIdentifier('github.copilot'), + id: 'gpt-4', + name: 'GPT-4', + family: 'gpt-4', + version: '1.0', + vendor: 'copilot', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Copilot', order: 1 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: true, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + languageModelsService.addModel('copilot', 'copilot-gpt-4o', { + extension: new ExtensionIdentifier('github.copilot'), + id: 'gpt-4o', + name: 'GPT-4o', + family: 'gpt-4', + version: '1.0', + vendor: 'copilot', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Copilot', order: 1 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: true, + agentMode: true + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + languageModelsService.addModel('openai', 'openai-gpt-3.5', { + extension: new ExtensionIdentifier('openai.api'), + id: 'gpt-3.5-turbo', + name: 'GPT-3.5 Turbo', + family: 'gpt-3.5', + version: '1.0', + vendor: 'openai', + maxInputTokens: 4096, + maxOutputTokens: 2048, + modelPickerCategory: { label: 'OpenAI', order: 2 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: false, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + languageModelsService.addModel('openai', 'openai-gpt-4-vision', { + extension: new ExtensionIdentifier('openai.api'), + id: 'gpt-4-vision', + name: 'GPT-4 Vision', + family: 'gpt-4', + version: '1.0', + vendor: 'openai', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'OpenAI', order: 2 }, + isUserSelectable: false, + capabilities: { + toolCalling: false, + vision: true, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + viewModel = store.add(new ChatModelsViewModel(languageModelsService)); + + await viewModel.refresh(); + }); + + test('should fetch all models without filters', () => { + const results = viewModel.filter(''); + + // Should have 2 vendor entries and 4 model entries (grouped by vendor) + assert.strictEqual(results.length, 6); + + const vendors = results.filter(isLanguageModelProviderEntry); + assert.strictEqual(vendors.length, 2); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 4); + }); + + test('should filter by provider name (vendor ID and display name)', () => { + const resultsByCopilotId = viewModel.filter('@provider:copilot'); + assert.strictEqual(resultsByCopilotId.length, 3); + assert.strictEqual(resultsByCopilotId[0].type, 'vendor'); + assert.strictEqual(resultsByCopilotId[0].vendorEntry.vendor.vendor, 'copilot'); + assert.strictEqual(resultsByCopilotId[1].type, 'model'); + assert.strictEqual(resultsByCopilotId[1].model.identifier, 'copilot-gpt-4'); + assert.strictEqual(resultsByCopilotId[2].type, 'model'); + assert.strictEqual(resultsByCopilotId[2].model.identifier, 'copilot-gpt-4o'); + + const resultsByOpenAIName = viewModel.filter('@provider:OpenAI'); + assert.strictEqual(resultsByOpenAIName.length, 3); + assert.strictEqual(resultsByOpenAIName[0].type, 'vendor'); + assert.strictEqual(resultsByOpenAIName[0].vendorEntry.vendor.vendor, 'openai'); + assert.strictEqual(resultsByOpenAIName[1].type, 'model'); + assert.strictEqual(resultsByOpenAIName[1].model.identifier, 'openai-gpt-3.5'); + assert.strictEqual(resultsByOpenAIName[2].type, 'model'); + assert.strictEqual(resultsByOpenAIName[2].model.identifier, 'openai-gpt-4-vision'); + }); + + test('should filter by multiple providers with OR logic', () => { + const results = viewModel.filter('@provider:copilot @provider:openai'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 4); + }); + + test('should filter by single capability - tools', () => { + const results = viewModel.filter('@capability:tools'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 3); + assert.ok(models.every(m => m.model.metadata.capabilities?.toolCalling === true)); + }); + + test('should filter by single capability - vision', () => { + const results = viewModel.filter('@capability:vision'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 3); + assert.ok(models.every(m => m.model.metadata.capabilities?.vision === true)); + }); + + test('should filter by single capability - agent', () => { + const results = viewModel.filter('@capability:agent'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.id, 'gpt-4o'); + }); + + test('should filter by multiple capabilities with AND logic', () => { + const results = viewModel.filter('@capability:tools @capability:vision'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Should only return models that have BOTH tools and vision + assert.strictEqual(models.length, 2); + assert.ok(models.every(m => + m.model.metadata.capabilities?.toolCalling === true && + m.model.metadata.capabilities?.vision === true + )); + }); + + test('should filter by three capabilities with AND logic', () => { + const results = viewModel.filter('@capability:tools @capability:vision @capability:agent'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Should only return gpt-4o which has all three + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.id, 'gpt-4o'); + }); + + test('should return no results when filtering by incompatible capabilities', () => { + const results = viewModel.filter('@capability:vision @capability:agent'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Only gpt-4o has both vision and agent, but gpt-4-vision doesn't have agent + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.id, 'gpt-4o'); + }); + + test('should filter by visibility - visible:true', () => { + const results = viewModel.filter('@visible:true'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 3); + assert.ok(models.every(m => m.model.visible === true)); + }); + + test('should filter by visibility - visible:false', () => { + const results = viewModel.filter('@visible:false'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.visible, false); + }); + + test('should combine provider and capability filters', () => { + const results = viewModel.filter('@provider:copilot @capability:vision'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 2); + assert.ok(models.every(m => + m.model.provider.vendor.vendor === 'copilot' && + m.model.metadata.capabilities?.vision === true + )); + }); + + test('should combine provider, capability, and visibility filters', () => { + const results = viewModel.filter('@provider:openai @capability:vision @visible:false'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.id, 'gpt-4-vision'); + }); + + test('should filter by text matching model name', () => { + const results = viewModel.filter('GPT-4o'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.name, 'GPT-4o'); + assert.ok(models[0].modelNameMatches); + }); + + test('should filter by text matching model id', () => { + const results = viewModel.filter('gpt-4o'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.identifier, 'copilot-gpt-4o'); + assert.ok(models[0].modelIdMatches); + }); + + test('should filter by text matching vendor name', () => { + const results = viewModel.filter('GitHub'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 2); + assert.ok(models.every(m => m.model.provider.group.name === 'GitHub Copilot')); + }); + + test('should combine text search with capability filter', () => { + const results = viewModel.filter('@capability:tools GPT'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Should match all models with tools capability and 'GPT' in name + assert.strictEqual(models.length, 3); + assert.ok(models.every(m => m.model.metadata.capabilities?.toolCalling === true)); + }); + + test('should handle empty search value', () => { + const results = viewModel.filter(''); + + // Should return all models grouped by vendor + assert.ok(results.length > 0); + }); + + test('should handle search value with only whitespace', () => { + const results = viewModel.filter(' '); + + // Should return all models grouped by vendor + assert.ok(results.length > 0); + }); + + test('should match capability text in free text search', () => { + const results = viewModel.filter('vision'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Should match models that have vision capability or "vision" in their name + assert.ok(models.length > 0); + assert.ok(models.every(m => + m.model.metadata.capabilities?.vision === true || + m.model.metadata.name.toLowerCase().includes('vision') + )); + }); + + test('should toggle vendor collapsed state', () => { + const vendorEntry = viewModel.viewModelEntries.find(r => isLanguageModelProviderEntry(r) && r.vendorEntry.vendor.vendor === 'copilot') as ILanguageModelProviderEntry; + viewModel.toggleCollapsed(vendorEntry); + + const results = viewModel.filter(''); + const copilotVendor = results.find(r => isLanguageModelProviderEntry(r) && (r as ILanguageModelProviderEntry).vendorEntry.vendor.vendor === 'copilot') as ILanguageModelProviderEntry; + assert.ok(copilotVendor); + assert.strictEqual(copilotVendor.collapsed, true); + + // Models should not be shown when vendor is collapsed + const copilotModelsAfterCollapse = results.filter(r => + !isLanguageModelProviderEntry(r) && (r as ILanguageModelEntry).model.provider.vendor.vendor === 'copilot' + ); + assert.strictEqual(copilotModelsAfterCollapse.length, 0); + + // Toggle back + viewModel.toggleCollapsed(vendorEntry); + const resultsAfterExpand = viewModel.filter(''); + const copilotModelsAfterExpand = resultsAfterExpand.filter(r => + !isLanguageModelProviderEntry(r) && (r as ILanguageModelEntry).model.provider.vendor.vendor === 'copilot' + ); + assert.strictEqual(copilotModelsAfterExpand.length, 2); + }); + + test('should handle quoted search strings', () => { + // When a search string is fully quoted (starts and ends with quotes), + // the completeMatch flag is set to true, which currently skips all matching + // This test verifies the quotes are processed without errors + const results = viewModel.filter('"GPT"'); + + // The function should complete without error + // Note: complete match logic (both quotes) currently doesn't perform matching + assert.ok(Array.isArray(results)); + }); + + test('should remove filter keywords from text search', () => { + const results = viewModel.filter('@provider:copilot @capability:vision GPT'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + // Should only search 'GPT' in model names, not the filter keywords + assert.strictEqual(models.length, 2); + assert.ok(models.every(m => m.model.provider.vendor.vendor === 'copilot')); + }); + + test('should handle case-insensitive capability matching', () => { + const results1 = viewModel.filter('@capability:TOOLS'); + const results2 = viewModel.filter('@capability:tools'); + const results3 = viewModel.filter('@capability:Tools'); + + const models1 = results1.filter(r => !isLanguageModelProviderEntry(r)); + const models2 = results2.filter(r => !isLanguageModelProviderEntry(r)); + const models3 = results3.filter(r => !isLanguageModelProviderEntry(r)); + + assert.strictEqual(models1.length, models2.length); + assert.strictEqual(models2.length, models3.length); + }); + + test('should support toolcalling alias for tools capability', () => { + const resultsTools = viewModel.filter('@capability:tools'); + const resultsToolCalling = viewModel.filter('@capability:toolcalling'); + + const modelsTools = resultsTools.filter(r => !isLanguageModelProviderEntry(r)); + const modelsToolCalling = resultsToolCalling.filter(r => !isLanguageModelProviderEntry(r)); + + assert.strictEqual(modelsTools.length, modelsToolCalling.length); + }); + + test('should support agentmode alias for agent capability', () => { + const resultsAgent = viewModel.filter('@capability:agent'); + const resultsAgentMode = viewModel.filter('@capability:agentmode'); + + const modelsAgent = resultsAgent.filter(r => !isLanguageModelProviderEntry(r)); + const modelsAgentMode = resultsAgentMode.filter(r => !isLanguageModelProviderEntry(r)); + + assert.strictEqual(modelsAgent.length, modelsAgentMode.length); + }); + + test('should include matched capabilities in results', () => { + const results = viewModel.filter('@capability:tools @capability:vision'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.ok(models.length > 0); + + for (const model of models) { + assert.ok(model.capabilityMatches); + assert.ok(model.capabilityMatches.length > 0); + // Should include both toolCalling and vision + assert.ok(model.capabilityMatches.some(c => c === 'toolCalling' || c === 'vision')); + } + }); + + function createSingleVendorViewModel(includeSecondModel: boolean = true): { service: MockLanguageModelsService; viewModel: ChatModelsViewModel } { + const service = new MockLanguageModelsService(); + service.addVendor({ + vendor: 'copilot', + displayName: 'GitHub Copilot', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + service.addModel('copilot', 'copilot-gpt-4', { + extension: new ExtensionIdentifier('github.copilot'), + id: 'gpt-4', + name: 'GPT-4', + family: 'gpt-4', + version: '1.0', + vendor: 'copilot', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Copilot', order: 1 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: true, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + if (includeSecondModel) { + service.addModel('copilot', 'copilot-gpt-4o', { + extension: new ExtensionIdentifier('github.copilot'), + id: 'gpt-4o', + name: 'GPT-4o', + family: 'gpt-4', + version: '1.0', + vendor: 'copilot', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Copilot', order: 1 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: true, + agentMode: true + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + } + + const viewModel = store.add(new ChatModelsViewModel(service)); + return { service, viewModel }; + } + + test('should not show vendor header when only one vendor exists', async () => { + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); + await singleVendorViewModel.refresh(); + + const results = singleVendorViewModel.filter(''); + + // Should have only model entries, no vendor entry + const vendors = results.filter(isLanguageModelProviderEntry); + assert.strictEqual(vendors.length, 0, 'Should not show vendor header when only one vendor exists'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 2, 'Should show all models'); + assert.ok(models.every(m => m.model.provider.vendor.vendor === 'copilot')); + }); + + test('should show vendor headers when multiple vendors exist', () => { + // This is the existing behavior test + const results = viewModel.filter(''); + + // Should have 2 vendor entries and 4 model entries (grouped by vendor) + const vendors = results.filter(isLanguageModelProviderEntry); + assert.strictEqual(vendors.length, 2, 'Should show vendor headers when multiple vendors exist'); + + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 4); + }); + + test('should filter single vendor models by capability', async () => { + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); + await singleVendorViewModel.refresh(); + + const results = singleVendorViewModel.filter('@capability:agent'); + + // Should not show vendor header + const vendors = results.filter(isLanguageModelProviderEntry); + assert.strictEqual(vendors.length, 0, 'Should not show vendor header'); + + // Should only show the model with agent capability + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 1); + assert.strictEqual(models[0].model.metadata.id, 'gpt-4o'); + }); + + test('should always place copilot vendor at the top when multiple vendors exist', async () => { + // Test with default setup (copilot and openai) + let results = viewModel.filter(''); + let vendors = results.filter(isLanguageModelProviderEntry) as ILanguageModelProviderEntry[]; + assert.strictEqual(vendors[0].vendorEntry.vendor.vendor, 'copilot'); + + // Add more vendors to ensure sorting works correctly + languageModelsService.addVendor({ + vendor: 'anthropic', + displayName: 'Anthropic', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + languageModelsService.addModel('anthropic', 'anthropic-claude', { + extension: new ExtensionIdentifier('anthropic.api'), + id: 'claude-3', + name: 'Claude 3', + family: 'claude', + version: '1.0', + vendor: 'anthropic', + maxInputTokens: 100000, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Anthropic', order: 3 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: false, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + languageModelsService.addVendor({ + vendor: 'azure', + displayName: 'Azure OpenAI', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + languageModelsService.addModel('azure', 'azure-gpt-4', { + extension: new ExtensionIdentifier('microsoft.azure'), + id: 'azure-gpt-4', + name: 'Azure GPT-4', + family: 'gpt-4', + version: '1.0', + vendor: 'azure', + maxInputTokens: 8192, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Azure', order: 4 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: false, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + await viewModel.refresh(); + + // Test with all filters and searches + results = viewModel.filter(''); + vendors = results.filter(isLanguageModelProviderEntry) as ILanguageModelProviderEntry[]; + assert.strictEqual(vendors.length, 4); + assert.strictEqual(vendors[0].vendorEntry.vendor.vendor, 'copilot'); + // Other vendors should be alphabetically sorted: anthropic, azure, openai + assert.strictEqual(vendors[1].vendorEntry.vendor.vendor, 'anthropic'); + assert.strictEqual(vendors[2].vendorEntry.vendor.vendor, 'azure'); + assert.strictEqual(vendors[3].vendorEntry.vendor.vendor, 'openai'); + + // Test with text search + results = viewModel.filter('GPT'); + vendors = results.filter(isLanguageModelProviderEntry) as ILanguageModelProviderEntry[]; + if (vendors.length > 1) { + assert.strictEqual(vendors[0].vendorEntry.vendor.vendor, 'copilot'); + } + + // Test with capability filter + results = viewModel.filter('@capability:tools'); + vendors = results.filter(isLanguageModelProviderEntry) as ILanguageModelProviderEntry[]; + if (vendors.length > 1) { + assert.strictEqual(vendors[0].vendorEntry.vendor.vendor, 'copilot'); + } + }); + + test('should show vendor headers when filtered', () => { + const results = viewModel.filter('GPT'); + const vendors = results.filter(isLanguageModelProviderEntry); + assert.ok(vendors.length > 0); + }); + + test('should not show vendor headers when filtered if only one vendor exists', async () => { + const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(); + await singleVendorViewModel.refresh(); + + const results = singleVendorViewModel.filter('GPT'); + const vendors = results.filter(isLanguageModelProviderEntry); + assert.strictEqual(vendors.length, 0); + }); + + test('should group by visibility', () => { + viewModel.groupBy = ChatModelGroup.Visibility; + const actuals = viewModel.viewModelEntries; + + assert.strictEqual(actuals.length, 6); + assert.strictEqual(actuals[0].type, 'group'); + assert.strictEqual(actuals[0].id, 'visible'); + assert.strictEqual(actuals[1].type, 'model'); + assert.strictEqual(actuals[1].model.identifier, 'copilot-gpt-4'); + assert.strictEqual(actuals[2].type, 'model'); + assert.strictEqual(actuals[2].model.identifier, 'copilot-gpt-4o'); + assert.strictEqual(actuals[3].type, 'model'); + assert.strictEqual(actuals[3].model.identifier, 'openai-gpt-3.5'); + assert.strictEqual(actuals[4].type, 'group'); + assert.strictEqual(actuals[4].id, 'hidden'); + assert.strictEqual(actuals[5].type, 'model'); + assert.strictEqual(actuals[5].model.identifier, 'openai-gpt-4-vision'); + }); + + test('should fire onDidChangeGrouping when grouping changes', () => { + let fired = false; + store.add(viewModel.onDidChangeGrouping(() => { + fired = true; + })); + + viewModel.groupBy = ChatModelGroup.Visibility; + assert.strictEqual(fired, true); + }); + + test('should reset collapsed state when grouping changes', () => { + const vendorEntry = viewModel.viewModelEntries.find(r => isLanguageModelProviderEntry(r) && r.vendorEntry.vendor.vendor === 'copilot') as ILanguageModelProviderEntry; + viewModel.toggleCollapsed(vendorEntry); + + viewModel.groupBy = ChatModelGroup.Visibility; + + const results = viewModel.filter(''); + const groups = results.filter(isLanguageModelGroupEntry) as ILanguageModelGroupEntry[]; + assert.ok(groups.every(v => !v.collapsed)); + }); + + test('should sort models within visibility groups', async () => { + languageModelsService.addVendor({ + vendor: 'anthropic', + displayName: 'Anthropic', + managementCommand: undefined, + when: undefined, + configuration: undefined + }); + + languageModelsService.addModel('anthropic', 'anthropic-claude', { + extension: new ExtensionIdentifier('anthropic.api'), + id: 'claude-3', + name: 'Claude 3', + family: 'claude', + version: '1.0', + vendor: 'anthropic', + maxInputTokens: 100000, + maxOutputTokens: 4096, + modelPickerCategory: { label: 'Anthropic', order: 3 }, + isUserSelectable: true, + capabilities: { + toolCalling: true, + vision: false, + agentMode: false + }, + isDefaultForLocation: { + [ChatAgentLocation.Chat]: true + } + }); + + await viewModel.refresh(); + + viewModel.groupBy = ChatModelGroup.Visibility; + const actuals = viewModel.viewModelEntries; + + assert.strictEqual(actuals.length, 7); + + assert.strictEqual(actuals[0].type, 'group'); + assert.strictEqual(actuals[0].id, 'visible'); + + assert.strictEqual(actuals[1].type, 'model'); + assert.strictEqual(actuals[1].model.metadata.id, 'gpt-4'); + + assert.strictEqual(actuals[2].type, 'model'); + assert.strictEqual(actuals[2].model.metadata.id, 'gpt-4o'); + + assert.strictEqual(actuals[3].type, 'model'); + assert.strictEqual(actuals[3].model.metadata.id, 'claude-3'); + + assert.strictEqual(actuals[4].type, 'model'); + assert.strictEqual(actuals[4].model.metadata.id, 'gpt-3.5-turbo'); + + assert.strictEqual(actuals[5].type, 'group'); + assert.strictEqual(actuals[5].id, 'hidden'); + + assert.strictEqual(actuals[6].type, 'model'); + assert.strictEqual(actuals[6].model.metadata.id, 'gpt-4-vision'); + }); + + test('should get configured vendors', () => { + const vendors = viewModel.getConfiguredVendors(); + assert.ok(vendors.length > 0); + assert.ok(vendors.some(v => v.vendor.vendor === 'copilot')); + assert.ok(vendors.some(v => v.vendor.vendor === 'openai')); + }); + + test('should return true for shouldRefilter when models not sorted', () => { + // After a new filter call, models should be sorted + viewModel.filter(''); + assert.strictEqual(viewModel.shouldRefilter(), false); + + // Simulate unsorted state by accessing private property indirectly + // This is a simple test that shouldRefilter works + const result = viewModel.shouldRefilter(); + assert.strictEqual(typeof result, 'boolean'); + }); + + test('should collapse all groups and models', () => { + // Expand everything first + const results1 = viewModel.filter(''); + let models = results1.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.ok(models.length > 0); + + // Collapse all + viewModel.collapseAll(); + + // After collapse all, only group/vendor headers should be shown + const results2 = viewModel.filter(''); + const vendors = results2.filter(isLanguageModelProviderEntry); + models = results2.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + + assert.ok(vendors.length > 0, 'Should have vendor headers'); + assert.strictEqual(models.length, 0, 'Should have no models visible after collapse all'); + }); + + test('should match quoted search strings with filters', () => { + // Test that quotes don't break when combined with other filters + const results = viewModel.filter('@capability:tools "GPT"'); + assert.ok(Array.isArray(results)); + // Should handle without error + }); + + test('should filter by case-insensitive provider name', () => { + const results1 = viewModel.filter('@provider:COPILOT'); + const results2 = viewModel.filter('@provider:copilot'); + const results3 = viewModel.filter('@provider:CopiloT'); + + const models1 = results1.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + const models2 = results2.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + const models3 = results3.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + + assert.strictEqual(models1.length, models2.length); + assert.strictEqual(models2.length, models3.length); + assert.strictEqual(models1.length, 2); + }); + + test('should handle empty search returning all results', () => { + const results = viewModel.filter(''); + assert.ok(results.length > 0); + + // Should include vendor headers and models + const vendors = results.filter(isLanguageModelProviderEntry); + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + + assert.strictEqual(vendors.length, 2); + assert.strictEqual(models.length, 4); + }); + + test('should not find matches when searching for non-existent model', () => { + const results = viewModel.filter('NonExistentModel123'); + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 0); + }); + + test('should not find matches when filtering by non-existent provider', () => { + const results = viewModel.filter('@provider:nonexistent'); + const models = results.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.strictEqual(models.length, 0); + }); + + test('setModelsVisibility should update visibility for multiple models', () => { + // Get initial results + const initialResults = viewModel.filter(''); + const modelEntries = initialResults.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.ok(modelEntries.length >= 2, 'Should have at least 2 models for testing'); + + // Get first two models + const modelsToHide = modelEntries.slice(0, 2); + const initialVisibility = modelsToHide.map(m => m.model.visible); + + // Hide the models + viewModel.setModelsVisibility(modelsToHide, false); + + // Verify visibility was updated + assert.strictEqual(modelsToHide[0].model.visible, false); + assert.strictEqual(modelsToHide[1].model.visible, false); + + // Verify language models service was called by checking metadata + const metadata1 = languageModelsService.lookupLanguageModel(modelsToHide[0].model.identifier); + const metadata2 = languageModelsService.lookupLanguageModel(modelsToHide[1].model.identifier); + assert.strictEqual(metadata1?.isUserSelectable, false); + assert.strictEqual(metadata2?.isUserSelectable, false); + + // Verify UI was updated by filtering + const updatedResults = viewModel.filter(''); + const updatedModelEntries = updatedResults.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.ok(updatedModelEntries.length > 0); + + // Restore original visibility - group by visibility state for efficient restoration + const modelsToMakeVisible = modelsToHide.filter((_, i) => initialVisibility[i]); + const modelsToMakeHidden = modelsToHide.filter((_, i) => !initialVisibility[i]); + if (modelsToMakeVisible.length > 0) { + viewModel.setModelsVisibility(modelsToMakeVisible, true); + } + if (modelsToMakeHidden.length > 0) { + viewModel.setModelsVisibility(modelsToMakeHidden, false); + } + }); + + test('setModelsVisibility should make hidden models visible', () => { + // Get initial results + const initialResults = viewModel.filter(''); + const modelEntries = initialResults.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + assert.ok(modelEntries.length >= 1, 'Should have at least 1 model for testing'); + + // Get a model and hide it first + const modelToTest = [modelEntries[0]]; + viewModel.setModelsVisibility(modelToTest, false); + assert.strictEqual(modelToTest[0].model.visible, false); + + // Now make it visible + viewModel.setModelsVisibility(modelToTest, true); + + // Verify visibility was updated + assert.strictEqual(modelToTest[0].model.visible, true); + + // Verify language models service was called + const metadata = languageModelsService.lookupLanguageModel(modelToTest[0].model.identifier); + assert.strictEqual(metadata?.isUserSelectable, true); + }); + + test('setGroupVisibility should update visibility for all models in a provider group', () => { + // Get initial results to find a provider group + const initialResults = viewModel.filter(''); + const providerGroups = initialResults.filter(isLanguageModelProviderEntry); + assert.ok(providerGroups.length > 0, 'Should have at least 1 provider group'); + + const providerGroup = providerGroups[0]; + const modelsInGroup = viewModel.getModelsForGroup(providerGroup); + assert.ok(modelsInGroup.length > 0, 'Provider group should have models'); + + // Store initial visibility + const initialVisibility = modelsInGroup.map(m => m.visible); + + // Hide all models in the group + viewModel.setGroupVisibility(providerGroup, false); + + // Verify all models in group are now hidden + const updatedModels = viewModel.getModelsForGroup(providerGroup); + for (const model of updatedModels) { + assert.strictEqual(model.visible, false, `Model ${model.identifier} should be hidden`); + + // Verify language models service was called + const metadata = languageModelsService.lookupLanguageModel(model.identifier); + assert.strictEqual(metadata?.isUserSelectable, false); + } + + // Restore original visibility using setGroupVisibility for models that were visible + const modelsToRestore = modelsInGroup.filter((_, i) => initialVisibility[i]); + if (modelsToRestore.length > 0) { + const modelEntries = initialResults.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + const entriesToRestore = modelEntries.filter(e => modelsToRestore.some(m => m.identifier === e.model.identifier)); + viewModel.setModelsVisibility(entriesToRestore, true); + } + }); + + test('setGroupVisibility should update visibility for all models in a visibility group', () => { + // Store initial visibility state + const allResults = viewModel.filter(''); + const allModelEntries = allResults.filter(r => !isLanguageModelProviderEntry(r) && !isLanguageModelGroupEntry(r)) as ILanguageModelEntry[]; + const initialModelStates = allModelEntries.map(m => ({ entry: m, visible: m.model.visible })); + + // First ensure we have some visible and some hidden models + if (allModelEntries.length >= 2) { + // Hide one model to create a mixed state + viewModel.setModelsVisibility([allModelEntries[0]], false); + viewModel.setModelsVisibility([allModelEntries[1]], true); + } + + // Filter to trigger visibility group creation - the visibility filter activates grouping by visibility + viewModel.filter('@visible:true'); + // Now get the results with visibility groups + const resultsWithGroups = viewModel.filter(''); + + // Find the visibility group entries + const visibilityGroups = resultsWithGroups.filter(isLanguageModelGroupEntry); + + if (visibilityGroups.length > 0) { + const visibleGroup = visibilityGroups.find(g => g.id === 'visible'); + if (visibleGroup) { + const visibleModels = viewModel.getModelsForGroup(visibleGroup); + const initialCount = visibleModels.length; + + if (initialCount > 0) { + // Hide all visible models + viewModel.setGroupVisibility(visibleGroup, false); + + // Verify all previously visible models are now hidden + const updatedVisibleModels = viewModel.getModelsForGroup(visibleGroup); + assert.strictEqual(updatedVisibleModels.length, 0, 'Should have no visible models after hiding the visible group'); + + // Verify the hidden group now contains those models + const hiddenGroup = visibilityGroups.find(g => g.id === 'hidden'); + if (hiddenGroup) { + const hiddenModels = viewModel.getModelsForGroup(hiddenGroup); + assert.ok(hiddenModels.length >= initialCount, 'Hidden group should contain the previously visible models'); + } + } + } + } + + // Restore original visibility state + const modelsToMakeVisible = initialModelStates.filter(s => s.visible).map(s => s.entry); + const modelsToMakeHidden = initialModelStates.filter(s => !s.visible).map(s => s.entry); + if (modelsToMakeVisible.length > 0) { + viewModel.setModelsVisibility(modelsToMakeVisible, true); + } + if (modelsToMakeHidden.length > 0) { + viewModel.setModelsVisibility(modelsToMakeHidden, false); + } + }); + + test('setGroupVisibility should trigger UI update through doFilter', () => { + // Get a provider group + const initialResults = viewModel.filter(''); + const providerGroups = initialResults.filter(isLanguageModelProviderEntry); + + if (providerGroups.length > 0) { + const providerGroup = providerGroups[0]; + + // Change visibility + viewModel.setGroupVisibility(providerGroup, false); + + // Filter again to ensure UI was updated + const updatedResults = viewModel.filter(''); + const updatedProviderGroups = updatedResults.filter(isLanguageModelProviderEntry); + + // Verify we can still get results (doFilter was called) + assert.ok(updatedProviderGroups.length > 0, 'Should still have provider groups after visibility change'); + } + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts deleted file mode 100644 index 0cbc89277a0..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/chatModelsViewModel.test.ts +++ /dev/null @@ -1,751 +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 { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatSelector, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js'; -import { ChatModelsViewModel, IModelItemEntry, IVendorItemEntry, isVendorEntry } from '../../browser/chatManagement/chatModelsViewModel.js'; -import { IChatEntitlementService, ChatEntitlement } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IObservable, observableValue } from '../../../../../base/common/observable.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; - -class MockLanguageModelsService implements ILanguageModelsService { - _serviceBrand: undefined; - - private vendors: IUserFriendlyLanguageModel[] = []; - private models = new Map(); - private modelsByVendor = new Map(); - - private readonly _onDidChangeLanguageModels = new Emitter(); - readonly onDidChangeLanguageModels = this._onDidChangeLanguageModels.event; - - addVendor(vendor: IUserFriendlyLanguageModel): void { - this.vendors.push(vendor); - this.modelsByVendor.set(vendor.vendor, []); - } - - addModel(vendorId: string, identifier: string, metadata: ILanguageModelChatMetadata): void { - this.models.set(identifier, metadata); - const models = this.modelsByVendor.get(vendorId) || []; - models.push(identifier); - this.modelsByVendor.set(vendorId, models); - } - - registerLanguageModelProvider(vendor: string, provider: ILanguageModelChatProvider): IDisposable { - throw new Error('Method not implemented.'); - } - - updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { - throw new Error('Method not implemented.'); - } - - getVendors(): IUserFriendlyLanguageModel[] { - return this.vendors; - } - - getLanguageModelIds(): string[] { - return Array.from(this.models.keys()); - } - - lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined { - return this.models.get(identifier); - } - - getLanguageModels(): ILanguageModelChatMetadataAndIdentifier[] { - const result: ILanguageModelChatMetadataAndIdentifier[] = []; - for (const [identifier, metadata] of this.models.entries()) { - result.push({ identifier, metadata }); - } - return result; - } - - setContributedSessionModels(): void { - } - - clearContributedSessionModels(): void { - } - - async selectLanguageModels(selector: ILanguageModelChatSelector, allowHidden?: boolean): Promise { - if (selector.vendor) { - return this.modelsByVendor.get(selector.vendor) || []; - } - return Array.from(this.models.keys()); - } - - sendChatRequest(): Promise { - throw new Error('Method not implemented.'); - } - - computeTokenLength(): Promise { - throw new Error('Method not implemented.'); - } -} - -class MockChatEntitlementService implements IChatEntitlementService { - _serviceBrand: undefined; - - private readonly _onDidChangeEntitlement = new Emitter(); - readonly onDidChangeEntitlement = this._onDidChangeEntitlement.event; - - readonly entitlement = ChatEntitlement.Unknown; - readonly entitlementObs: IObservable = observableValue('entitlement', ChatEntitlement.Unknown); - - readonly organisations: string[] | undefined = undefined; - readonly isInternal = false; - readonly sku: string | undefined = undefined; - - readonly onDidChangeQuotaExceeded = Event.None; - readonly onDidChangeQuotaRemaining = Event.None; - - readonly quotas = { - chat: { - total: 100, - remaining: 100, - percentRemaining: 100, - overageEnabled: false, - overageCount: 0, - unlimited: false - }, - completions: { - total: 100, - remaining: 100, - percentRemaining: 100, - overageEnabled: false, - overageCount: 0, - unlimited: false - } - }; - - readonly onDidChangeSentiment = Event.None; - readonly sentiment: any = { installed: true, hidden: false, disabled: false }; - readonly sentimentObs: IObservable = observableValue('sentiment', { installed: true, hidden: false, disabled: false }); - - readonly onDidChangeAnonymous = Event.None; - readonly anonymous = false; - readonly anonymousObs: IObservable = observableValue('anonymous', false); - - fireEntitlementChange(): void { - this._onDidChangeEntitlement.fire(); - } - - async update(): Promise { - // Not needed for tests - } -} - -suite('ChatModelsViewModel', () => { - let store: DisposableStore; - let languageModelsService: MockLanguageModelsService; - let chatEntitlementService: MockChatEntitlementService; - let viewModel: ChatModelsViewModel; - - setup(async () => { - store = new DisposableStore(); - languageModelsService = new MockLanguageModelsService(); - chatEntitlementService = new MockChatEntitlementService(); - - // Setup test data - languageModelsService.addVendor({ - vendor: 'copilot', - displayName: 'GitHub Copilot', - managementCommand: undefined, - when: undefined - }); - - languageModelsService.addVendor({ - vendor: 'openai', - displayName: 'OpenAI', - managementCommand: undefined, - when: undefined - }); - - languageModelsService.addModel('copilot', 'copilot-gpt-4', { - extension: new ExtensionIdentifier('github.copilot'), - id: 'gpt-4', - name: 'GPT-4', - family: 'gpt-4', - version: '1.0', - vendor: 'copilot', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Copilot', order: 1 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: true, - agentMode: false - } - }); - - languageModelsService.addModel('copilot', 'copilot-gpt-4o', { - extension: new ExtensionIdentifier('github.copilot'), - id: 'gpt-4o', - name: 'GPT-4o', - family: 'gpt-4', - version: '1.0', - vendor: 'copilot', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Copilot', order: 1 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: true, - agentMode: true - } - }); - - languageModelsService.addModel('openai', 'openai-gpt-3.5', { - extension: new ExtensionIdentifier('openai.api'), - id: 'gpt-3.5-turbo', - name: 'GPT-3.5 Turbo', - family: 'gpt-3.5', - version: '1.0', - vendor: 'openai', - maxInputTokens: 4096, - maxOutputTokens: 2048, - modelPickerCategory: { label: 'OpenAI', order: 2 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: false, - agentMode: false - } - }); - - languageModelsService.addModel('openai', 'openai-gpt-4-vision', { - extension: new ExtensionIdentifier('openai.api'), - id: 'gpt-4-vision', - name: 'GPT-4 Vision', - family: 'gpt-4', - version: '1.0', - vendor: 'openai', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'OpenAI', order: 2 }, - isUserSelectable: false, - capabilities: { - toolCalling: false, - vision: true, - agentMode: false - } - }); - - viewModel = store.add(new ChatModelsViewModel( - languageModelsService, - chatEntitlementService - )); - - await viewModel.resolve(); - }); - - teardown(() => { - store.dispose(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('should fetch all models without filters', () => { - const results = viewModel.fetch(''); - - // Should have 2 vendor entries and 4 model entries (grouped by vendor) - assert.strictEqual(results.length, 6); - - const vendors = results.filter(isVendorEntry); - assert.strictEqual(vendors.length, 2); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 4); - }); - - test('should filter by provider name', () => { - const results = viewModel.fetch('@provider:copilot'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => m.modelEntry.vendor === 'copilot')); - }); - - test('should filter by provider display name', () => { - const results = viewModel.fetch('@provider:OpenAI'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => m.modelEntry.vendor === 'openai')); - }); - - test('should filter by multiple providers with OR logic', () => { - const results = viewModel.fetch('@provider:copilot @provider:openai'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 4); - }); - - test('should filter by single capability - tools', () => { - const results = viewModel.fetch('@capability:tools'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 3); - assert.ok(models.every(m => m.modelEntry.metadata.capabilities?.toolCalling === true)); - }); - - test('should filter by single capability - vision', () => { - const results = viewModel.fetch('@capability:vision'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 3); - assert.ok(models.every(m => m.modelEntry.metadata.capabilities?.vision === true)); - }); - - test('should filter by single capability - agent', () => { - const results = viewModel.fetch('@capability:agent'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.id, 'gpt-4o'); - }); - - test('should filter by multiple capabilities with AND logic', () => { - const results = viewModel.fetch('@capability:tools @capability:vision'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Should only return models that have BOTH tools and vision - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => - m.modelEntry.metadata.capabilities?.toolCalling === true && - m.modelEntry.metadata.capabilities?.vision === true - )); - }); - - test('should filter by three capabilities with AND logic', () => { - const results = viewModel.fetch('@capability:tools @capability:vision @capability:agent'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Should only return gpt-4o which has all three - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.id, 'gpt-4o'); - }); - - test('should return no results when filtering by incompatible capabilities', () => { - const results = viewModel.fetch('@capability:vision @capability:agent'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Only gpt-4o has both vision and agent, but gpt-4-vision doesn't have agent - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.id, 'gpt-4o'); - }); - - test('should filter by visibility - visible:true', () => { - const results = viewModel.fetch('@visible:true'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 3); - assert.ok(models.every(m => m.modelEntry.metadata.isUserSelectable === true)); - }); - - test('should filter by visibility - visible:false', () => { - const results = viewModel.fetch('@visible:false'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.isUserSelectable, false); - }); - - test('should combine provider and capability filters', () => { - const results = viewModel.fetch('@provider:copilot @capability:vision'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => - m.modelEntry.vendor === 'copilot' && - m.modelEntry.metadata.capabilities?.vision === true - )); - }); - - test('should combine provider, capability, and visibility filters', () => { - const results = viewModel.fetch('@provider:openai @capability:vision @visible:false'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.id, 'gpt-4-vision'); - }); - - test('should filter by text matching model name', () => { - const results = viewModel.fetch('GPT-4o'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.name, 'GPT-4o'); - assert.ok(models[0].modelNameMatches); - }); - - test('should filter by text matching vendor name', () => { - const results = viewModel.fetch('GitHub'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => m.modelEntry.vendorDisplayName === 'GitHub Copilot')); - }); - - test('should combine text search with capability filter', () => { - const results = viewModel.fetch('@capability:tools GPT'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Should match all models with tools capability and 'GPT' in name - assert.strictEqual(models.length, 3); - assert.ok(models.every(m => m.modelEntry.metadata.capabilities?.toolCalling === true)); - }); - - test('should handle empty search value', () => { - const results = viewModel.fetch(''); - - // Should return all models grouped by vendor - assert.ok(results.length > 0); - }); - - test('should handle search value with only whitespace', () => { - const results = viewModel.fetch(' '); - - // Should return all models grouped by vendor - assert.ok(results.length > 0); - }); - - test('should match capability text in free text search', () => { - const results = viewModel.fetch('vision'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Should match models that have vision capability or "vision" in their name - assert.ok(models.length > 0); - assert.ok(models.every(m => - m.modelEntry.metadata.capabilities?.vision === true || - m.modelEntry.metadata.name.toLowerCase().includes('vision') - )); - }); - - test('should toggle vendor collapsed state', () => { - viewModel.toggleVendorCollapsed('copilot'); - - const results = viewModel.fetch(''); - const copilotVendor = results.find(r => isVendorEntry(r) && (r as IVendorItemEntry).vendorEntry.vendor === 'copilot') as IVendorItemEntry; - - assert.ok(copilotVendor); - assert.strictEqual(copilotVendor.collapsed, true); - - // Models should not be shown when vendor is collapsed - const copilotModelsAfterCollapse = results.filter(r => - !isVendorEntry(r) && (r as IModelItemEntry).modelEntry.vendor === 'copilot' - ); - assert.strictEqual(copilotModelsAfterCollapse.length, 0); - - // Toggle back - viewModel.toggleVendorCollapsed('copilot'); - const resultsAfterExpand = viewModel.fetch(''); - const copilotModelsAfterExpand = resultsAfterExpand.filter(r => - !isVendorEntry(r) && (r as IModelItemEntry).modelEntry.vendor === 'copilot' - ); - assert.strictEqual(copilotModelsAfterExpand.length, 2); - }); - - test('should fire onDidChangeModelEntries when entitlement changes', async () => { - let fired = false; - store.add(viewModel.onDidChangeModelEntries(() => { - fired = true; - })); - - chatEntitlementService.fireEntitlementChange(); - - // Wait a bit for async resolve - await new Promise(resolve => setTimeout(resolve, 10)); - - assert.strictEqual(fired, true); - }); - - test('should handle quoted search strings', () => { - // When a search string is fully quoted (starts and ends with quotes), - // the completeMatch flag is set to true, which currently skips all matching - // This test verifies the quotes are processed without errors - const results = viewModel.fetch('"GPT"'); - - // The function should complete without error - // Note: complete match logic (both quotes) currently doesn't perform matching - assert.ok(Array.isArray(results)); - }); - - test('should remove filter keywords from text search', () => { - const results = viewModel.fetch('@provider:copilot @capability:vision GPT'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - // Should only search 'GPT' in model names, not the filter keywords - assert.strictEqual(models.length, 2); - assert.ok(models.every(m => m.modelEntry.vendor === 'copilot')); - }); - - test('should handle case-insensitive capability matching', () => { - const results1 = viewModel.fetch('@capability:TOOLS'); - const results2 = viewModel.fetch('@capability:tools'); - const results3 = viewModel.fetch('@capability:Tools'); - - const models1 = results1.filter(r => !isVendorEntry(r)); - const models2 = results2.filter(r => !isVendorEntry(r)); - const models3 = results3.filter(r => !isVendorEntry(r)); - - assert.strictEqual(models1.length, models2.length); - assert.strictEqual(models2.length, models3.length); - }); - - test('should support toolcalling alias for tools capability', () => { - const resultsTools = viewModel.fetch('@capability:tools'); - const resultsToolCalling = viewModel.fetch('@capability:toolcalling'); - - const modelsTools = resultsTools.filter(r => !isVendorEntry(r)); - const modelsToolCalling = resultsToolCalling.filter(r => !isVendorEntry(r)); - - assert.strictEqual(modelsTools.length, modelsToolCalling.length); - }); - - test('should support agentmode alias for agent capability', () => { - const resultsAgent = viewModel.fetch('@capability:agent'); - const resultsAgentMode = viewModel.fetch('@capability:agentmode'); - - const modelsAgent = resultsAgent.filter(r => !isVendorEntry(r)); - const modelsAgentMode = resultsAgentMode.filter(r => !isVendorEntry(r)); - - assert.strictEqual(modelsAgent.length, modelsAgentMode.length); - }); - - test('should include matched capabilities in results', () => { - const results = viewModel.fetch('@capability:tools @capability:vision'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.ok(models.length > 0); - - for (const model of models) { - assert.ok(model.capabilityMatches); - assert.ok(model.capabilityMatches.length > 0); - // Should include both toolCalling and vision - assert.ok(model.capabilityMatches.some(c => c === 'toolCalling' || c === 'vision')); - } - }); - - // Helper function to create a single vendor test environment - function createSingleVendorViewModel(store: DisposableStore, chatEntitlementService: IChatEntitlementService, includeSecondModel: boolean = true): { service: MockLanguageModelsService; viewModel: ChatModelsViewModel } { - const service = new MockLanguageModelsService(); - service.addVendor({ - vendor: 'copilot', - displayName: 'GitHub Copilot', - managementCommand: undefined, - when: undefined - }); - - service.addModel('copilot', 'copilot-gpt-4', { - extension: new ExtensionIdentifier('github.copilot'), - id: 'gpt-4', - name: 'GPT-4', - family: 'gpt-4', - version: '1.0', - vendor: 'copilot', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Copilot', order: 1 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: true, - agentMode: false - } - }); - - if (includeSecondModel) { - service.addModel('copilot', 'copilot-gpt-4o', { - extension: new ExtensionIdentifier('github.copilot'), - id: 'gpt-4o', - name: 'GPT-4o', - family: 'gpt-4', - version: '1.0', - vendor: 'copilot', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Copilot', order: 1 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: true, - agentMode: true - } - }); - } - - const viewModel = store.add(new ChatModelsViewModel(service, chatEntitlementService)); - return { service, viewModel }; - } - - test('should not show vendor header when only one vendor exists', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(store, chatEntitlementService); - await singleVendorViewModel.resolve(); - - const results = singleVendorViewModel.fetch(''); - - // Should have only model entries, no vendor entry - const vendors = results.filter(isVendorEntry); - assert.strictEqual(vendors.length, 0, 'Should not show vendor header when only one vendor exists'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 2, 'Should show all models'); - assert.ok(models.every(m => m.modelEntry.vendor === 'copilot')); - }); - - test('should show vendor headers when multiple vendors exist', () => { - // This is the existing behavior test - const results = viewModel.fetch(''); - - // Should have 2 vendor entries and 4 model entries (grouped by vendor) - const vendors = results.filter(isVendorEntry); - assert.strictEqual(vendors.length, 2, 'Should show vendor headers when multiple vendors exist'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 4); - }); - - test('should show models even when single vendor is collapsed', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(store, chatEntitlementService, false); - await singleVendorViewModel.resolve(); - - // Try to collapse the single vendor - singleVendorViewModel.toggleVendorCollapsed('copilot'); - - const results = singleVendorViewModel.fetch(''); - - // Should still show models even though vendor is "collapsed" - // because there's no vendor header to collapse - const vendors = results.filter(isVendorEntry); - assert.strictEqual(vendors.length, 0, 'Should not show vendor header'); - - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1, 'Should still show models even when single vendor is collapsed'); - }); - - test('should filter single vendor models by capability', async () => { - const { viewModel: singleVendorViewModel } = createSingleVendorViewModel(store, chatEntitlementService); - await singleVendorViewModel.resolve(); - - const results = singleVendorViewModel.fetch('@capability:agent'); - - // Should not show vendor header - const vendors = results.filter(isVendorEntry); - assert.strictEqual(vendors.length, 0, 'Should not show vendor header'); - - // Should only show the model with agent capability - const models = results.filter(r => !isVendorEntry(r)) as IModelItemEntry[]; - assert.strictEqual(models.length, 1); - assert.strictEqual(models[0].modelEntry.metadata.id, 'gpt-4o'); - }); - - test('should always place copilot vendor at the top', () => { - const results = viewModel.fetch(''); - - const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; - assert.ok(vendors.length >= 2); - - // First vendor should always be copilot - assert.strictEqual(vendors[0].vendorEntry.vendor, 'copilot'); - }); - - test('should maintain copilot at top with multiple vendors', async () => { - // Add more vendors to ensure sorting works correctly - languageModelsService.addVendor({ - vendor: 'anthropic', - displayName: 'Anthropic', - managementCommand: undefined, - when: undefined - }); - - languageModelsService.addModel('anthropic', 'anthropic-claude', { - extension: new ExtensionIdentifier('anthropic.api'), - id: 'claude-3', - name: 'Claude 3', - family: 'claude', - version: '1.0', - vendor: 'anthropic', - maxInputTokens: 100000, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Anthropic', order: 3 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: false, - agentMode: false - } - }); - - languageModelsService.addVendor({ - vendor: 'azure', - displayName: 'Azure OpenAI', - managementCommand: undefined, - when: undefined - }); - - languageModelsService.addModel('azure', 'azure-gpt-4', { - extension: new ExtensionIdentifier('microsoft.azure'), - id: 'azure-gpt-4', - name: 'Azure GPT-4', - family: 'gpt-4', - version: '1.0', - vendor: 'azure', - maxInputTokens: 8192, - maxOutputTokens: 4096, - modelPickerCategory: { label: 'Azure', order: 4 }, - isUserSelectable: true, - capabilities: { - toolCalling: true, - vision: false, - agentMode: false - } - }); - - await viewModel.resolve(); - - const results = viewModel.fetch(''); - const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; - - // Should have 4 vendors: copilot, openai, anthropic, azure - assert.strictEqual(vendors.length, 4); - - // First vendor should always be copilot - assert.strictEqual(vendors[0].vendorEntry.vendor, 'copilot'); - - // Other vendors should be alphabetically sorted: anthropic, azure, openai - assert.strictEqual(vendors[1].vendorEntry.vendor, 'anthropic'); - assert.strictEqual(vendors[2].vendorEntry.vendor, 'azure'); - assert.strictEqual(vendors[3].vendorEntry.vendor, 'openai'); - }); - - test('should keep copilot at top even with text search', () => { - // Even when searching, if results include multiple vendors, copilot should be first - const results = viewModel.fetch('GPT'); - - const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; - - if (vendors.length > 1) { - // If multiple vendors match, copilot should be first - const copilotVendor = vendors.find(v => v.vendorEntry.vendor === 'copilot'); - if (copilotVendor) { - assert.strictEqual(vendors[0].vendorEntry.vendor, 'copilot'); - } - } - }); - - test('should keep copilot at top when filtering by capability', () => { - const results = viewModel.fetch('@capability:tools'); - - const vendors = results.filter(isVendorEntry) as IVendorItemEntry[]; - - // Both copilot and openai have models with tools capability - if (vendors.length > 1) { - assert.strictEqual(vendors[0].vendorEntry.vendor, 'copilot'); - } - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSessions/chatSessionsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatSessions/chatSessionsService.test.ts new file mode 100644 index 00000000000..cc0e45205dd --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatSessions/chatSessionsService.test.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ChatSessionsService } from '../../../browser/chatSessions/chatSessions.contribution.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; + +suite.skip('ChatSessionsService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let chatSessionsService: ChatSessionsService; + + setup(() => { + const instantiationService = store.add(workbenchInstantiationService(undefined, store)); + chatSessionsService = store.add(instantiationService.createInstance(ChatSessionsService)); + }); + + suite('extractFileNameFromLink', () => { + + function callExtractFileNameFromLink(filePath: string): string { + // Access the private method using bracket notation with proper typing + type ServiceWithPrivateMethod = Record<'extractFileNameFromLink', (filePath: string) => string>; + return (chatSessionsService as unknown as ServiceWithPrivateMethod)['extractFileNameFromLink'](filePath); + } + + test('should extract filename from markdown link with link text', () => { + const input = 'Read [README](file:///path/to/README.md) for more info'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Read README for more info'); + }); + + test('should extract filename from markdown link without link text', () => { + const input = 'Read [](file:///index.js) for instructions'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Read index.js for instructions'); + }); + + test('should extract filename from markdown link with empty link text', () => { + const input = 'Check [ ](file:///config.json) settings'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Check config.json settings'); + }); + + test('should handle multiple file links in same string', () => { + const input = 'See [main](file:///main.js) and [utils](file:///utils/helper.ts)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'See main and utils'); + }); + + test('should handle file path without extension', () => { + const input = 'Open [](file:///src/components/Button)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Open Button'); + }); + + test('should handle deep file paths', () => { + const input = 'Edit [](file:///very/deep/nested/path/to/file.tsx)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Edit file.tsx'); + }); + + test('should handle file path that is just a filename', () => { + const input = 'View [script](file:///script.py)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'View script'); + }); + + test('should handle link text with special characters', () => { + const input = 'See [App.js (main)](file:///App.js)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'See App.js (main)'); + }); + + test('should return original string if no file links present', () => { + const input = 'This is just regular text with no links'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'This is just regular text with no links'); + }); + + test('should handle mixed content with file links and regular text', () => { + const input = 'Check [config](file:///config.yml) and visit https://example.com'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Check config and visit https://example.com'); + }); + + test('should handle file path with query parameters or fragments', () => { + const input = 'Open [](file:///index.html?param=value#section)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Open index.html?param=value#section'); + }); + + test('should handle Windows-style paths', () => { + const input = 'Edit [](file:///C:/Users/user/Documents/file.txt)'; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, 'Edit file.txt'); + }); + + test('should preserve whitespace around replacements', () => { + const input = ' Check [](file:///test.js) '; + const result = callExtractFileNameFromLink(input); + assert.strictEqual(result, ' Check test.js '); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts new file mode 100644 index 00000000000..453f76531c1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/chatTipService.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { IProductService } from '../../../../../platform/product/common/productService.js'; +import { ChatTipService } from '../../browser/chatTipService.js'; + +class MockContextKeyServiceWithRulesMatching extends MockContextKeyService { + override contextMatchesRules(): boolean { + return true; + } +} + +suite('ChatTipService', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + let contextKeyService: MockContextKeyServiceWithRulesMatching; + let configurationService: TestConfigurationService; + + function createProductService(hasCopilot: boolean): IProductService { + return { + _serviceBrand: undefined, + defaultChatAgent: hasCopilot ? { chatExtensionId: 'github.copilot-chat' } : undefined, + } as IProductService; + } + + function createService(hasCopilot: boolean = true, tipsEnabled: boolean = true): ChatTipService { + instantiationService.stub(IProductService, createProductService(hasCopilot)); + configurationService.setUserConfiguration('chat.tips.enabled', tipsEnabled); + return instantiationService.createInstance(ChatTipService); + } + + setup(() => { + instantiationService = testDisposables.add(new TestInstantiationService()); + contextKeyService = new MockContextKeyServiceWithRulesMatching(); + configurationService = new TestConfigurationService(); + instantiationService.stub(IContextKeyService, contextKeyService); + instantiationService.stub(IConfigurationService, configurationService); + }); + + test('returns a tip for new requests with timestamp after service creation', () => { + const service = createService(); + const now = Date.now(); + + // Request created after service initialization + const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip, 'Should return a tip for requests created after service instantiation'); + assert.ok(tip.id.startsWith('tip.'), 'Tip should have a valid ID'); + assert.ok(tip.content.value.length > 0, 'Tip should have content'); + }); + + test('returns undefined for old requests with timestamp before service creation', () => { + const service = createService(); + const now = Date.now(); + + // Request created before service initialization (simulating restored chat) + const tip = service.getNextTip('old-request', now - 10000, contextKeyService); + assert.strictEqual(tip, undefined, 'Should not return a tip for requests created before service instantiation'); + }); + + test('only shows one tip per session', () => { + const service = createService(); + const now = Date.now(); + + // First request gets a tip + const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip1, 'First request should get a tip'); + + // Second request does not get a tip + const tip2 = service.getNextTip('request-2', now + 2000, contextKeyService); + assert.strictEqual(tip2, undefined, 'Second request should not get a tip'); + }); + + test('returns same tip on rerender of same request', () => { + const service = createService(); + const now = Date.now(); + + // First call gets a tip + const tip1 = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip1); + + // Same request ID gets the same tip on rerender + const tip2 = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.ok(tip2); + assert.strictEqual(tip1.id, tip2.id, 'Should return same tip for stable rerender'); + assert.strictEqual(tip1.content.value, tip2.content.value); + }); + + test('returns undefined when Copilot is not enabled', () => { + const service = createService(/* hasCopilot */ false); + const now = Date.now(); + + const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.strictEqual(tip, undefined, 'Should not return a tip when Copilot is not enabled'); + }); + + test('returns undefined when tips setting is disabled', () => { + const service = createService(/* hasCopilot */ true, /* tipsEnabled */ false); + const now = Date.now(); + + const tip = service.getNextTip('request-1', now + 1000, contextKeyService); + assert.strictEqual(tip, undefined, 'Should not return a tip when tips setting is disabled'); + }); + + test('old requests do not consume the session tip allowance', () => { + const service = createService(); + const now = Date.now(); + + // Old request should not consume the tip allowance + const oldTip = service.getNextTip('old-request', now - 10000, contextKeyService); + assert.strictEqual(oldTip, undefined); + + // New request should still be able to get a tip + const newTip = service.getNextTip('new-request', now + 1000, contextKeyService); + assert.ok(newTip, 'New request should get a tip after old request was skipped'); + }); + + test('multiple old requests do not affect new request tip', () => { + const service = createService(); + const now = Date.now(); + + // Simulate multiple restored requests being rendered + service.getNextTip('old-1', now - 30000, contextKeyService); + service.getNextTip('old-2', now - 20000, contextKeyService); + service.getNextTip('old-3', now - 10000, contextKeyService); + + // New request should still get a tip + const tip = service.getNextTip('new-request', now + 1000, contextKeyService); + assert.ok(tip, 'New request should get a tip after multiple old requests'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts deleted file mode 100644 index 8dca180719e..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ /dev/null @@ -1,1760 +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 * as assert from 'assert'; -import { Barrier } from '../../../../../base/common/async.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { CancellationError, isCancellationError } from '../../../../../base/common/errors.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; -import { TestAccessibilityService } from '../../../../../platform/accessibility/test/common/testAccessibilityService.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { ConfigurationTarget, IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; -import { ContextKeyEqualsExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js'; -import { IChatModel } from '../../common/chatModel.js'; -import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService.js'; -import { ChatConfiguration } from '../../common/constants.js'; -import { GithubCopilotToolReference, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet, VSCodeToolReference } from '../../common/languageModelToolsService.js'; -import { MockChatService } from '../common/mockChatService.js'; -import { ChatToolInvocation } from '../../common/chatProgressTypes/chatToolInvocation.js'; -import { LocalChatSessionUri } from '../../common/chatUri.js'; -import { ILanguageModelToolsConfirmationService } from '../../common/languageModelToolsConfirmationService.js'; -import { MockLanguageModelToolsConfirmationService } from '../common/mockLanguageModelToolsConfirmationService.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; - -// --- Test helpers to reduce repetition and improve readability --- - -class TestAccessibilitySignalService implements Partial { - public signalPlayedCalls: { signal: AccessibilitySignal; options?: any }[] = []; - - async playSignal(signal: AccessibilitySignal, options?: any): Promise { - this.signalPlayedCalls.push({ signal, options }); - } - - reset() { - this.signalPlayedCalls = []; - } -} - -class TestTelemetryService implements Partial { - public events: Array<{ eventName: string; data: any }> = []; - - publicLog2, T extends Record>(eventName: string, data?: E): void { - this.events.push({ eventName, data }); - } - - reset() { - this.events = []; - } -} - -function registerToolForTest(service: LanguageModelToolsService, store: any, id: string, impl: IToolImpl, data?: Partial) { - const toolData: IToolData = { - id, - modelDescription: data?.modelDescription ?? 'Test Tool', - displayName: data?.displayName ?? 'Test Tool', - source: ToolDataSource.Internal, - ...data, - }; - store.add(service.registerTool(toolData, impl)); - return { - id, - makeDto: (parameters: any, context?: { sessionId: string }, callId: string = '1'): IToolInvocation => ({ - callId, - toolId: id, - tokenBudget: 100, - parameters, - context, - }), - }; -} - -function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel { - const requestId = options?.requestId ?? 'requestId'; - const capture = options?.capture; - const fakeModel = { - sessionId, - sessionResource: LocalChatSessionUri.forSession(sessionId), - getRequests: () => [{ id: requestId, modelId: 'test-model' }], - acceptResponseProgress: (_req: any, progress: any) => { if (capture) { capture.invocation = progress; } }, - } as IChatModel; - chatService.addSession(fakeModel); - return fakeModel; -} - -async function waitForPublishedInvocation(capture: { invocation?: any }, tries = 5): Promise { - for (let i = 0; i < tries && !capture.invocation; i++) { - await Promise.resolve(); - } - return capture.invocation; -} - -suite('LanguageModelToolsService', () => { - const store = ensureNoDisposablesAreLeakedInTestSuite(); - - let contextKeyService: IContextKeyService; - let service: LanguageModelToolsService; - let chatService: MockChatService; - let configurationService: TestConfigurationService; - - setup(() => { - configurationService = new TestConfigurationService(); - configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(configurationService)), - configurationService: () => configurationService - }, store); - contextKeyService = instaService.get(IContextKeyService); - chatService = new MockChatService(); - instaService.stub(IChatService, chatService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - service = store.add(instaService.createInstance(LanguageModelToolsService)); - }); - - function setupToolsForTest(service: LanguageModelToolsService, store: any) { - - // Create a variety of tools and tool sets for testing - // Some with toolReferenceName, some without, some from extensions, mcp and user defined - - const tool1: IToolData = { - id: 'tool1', - toolReferenceName: 'tool1RefName', - modelDescription: 'Test Tool 1', - displayName: 'Tool1 Display Name', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(tool1)); - - const tool2: IToolData = { - id: 'tool2', - modelDescription: 'Test Tool 2', - displayName: 'Tool2 Display Name', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(tool2)); - - /** Extension Tool 1 */ - - const extTool1: IToolData = { - id: 'extTool1', - toolReferenceName: 'extTool1RefName', - modelDescription: 'Test Extension Tool 1', - displayName: 'ExtTool1 Display Name', - source: { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('my.extension') }, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(extTool1)); - - /** Internal Tool Set with internalToolSetTool1 */ - - const internalToolSetTool1: IToolData = { - id: 'internalToolSetTool1', - toolReferenceName: 'internalToolSetTool1RefName', - modelDescription: 'Test Internal Tool Set 1', - displayName: 'InternalToolSet1 Display Name', - source: ToolDataSource.Internal, - }; - store.add(service.registerToolData(internalToolSetTool1)); - - const internalToolSet = store.add(service.createToolSet( - ToolDataSource.Internal, - 'internalToolSet', - 'internalToolSetRefName', - { description: 'Test Set' } - )); - store.add(internalToolSet.addTool(internalToolSetTool1)); - - /** User Tool Set with tool1 */ - - const userToolSet = store.add(service.createToolSet( - { type: 'user', label: 'User', file: URI.file('/test/userToolSet.json') }, - 'userToolSet', - 'userToolSetRefName', - { description: 'Test Set' } - )); - store.add(userToolSet.addTool(tool2)); - - /** MCP tool in a MCP tool set */ - - const mcpDataSource: ToolDataSource = { type: 'mcp', label: 'My MCP Server', serverLabel: 'MCP Server', instructions: undefined, collectionId: 'testMCPCollection', definitionId: 'testMCPDefId' }; - const mcpTool1: IToolData = { - id: 'mcpTool1', - toolReferenceName: 'mcpTool1RefName', - modelDescription: 'Test MCP Tool 1', - displayName: 'McpTool1 Display Name', - source: mcpDataSource, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(mcpTool1)); - - const mcpToolSet = store.add(service.createToolSet( - mcpDataSource, - 'mcpToolSet', - 'mcpToolSetRefName', - { description: 'MCP Test ToolSet' } - )); - store.add(mcpToolSet.addTool(mcpTool1)); - } - - - test('registerToolData', () => { - const toolData: IToolData = { - id: 'testTool', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const disposable = service.registerToolData(toolData); - assert.strictEqual(service.getTool('testTool')?.id, 'testTool'); - disposable.dispose(); - assert.strictEqual(service.getTool('testTool'), undefined); - }); - - test('registerToolImplementation', () => { - const toolData: IToolData = { - id: 'testTool', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolData)); - - const toolImpl: IToolImpl = { - invoke: async () => ({ content: [{ kind: 'text', value: 'result' }] }), - }; - - store.add(service.registerToolImplementation('testTool', toolImpl)); - assert.strictEqual(service.getTool('testTool')?.id, 'testTool'); - }); - - test('getTools', () => { - contextKeyService.createKey('testKey', true); - const toolData1: IToolData = { - id: 'testTool1', - modelDescription: 'Test Tool 1', - when: ContextKeyEqualsExpr.create('testKey', false), - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const toolData2: IToolData = { - id: 'testTool2', - modelDescription: 'Test Tool 2', - when: ContextKeyEqualsExpr.create('testKey', true), - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const toolData3: IToolData = { - id: 'testTool3', - modelDescription: 'Test Tool 3', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolData1)); - store.add(service.registerToolData(toolData2)); - store.add(service.registerToolData(toolData3)); - - const tools = Array.from(service.getTools()); - assert.strictEqual(tools.length, 2); - assert.strictEqual(tools[0].id, 'testTool2'); - assert.strictEqual(tools[1].id, 'testTool3'); - }); - - test('getToolByName', () => { - contextKeyService.createKey('testKey', true); - const toolData1: IToolData = { - id: 'testTool1', - toolReferenceName: 'testTool1', - modelDescription: 'Test Tool 1', - when: ContextKeyEqualsExpr.create('testKey', false), - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const toolData2: IToolData = { - id: 'testTool2', - toolReferenceName: 'testTool2', - modelDescription: 'Test Tool 2', - when: ContextKeyEqualsExpr.create('testKey', true), - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const toolData3: IToolData = { - id: 'testTool3', - toolReferenceName: 'testTool3', - modelDescription: 'Test Tool 3', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolData1)); - store.add(service.registerToolData(toolData2)); - store.add(service.registerToolData(toolData3)); - - assert.strictEqual(service.getToolByName('testTool1'), undefined); - assert.strictEqual(service.getToolByName('testTool1', true)?.id, 'testTool1'); - assert.strictEqual(service.getToolByName('testTool2')?.id, 'testTool2'); - assert.strictEqual(service.getToolByName('testTool3')?.id, 'testTool3'); - }); - - test('invokeTool', async () => { - const toolData: IToolData = { - id: 'testTool', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolData)); - - const toolImpl: IToolImpl = { - invoke: async (invocation) => { - assert.strictEqual(invocation.callId, '1'); - assert.strictEqual(invocation.toolId, 'testTool'); - assert.deepStrictEqual(invocation.parameters, { a: 1 }); - return { content: [{ kind: 'text', value: 'result' }] }; - } - }; - - store.add(service.registerToolImplementation('testTool', toolImpl)); - - const dto: IToolInvocation = { - callId: '1', - toolId: 'testTool', - tokenBudget: 100, - parameters: { - a: 1 - }, - context: undefined, - }; - - const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); - assert.strictEqual(result.content[0].value, 'result'); - }); - - test('invocation parameters are overridden by input toolSpecificData', async () => { - const rawInput = { b: 2 }; - const tool = registerToolForTest(service, store, 'testToolInputOverride', { - prepareToolInvocation: async () => ({ - toolSpecificData: { kind: 'input', rawInput } satisfies IChatToolInputInvocationData, - confirmationMessages: { - title: 'a', - message: 'b', - } - }), - invoke: async (invocation) => { - // The service should replace parameters with rawInput and strip toolSpecificData - assert.deepStrictEqual(invocation.parameters, rawInput); - assert.strictEqual(invocation.toolSpecificData, undefined); - return { content: [{ kind: 'text', value: 'ok' }] }; - }, - }); - - const sessionId = 'sessionId'; - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture }); - const dto = tool.makeDto({ a: 1 }, { sessionId }); - - const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None); - const published = await waitForPublishedInvocation(capture); - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await invokeP; - assert.strictEqual(result.content[0].value, 'ok'); - }); - - test('chat invocation injects input toolSpecificData for confirmation when alwaysDisplayInputOutput', async () => { - const toolData: IToolData = { - id: 'testToolDisplayIO', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - alwaysDisplayInputOutput: true, - }; - - const tool = registerToolForTest(service, store, 'testToolDisplayIO', { - prepareToolInvocation: async () => ({ - confirmationMessages: { title: 'Confirm', message: 'Proceed?' } - }), - invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }), - }, toolData); - - const sessionId = 'sessionId-io'; - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture }); - - const dto = tool.makeDto({ a: 1 }, { sessionId }); - - const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None); - const published = await waitForPublishedInvocation(capture); - assert.ok(published, 'expected ChatToolInvocation to be published'); - assert.strictEqual(published.toolId, tool.id); - // The service should have injected input toolSpecificData with the raw parameters - assert.strictEqual(published.toolSpecificData?.kind, 'input'); - assert.deepStrictEqual(published.toolSpecificData?.rawInput, dto.parameters); - - // Confirm to let invoke proceed - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await invokeP; - assert.strictEqual(result.content[0].value, 'done'); - }); - - test('chat invocation waits for user confirmation before invoking', async () => { - const toolData: IToolData = { - id: 'testToolConfirm', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - let invoked = false; - const tool = registerToolForTest(service, store, toolData.id, { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm', message: 'Go?' } }), - invoke: async () => { - invoked = true; - return { content: [{ kind: 'text', value: 'ran' }] }; - }, - }, toolData); - - const sessionId = 'sessionId-confirm'; - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId, { requestId: 'requestId-confirm', capture }); - - const dto = tool.makeDto({ x: 1 }, { sessionId }); - - const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); - const published = await waitForPublishedInvocation(capture); - assert.ok(published, 'expected ChatToolInvocation to be published'); - assert.strictEqual(invoked, false, 'invoke should not run before confirmation'); - - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await promise; - assert.strictEqual(invoked, true, 'invoke should have run after confirmation'); - assert.strictEqual(result.content[0].value, 'ran'); - }); - - test('cancel tool call', async () => { - const toolBarrier = new Barrier(); - const tool = registerToolForTest(service, store, 'testTool', { - invoke: async (invocation, countTokens, progress, cancelToken) => { - assert.strictEqual(invocation.callId, '1'); - assert.strictEqual(invocation.toolId, 'testTool'); - assert.deepStrictEqual(invocation.parameters, { a: 1 }); - await toolBarrier.wait(); - if (cancelToken.isCancellationRequested) { - throw new CancellationError(); - } else { - throw new Error('Tool call should be cancelled'); - } - } - }); - - const sessionId = 'sessionId'; - const requestId = 'requestId'; - const dto = tool.makeDto({ a: 1 }, { sessionId }); - stubGetSession(chatService, sessionId, { requestId }); - const toolPromise = service.invokeTool(dto, async () => 0, CancellationToken.None); - service.cancelToolCallsForRequest(requestId); - toolBarrier.open(); - await assert.rejects(toolPromise, err => { - return isCancellationError(err); - }, 'Expected tool call to be cancelled'); - }); - - test('toQualifiedToolNames', () => { - setupToolsForTest(service, store); - - const tool1 = service.getToolByQualifiedName('tool1RefName'); - const extTool1 = service.getToolByQualifiedName('my.extension/extTool1RefName'); - const mcpToolSet = service.getToolByQualifiedName('mcpToolSetRefName/*'); - const mcpTool1 = service.getToolByQualifiedName('mcpToolSetRefName/mcpTool1RefName'); - const internalToolSet = service.getToolByQualifiedName('internalToolSetRefName'); - const internalTool = service.getToolByQualifiedName('internalToolSetRefName/internalToolSetTool1RefName'); - const userToolSet = service.getToolSet('userToolSet'); - const unknownTool = { id: 'unregisteredTool', toolReferenceName: 'unregisteredToolRefName', modelDescription: 'Unregistered Tool', displayName: 'Unregistered Tool', source: ToolDataSource.Internal, canBeReferencedInPrompt: true } satisfies IToolData; - const unknownToolSet = service.createToolSet(ToolDataSource.Internal, 'unknownToolSet', 'unknownToolSetRefName', { description: 'Unknown Test Set' }); - unknownToolSet.dispose(); // unregister the set - assert.ok(tool1); - assert.ok(extTool1); - assert.ok(mcpTool1); - assert.ok(mcpToolSet); - assert.ok(internalToolSet); - assert.ok(internalTool); - assert.ok(userToolSet); - - // Test with some enabled tool - { - // creating a map by hand is a no-go, we just do it for this test - const map = new Map([[tool1, true], [extTool1, true], [mcpToolSet, true], [mcpTool1, true]]); - const qualifiedNames = service.toQualifiedToolNames(map); - const expectedQualifiedNames = ['tool1RefName', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*']; - assert.deepStrictEqual(qualifiedNames.sort(), expectedQualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - // Test with user data - { - // creating a map by hand is a no-go, we just do it for this test - const map = new Map([[tool1, true], [userToolSet, true], [internalToolSet, false], [internalTool, true]]); - const qualifiedNames = service.toQualifiedToolNames(map); - const expectedQualifiedNames = ['tool1RefName', 'internalToolSetRefName/internalToolSetTool1RefName']; - assert.deepStrictEqual(qualifiedNames.sort(), expectedQualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - // Test with unknown tool and tool set - { - // creating a map by hand is a no-go, we just do it for this test - const map = new Map([[unknownTool, true], [unknownToolSet, true], [internalToolSet, true], [internalTool, true]]); - const qualifiedNames = service.toQualifiedToolNames(map); - const expectedQualifiedNames = ['internalToolSetRefName']; - assert.deepStrictEqual(qualifiedNames.sort(), expectedQualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - }); - - test('toToolAndToolSetEnablementMap', () => { - setupToolsForTest(service, store); - - const allQualifiedNames = [ - 'tool1RefName', - 'Tool2 Display Name', - 'my.extension/extTool1RefName', - 'mcpToolSetRefName/*', - 'mcpToolSetRefName/mcpTool1RefName', - 'internalToolSetRefName', - 'internalToolSetRefName/internalToolSetTool1RefName', - ]; - const numOfTools = allQualifiedNames.length + 1; // +1 for userToolSet which has no qualified name but is a tool set - - const tool1 = service.getToolByQualifiedName('tool1RefName'); - const tool2 = service.getToolByQualifiedName('Tool2 Display Name'); - const extTool1 = service.getToolByQualifiedName('my.extension/extTool1RefName'); - const mcpToolSet = service.getToolByQualifiedName('mcpToolSetRefName/*'); - const mcpTool1 = service.getToolByQualifiedName('mcpToolSetRefName/mcpTool1RefName'); - const internalToolSet = service.getToolByQualifiedName('internalToolSetRefName'); - const internalTool = service.getToolByQualifiedName('internalToolSetRefName/internalToolSetTool1RefName'); - const userToolSet = service.getToolSet('userToolSet'); - assert.ok(tool1); - assert.ok(tool2); - assert.ok(extTool1); - assert.ok(mcpTool1); - assert.ok(mcpToolSet); - assert.ok(internalToolSet); - assert.ok(internalTool); - assert.ok(userToolSet); - // Test with enabled tool - { - const qualifiedNames = ['tool1RefName']; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 1, 'Expected 1 tool to be enabled'); - assert.strictEqual(result1.get(tool1), true, 'tool1 should be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - assert.deepStrictEqual(qualifiedNames1.sort(), qualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - - } - // Test with multiple enabled tools - { - const qualifiedNames = ['my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName/internalToolSetTool1RefName']; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 4, 'Expected 4 tools to be enabled'); - assert.strictEqual(result1.get(extTool1), true, 'extTool1 should be enabled'); - assert.strictEqual(result1.get(mcpToolSet), true, 'mcpToolSet should be enabled'); - assert.strictEqual(result1.get(mcpTool1), true, 'mcpTool1 should be enabled because the set is enabled'); - assert.strictEqual(result1.get(internalTool), true, 'internalTool should be enabled because the set is enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - assert.deepStrictEqual(qualifiedNames1.sort(), qualifiedNames.sort(), 'toQualifiedToolNames should return the expected names'); - } - // Test with all enabled tools, redundant names - { - const result1 = service.toToolAndToolSetEnablementMap(allQualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 8, 'Expected 8 tools to be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - const expectedQualifiedNames = ['tool1RefName', 'Tool2 Display Name', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName']; - assert.deepStrictEqual(qualifiedNames1.sort(), expectedQualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - // Test with no enabled tools - { - const qualifiedNames: string[] = []; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 0, 'Expected 0 tools to be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - assert.deepStrictEqual(qualifiedNames1.sort(), qualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - // Test with unknown tool - { - const qualifiedNames: string[] = ['unknownToolRefName']; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 0, 'Expected 0 tools to be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - assert.deepStrictEqual(qualifiedNames1.sort(), [], 'toQualifiedToolNames should return no enabled names'); - } - // Test with legacy tool names - { - const qualifiedNames: string[] = ['extTool1RefName', 'mcpToolSetRefName', 'internalToolSetTool1RefName']; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 4, 'Expected 4 tools to be enabled'); - assert.strictEqual(result1.get(extTool1), true, 'extTool1 should be enabled'); - assert.strictEqual(result1.get(mcpToolSet), true, 'mcpToolSet should be enabled'); - assert.strictEqual(result1.get(mcpTool1), true, 'mcpTool1 should be enabled because the set is enabled'); - assert.strictEqual(result1.get(internalTool), true, 'internalTool should be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - const expectedQualifiedNames: string[] = ['my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName/internalToolSetTool1RefName']; - assert.deepStrictEqual(qualifiedNames1.sort(), expectedQualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - } - // Test with tool in user tool set - { - const qualifiedNames = ['Tool2 Display Name']; - const result1 = service.toToolAndToolSetEnablementMap(qualifiedNames, undefined); - assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); - assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 2, 'Expected 1 tool and user tool set to be enabled'); - assert.strictEqual(result1.get(tool2), true, 'tool2 should be enabled'); - assert.strictEqual(result1.get(userToolSet), true, 'userToolSet should be enabled'); - - const qualifiedNames1 = service.toQualifiedToolNames(result1); - assert.deepStrictEqual(qualifiedNames1.sort(), qualifiedNames.sort(), 'toQualifiedToolNames should return the original enabled names'); - - } - }); - - 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, undefined); - - 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', - toolReferenceName: 'refTool1', - modelDescription: 'Test Tool 1', - displayName: 'Test Tool 1', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - - const toolData2: IToolData = { - id: 'tool2', - modelDescription: 'Test Tool 2', - displayName: 'Test Tool 2', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - - store.add(service.registerToolData(toolData1)); - store.add(service.registerToolData(toolData2)); - - // Create a tool set - const toolSet = store.add(service.createToolSet( - ToolDataSource.Internal, - 'testToolSet', - 'refToolSet', - { description: 'Test Tool Set' } - )); - - // Add tools to the tool set - const toolSetTool1: IToolData = { - id: 'toolSetTool1', - modelDescription: 'Tool Set Tool 1', - displayName: 'Tool Set Tool 1', - source: ToolDataSource.Internal, - }; - - const toolSetTool2: IToolData = { - id: 'toolSetTool2', - modelDescription: 'Tool Set Tool 2', - displayName: 'Tool Set Tool 2', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolSetTool1)); - store.add(service.registerToolData(toolSetTool2)); - store.add(toolSet.addTool(toolSetTool1)); - store.add(toolSet.addTool(toolSetTool2)); - - // Test enabling the tool set - const enabledNames = [toolSet, toolData1].map(t => service.getQualifiedToolName(t)); - const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined); - - 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('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)); - - const unregisteredToolData: IToolData = { - id: 'toolX', - toolReferenceName: 'refToolX', - modelDescription: 'Test Tool X', - displayName: 'Test Tool X', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - - // Test with non-existent tool names - const enabledNames = [toolData, unregisteredToolData].map(t => service.getQualifiedToolName(t)); - const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined); - - assert.strictEqual(result.get(toolData), true, 'existing tool should be enabled'); - // Non-existent tools should not appear in the result map - 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('toToolAndToolSetEnablementMap map Github to VSCode tools', () => { - const runCommandsToolData: IToolData = { - id: VSCodeToolReference.runCommands, - toolReferenceName: VSCodeToolReference.runCommands, - modelDescription: 'runCommands', - displayName: 'runCommands', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - - store.add(service.registerToolData(runCommandsToolData)); - const runSubagentToolData: IToolData = { - id: VSCodeToolReference.runSubagent, - toolReferenceName: VSCodeToolReference.runSubagent, - modelDescription: 'runSubagent', - displayName: 'runSubagent', - source: ToolDataSource.Internal, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(runSubagentToolData)); - - const githubMcpDataSource: ToolDataSource = { type: 'mcp', label: 'Github', serverLabel: 'Github MCP Server', instructions: undefined, collectionId: 'githubMCPCollection', definitionId: 'githubMCPDefId' }; - const githubMcpTool1: IToolData = { - id: 'create_branch', - toolReferenceName: 'create_branch', - modelDescription: 'Test Github MCP Tool 1', - displayName: 'Create Branch', - source: githubMcpDataSource, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(githubMcpTool1)); - - const githubMcpToolSet = store.add(service.createToolSet( - githubMcpDataSource, - 'githubMcpToolSet', - 'github/github-mcp-server', - { description: 'Github MCP Test ToolSet' } - )); - store.add(githubMcpToolSet.addTool(githubMcpTool1)); - - const playwrightMcpDataSource: ToolDataSource = { type: 'mcp', label: 'playwright', serverLabel: 'playwright MCP Server', instructions: undefined, collectionId: 'playwrightMCPCollection', definitionId: 'playwrightMCPDefId' }; - const playwrightMcpTool1: IToolData = { - id: 'browser_click', - toolReferenceName: 'browser_click', - modelDescription: 'Test playwright MCP Tool 1', - displayName: 'Create Branch', - source: playwrightMcpDataSource, - canBeReferencedInPrompt: true, - }; - store.add(service.registerToolData(playwrightMcpTool1)); - - const playwrightMcpToolSet = store.add(service.createToolSet( - playwrightMcpDataSource, - 'playwrightMcpToolSet', - 'microsoft/playwright-mcp', - { description: 'playwright MCP Test ToolSet' } - )); - store.add(playwrightMcpToolSet.addTool(playwrightMcpTool1)); - { - const toolNames = [GithubCopilotToolReference.customAgent, GithubCopilotToolReference.shell]; - const result = service.toToolAndToolSetEnablementMap(toolNames, undefined); - - assert.strictEqual(result.get(runSubagentToolData), true, 'runSubagentToolData should be enabled'); - assert.strictEqual(result.get(runCommandsToolData), true, 'runCommandsToolData should be enabled'); - const qualifiedNames = service.toQualifiedToolNames(result).sort(); - assert.deepStrictEqual(qualifiedNames, [VSCodeToolReference.runCommands, VSCodeToolReference.runSubagent], 'toQualifiedToolNames should return the VS Code tool names'); - } - { - const toolNames = ['github/*', 'playwright/*']; - const result = service.toToolAndToolSetEnablementMap(toolNames, undefined); - - assert.strictEqual(result.get(githubMcpToolSet), true, 'githubMcpToolSet should be enabled'); - assert.strictEqual(result.get(playwrightMcpToolSet), true, 'playwrightMcpToolSet should be enabled'); - const qualifiedNames = service.toQualifiedToolNames(result).sort(); - assert.deepStrictEqual(qualifiedNames, ['github/github-mcp-server/*', 'microsoft/playwright-mcp/*'], 'toQualifiedToolNames should return the VS Code tool names'); - } - - { - // map the qualified tool names for github and playwright MCP tools - const toolNames = ['github/create_branch', 'playwright/browser_click']; - const result = service.toToolAndToolSetEnablementMap(toolNames, undefined); - - assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); - assert.strictEqual(result.get(playwrightMcpTool1), true, 'playwrightMcpTool1 should be enabled'); - const qualifiedNames = service.toQualifiedToolNames(result).sort(); - assert.deepStrictEqual(qualifiedNames, ['github/github-mcp-server/create_branch', 'microsoft/playwright-mcp/browser_click'], 'toQualifiedToolNames should return the VS Code tool names'); - } - - { - // test that already qualified names are not altered - const toolNames = ['github/github-mcp-server/create_branch', 'microsoft/playwright-mcp/browser_click']; - const result = service.toToolAndToolSetEnablementMap(toolNames, undefined); - - assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); - assert.strictEqual(result.get(playwrightMcpTool1), true, 'playwrightMcpTool1 should be enabled'); - const qualifiedNames = service.toQualifiedToolNames(result).sort(); - assert.deepStrictEqual(qualifiedNames, ['github/github-mcp-server/create_branch', 'microsoft/playwright-mcp/browser_click'], 'toQualifiedToolNames should return the VS Code tool names'); - } - - }); - - test('accessibility signal for tool confirmation', async () => { - // Create a test configuration service with proper settings - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.global.autoApprove', false); - testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); - - // Create a test accessibility service that simulates screen reader being enabled - const testAccessibilityService = new class extends TestAccessibilityService { - override isScreenReaderOptimized(): boolean { return true; } - }(); - - // Create a test accessibility signal service that tracks calls - const testAccessibilitySignalService = new TestAccessibilitySignalService(); - - // Create a new service instance with the test services - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(IAccessibilityService, testAccessibilityService); - instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - const toolData: IToolData = { - id: 'testAccessibilityTool', - modelDescription: 'Test Accessibility Tool', - displayName: 'Test Accessibility Tool', - source: ToolDataSource.Internal, - }; - - const tool = registerToolForTest(testService, store, toolData.id, { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Accessibility Test', message: 'Testing accessibility signal' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }), - }, toolData); - - const sessionId = 'sessionId-accessibility'; - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId, { requestId: 'requestId-accessibility', capture }); - - const dto = tool.makeDto({ param: 'value' }, { sessionId }); - - const promise = testService.invokeTool(dto, async () => 0, CancellationToken.None); - const published = await waitForPublishedInvocation(capture); - - assert.ok(published, 'expected ChatToolInvocation to be published'); - assert.ok(published.confirmationMessages, 'should have confirmation messages'); - - // The accessibility signal should have been played - assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'accessibility signal should have been played once'); - const signalCall = testAccessibilitySignalService.signalPlayedCalls[0]; - assert.strictEqual(signalCall.signal, AccessibilitySignal.chatUserActionRequired, 'correct signal should be played'); - assert.ok(signalCall.options?.customAlertMessage.includes('Accessibility Test'), 'alert message should include tool title'); - assert.ok(signalCall.options?.customAlertMessage.includes('Chat confirmation required'), 'alert message should include confirmation text'); - - // Complete the invocation - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const result = await promise; - assert.strictEqual(result.content[0].value, 'executed'); - }); - - test('accessibility signal respects autoApprove configuration', async () => { - // Create a test configuration service with auto-approve enabled - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true); - testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); - - // Create a test accessibility service that simulates screen reader being enabled - const testAccessibilityService = new class extends TestAccessibilityService { - override isScreenReaderOptimized(): boolean { return true; } - }(); - - // Create a test accessibility signal service that tracks calls - const testAccessibilitySignalService = new TestAccessibilitySignalService(); - - // Create a new service instance with the test services - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(IAccessibilityService, testAccessibilityService); - instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - const toolData: IToolData = { - id: 'testAutoApproveTool', - modelDescription: 'Test Auto Approve Tool', - displayName: 'Test Auto Approve Tool', - source: ToolDataSource.Internal, - }; - - const tool = registerToolForTest(testService, store, toolData.id, { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Approve Test', message: 'Testing auto approve' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] }), - }, toolData); - - const sessionId = 'sessionId-auto-approve'; - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId, { requestId: 'requestId-auto-approve', capture }); - - const dto = tool.makeDto({ config: 'test' }, { sessionId }); - - // When auto-approve is enabled, tool should complete without user intervention - const result = await testService.invokeTool(dto, async () => 0, CancellationToken.None); - - // Verify the tool completed and no accessibility signal was played - assert.strictEqual(result.content[0].value, 'auto approved'); - assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'accessibility signal should not be played when auto-approve is enabled'); - }); - - test('shouldAutoConfirm with basic configuration', async () => { - // Test basic shouldAutoConfirm behavior with simple configuration - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true); // Global enabled - - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - // Register a tool that should be auto-approved - const autoTool = registerToolForTest(testService, store, 'autoTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] }) - }); - - const sessionId = 'test-basic-config'; - stubGetSession(chatService, sessionId, { requestId: 'req1' }); - - // Tool should be auto-approved (global config = true) - const result = await testService.invokeTool( - autoTool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(result.content[0].value, 'auto approved'); - }); - - test('shouldAutoConfirm with per-tool configuration object', async () => { - // Test per-tool configuration: { toolId: true/false } - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration('chat.tools.global.autoApprove', { - 'approvedTool': true, - 'deniedTool': false - }); - - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - // Tool explicitly approved - const approvedTool = registerToolForTest(testService, store, 'approvedTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'approved' }] }) - }); - - const sessionId = 'test-per-tool'; - stubGetSession(chatService, sessionId, { requestId: 'req1' }); - - // Approved tool should auto-approve - const approvedResult = await testService.invokeTool( - approvedTool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(approvedResult.content[0].value, 'approved'); - - // Test that non-specified tools require confirmation (default behavior) - const unspecifiedTool = registerToolForTest(testService, store, 'unspecifiedTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should require confirmation' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'unspecified' }] }) - }); - - const capture: { invocation?: any } = {}; - stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture }); - const unspecifiedPromise = testService.invokeTool( - unspecifiedTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), - async () => 0, - CancellationToken.None - ); - const published = await waitForPublishedInvocation(capture); - assert.ok(published?.confirmationMessages, 'unspecified tool should require confirmation'); - - IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); - const unspecifiedResult = await unspecifiedPromise; - assert.strictEqual(unspecifiedResult.content[0].value, 'unspecified'); - }); - - test('tool content formatting with alwaysDisplayInputOutput', async () => { - // Test ensureToolDetails, formatToolInput, and toolResultToIO - const toolData: IToolData = { - id: 'formatTool', - modelDescription: 'Format Test Tool', - displayName: 'Format Test Tool', - source: ToolDataSource.Internal, - alwaysDisplayInputOutput: true - }; - - const tool = registerToolForTest(service, store, toolData.id, { - prepareToolInvocation: async () => ({}), - invoke: async (invocation) => ({ - content: [ - { kind: 'text', value: 'Text result' }, - { kind: 'data', value: { data: VSBuffer.fromByteArray([1, 2, 3]), mimeType: 'application/octet-stream' } } - ] - }) - }, toolData); - - const input = { a: 1, b: 'test', c: [1, 2, 3] }; - const result = await service.invokeTool( - tool.makeDto(input), - async () => 0, - CancellationToken.None - ); - - // Should have tool result details because alwaysDisplayInputOutput = true - assert.ok(result.toolResultDetails, 'should have toolResultDetails'); - const details = result.toolResultDetails; - assert.ok(isToolResultInputOutputDetails(details)); - - // Test formatToolInput - should be formatted JSON - const expectedInputJson = JSON.stringify(input, undefined, 2); - assert.strictEqual(details.input, expectedInputJson, 'input should be formatted JSON'); - - // Test toolResultToIO - should convert different content types - assert.strictEqual(details.output.length, 2, 'should have 2 output items'); - - // Text content - const textOutput = details.output[0]; - assert.strictEqual(textOutput.type, 'embed'); - assert.strictEqual(textOutput.isText, true); - assert.strictEqual(textOutput.value, 'Text result'); - - // Data content (base64 encoded) - const dataOutput = details.output[1]; - assert.strictEqual(dataOutput.type, 'embed'); - assert.strictEqual(dataOutput.mimeType, 'application/octet-stream'); - assert.strictEqual(dataOutput.value, 'AQID'); // base64 of [1,2,3] - }); - - test('tool error handling and telemetry', async () => { - const testTelemetryService = new TestTelemetryService(); - - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(configurationService)), - configurationService: () => configurationService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(ITelemetryService, testTelemetryService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - // Test successful invocation telemetry - const successTool = registerToolForTest(testService, store, 'successTool', { - prepareToolInvocation: async () => ({}), - invoke: async () => ({ content: [{ kind: 'text', value: 'success' }] }) - }); - - const sessionId = 'telemetry-test'; - stubGetSession(chatService, sessionId, { requestId: 'req1' }); - - await testService.invokeTool( - successTool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - - // Check success telemetry - const successEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked'); - assert.strictEqual(successEvents.length, 1, 'should have success telemetry event'); - assert.strictEqual(successEvents[0].data.result, 'success'); - assert.strictEqual(successEvents[0].data.toolId, 'successTool'); - assert.strictEqual(successEvents[0].data.chatSessionId, sessionId); - - testTelemetryService.reset(); - - // Test error telemetry - const errorTool = registerToolForTest(testService, store, 'errorTool', { - prepareToolInvocation: async () => ({}), - invoke: async () => { throw new Error('Tool error'); } - }); - - stubGetSession(chatService, sessionId + '2', { requestId: 'req2' }); - - try { - await testService.invokeTool( - errorTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), - async () => 0, - CancellationToken.None - ); - assert.fail('Should have thrown'); - } catch (err) { - // Expected - } - - // Check error telemetry - const errorEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked'); - assert.strictEqual(errorEvents.length, 1, 'should have error telemetry event'); - assert.strictEqual(errorEvents[0].data.result, 'error'); - assert.strictEqual(errorEvents[0].data.toolId, 'errorTool'); - }); - - test('call tracking and cleanup', async () => { - // Test that cancelToolCallsForRequest method exists and can be called - // (The detailed cancellation behavior is already tested in "cancel tool call" test) - const sessionId = 'tracking-session'; - const requestId = 'tracking-request'; - stubGetSession(chatService, sessionId, { requestId }); - - // Just verify the method exists and doesn't throw - assert.doesNotThrow(() => { - service.cancelToolCallsForRequest(requestId); - }, 'cancelToolCallsForRequest should not throw'); - - // Verify calling with non-existent request ID doesn't throw - assert.doesNotThrow(() => { - service.cancelToolCallsForRequest('non-existent-request'); - }, 'cancelToolCallsForRequest with non-existent ID should not throw'); - }); - - test('accessibility signal with different settings combinations', async () => { - const testAccessibilitySignalService = new TestAccessibilitySignalService(); - - // Test case 1: Sound enabled, announcement disabled, screen reader off - const testConfigService1 = new TestConfigurationService(); - testConfigService1.setUserConfiguration('chat.tools.global.autoApprove', false); - testConfigService1.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'on', announcement: 'off' }); - - const testAccessibilityService1 = new class extends TestAccessibilityService { - override isScreenReaderOptimized(): boolean { return false; } - }(); - - const instaService1 = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService1)), - configurationService: () => testConfigService1 - }, store); - instaService1.stub(IChatService, chatService); - instaService1.stub(IAccessibilityService, testAccessibilityService1); - instaService1.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); - instaService1.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService1 = store.add(instaService1.createInstance(LanguageModelToolsService)); - - const tool1 = registerToolForTest(testService1, store, 'soundOnlyTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Sound Test', message: 'Testing sound only' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) - }); - - const sessionId1 = 'sound-test'; - const capture1: { invocation?: any } = {}; - stubGetSession(chatService, sessionId1, { requestId: 'req1', capture: capture1 }); - - const promise1 = testService1.invokeTool(tool1.makeDto({ test: 1 }, { sessionId: sessionId1 }), async () => 0, CancellationToken.None); - const published1 = await waitForPublishedInvocation(capture1); - - // Signal should be played (sound=on, no screen reader requirement) - assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'sound should be played when sound=on'); - const call1 = testAccessibilitySignalService.signalPlayedCalls[0]; - assert.strictEqual(call1.options?.modality, undefined, 'should use default modality for sound'); - - IChatToolInvocation.confirmWith(published1, { type: ToolConfirmKind.UserAction }); - await promise1; - - testAccessibilitySignalService.reset(); - - // Test case 2: Sound auto, announcement auto, screen reader on - const testConfigService2 = new TestConfigurationService(); - testConfigService2.setUserConfiguration('chat.tools.global.autoApprove', false); - testConfigService2.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); - - const testAccessibilityService2 = new class extends TestAccessibilityService { - override isScreenReaderOptimized(): boolean { return true; } - }(); - - const instaService2 = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService2)), - configurationService: () => testConfigService2 - }, store); - instaService2.stub(IChatService, chatService); - instaService2.stub(IAccessibilityService, testAccessibilityService2); - instaService2.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); - instaService2.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService2 = store.add(instaService2.createInstance(LanguageModelToolsService)); - - const tool2 = registerToolForTest(testService2, store, 'autoScreenReaderTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Test', message: 'Testing auto with screen reader' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) - }); - - const sessionId2 = 'auto-sr-test'; - const capture2: { invocation?: any } = {}; - stubGetSession(chatService, sessionId2, { requestId: 'req2', capture: capture2 }); - - const promise2 = testService2.invokeTool(tool2.makeDto({ test: 2 }, { sessionId: sessionId2 }), async () => 0, CancellationToken.None); - const published2 = await waitForPublishedInvocation(capture2); - - // Signal should be played (both sound and announcement enabled for screen reader) - assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'signal should be played with screen reader optimization'); - const call2 = testAccessibilitySignalService.signalPlayedCalls[0]; - assert.ok(call2.options?.customAlertMessage, 'should have custom alert message'); - assert.strictEqual(call2.options?.userGesture, true, 'should mark as user gesture'); - - IChatToolInvocation.confirmWith(published2, { type: ToolConfirmKind.UserAction }); - await promise2; - - testAccessibilitySignalService.reset(); - - // Test case 3: Sound off, announcement off - no signal - const testConfigService3 = new TestConfigurationService(); - testConfigService3.setUserConfiguration('chat.tools.global.autoApprove', false); - testConfigService3.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'off', announcement: 'off' }); - - const testAccessibilityService3 = new class extends TestAccessibilityService { - override isScreenReaderOptimized(): boolean { return true; } - }(); - - const instaService3 = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService3)), - configurationService: () => testConfigService3 - }, store); - instaService3.stub(IChatService, chatService); - instaService3.stub(IAccessibilityService, testAccessibilityService3); - instaService3.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); - instaService3.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService3 = store.add(instaService3.createInstance(LanguageModelToolsService)); - - const tool3 = registerToolForTest(testService3, store, 'offTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Off Test', message: 'Testing off settings' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) - }); - - const sessionId3 = 'off-test'; - const capture3: { invocation?: any } = {}; - stubGetSession(chatService, sessionId3, { requestId: 'req3', capture: capture3 }); - - const promise3 = testService3.invokeTool(tool3.makeDto({ test: 3 }, { sessionId: sessionId3 }), async () => 0, CancellationToken.None); - const published3 = await waitForPublishedInvocation(capture3); - - // No signal should be played - assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'no signal should be played when both sound and announcement are off'); - - IChatToolInvocation.confirmWith(published3, { type: ToolConfirmKind.UserAction }); - await promise3; - }); - - test('createToolSet and getToolSet', () => { - const toolSet = store.add(service.createToolSet( - ToolDataSource.Internal, - 'testToolSetId', - 'testToolSetName', - { icon: undefined, description: 'Test tool set' } - )); - - // Should be able to retrieve by ID - const retrieved = service.getToolSet('testToolSetId'); - assert.ok(retrieved); - assert.strictEqual(retrieved.id, 'testToolSetId'); - assert.strictEqual(retrieved.referenceName, 'testToolSetName'); - - // Should not find non-existent tool set - assert.strictEqual(service.getToolSet('nonExistentId'), undefined); - - // Dispose should remove it - toolSet.dispose(); - assert.strictEqual(service.getToolSet('testToolSetId'), undefined); - }); - - test('getToolSetByName', () => { - store.add(service.createToolSet( - ToolDataSource.Internal, - 'toolSet1', - 'refName1' - )); - - store.add(service.createToolSet( - ToolDataSource.Internal, - 'toolSet2', - 'refName2' - )); - - // Should find by reference name - assert.strictEqual(service.getToolSetByName('refName1')?.id, 'toolSet1'); - assert.strictEqual(service.getToolSetByName('refName2')?.id, 'toolSet2'); - - // Should not find non-existent name - assert.strictEqual(service.getToolSetByName('nonExistentName'), undefined); - }); - - test('getTools with includeDisabled parameter', () => { - // Test the includeDisabled parameter behavior with context keys - contextKeyService.createKey('testKey', false); - const disabledTool: IToolData = { - id: 'disabledTool', - modelDescription: 'Disabled Tool', - displayName: 'Disabled Tool', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('testKey', true), // Will be disabled since testKey is false - }; - - const enabledTool: IToolData = { - id: 'enabledTool', - modelDescription: 'Enabled Tool', - displayName: 'Enabled Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(disabledTool)); - store.add(service.registerToolData(enabledTool)); - - const enabledTools = Array.from(service.getTools()); - assert.strictEqual(enabledTools.length, 1, 'Should only return enabled tools'); - assert.strictEqual(enabledTools[0].id, 'enabledTool'); - - const allTools = Array.from(service.getTools(true)); - assert.strictEqual(allTools.length, 2, 'includeDisabled should return all tools'); - }); - - test('tool registration duplicate error', () => { - const toolData: IToolData = { - id: 'duplicateTool', - modelDescription: 'Duplicate Tool', - displayName: 'Duplicate Tool', - source: ToolDataSource.Internal, - }; - - // First registration should succeed - store.add(service.registerToolData(toolData)); - - // Second registration should throw - assert.throws(() => { - service.registerToolData(toolData); - }, /Tool "duplicateTool" is already registered/); - }); - - test('tool implementation registration without data throws', () => { - const toolImpl: IToolImpl = { - invoke: async () => ({ content: [] }), - }; - - // Should throw when registering implementation for non-existent tool - assert.throws(() => { - service.registerToolImplementation('nonExistentTool', toolImpl); - }, /Tool "nonExistentTool" was not contributed/); - }); - - test('tool implementation duplicate registration throws', () => { - const toolData: IToolData = { - id: 'testTool', - modelDescription: 'Test Tool', - displayName: 'Test Tool', - source: ToolDataSource.Internal, - }; - - const toolImpl1: IToolImpl = { - invoke: async () => ({ content: [] }), - }; - - const toolImpl2: IToolImpl = { - invoke: async () => ({ content: [] }), - }; - - store.add(service.registerToolData(toolData)); - store.add(service.registerToolImplementation('testTool', toolImpl1)); - - // Second implementation should throw - assert.throws(() => { - service.registerToolImplementation('testTool', toolImpl2); - }, /Tool "testTool" already has an implementation/); - }); - - test('invokeTool with unknown tool throws', async () => { - const dto: IToolInvocation = { - callId: '1', - toolId: 'unknownTool', - tokenBudget: 100, - parameters: {}, - context: undefined, - }; - - await assert.rejects( - service.invokeTool(dto, async () => 0, CancellationToken.None), - /Tool unknownTool was not contributed/ - ); - }); - - test('invokeTool without implementation activates extension and throws if still not found', async () => { - const toolData: IToolData = { - id: 'extensionActivationTool', - modelDescription: 'Extension Tool', - displayName: 'Extension Tool', - source: ToolDataSource.Internal, - }; - - store.add(service.registerToolData(toolData)); - - const dto: IToolInvocation = { - callId: '1', - toolId: 'extensionActivationTool', - tokenBudget: 100, - parameters: {}, - context: undefined, - }; - - // Should throw after attempting extension activation - await assert.rejects( - service.invokeTool(dto, async () => 0, CancellationToken.None), - /Tool extensionActivationTool does not have an implementation registered/ - ); - }); - - test('invokeTool without context (non-chat scenario)', async () => { - const tool = registerToolForTest(service, store, 'nonChatTool', { - invoke: async (invocation) => { - assert.strictEqual(invocation.context, undefined); - return { content: [{ kind: 'text', value: 'non-chat result' }] }; - } - }); - - const dto = tool.makeDto({ test: 1 }); // No context - - const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); - assert.strictEqual(result.content[0].value, 'non-chat result'); - }); - - test('invokeTool with unknown chat session throws', async () => { - const tool = registerToolForTest(service, store, 'unknownSessionTool', { - invoke: async () => ({ content: [{ kind: 'text', value: 'should not reach' }] }) - }); - - const dto = tool.makeDto({ test: 1 }, { sessionId: 'unknownSession' }); - - // Test that it throws, regardless of exact error message - let threwError = false; - try { - await service.invokeTool(dto, async () => 0, CancellationToken.None); - } catch (err) { - threwError = true; - // Verify it's one of the expected error types - assert.ok( - err instanceof Error && ( - err.message.includes('Tool called for unknown chat session') || - err.message.includes('getRequests is not a function') - ), - `Unexpected error: ${err.message}` - ); - } - assert.strictEqual(threwError, true, 'Should have thrown an error'); - }); - - test('tool error with alwaysDisplayInputOutput includes details', async () => { - const toolData: IToolData = { - id: 'errorToolWithIO', - modelDescription: 'Error Tool With IO', - displayName: 'Error Tool With IO', - source: ToolDataSource.Internal, - alwaysDisplayInputOutput: true - }; - - const tool = registerToolForTest(service, store, toolData.id, { - invoke: async () => { throw new Error('Tool execution failed'); } - }, toolData); - - const input = { param: 'testValue' }; - - try { - await service.invokeTool( - tool.makeDto(input), - async () => 0, - CancellationToken.None - ); - assert.fail('Should have thrown'); - } catch (err: any) { - // The error should bubble up, but we need to check if toolResultError is set - // This tests the internal error handling path - assert.strictEqual(err.message, 'Tool execution failed'); - } - }); - - test('context key changes trigger tool updates', async () => { - let changeEventFired = false; - const disposable = service.onDidChangeTools(() => { - changeEventFired = true; - }); - store.add(disposable); - - // Create a tool with a context key dependency - contextKeyService.createKey('dynamicKey', false); - const toolData: IToolData = { - id: 'contextTool', - modelDescription: 'Context Tool', - displayName: 'Context Tool', - source: ToolDataSource.Internal, - when: ContextKeyEqualsExpr.create('dynamicKey', true), - }; - - store.add(service.registerToolData(toolData)); - - // Change the context key value - contextKeyService.createKey('dynamicKey', true); - - // Wait a bit for the scheduler - await new Promise(resolve => setTimeout(resolve, 800)); - - assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when context keys change'); - }); - - test('configuration changes trigger tool updates', async () => { - return runWithFakedTimers({}, async () => { - let changeEventFired = false; - const disposable = service.onDidChangeTools(() => { - changeEventFired = true; - }); - store.add(disposable); - - // Change the correct configuration key - configurationService.setUserConfiguration('chat.extensionTools.enabled', false); - // Fire the configuration change event manually - configurationService.onDidChangeConfigurationEmitter.fire({ - affectsConfiguration: () => true, - affectedKeys: new Set(['chat.extensionTools.enabled']), - change: null!, - source: ConfigurationTarget.USER - } satisfies IConfigurationChangeEvent); - - // Wait a bit for the scheduler - await new Promise(resolve => setTimeout(resolve, 800)); - - assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when configuration changes'); - }); - }); - - test('toToolAndToolSetEnablementMap with MCP toolset enables contained tools', () => { - // Create MCP toolset - const mcpToolSet = store.add(service.createToolSet( - { type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' }, - 'mcpSet', - 'mcpSetRef' - )); - - const mcpTool: IToolData = { - id: 'mcpTool', - modelDescription: 'MCP Tool', - displayName: 'MCP Tool', - source: { type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' }, - canBeReferencedInPrompt: true, - toolReferenceName: 'mcpToolRef' - }; - - store.add(service.registerToolData(mcpTool)); - store.add(mcpToolSet.addTool(mcpTool)); - - // Enable the MCP toolset - { - const enabledNames = [mcpToolSet].map(t => service.getQualifiedToolName(t)); - const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined); - - 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, undefined); - - 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'); - } - - }); - - test('shouldAutoConfirm with workspace-specific tool configuration', async () => { - const testConfigService = new TestConfigurationService(); - // Configure per-tool settings at different scopes - testConfigService.setUserConfiguration('chat.tools.global.autoApprove', { 'workspaceTool': true }); - - const instaService = workbenchInstantiationService({ - contextKeyService: () => store.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, store); - instaService.stub(IChatService, chatService); - instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); - const testService = store.add(instaService.createInstance(LanguageModelToolsService)); - - const workspaceTool = registerToolForTest(testService, store, 'workspaceTool', { - prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Workspace tool' } }), - invoke: async () => ({ content: [{ kind: 'text', value: 'workspace result' }] }) - }, { runsInWorkspace: true }); - - const sessionId = 'workspace-test'; - stubGetSession(chatService, sessionId, { requestId: 'req1' }); - - // Should auto-approve based on user configuration - const result = await testService.invokeTool( - workspaceTool.makeDto({ test: 1 }, { sessionId }), - async () => 0, - CancellationToken.None - ); - assert.strictEqual(result.content[0].value, 'workspace result'); - }); - - test('getQualifiedToolNames', () => { - setupToolsForTest(service, store); - - const qualifiedNames = Array.from(service.getQualifiedToolNames()).sort(); - - const expectedNames = [ - 'tool1RefName', - 'Tool2 Display Name', - 'my.extension/extTool1RefName', - 'mcpToolSetRefName/*', - 'mcpToolSetRefName/mcpTool1RefName', - 'internalToolSetRefName', - 'internalToolSetRefName/internalToolSetTool1RefName', - ].sort(); - - assert.deepStrictEqual(qualifiedNames, expectedNames, 'getQualifiedToolNames should return correct qualified names'); - }); - - test('getDeprecatedQualifiedToolNames', () => { - setupToolsForTest(service, store); - - const deprecatedNames = service.getDeprecatedQualifiedToolNames(); - - // Tools in internal tool sets should have their qualified names with toolset prefix, tools sets keep their name - assert.strictEqual(deprecatedNames.get('internalToolSetTool1RefName'), 'internalToolSetRefName/internalToolSetTool1RefName'); - assert.strictEqual(deprecatedNames.get('internalToolSetRefName'), undefined); - - // For extension tools, the qualified name includes the extension ID - assert.strictEqual(deprecatedNames.get('extTool1RefName'), 'my.extension/extTool1RefName'); - - // For MCP tool sets, the qualified name includes the /* suffix - assert.strictEqual(deprecatedNames.get('mcpToolSetRefName'), 'mcpToolSetRefName/*'); - assert.strictEqual(deprecatedNames.get('mcpTool1RefName'), 'mcpToolSetRefName/mcpTool1RefName'); - - // Internal tool sets and user tools sets and tools without namespace changes should not appear - assert.strictEqual(deprecatedNames.get('Tool2 Display Name'), undefined); - assert.strictEqual(deprecatedNames.get('tool1RefName'), undefined); - assert.strictEqual(deprecatedNames.get('userToolSetRefName'), undefined); - }); - - test('getToolByQualifiedName', () => { - setupToolsForTest(service, store); - - // Test finding tools by their qualified names - const tool1 = service.getToolByQualifiedName('tool1RefName'); - assert.ok(tool1); - assert.strictEqual(tool1.id, 'tool1'); - - const tool2 = service.getToolByQualifiedName('Tool2 Display Name'); - assert.ok(tool2); - assert.strictEqual(tool2.id, 'tool2'); - - const extTool = service.getToolByQualifiedName('my.extension/extTool1RefName'); - assert.ok(extTool); - assert.strictEqual(extTool.id, 'extTool1'); - - const mcpTool = service.getToolByQualifiedName('mcpToolSetRefName/mcpTool1RefName'); - assert.ok(mcpTool); - assert.strictEqual(mcpTool.id, 'mcpTool1'); - - - const mcpToolSet = service.getToolByQualifiedName('mcpToolSetRefName/*'); - assert.ok(mcpToolSet); - assert.strictEqual(mcpToolSet.id, 'mcpToolSet'); - - const internalToolSet = service.getToolByQualifiedName('internalToolSetRefName/internalToolSetTool1RefName'); - assert.ok(internalToolSet); - assert.strictEqual(internalToolSet.id, 'internalToolSetTool1'); - - // Test finding tools within tool sets - const toolInSet = service.getToolByQualifiedName('internalToolSetRefName'); - assert.ok(toolInSet); - assert.strictEqual(toolInSet!.id, 'internalToolSet'); - - }); - - - -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelsConfiguration.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelsConfiguration.test.ts new file mode 100644 index 00000000000..0d1a184a8e4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelsConfiguration.test.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { createTextModel } from '../../../../../editor/test/common/testTextModel.js'; +import { parseLanguageModelsProviderGroups } from '../../browser/languageModelsConfigurationService.js'; + +suite('LanguageModelsConfiguration', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + test('parseLanguageModelsConfiguration - empty', () => { + const model = testDisposables.add(createTextModel('[]')); + const result = parseLanguageModelsProviderGroups(model); + assert.deepStrictEqual(result, []); + }); + + test('parseLanguageModelsConfiguration - simple', () => { + const content = JSON.stringify([{ + vendor: 'vendor', + name: 'group', + configurations: [] + }], null, '\t'); + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'group'); + assert.strictEqual(result[0].vendor, 'vendor'); + assert.ok(result[0].range); + }); + + test('parseLanguageModelsConfiguration - with configuration range', () => { + const content = `[ + { + "vendor": "vendor", + "name": "group", + "configurations": [ + { + "configuration": { + "foo": "bar" + } + } + ] + } +]`; + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + const configurations = result[0].configurations as { configuration: Record }[]; + const config = configurations[0].configuration; + assert.deepStrictEqual(config, { foo: 'bar' }); + }); + + test('parseLanguageModelsConfiguration - multiple vendors and groups', () => { + const content = `[ + { "vendor": "vendor1", "name": "g1", "configurations": [] }, + { "vendor": "vendor1", "name": "g2", "configurations": [] }, + { "vendor": "vendor2", "name": "g3", "configurations": [] } +]`; + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].name, 'g1'); + assert.strictEqual(result[0].vendor, 'vendor1'); + assert.strictEqual(result[1].name, 'g2'); + assert.strictEqual(result[1].vendor, 'vendor1'); + assert.strictEqual(result[2].name, 'g3'); + assert.strictEqual(result[2].vendor, 'vendor2'); + }); + + test('parseLanguageModelsConfiguration - complex configuration values', () => { + const content = `[ + { + "vendor": "vendor", + "name": "group", + "configurations": [ + { + "configuration": { + "str": "value", + "num": 123, + "bool": true, + "null": null, + "arr": [1, 2], + "obj": { "nested": "val" } + } + } + ] + } +]`; + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + const configurations = result[0]?.configurations as { configuration: Record }[]; + const config = configurations[0].configuration; + assert.strictEqual(config.str, 'value'); + assert.strictEqual(config.num, 123); + assert.strictEqual(config.bool, true); + assert.strictEqual(config.null, null); + assert.deepStrictEqual(config.arr, [1, 2]); + assert.deepStrictEqual(config.obj, { nested: 'val' }); + }); + + test('parseLanguageModelsConfiguration - with comments', () => { + const content = `[ + // This is a comment + /* Block comment */ + { + "vendor": "vendor", + "name": "group", + "configurations": [] + } +]`; + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'group'); + assert.strictEqual(result[0].vendor, 'vendor'); + }); + + test('parseLanguageModelsConfiguration - ranges', () => { + const content = `[ + { + "vendor": "vendor", + "name": "g1", + "configurations": [] + }, + { + "vendor": "vendor", + "name": "g2", + "configurations": [] + } +]`; + const model = testDisposables.add(createTextModel(content)); + const result = parseLanguageModelsProviderGroups(model); + + const g1 = result[0]; + const g2 = result[1]; + + assert.ok(g1.range); + assert.ok(g2.range); + assert.strictEqual(g1.range.startLineNumber, 2); + assert.strictEqual(g1.range.endLineNumber, 6); + assert.strictEqual(g2.range.startLineNumber, 7); + assert.strictEqual(g2.range.endLineNumber, 11); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts b/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.ts deleted file mode 100644 index 518749d6e57..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/mockChatWidget.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 { Event } from '../../../../../base/common/event.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { IChatWidget, IChatWidgetService } from '../../browser/chat.js'; -import { ChatAgentLocation } from '../../common/constants.js'; - -export class MockChatWidgetService implements IChatWidgetService { - readonly onDidAddWidget: Event = Event.None; - - readonly _serviceBrand: undefined; - - /** - * Returns the most recently focused widget if any. - */ - readonly lastFocusedWidget: IChatWidget | undefined; - - getWidgetByInputUri(uri: URI): IChatWidget | undefined { - return undefined; - } - - getWidgetBySessionId(sessionId: string): IChatWidget | undefined { - return undefined; - } - - getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined { - return undefined; - } - - getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray { - return []; - } - - getAllWidgets(): ReadonlyArray { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts new file mode 100644 index 00000000000..4cc72c0b22b --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptBodyAutocompletion.test.ts @@ -0,0 +1,185 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; +import { CompletionContext, CompletionTriggerKind } from '../../../../../../../editor/common/languages.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { PromptBodyAutocompletion } from '../../../../common/promptSyntax/languageProviders/promptBodyAutocompletion.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { FileService } from '../../../../../../../platform/files/common/fileService.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { InMemoryFileSystemProvider } from '../../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { ILogService, NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; + +suite('PromptBodyAutocompletion', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + let completionProvider: PromptBodyAutocompletion; + + setup(async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + instaService.stub(ILogService, new NullLogService()); + const fileService = disposables.add(instaService.createInstance(FileService)); + instaService.stub(IFileService, fileService); + + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider('test', fileSystemProvider)); + + // Create some test files and directories + await fileService.createFolder(URI.parse('test:///workspace')); + await fileService.createFolder(URI.parse('test:///workspace/src')); + await fileService.createFolder(URI.parse('test:///workspace/docs')); + await fileService.writeFile(URI.parse('test:///workspace/src/index.ts'), VSBuffer.fromString('export function hello() {}')); + await fileService.writeFile(URI.parse('test:///workspace/README.md'), VSBuffer.fromString('# Project')); + await fileService.writeFile(URI.parse('test:///workspace/package.json'), VSBuffer.fromString('{}')); + + 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; + disposables.add(toolService.registerToolData(testTool1)); + + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool2)); + + const myExtSource = { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('My.extension') } satisfies ToolDataSource; + const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: myExtSource, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool3)); + + const prExtSource = { type: 'extension', label: 'GitHub Pull Request Extension', extensionId: new ExtensionIdentifier('github.vscode-pull-request-github') } satisfies ToolDataSource; + const prExtTool1 = { id: 'suggestFix', canBeReferencedInPrompt: true, toolReferenceName: 'suggest-fix', modelDescription: 'tool4', displayName: 'Test Tool 4', source: prExtSource, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(prExtTool1)); + + instaService.set(ILanguageModelToolsService, toolService); + + completionProvider = instaService.createInstance(PromptBodyAutocompletion); + }); + + async function getCompletions(content: string, line: number, column: number, promptType: PromptsType) { + const languageId = getLanguageIdForPromptsType(promptType); + const model = disposables.add(createTextModel(content, languageId, undefined, URI.parse('test://workspace/test' + getPromptFileExtension(promptType)))); + const position = new Position(line, column); + const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; + const result = await completionProvider.provideCompletionItems(model, position, context, CancellationToken.None); + if (!result || !result.suggestions) { + return []; + } + const lineContent = model.getLineContent(position.lineNumber); + return result.suggestions.map(s => { + assert(s.range instanceof Range); + return { + label: s.label, + result: lineContent.substring(0, s.range.startColumn - 1) + s.insertText + lineContent.substring(s.range.endColumn - 1) + }; + }); + } + + suite('prompt body completions', () => { + test('default suggestions', async () => { + const content = [ + '---', + 'description: "Test"', + '---', + '', + 'Use # to reference a file or tool.', + 'One more #to' + ].join('\n'); + + { + const actual = (await getCompletions(content, 5, 6, PromptsType.prompt)); + assert.deepEqual(actual, [ + { + label: 'file:', + result: 'Use #file: to reference a file or tool.' + }, + { + label: 'tool:', + result: 'Use #tool: to reference a file or tool.' + } + ]); + } + { + const actual = (await getCompletions(content, 6, 13, PromptsType.prompt)); + assert.deepEqual(actual, [ + { + label: 'file:', + result: 'One more #file:' + }, + { + label: 'tool:', + result: 'One more #tool:' + } + ]); + } + }); + + test('tool suggestions', async () => { + const content = [ + '---', + 'description: "Test"', + '---', + '', + 'Use #tool: to reference a tool.', + ].join('\n'); + { + const actual = (await getCompletions(content, 5, 11, PromptsType.prompt)); + assert.deepEqual(actual, [ + { + label: 'vscode', + result: 'Use #tool:vscode to reference a tool.' + }, + { + label: 'execute', + result: 'Use #tool:execute to reference a tool.' + }, + { + label: 'read', + result: 'Use #tool:read to reference a tool.' + }, + { + label: 'agent', + result: 'Use #tool:agent to reference a tool.' + }, + { + label: 'tool1', + result: 'Use #tool:tool1 to reference a tool.' + }, + { + label: 'tool2', + result: 'Use #tool:tool2 to reference a tool.' + }, + { + label: 'my.extension/tool3', + result: 'Use #tool:my.extension/tool3 to reference a tool.' + }, + { + label: 'github.vscode-pull-request-github/suggest-fix', + result: 'Use #tool:github.vscode-pull-request-github/suggest-fix to reference a tool.' + } + ]); + } + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts new file mode 100644 index 00000000000..2a1a20b946b --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptCodeActions.test.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; +import { CodeActionContext, CodeActionTriggerType, IWorkspaceTextEdit, IWorkspaceFileEdit } from '../../../../../../../editor/common/languages.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IMarkerService } from '../../../../../../../platform/markers/common/markers.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { IPromptsService } from '../../../../common/promptSyntax/service/promptsService.js'; +import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; +import { PromptCodeActionProvider } from '../../../../common/promptSyntax/languageProviders/promptCodeActions.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { CodeActionKind } from '../../../../../../../editor/contrib/codeAction/common/types.js'; + +suite('PromptCodeActionProvider', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + let codeActionProvider: PromptCodeActionProvider; + let fileService: IFileService; + + setup(async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + + const toolService = disposables.add(instaService.createInstance(LanguageModelToolsService)); + + // Register test tools including deprecated ones + const testTool1 = { id: 'testTool1', displayName: 'tool1', canBeReferencedInPrompt: true, modelDescription: 'Test Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool1)); + + const deprecatedTool = { id: 'oldTool', displayName: 'oldTool', canBeReferencedInPrompt: true, modelDescription: 'Deprecated Tool', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(deprecatedTool)); + + // Mock deprecated tool names + toolService.getDeprecatedFullReferenceNames = () => { + const map = new Map>(); + map.set('oldTool', new Set(['newTool1', 'newTool2'])); + map.set('singleDeprecated', new Set(['singleReplacement'])); + return map; + }; + + instaService.set(ILanguageModelToolsService, toolService); + instaService.stub(IMarkerService, { read: () => [] }); + + fileService = { + canMove: async (source: URI, target: URI) => { + // Mock file service that allows moves for testing + return true; + } + } as IFileService; + instaService.set(IFileService, fileService); + + const parser = new PromptFileParser(); + instaService.stub(IPromptsService, { + getParsedPromptFile(model: ITextModel) { + return parser.parse(model.uri, model.getValue()); + }, + getAgentFileURIFromModeFile(uri: URI) { + // Mock conversion from .chatmode.md to .agent.md + if (uri.path.endsWith('.chatmode.md')) { + return uri.with({ path: uri.path.replace('.chatmode.md', '.agent.md') }); + } + return undefined; + } + }); + + codeActionProvider = instaService.createInstance(PromptCodeActionProvider); + }); + + async function getCodeActions(content: string, line: number, column: number, promptType: PromptsType, fileExtension?: string): Promise<{ title: string; textEdits?: IWorkspaceTextEdit[]; fileEdits?: IWorkspaceFileEdit[] }[]> { + const languageId = getLanguageIdForPromptsType(promptType); + const uri = URI.parse('test:///test' + (fileExtension ?? getPromptFileExtension(promptType))); + const model = disposables.add(createTextModel(content, languageId, undefined, uri)); + const range = new Range(line, column, line, column); + const context: CodeActionContext = { trigger: CodeActionTriggerType.Invoke }; + + const result = await codeActionProvider.provideCodeActions(model, range, context, CancellationToken.None); + if (!result || result.actions.length === 0) { + return []; + } + + for (const action of result.actions) { + assert.equal(action.kind, CodeActionKind.QuickFix.value); + } + + return result.actions.map(action => ({ + title: action.title, + textEdits: action.edit?.edits?.filter((edit): edit is IWorkspaceTextEdit => 'textEdit' in edit), + fileEdits: action.edit?.edits?.filter((edit): edit is IWorkspaceFileEdit => 'oldResource' in edit) + })); + } + + suite('agent code actions', () => { + test('no code actions for instructions files', async () => { + const content = [ + '---', + 'description: "Test instruction"', + 'applyTo: "**/*.ts"', + '---', + ].join('\n'); + const actions = await getCodeActions(content, 2, 1, PromptsType.instructions); + assert.strictEqual(actions.length, 0); + }); + + test('migrate mode file to agent file', async () => { + const content = [ + '---', + 'name: "Test Mode"', + 'description: "Test mode file"', + '---', + ].join('\n'); + const actions = await getCodeActions(content, 1, 1, PromptsType.agent, '.chatmode.md'); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Migrate to custom agent file`); + }); + + test('update deprecated tool names - single replacement', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['singleDeprecated']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Update to 'singleReplacement'`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits!.length, 1); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `'singleReplacement'`); + }); + + test('update deprecated tool names - multiple replacements', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['oldTool']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Expand to 2 tools`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits!.length, 1); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `'newTool1','newTool2'`); + }); + + test('update all deprecated tool names', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['oldTool', 'singleDeprecated', 'validTool']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 8, PromptsType.agent); // Position at the bracket + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Update all tool names`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits!.length, 2); // Only deprecated tools are updated + }); + + test('handles double quotes in tool names', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ["singleDeprecated"]`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Update to 'singleReplacement'`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `"singleReplacement"`); + }); + + test('handles unquoted tool names', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'tools: [singleDeprecated]', // No quotes + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Update to 'singleReplacement'`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `singleReplacement`); // No quotes preserved + }); + + test('no code actions when range not in tools array', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['singleDeprecated']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 2, 1, PromptsType.agent); // Range in description, not tools + assert.strictEqual(actions.length, 0); + }); + }); + + suite('prompt code actions', () => { + test('rename mode to agent', async () => { + const content = [ + '---', + 'description: "Test"', + 'mode: edit', + '---', + ].join('\n'); + const actions = await getCodeActions(content, 3, 1, PromptsType.prompt); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Rename to 'agent'`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits!.length, 1); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, 'agent'); + }); + + test('update deprecated tool names in prompt', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['singleDeprecated']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 3, 10, PromptsType.prompt); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Update to 'singleReplacement'`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits!.length, 1); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `'singleReplacement'`); + }); + + test('no code actions when range not in mode attribute', async () => { + const content = [ + '---', + 'description: "Test"', + 'mode: edit', + '---', + ].join('\n'); + const actions = await getCodeActions(content, 2, 1, PromptsType.prompt); // Range in description, not mode + assert.strictEqual(actions.length, 0); + }); + + test('both mode and tools code actions available', async () => { + const content = [ + '---', + 'description: "Test"', + 'mode: edit', + `tools: ['singleDeprecated']`, + '---', + ].join('\n'); + // Test mode action + const modeActions = await getCodeActions(content, 3, 1, PromptsType.prompt); + assert.strictEqual(modeActions.length, 1); + assert.strictEqual(modeActions[0].title, `Rename to 'agent'`); + + // Test tools action + const toolActions = await getCodeActions(content, 4, 10, PromptsType.prompt); + assert.strictEqual(toolActions.length, 1); + assert.strictEqual(toolActions[0].title, `Update to 'singleReplacement'`); + }); + }); + + test('returns undefined when no code actions available', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['validTool']`, // No deprecated tools + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 0); + }); + + test('uses comma-space delimiter when separator includes comma', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['oldTool', 'validTool']`, + '---', + ].join('\n'); + const actions = await getCodeActions(content, 4, 10, PromptsType.agent); + assert.strictEqual(actions.length, 1); + assert.strictEqual(actions[0].title, `Expand to 2 tools`); + assert.ok(actions[0].textEdits); + assert.strictEqual(actions[0].textEdits![0].textEdit.text, `'newTool1', 'newTool2'`); + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts new file mode 100644 index 00000000000..cd2fa76b689 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHeaderAutocompletion.test.ts @@ -0,0 +1,367 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; +import { CompletionContext, CompletionTriggerKind } from '../../../../../../../editor/common/languages.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; +import { IChatModeService } from '../../../../common/chatModes.js'; +import { PromptHeaderAutocompletion } from '../../../../common/promptSyntax/languageProviders/promptHeaderAutocompletion.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { Range } from '../../../../../../../editor/common/core/range.js'; + +suite('PromptHeaderAutocompletion', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + let completionProvider: PromptHeaderAutocompletion; + + setup(async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + + 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; + disposables.add(toolService.registerToolData(testTool1)); + + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool2)); + + 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + { id: 'gpt-4', name: 'GPT 4', vendor: 'openai', version: '1.0', family: 'gpt', modelPickerCategory: undefined, extension: new ExtensionIdentifier('a.b'), isUserSelectable: true, maxInputTokens: 8192, maxOutputTokens: 1024, capabilities: { agentMode: false, toolCalling: true }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + ]; + + instaService.stub(ILanguageModelsService, { + getLanguageModelIds() { return testModels.map(m => m.id); }, + lookupLanguageModel(name: string) { + return testModels.find(m => m.id === name); + } + }); + + const customAgent: ICustomAgent = { + name: 'agent1', + description: 'Agent file 1.', + agentInstructions: { + content: '', + toolReferences: [], + metadata: undefined + }, + uri: URI.parse('myFs://.github/agents/agent1.agent.md'), + source: { storage: PromptsStorage.local }, + visibility: { userInvokable: true, agentInvokable: true } + }; + + const parser = new PromptFileParser(); + instaService.stub(IPromptsService, { + getParsedPromptFile(model: ITextModel) { + return parser.parse(model.uri, model.getValue()); + }, + async getCustomAgents(token: CancellationToken) { + return Promise.resolve([customAgent]); + } + }); + + instaService.stub(IChatModeService, { + getModes() { + return { builtin: [], custom: [] }; + } + }); + + completionProvider = instaService.createInstance(PromptHeaderAutocompletion); + }); + + async function getCompletions(content: string, promptType: PromptsType) { + const languageId = getLanguageIdForPromptsType(promptType); + const uri = URI.parse('test:///test' + getPromptFileExtension(promptType)); + const model = disposables.add(createTextModel(content, languageId, undefined, uri)); + // get the completion location from the '|' marker + const lineColumnMarkerRange = model.findNextMatch('|', new Position(1, 1), false, false, '', false)?.range; + assert.ok(lineColumnMarkerRange, 'No completion marker found in test content'); + model.applyEdits([{ range: lineColumnMarkerRange, text: '' }]); + + const position = lineColumnMarkerRange.getStartPosition(); + const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; + const result = await completionProvider.provideCompletionItems(model, position, context, CancellationToken.None); + if (!result || !result.suggestions) { + return []; + } + const lineContent = model.getLineContent(position.lineNumber); + return result.suggestions.map(s => { + assert(s.range instanceof Range); + return { + label: typeof s.label === 'string' ? s.label : s.label.label, + result: lineContent.substring(0, s.range.startColumn - 1) + s.insertText + lineContent.substring(s.range.endColumn - 1) + }; + }); + } + + const sortByLabel = (a: { label: string }, b: { label: string }) => a.label.localeCompare(b.label); + + suite('agent header completions', () => { + test('complete model attribute name', async () => { + const content = [ + '---', + 'description: "Test"', + '|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agents', result: 'agents: ${0:["*"]}' }, + { label: 'argument-hint', result: 'argument-hint: $0' }, + { label: 'disable-model-invocation', result: 'disable-model-invocation: ${0:true}' }, + { label: 'handoffs', result: 'handoffs: $0' }, + { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, + { label: 'name', result: 'name: $0' }, + { label: 'target', result: 'target: ${0:vscode}' }, + { label: 'tools', result: 'tools: ${0:[]}' }, + { label: 'user-invokable', result: 'user-invokable: ${0:true}' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value with partial input', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: MA|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual, [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + ]); + }); + + test('complete model names inside model array', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: `model: ['MAE 4 (olama)']` }, + { label: 'MAE 4.1 (copilot)', result: `model: ['MAE 4.1 (copilot)']` }, + ].sort(sortByLabel)); + }); + + test('complete model names inside model array with existing entries', async () => { + const content = [ + '---', + 'description: "Test"', + `model: ['MAE 4 (olama)', |]`, + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + // GPT 4 is excluded because it has agentMode: false + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: `model: ['MAE 4 (olama)', 'MAE 4 (olama)']` }, + { label: 'MAE 4.1 (copilot)', result: `model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']` }, + ].sort(sortByLabel)); + }); + + test('complete tool names inside tools array', async () => { + const content = [ + '---', + 'description: "Test"', + 'tools: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['agent']` }, + { label: 'execute', result: `tools: ['execute']` }, + { label: 'read', result: `tools: ['read']` }, + { label: 'tool1', result: `tools: ['tool1']` }, + { label: 'tool2', result: `tools: ['tool2']` }, + { label: 'vscode', result: `tools: ['vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete tool names inside tools array with existing entries', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['read', |]`, + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['read', 'agent']` }, + { label: 'execute', result: `tools: ['read', 'execute']` }, + { label: 'read', result: `tools: ['read', 'read']` }, + { label: 'tool1', result: `tools: ['read', 'tool1']` }, + { label: 'tool2', result: `tools: ['read', 'tool2']` }, + { label: 'vscode', result: `tools: ['read', 'vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete tool names inside tools array with existing entries 2', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['read', 'exe|cute']`, + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: `tools: ['read', 'agent']` }, + { label: 'execute', result: `tools: ['read', 'execute']` }, + { label: 'read', result: `tools: ['read', 'read']` }, + { label: 'tool1', result: `tools: ['read', 'tool1']` }, + { label: 'tool2', result: `tools: ['read', 'tool2']` }, + { label: 'vscode', result: `tools: ['read', 'vscode']` }, + ].sort(sortByLabel)); + }); + + test('complete agents inside agents array', async () => { + const content = [ + '---', + 'description: "Test"', + 'agents: [|]', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent1', result: `agents: ['agent1']` }, + ].sort(sortByLabel)); + }); + + test('complete infer attribute value', async () => { + const content = [ + '---', + 'description: "Test"', + 'infer: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'false', result: 'infer: false' }, + { label: 'true', result: 'infer: true' }, + ].sort(sortByLabel)); + }); + + test('complete user-invokable attribute value', async () => { + const content = [ + '---', + 'description: "Test"', + 'user-invokable: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'false', result: 'user-invokable: false' }, + { label: 'true', result: 'user-invokable: true' }, + ].sort(sortByLabel)); + }); + + test('complete disable-model-invocation attribute value', async () => { + const content = [ + '---', + 'description: "Test"', + 'disable-model-invocation: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.agent); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'false', result: 'disable-model-invocation: false' }, + { label: 'true', result: 'disable-model-invocation: true' }, + ].sort(sortByLabel)); + }); + }); + + suite('prompt header completions', () => { + test('complete model attribute name', async () => { + const content = [ + '---', + 'description: "Test"', + '|', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.prompt); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'agent', result: 'agent: $0' }, + { label: 'argument-hint', result: 'argument-hint: $0' }, + { label: 'model', result: 'model: ${0:MAE 4 (olama)}' }, + { label: 'name', result: 'name: $0' }, + { label: 'tools', result: 'tools: ${0:[]}' }, + ].sort(sortByLabel)); + }); + + test('complete model attribute value in prompt', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: |', + '---', + ].join('\n'); + + const actual = await getCompletions(content, PromptsType.prompt); + assert.deepStrictEqual(actual.sort(sortByLabel), [ + { label: 'MAE 4 (olama)', result: 'model: MAE 4 (olama)' }, + { label: 'MAE 4.1 (copilot)', result: 'model: MAE 4.1 (copilot)' }, + { label: 'GPT 4 (openai)', result: 'model: GPT 4 (openai)' }, + ].sort(sortByLabel)); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts new file mode 100644 index 00000000000..4a8d29fc2b7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptHovers.test.ts @@ -0,0 +1,498 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { Position } from '../../../../../../../editor/common/core/position.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { ChatMode, CustomChatMode, IChatModeService } from '../../../../common/chatModes.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; +import { PromptHoverProvider } from '../../../../common/promptSyntax/languageProviders/promptHovers.js'; +import { IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { MockChatModeService } from '../../../common/mockChatModeService.js'; +import { createTextModel } from '../../../../../../../editor/test/common/testTextModel.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { MarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { getLanguageIdForPromptsType, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; + +suite('PromptHoverProvider', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + let hoverProvider: PromptHoverProvider; + + setup(async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + + 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; + disposables.add(toolService.registerToolData(testTool1)); + + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool2)); + + 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata, + ]; + + instaService.stub(ILanguageModelsService, { + getLanguageModelIds() { return testModels.map(m => m.id); }, + lookupLanguageModelByQualifiedName(qualifiedName: string) { + for (const metadata of testModels) { + if (ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, metadata)) { + return metadata; + } + } + return undefined; + } + }); + + const customChatMode = new CustomChatMode({ + uri: URI.parse('myFs://test/test/chatmode.md'), + name: 'BeastMode', + agentInstructions: { content: 'Beast mode instructions', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + visibility: { userInvokable: true, agentInvokable: true } + }); + instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); + + const parser = new PromptFileParser(); + instaService.stub(IPromptsService, { + getParsedPromptFile(model: ITextModel) { + return parser.parse(model.uri, model.getValue()); + } + }); + + hoverProvider = instaService.createInstance(PromptHoverProvider); + }); + + async function getHover(content: string, line: number, column: number, promptType: PromptsType): Promise { + const languageId = getLanguageIdForPromptsType(promptType); + const uri = URI.parse('test:///test' + getPromptFileExtension(promptType)); + const model = disposables.add(createTextModel(content, languageId, undefined, uri)); + const position = new Position(line, column); + const hover = await hoverProvider.provideHover(model, position, CancellationToken.None); + if (!hover || hover.contents.length === 0) { + return undefined; + } + // Return the markdown value from the first content + const firstContent = hover.contents[0]; + if (firstContent instanceof MarkdownString) { + return firstContent.value; + } + return undefined; + } + + suite('agent hovers', () => { + test('hover on target attribute shows description', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.agent); + assert.strictEqual(hover, 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'); + }); + + test('hover on model attribute with github-copilot target shows note', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: github-copilot', + 'model: MAE 4', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + const expected = [ + 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.', + '', + 'Note: This attribute is not used when target is github-copilot.' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on model attribute with vscode target shows model info', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'model: MAE 4 (olama)', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + const expected = [ + 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.', + '', + '- Name: MAE 4', + '- Family: mae', + '- Vendor: olama' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on handoffs attribute with github-copilot target shows note', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: github-copilot', + 'handoffs:', + ' - label: Test', + ' agent: Default', + ' prompt: Test', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + const expected = [ + 'Possible handoff actions when the agent has completed its task.', + '', + 'Note: This attribute is not used when target is github-copilot.' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on handoffs attribute with vscode target shows description', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + 'handoffs:', + ' - label: Test', + ' agent: Default', + ' prompt: Test', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'Possible handoff actions when the agent has completed its task.'); + }); + + test('hover on github-copilot tool shows simple description', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: github-copilot', + `tools: ['execute', 'read']`, + '---', + ].join('\n'); + // Hover on 'shell' tool + const hoverShell = await getHover(content, 4, 10, PromptsType.agent); + assert.strictEqual(hoverShell, 'ToolSet: execute\n\n\nExecute code and applications on your machine'); + + // Hover on 'read' tool + const hoverEdit = await getHover(content, 4, 20, PromptsType.agent); + assert.strictEqual(hoverEdit, 'ToolSet: read\n\n\nRead files in your workspace'); + }); + + test('hover on github-copilot tool with target undefined', async () => { + const content = [ + '---', + 'name: "Test"', + 'description: "Test"', + `tools: ['shell', 'read']`, + '---', + ].join('\n'); + // Hover on 'shell' tool + const hoverShell = await getHover(content, 4, 10, PromptsType.agent); + assert.strictEqual(hoverShell, 'ToolSet: execute\n\n\nExecute code and applications on your machine'); + + // Hover on 'read' tool + const hoverEdit = await getHover(content, 4, 20, PromptsType.agent); + assert.strictEqual(hoverEdit, 'ToolSet: read\n\n\nRead files in your workspace'); + }); + + test('hover on vscode tool shows detailed description', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `tools: ['tool1', 'tool2']`, + '---', + ].join('\n'); + // Hover on 'tool1' + const hover = await getHover(content, 4, 10, PromptsType.agent); + assert.strictEqual(hover, 'Test Tool 1'); + }); + + test('hover on model attribute with vscode target and model array', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']`, + '---', + ].join('\n'); + const hover = await getHover(content, 4, 10, PromptsType.agent); + const expected = [ + 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.', + '', + '- Name: MAE 4', + '- Family: mae', + '- Vendor: olama' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on second model in model array', async () => { + const content = [ + '---', + 'description: "Test"', + 'target: vscode', + `model: ['MAE 4 (olama)', 'MAE 4.1 (copilot)']`, + '---', + ].join('\n'); + const hover = await getHover(content, 4, 30, PromptsType.agent); + const expected = [ + 'Specify the model that runs this custom agent. Can also be a list of models. The first available model will be used.', + '', + '- Name: MAE 4.1', + '- Family: mae', + '- Vendor: copilot' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on description attribute', async () => { + const content = [ + '---', + 'description: "Test agent"', + 'target: vscode', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.agent); + assert.strictEqual(hover, 'The description of the custom agent, what it does and when to use it.'); + }); + + test('hover on argument-hint attribute', async () => { + const content = [ + '---', + 'description: "Test"', + 'argument-hint: "test hint"', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.agent); + assert.strictEqual(hover, 'The argument-hint describes what inputs the custom agent expects or supports.'); + }); + + test('hover on name attribute', async () => { + const content = [ + '---', + 'name: "My Agent"', + 'description: "Test agent"', + 'target: vscode', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.agent); + assert.strictEqual(hover, 'The name of the agent as shown in the UI.'); + }); + + test('hover on infer attribute shows description', async () => { + const content = [ + '---', + 'name: "Test Agent"', + 'description: "Test agent"', + 'infer: true', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'Controls visibility of the agent.\n\nDeprecated: Use `user-invokable` and `disable-model-invocation` instead.'); + }); + + test('hover on agents attribute shows description', async () => { + const content = [ + '---', + 'name: "Test Agent"', + 'description: "Test agent"', + 'agents: ["*"]', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'One or more agents that this agent can use as subagents. Use \'*\' to specify all available agents.'); + }); + + test('hover on user-invokable attribute shows description', async () => { + const content = [ + '---', + 'name: "Test Agent"', + 'description: "Test agent"', + 'user-invokable: true', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'Whether the agent can be selected and invoked by users in the UI.'); + }); + + test('hover on disable-model-invocation attribute shows description', async () => { + const content = [ + '---', + 'name: "Test Agent"', + 'description: "Test agent"', + 'disable-model-invocation: true', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.agent); + assert.strictEqual(hover, 'If true, prevents the agent from being invoked as a subagent.'); + }); + }); + + suite('prompt hovers', () => { + test('hover on model attribute shows model info', async () => { + const content = [ + '---', + 'description: "Test"', + 'model: MAE 4 (olama)', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.prompt); + const expected = [ + 'The model to use in this prompt. Can also be a list of models. The first available model will be used.', + '', + '- Name: MAE 4', + '- Family: mae', + '- Vendor: olama' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on tools attribute shows tool description', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1']`, + '---', + ].join('\n'); + const hover = await getHover(content, 3, 10, PromptsType.prompt); + assert.strictEqual(hover, 'Test Tool 1'); + }); + + test('hover on agent attribute shows agent info', async () => { + const content = [ + '---', + 'description: "Test"', + 'agent: BeastMode', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.prompt); + const expected = [ + 'The agent to use when running this prompt.', + '', + '**Built-in agents:**', + '- `agent`: Describe what to build next', + '- `ask`: Explore and understand your code', + '- `edit`: Edit or refactor selected code', + '', + '**Custom agents:**', + '- `BeastMode`: Custom agent' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on name attribute', async () => { + const content = [ + '---', + 'name: "My Prompt"', + 'description: "Test prompt"', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.prompt); + assert.strictEqual(hover, 'The name of the prompt. This is also the name of the slash command that will run this prompt.'); + }); + }); + + suite('instructions hovers', () => { + test('hover on description attribute', async () => { + const content = [ + '---', + 'description: "Test instruction"', + 'applyTo: "**/*.ts"', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.instructions); + assert.strictEqual(hover, '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.'); + }); + + test('hover on applyTo attribute', async () => { + const content = [ + '---', + 'description: "Test"', + 'applyTo: "**/*.ts"', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.instructions); + const expected = [ + '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.', + 'Example: `**/*.ts`, `**/*.js`, `client/**`' + ].join('\n'); + assert.strictEqual(hover, expected); + }); + + test('hover on name attribute', async () => { + const content = [ + '---', + 'name: "My Instructions"', + 'description: "Test instruction"', + 'applyTo: "**/*.ts"', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.instructions); + assert.strictEqual(hover, 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'); + }); + }); + + suite('skill hovers', () => { + test('hover on name attribute', async () => { + const content = [ + '---', + 'name: "My Skill"', + 'description: "Test skill"', + '---', + ].join('\n'); + const hover = await getHover(content, 2, 1, PromptsType.skill); + assert.strictEqual(hover, 'The name of the skill.'); + }); + + test('hover on description attribute', async () => { + const content = [ + '---', + 'name: "Test Skill"', + 'description: "Test skill description"', + '---', + ].join('\n'); + const hover = await getHover(content, 3, 1, PromptsType.skill); + assert.strictEqual(hover, 'The description of the skill. The description is added to every request and will be used by the agent to decide when to load the skill.'); + }); + + test('hover on file attribute', async () => { + const content = [ + '---', + 'name: "Test Skill"', + 'description: "Test skill"', + 'file: "SKILL.md"', + '---', + ].join('\n'); + const hover = await getHover(content, 4, 1, PromptsType.skill); + assert.strictEqual(hover, undefined); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts new file mode 100644 index 00000000000..45e77e43d2c --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/promptSyntax/languageProviders/promptValidator.test.ts @@ -0,0 +1,1678 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ResourceSet } from '../../../../../../../base/common/map.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { TestInstantiationService } from '../../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILabelService } from '../../../../../../../platform/label/common/label.js'; +import { IMarkerData, MarkerSeverity } from '../../../../../../../platform/markers/common/markers.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { ChatMode, CustomChatMode, IChatModeService } from '../../../../common/chatModes.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../../common/constants.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; +import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../common/languageModels.js'; +import { getPromptFileExtension } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { PromptValidator } from '../../../../common/promptSyntax/languageProviders/promptValidator.js'; +import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { PromptFileParser } from '../../../../common/promptSyntax/promptFileParser.js'; +import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { MockChatModeService } from '../../../common/mockChatModeService.js'; +import { MockPromptsService } from '../../../common/promptSyntax/service/mockPromptsService.js'; + +suite('PromptValidator', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instaService: TestInstantiationService; + + const existingRef1 = URI.parse('myFs://test/reference1.md'); + const existingRef2 = URI.parse('myFs://test/reference2.md'); + + setup(async () => { + + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + instaService = workbenchInstantiationService({ + contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, disposables); + 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; + disposables.add(toolService.registerToolData(testTool1)); + const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool2)); + const shellTool = { id: 'shell', displayName: 'shell', canBeReferencedInPrompt: true, toolReferenceName: 'shell', modelDescription: 'Runs commands in the terminal', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(shellTool)); + + const myExtSource = { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('My.extension') } satisfies ToolDataSource; + const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: myExtSource, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(testTool3)); + + const prExtSource = { type: 'extension', label: 'GitHub Pull Request Extension', extensionId: new ExtensionIdentifier('github.vscode-pull-request-github') } satisfies ToolDataSource; + const prExtTool1 = { id: 'suggestFix', canBeReferencedInPrompt: true, toolReferenceName: 'suggest-fix', modelDescription: 'tool4', displayName: 'Test Tool 4', source: prExtSource, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(prExtTool1)); + + const toolWithLegacy = { id: 'newTool', toolReferenceName: 'newToolRef', displayName: 'New Tool', canBeReferencedInPrompt: true, modelDescription: 'New Tool', source: ToolDataSource.External, inputSchema: {}, legacyToolReferenceFullNames: ['oldToolName', 'deprecatedToolName'] } satisfies IToolData; + disposables.add(toolService.registerToolData(toolWithLegacy)); + + const toolSetWithLegacy = disposables.add(toolService.createToolSet( + ToolDataSource.External, + 'newToolSet', + 'newToolSetRef', + { description: 'New Tool Set', legacyFullNames: ['oldToolSet', 'deprecatedToolSet'] } + )); + const toolInSet = { id: 'toolInSet', toolReferenceName: 'toolInSetRef', displayName: 'Tool In Set', canBeReferencedInPrompt: false, modelDescription: 'Tool In Set', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(toolInSet)); + disposables.add(toolSetWithLegacy.addTool(toolInSet)); + + const anotherToolWithLegacy = { id: 'anotherTool', toolReferenceName: 'anotherToolRef', displayName: 'Another Tool', canBeReferencedInPrompt: true, modelDescription: 'Another Tool', source: ToolDataSource.External, inputSchema: {}, legacyToolReferenceFullNames: ['legacyTool'] } satisfies IToolData; + disposables.add(toolService.registerToolData(anotherToolWithLegacy)); + + const anotherToolSetWithLegacy = disposables.add(toolService.createToolSet( + ToolDataSource.External, + 'anotherToolSet', + 'anotherToolSetRef', + { description: 'Another Tool Set', legacyFullNames: ['legacyToolSet'] } + )); + const anotherToolInSet = { id: 'anotherToolInSet', toolReferenceName: 'anotherToolInSetRef', displayName: 'Another Tool In Set', canBeReferencedInPrompt: false, modelDescription: 'Another Tool In Set', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(anotherToolInSet)); + disposables.add(anotherToolSetWithLegacy.addTool(anotherToolInSet)); + + const conflictToolSet1 = disposables.add(toolService.createToolSet( + ToolDataSource.External, + 'conflictSet1', + 'conflictSet1Ref', + { legacyFullNames: ['sharedLegacyName'] } + )); + const conflictTool1 = { id: 'conflictTool1', toolReferenceName: 'conflictTool1Ref', displayName: 'Conflict Tool 1', canBeReferencedInPrompt: false, modelDescription: 'Conflict Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(conflictTool1)); + disposables.add(conflictToolSet1.addTool(conflictTool1)); + + const conflictToolSet2 = disposables.add(toolService.createToolSet( + ToolDataSource.External, + 'conflictSet2', + 'conflictSet2Ref', + { legacyFullNames: ['sharedLegacyName'] } + )); + const conflictTool2 = { id: 'conflictTool2', toolReferenceName: 'conflictTool2Ref', displayName: 'Conflict Tool 2', canBeReferencedInPrompt: false, modelDescription: 'Conflict Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(toolService.registerToolData(conflictTool2)); + disposables.add(conflictToolSet2.addTool(conflictTool2)); + + 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: 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 }, isDefaultForLocation: { [ChatAgentLocation.Chat]: 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, isDefaultForLocation: { [ChatAgentLocation.Chat]: true } } satisfies ILanguageModelChatMetadata + ]; + + instaService.stub(ILanguageModelsService, { + getLanguageModelIds() { return testModels.map(m => m.id); }, + lookupLanguageModelByQualifiedName(qualifiedName: string) { + for (const metadata of testModels) { + if (ILanguageModelChatMetadata.matchesQualifiedName(qualifiedName, metadata)) { + return metadata; + } + } + return undefined; + } + }); + + const customChatMode = new CustomChatMode({ + uri: URI.parse('myFs://test/test/chatmode.md'), + name: 'BeastMode', + agentInstructions: { content: 'Beast mode instructions', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + visibility: { userInvokable: true, agentInvokable: true } + }); + instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); + + + const existingFiles = new ResourceSet([existingRef1, existingRef2]); + instaService.stub(IFileService, { + exists(uri: URI) { + return Promise.resolve(existingFiles.has(uri)); + } + }); + const promptsService = new MockPromptsService(); + const customMode: ICustomAgent = { + uri: URI.parse('file:///test/custom-mode.md'), + name: 'Plan', + description: 'A test custom mode', + tools: ['tool1', 'tool2'], + agentInstructions: { content: 'Custom mode body', toolReferences: [] }, + source: { storage: PromptsStorage.local }, + visibility: { userInvokable: true, agentInvokable: true } + }; + promptsService.setCustomModes([customMode]); + instaService.stub(IPromptsService, promptsService); + }); + + async function validate(code: string, promptType: PromptsType, uri?: URI): Promise { + if (!uri) { + uri = URI.parse('myFs://test/testFile' + getPromptFileExtension(promptType)); + } + const result = new PromptFileParser().parse(uri, code); + const validator = instaService.createInstance(PromptValidator); + const markers: IMarkerData[] = []; + await validator.validate(result, promptType, m => markers.push(m)); + return markers; + } + suite('agents', () => { + + test('correct agent', async () => { + const content = [ + /* 01 */'---', + /* 02 */`description: "Agent mode test"`, + /* 03 */'model: MAE 4.1', + /* 04 */`tools: ['tool1', 'tool2']`, + /* 05 */'---', + /* 06 */'This is a chat agent 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.agent); + assert.deepStrictEqual(markers, []); + }); + + test('agent 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.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Error, message: `The 'description' attribute should not be empty.` }, + { severity: MarkerSeverity.Warning, 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.agent); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'tools' attribute must be an array.`]); + }); + + test('model as string array - valid', async () => { + const content = [ + '---', + 'description: "Test with model array"', + `model: ['MAE 4 (olama)', 'MAE 4.1']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, []); + }); + + test('model as string array - unknown model', async () => { + const content = [ + '---', + 'description: "Test with model array"', + `model: ['MAE 4 (olama)', 'Unknown Model']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `Unknown model 'Unknown Model'.`); + }); + + test('model as string array - unsuitable model', async () => { + const content = [ + '---', + 'description: "Test with model array"', + `model: ['MAE 4 (olama)', 'MAE 3.5 Turbo']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `Model 'MAE 3.5 Turbo' is not suited for agent mode.`); + }); + + test('model as string array - empty array', async () => { + const content = [ + '---', + 'description: "Test with empty model array"', + `model: []`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'model' array must not be empty.`); + }); + + test('model as string array - non-string item', async () => { + const content = [ + '---', + 'description: "Test with invalid model array"', + `model: ['MAE 4 (olama)', 123]`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'model' array must contain only strings.`); + }); + + test('model as string array - empty string item', async () => { + const content = [ + '---', + 'description: "Test with empty string in model array"', + `model: ['MAE 4 (olama)', '']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `Model names in the array must be non-empty strings.`); + }); + + test('model as invalid type', async () => { + const content = [ + '---', + 'description: "Test with invalid model type"', + `model: 123`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'model' attribute must be a string or an array of strings.`); + }); + + test('each tool must be string', async () => { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1', 2]`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + 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.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'tool3' has been renamed, use 'my.extension/tool3' instead.` }, + ] + ); + }); + + test('legacy tool reference names', async () => { + // Test using legacy tool reference name + { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1', 'oldToolName']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'oldToolName' has been renamed, use 'newToolRef' instead.` }, + ] + ); + } + + // Test using another legacy tool reference name + { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1', 'deprecatedToolName']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'deprecatedToolName' has been renamed, use 'newToolRef' instead.` }, + ] + ); + } + }); + + test('legacy toolset names', async () => { + // Test using legacy toolset name + { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1', 'oldToolSet']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'oldToolSet' has been renamed, use 'newToolSetRef' instead.` }, + ] + ); + } + + // Test using another legacy toolset name + { + const content = [ + '---', + 'description: "Test"', + `tools: ['tool1', 'deprecatedToolSet']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'deprecatedToolSet' has been renamed, use 'newToolSetRef' instead.` }, + ] + ); + } + }); + + test('multiple legacy names in same tools list', async () => { + // Test multiple legacy names together + const content = [ + '---', + 'description: "Test"', + `tools: ['legacyTool', 'legacyToolSet', 'tool3']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Info, message: `Tool or toolset 'legacyTool' has been renamed, use 'anotherToolRef' instead.` }, + { severity: MarkerSeverity.Info, message: `Tool or toolset 'legacyToolSet' has been renamed, use 'anotherToolSetRef' instead.` }, + { severity: MarkerSeverity.Info, message: `Tool or toolset 'tool3' has been renamed, use 'my.extension/tool3' instead.` }, + ] + ); + }); + + test('deprecated tool name mapping to multiple new names', async () => { + // The toolsets are registered in setup with a shared legacy name 'sharedLegacyName' + // This simulates the case where one deprecated name maps to multiple current names + const content = [ + '---', + 'description: "Test"', + `tools: ['sharedLegacyName']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Info); + // When multiple toolsets share the same legacy name, the message should indicate multiple options + // The message will say "use the following tools instead:" for multiple mappings + const expectedMessage = `Tool or toolset 'sharedLegacyName' has been renamed, use the following tools instead: conflictSet1Ref, conflictSet2Ref`; + assert.strictEqual(markers[0].message, expectedMessage); + }); + + test('deprecated tool name in body variable reference - single mapping', async () => { + // Test deprecated tool name used as variable reference in body + const content = [ + '---', + 'description: "Test"', + '---', + 'Body with #tool:oldToolName reference', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Info); + assert.strictEqual(markers[0].message, `Tool or toolset 'oldToolName' has been renamed, use 'newToolRef' instead.`); + }); + + test('deprecated tool name in body variable reference - multiple mappings', async () => { + // Register tools with the same legacy name to create multiple mappings + const multiMapToolSet1 = disposables.add(instaService.get(ILanguageModelToolsService).createToolSet( + ToolDataSource.External, + 'multiMapSet1', + 'multiMapSet1Ref', + { legacyFullNames: ['multiMapLegacy'] } + )); + const multiMapTool1 = { id: 'multiMapTool1', toolReferenceName: 'multiMapTool1Ref', displayName: 'Multi Map Tool 1', canBeReferencedInPrompt: true, modelDescription: 'Multi Map Tool 1', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(instaService.get(ILanguageModelToolsService).registerToolData(multiMapTool1)); + disposables.add(multiMapToolSet1.addTool(multiMapTool1)); + + const multiMapToolSet2 = disposables.add(instaService.get(ILanguageModelToolsService).createToolSet( + ToolDataSource.External, + 'multiMapSet2', + 'multiMapSet2Ref', + { legacyFullNames: ['multiMapLegacy'] } + )); + const multiMapTool2 = { id: 'multiMapTool2', toolReferenceName: 'multiMapTool2Ref', displayName: 'Multi Map Tool 2', canBeReferencedInPrompt: true, modelDescription: 'Multi Map Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; + disposables.add(instaService.get(ILanguageModelToolsService).registerToolData(multiMapTool2)); + disposables.add(multiMapToolSet2.addTool(multiMapTool2)); + + const content = [ + '---', + 'description: "Test"', + '---', + 'Body with #tool:multiMapLegacy reference', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Info); + // When multiple toolsets share the same legacy name, the message should indicate multiple options + // The message will say "use the following tools instead:" for multiple mappings in body references + const expectedMessage = `Tool or toolset 'multiMapLegacy' has been renamed, use the following tools instead: multiMapSet1Ref, multiMapSet2Ref`; + assert.strictEqual(markers[0].message, expectedMessage); + }); + + test('unknown attribute in agent file', async () => { + const content = [ + '---', + 'description: "Test"', + `applyTo: '*.ts'`, // not allowed in agent file + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual( + markers.map(m => ({ severity: m.severity, message: m.message })), + [ + { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: agents, argument-hint, description, disable-model-invocation, handoffs, model, name, target, tools, user-invokable.` }, + ] + ); + }); + + test('tools with invalid handoffs', async () => { + { + const content = [ + '---', + 'description: "Test"', + `handoffs: next`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'handoffs' attribute must be an array.`]); + } + { + const content = [ + '---', + 'description: "Test"', + `handoffs:`, + ` - label: '123'`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`Missing required properties 'agent', 'prompt' in handoff object.`]); + } + { + const content = [ + '---', + 'description: "Test"', + `handoffs:`, + ` - label: '123'`, + ` agent: ''`, + ` prompt: ''`, + ` send: true`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'agent' property in a handoff must be a non-empty string.`]); + } + { + const content = [ + '---', + 'description: "Test"', + `handoffs:`, + ` - label: '123'`, + ` agent: 'Cool'`, + ` prompt: ''`, + ` send: true`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`Unknown agent 'Cool'. Available agents: agent, ask, edit, BeastMode.`]); + } + }); + + test('agent with handoffs attribute', async () => { + const content = [ + '---', + 'description: \"Test agent with handoffs\"', + `handoffs:`, + ' - label: Test Prompt', + ' agent: agent', + ' prompt: Add tests for this code', + ' - label: Optimize Performance', + ' agent: agent', + ' prompt: Optimize for performance', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Expected no validation issues for handoffs attribute'); + }); + + test('github-copilot agent with supported attributes', async () => { + const content = [ + '---', + 'name: "GitHub_Copilot_Custom_Agent"', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + `tools: ['shell', 'edit', 'search', 'custom-agent']`, + 'mcp-servers: []', + '---', + 'Body with #search and #edit references', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Expected no validation issues for github-copilot target'); + }); + + test('github-copilot agent warns about model and handoffs attributes', async () => { + const content = [ + '---', + 'name: "GitHubAgent"', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + 'model: MAE 4.1', + `tools: ['shell', 'edit']`, + `handoffs:`, + ' - label: Test', + ' agent: Default', + ' prompt: Test', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + const messages = markers.map(m => m.message); + assert.deepStrictEqual(messages, [ + 'Attribute \'model\' is not supported in custom GitHub Copilot agent files. Supported: description, infer, mcp-servers, name, target, tools.', + 'Attribute \'handoffs\' is not supported in custom GitHub Copilot agent files. Supported: description, infer, mcp-servers, name, target, tools.', + ], 'Model and handoffs are not validated for github-copilot target'); + }); + + test('github-copilot agent does not validate variable references', async () => { + const content = [ + '---', + 'name: "GitHubAgent"', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + `tools: ['shell', 'edit']`, + '---', + 'Body with #unknownTool reference', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + // Variable references should not be validated for github-copilot target + assert.deepStrictEqual(markers, [], 'Variable references are not validated for github-copilot target'); + }); + + test('github-copilot agent rejects unsupported attributes', async () => { + const content = [ + '---', + 'name: "GitHubAgent"', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + 'argument-hint: "test hint"', + `tools: ['shell']`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.ok(markers[0].message.includes(`Attribute 'argument-hint' is not supported`), 'Expected warning about unsupported attribute'); + }); + + test('vscode target agent validates normally', async () => { + const content = [ + '---', + 'description: "VS Code agent"', + 'target: vscode', + 'model: MAE 4.1', + `tools: ['tool1', 'tool2']`, + '---', + 'Body with #tool1', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'VS Code target should validate normally'); + }); + + test('vscode target agent warns about unknown tools', async () => { + const content = [ + '---', + 'description: "VS Code agent"', + 'target: vscode', + `tools: ['tool1', 'unknownTool']`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `Unknown tool 'unknownTool'.`); + }); + + test('vscode target agent with mcp-servers and github-tools', async () => { + const content = [ + '---', + 'description: "VS Code agent"', + 'target: vscode', + `tools: ['tool1', 'edit']`, + `mcp-servers: {}`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + const messages = markers.map(m => m.message); + assert.deepStrictEqual(messages, [ + 'Attribute \'mcp-servers\' is ignored when running locally in VS Code.', + 'Unknown tool \'edit\'.', + ]); + }); + + test('undefined target with mcp-servers and github-tools', async () => { + const content = [ + '---', + 'description: "VS Code agent"', + `tools: ['tool1', 'shell']`, + `mcp-servers: {}`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + const messages = markers.map(m => m.message); + assert.deepStrictEqual(messages, [ + 'Attribute \'mcp-servers\' is ignored when running locally in VS Code.', + ]); + }); + + test('default target (no target specified) validates as vscode', async () => { + const content = [ + '---', + 'description: "Agent without target"', + 'model: MAE 4.1', + `tools: ['tool1']`, + 'argument-hint: "test hint"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + // Should validate normally as if target was vscode + assert.deepStrictEqual(markers, [], 'Agent without target should validate as vscode'); + }); + + test('name attribute validation', async () => { + // Valid name + { + const content = [ + '---', + 'name: "MyAgent"', + 'description: "Test agent"', + 'target: vscode', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); + } + + // Empty name + { + const content = [ + '---', + 'name: ""', + 'description: "Test agent"', + 'target: vscode', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); + } + + // Non-string name + { + const content = [ + '---', + 'name: 123', + 'description: "Test agent"', + 'target: vscode', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'name' attribute must be a string.`); + } + + // Valid name with allowed characters + { + const content = [ + '---', + 'name: "My_Agent-2.0 with spaces"', + 'description: "Test agent"', + 'target: vscode', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Name with allowed characters should be valid'); + } + }); + + test('github-copilot target requires name attribute', async () => { + // Missing name with github-copilot target + { + const content = [ + '---', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + `tools: ['shell']`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 0); + } + + // Valid name with github-copilot target + { + const content = [ + '---', + 'name: "GitHubAgent"', + 'description: "GitHub Copilot agent"', + 'target: github-copilot', + `tools: ['shell']`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid github-copilot agent with name should not produce errors'); + } + + // Missing name with vscode target (should be optional) + { + const content = [ + '---', + 'description: "VS Code agent"', + 'target: vscode', + `tools: ['tool1']`, + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Name should be optional for vscode target'); + } + }); + + test('infer attribute validation', async () => { + const deprecationMessage = `The 'infer' attribute is deprecated in favour of 'user-invokable' and 'disable-model-invocation'.`; + + // Valid infer: true (maps to 'all') - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: true', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: true should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Valid infer: false (maps to 'user') - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: false', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: false should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Valid infer: 'all' - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: all', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: all should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Valid infer: 'user' - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: user', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: user should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Valid infer: 'agent' - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: agent', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: agent should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Valid infer: 'hidden' - shows deprecation warning + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: hidden', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: hidden should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Invalid infer: unknown string value - shows deprecation warning (validation removed for deprecated attribute) + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: "yes"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: "yes" should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Invalid infer: number value - shows deprecation warning (validation removed for deprecated attribute) + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'infer: 1', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1, 'infer: 1 should produce deprecation warning'); + assert.strictEqual(markers[0].message, deprecationMessage); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + } + + // Missing infer attribute (should be optional) + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Missing infer attribute should be allowed'); + } + }); + + test('agents attribute must be an array', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: 'myAgent'`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'agents' attribute must be an array.`]); + }); + + test('each agent name in agents attribute must be a string', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['agent', 123]`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`Each agent name in the 'agents' attribute must be a string.`]); + }); + + test('unknown agent in agents attribute shows warning', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['UnknownAgent']`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `Unknown agent 'UnknownAgent'. Available agents: Plan, agent.`); + }); + + test('agents attribute with non-empty value requires agent tool 1', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['agent', 'Plan']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [], `No warnings about agents attribute when no tools are specified`); + }); + + test('agents attribute with non-empty value requires agent tool 2', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['agent', 'Plan']`, + `tools: ['shell']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute.`]); + }); + + test('agents attribute with non-empty value requires agent tool 3', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['agent', 'Plan']`, + `tools: ['agent']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [], `No warnings about agents attribute when agent tool is in header`); + }); + + test('agents attribute with non-empty value requires agent tool 4', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: ['*']`, + `tools: ['shell']`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers.map(m => m.message), [`When 'agents' and 'tools' are specified, the 'agent' tool must be included in the 'tools' attribute.`]); + }); + + test('agents attribute with empty array does not require agent tool', async () => { + const content = [ + '---', + 'description: "Test"', + `agents: []`, + '---', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Empty array should not require agent tool'); + }); + + test('user-invokable attribute validation', async () => { + // Valid user-invokable: true + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'user-invokable: true', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid user-invokable: true should not produce errors'); + } + + // Valid user-invokable: false + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'user-invokable: false', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid user-invokable: false should not produce errors'); + } + + // Invalid user-invokable: string value + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'user-invokable: "yes"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'user-invokable' attribute must be a boolean.`); + } + + // Invalid user-invokable: number value + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'user-invokable: 1', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'user-invokable' attribute must be a boolean.`); + } + }); + + test('disable-model-invocation attribute validation', async () => { + // Valid disable-model-invocation: true + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'disable-model-invocation: true', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid disable-model-invocation: true should not produce errors'); + } + + // Valid disable-model-invocation: false + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'disable-model-invocation: false', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.deepStrictEqual(markers, [], 'Valid disable-model-invocation: false should not produce errors'); + } + + // Invalid disable-model-invocation: string value + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'disable-model-invocation: "yes"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'disable-model-invocation' attribute must be a boolean.`); + } + + // Invalid disable-model-invocation: number value + { + const content = [ + '---', + 'name: "TestAgent"', + 'description: "Test agent"', + 'disable-model-invocation: 0', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.agent); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'disable-model-invocation' attribute must be a boolean.`); + } + }); + }); + + 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'); + }); + + test('name attribute validation in instructions', async () => { + // Valid name + { + const content = [ + '---', + 'name: "MyInstructions"', + 'description: "Test instructions"', + 'applyTo: "**/*.ts"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); + } + + // Empty name + { + const content = [ + '---', + 'name: ""', + 'description: "Test instructions"', + 'applyTo: "**/*.ts"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.instructions); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); + } + }); + }); + + 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) + 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 agent BeastMode and tools', async () => { + // Explicit custom agent should be recognized; BeastMode kind comes from setup; ensure tools accepted + const content = [ + '---', + 'description: "Prompt custom mode"', + 'agent: BeastMode', + `tools: ['tool1']`, + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, []); + }); + + 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.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'mode' attribute has been deprecated. Please rename it to 'agent'.`]); + + }); + + test('prompt with custom mode an agent', async () => { + // Explicit custom mode should be recognized; BeastMode kind comes from setup; ensure tools accepted + const content = [ + '---', + 'description: "Prompt custom mode"', + 'mode: BeastMode', + `agent: agent`, + '---', + 'Body' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1); + assert.deepStrictEqual(markers.map(m => m.message), [`The 'mode' attribute has been deprecated. The 'agent' attribute is used instead.`]); + + }); + + test('prompt with unknown agent Ask', async () => { + const content = [ + '---', + 'description: "Prompt unknown agent Ask"', + 'agent: 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 agent 'Ask'. Available agents: agent, ask, edit, BeastMode.`); + }); + + test('prompt with agent edit', async () => { + const content = [ + '---', + 'description: "Prompt edit mode with tool"', + 'agent: 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 when using agents. Attribute will be ignored.`); + }); + + test('name attribute validation in prompts', async () => { + // Valid name + { + const content = [ + '---', + 'name: "MyPrompt"', + 'description: "Test prompt"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); + } + + // Empty name + { + const content = [ + '---', + 'name: ""', + 'description: "Test prompt"', + '---', + 'Body', + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Error); + assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); + } + }); + }); + + 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 http link', async () => { + const content = [ + '---', + 'description: "HTTP Link"', + '---', + 'Here is a [http link](http://example.com).' + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + assert.deepStrictEqual(markers, [], 'Expected no validation issues'); + }); + + test('body with url link', async () => { + const nonExistingRef = existingRef1.with({ path: '/nonexisting' }); + const content = [ + '---', + 'description: "URL Links"', + '---', + `Here is a [url link](${existingRef1.toString()}).`, + `Here is a [url link](${nonExistingRef.toString()}).` + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + const messages = markers.map(m => m.message).sort(); + assert.deepStrictEqual(messages, [ + `File 'myFs://test/nonexisting' not found at '/nonexisting'.`, + ]); + }); + + test('body with unknown tool variable reference warns', async () => { + const content = [ + '---', + 'description: "Unknown tool var"', + '---', + 'This line references known #tool:tool1 and unknown #tool: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.Warning); + assert.strictEqual(markers[0].message, `Unknown tool or toolset 'toolX'.`); + }); + + test('body with tool not present in tools list', async () => { + const content = [ + '---', + 'tools: []', + '---', + 'I need', + '#tool:ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes', + '#tool:github.vscode-pull-request-github/suggest-fix', + '#tool:openSimpleBrowser', + ].join('\n'); + const markers = await validate(content, PromptsType.prompt); + const actual = markers.sort((a, b) => a.startLineNumber - b.startLineNumber).map(m => ({ message: m.message, startColumn: m.startColumn, endColumn: m.endColumn })); + assert.deepEqual(actual, [ + { message: `Unknown tool or toolset 'ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes'.`, startColumn: 7, endColumn: 77 }, + { message: `Tool or toolset 'github.vscode-pull-request-github/suggest-fix' also needs to be enabled in the header.`, startColumn: 7, endColumn: 52 }, + { message: `Unknown tool or toolset 'openSimpleBrowser'.`, startColumn: 7, endColumn: 24 }, + ]); + }); + + }); + + suite('skills', () => { + + test('skill name matches folder name', async () => { + const content = [ + '---', + 'name: my-skill', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when name matches folder'); + }); + + test('skill name does not match folder name', async () => { + const content = [ + '---', + 'name: different-name', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); + assert.strictEqual(markers[0].message, `The skill name 'different-name' should match the folder name 'my-skill'.`); + }); + + test('skill without name attribute does not error', async () => { + const content = [ + '---', + 'description: Test Skill', + '---', + 'This is a skill without a name.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when name is missing'); + }); + + test('skill with empty name does not validate folder match', async () => { + const content = [ + '---', + 'name: ""', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + // Should get error for empty name, but no folder mismatch warning since name is empty + assert.ok(markers.some(m => m.message.includes('must not be empty')), 'Expected error for empty name'); + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not warn about folder mismatch for empty name'); + }); + + test('skill name with whitespace trimmed matches folder name', async () => { + const content = [ + '---', + 'name: " my-skill "', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no validation issues when trimmed name matches folder'); + }); + + test('skill name validation with different folder depths', async () => { + // Test with deeper path structure + { + const content = [ + '---', + 'name: advanced-skill', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///home/user/.github/skills/advanced-skill/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no issues for deeper path when name matches'); + } + + // Test with mismatch in deeper path + { + const content = [ + '---', + 'name: wrong-name', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///home/user/.github/skills/correct-folder/SKILL.md')); + assert.strictEqual(markers.length, 1); + assert.strictEqual(markers[0].message, `The skill name 'wrong-name' should match the folder name 'correct-folder'.`); + } + }); + + test('skill name validation with special characters in folder', async () => { + const content = [ + '---', + 'name: my_special-skill.v2', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my_special-skill.v2/SKILL.md')); + assert.deepStrictEqual(markers, [], 'Expected no issues when name with special chars matches folder'); + }); + + test('skill with non-string name type does not validate folder match', async () => { + const content = [ + '---', + 'name: 123', + 'description: Test Skill', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + // Should get error for non-string name type, but no folder mismatch warning + assert.ok(markers.some(m => m.message.includes('must be a string')), 'Expected error for non-string name'); + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not warn about folder mismatch for non-string name'); + }); + + test('skill folder name validation only for skill type', async () => { + // Verify that folder name validation doesn't run for non-skill prompt types + const content = [ + '---', + 'name: different-name', + 'description: Test Agent', + '---', + 'This is an agent.' + ].join('\n'); + const markers = await validate(content, PromptsType.agent, URI.parse('file:///.github/agents/my-agent/AGENT.md')); + // Should not get folder name mismatch warning for agents + assert.ok(!markers.some(m => m.message.includes('should match the folder name')), 'Should not validate folder names for agents'); + }); + + test('skill with unknown attributes shows warning', async () => { + const content = [ + '---', + 'name: my-skill', + 'description: Test Skill', + 'unknownAttr: value', + 'anotherUnknown: 123', + '---', + 'This is a skill.' + ].join('\n'); + const markers = await validate(content, PromptsType.skill, URI.parse('file:///.github/skills/my-skill/SKILL.md')); + assert.strictEqual(markers.length, 2); + assert.ok(markers.every(m => m.severity === MarkerSeverity.Warning)); + assert.ok(markers.some(m => m.message.includes('unknownAttr'))); + assert.ok(markers.some(m => m.message.includes('anotherUnknown'))); + assert.ok(markers.every(m => m.message.includes('Supported: '))); + }); + + }); + +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptBodyAutocompletion.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptBodyAutocompletion.test.ts deleted file mode 100644 index 51e62eff220..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptBodyAutocompletion.test.ts +++ /dev/null @@ -1,169 +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 { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { Position } from '../../../../../../editor/common/core/position.js'; -import { CompletionContext, CompletionTriggerKind } from '../../../../../../editor/common/languages.js'; -import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; -import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { LanguageModelToolsService } from '../../../browser/languageModelToolsService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../common/languageModelToolsService.js'; -import { PromptBodyAutocompletion } from '../../../common/promptSyntax/languageProviders/promptBodyAutocompletion.js'; -import { createTextModel } from '../../../../../../editor/test/common/testTextModel.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { getLanguageIdForPromptsType, PromptsType } from '../../../common/promptSyntax/promptTypes.js'; -import { getPromptFileExtension } from '../../../common/promptSyntax/config/promptFileLocations.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { FileService } from '../../../../../../platform/files/common/fileService.js'; -import { VSBuffer } from '../../../../../../base/common/buffer.js'; -import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; -import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; -import { Range } from '../../../../../../editor/common/core/range.js'; - -suite('PromptBodyAutocompletion', () => { - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instaService: TestInstantiationService; - let completionProvider: PromptBodyAutocompletion; - - setup(async () => { - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); - instaService = workbenchInstantiationService({ - contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, disposables); - instaService.stub(ILogService, new NullLogService()); - const fileService = disposables.add(instaService.createInstance(FileService)); - instaService.stub(IFileService, fileService); - - const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); - disposables.add(fileService.registerProvider('test', fileSystemProvider)); - - // Create some test files and directories - await fileService.createFolder(URI.parse('test:///workspace')); - await fileService.createFolder(URI.parse('test:///workspace/src')); - await fileService.createFolder(URI.parse('test:///workspace/docs')); - await fileService.writeFile(URI.parse('test:///workspace/src/index.ts'), VSBuffer.fromString('export function hello() {}')); - await fileService.writeFile(URI.parse('test:///workspace/README.md'), VSBuffer.fromString('# Project')); - await fileService.writeFile(URI.parse('test:///workspace/package.json'), VSBuffer.fromString('{}')); - - 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; - disposables.add(toolService.registerToolData(testTool1)); - - const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(testTool2)); - - const myExtSource = { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('My.extension') } satisfies ToolDataSource; - const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: myExtSource, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(testTool3)); - - const prExtSource = { type: 'extension', label: 'GitHub Pull Request Extension', extensionId: new ExtensionIdentifier('github.vscode-pull-request-github') } satisfies ToolDataSource; - const prExtTool1 = { id: 'suggestFix', canBeReferencedInPrompt: true, toolReferenceName: 'suggest-fix', modelDescription: 'tool4', displayName: 'Test Tool 4', source: prExtSource, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(prExtTool1)); - - instaService.set(ILanguageModelToolsService, toolService); - - completionProvider = instaService.createInstance(PromptBodyAutocompletion); - }); - - async function getCompletions(content: string, line: number, column: number, promptType: PromptsType) { - const languageId = getLanguageIdForPromptsType(promptType); - const model = disposables.add(createTextModel(content, languageId, undefined, URI.parse('test://workspace/test' + getPromptFileExtension(promptType)))); - const position = new Position(line, column); - const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke }; - const result = await completionProvider.provideCompletionItems(model, position, context, CancellationToken.None); - if (!result || !result.suggestions) { - return []; - } - const lineContent = model.getLineContent(position.lineNumber); - return result.suggestions.map(s => { - assert(s.range instanceof Range); - return { - label: s.label, - result: lineContent.substring(0, s.range.startColumn - 1) + s.insertText + lineContent.substring(s.range.endColumn - 1) - }; - }); - } - - suite('prompt body completions', () => { - test('default suggestions', async () => { - const content = [ - '---', - 'description: "Test"', - '---', - '', - 'Use # to reference a file or tool.', - 'One more #to' - ].join('\n'); - - { - const actual = (await getCompletions(content, 5, 6, PromptsType.prompt)); - assert.deepEqual(actual, [ - { - label: 'file:', - result: 'Use #file: to reference a file or tool.' - }, - { - label: 'tool:', - result: 'Use #tool: to reference a file or tool.' - } - ]); - } - { - const actual = (await getCompletions(content, 6, 13, PromptsType.prompt)); - assert.deepEqual(actual, [ - { - label: 'file:', - result: 'One more #file:' - }, - { - label: 'tool:', - result: 'One more #tool:' - } - ]); - } - }); - - test('tool suggestions', async () => { - const content = [ - '---', - 'description: "Test"', - '---', - '', - 'Use #tool: to reference a tool.', - ].join('\n'); - { - const actual = (await getCompletions(content, 5, 11, PromptsType.prompt)); - assert.deepEqual(actual, [ - { - label: 'tool1', - result: 'Use #tool:tool1 to reference a tool.' - }, - { - label: 'tool2', - result: 'Use #tool:tool2 to reference a tool.' - }, - { - label: 'my.extension/tool3', - result: 'Use #tool:my.extension/tool3 to reference a tool.' - }, - { - label: 'github.vscode-pull-request-github/suggest-fix', - result: 'Use #tool:github.vscode-pull-request-github/suggest-fix to reference a tool.' - } - ]); - } - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts b/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts deleted file mode 100644 index c63a0723a99..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptHovers.test.ts +++ /dev/null @@ -1,385 +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 { CancellationToken } from '../../../../../../base/common/cancellation.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { Position } from '../../../../../../editor/common/core/position.js'; -import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; -import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { LanguageModelToolsService } from '../../../browser/languageModelToolsService.js'; -import { ChatMode, CustomChatMode, IChatModeService } from '../../../common/chatModes.js'; -import { IChatService } from '../../../common/chatService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../common/languageModelToolsService.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../common/languageModels.js'; -import { PromptHoverProvider } from '../../../common/promptSyntax/languageProviders/promptHovers.js'; -import { IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; -import { MockChatModeService } from '../../common/mockChatModeService.js'; -import { MockChatService } from '../../common/mockChatService.js'; -import { createTextModel } from '../../../../../../editor/test/common/testTextModel.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { PromptFileParser } from '../../../common/promptSyntax/promptFileParser.js'; -import { ITextModel } from '../../../../../../editor/common/model.js'; -import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; -import { getLanguageIdForPromptsType, PromptsType } from '../../../common/promptSyntax/promptTypes.js'; -import { getPromptFileExtension } from '../../../common/promptSyntax/config/promptFileLocations.js'; - -suite('PromptHoverProvider', () => { - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instaService: TestInstantiationService; - let hoverProvider: PromptHoverProvider; - - setup(async () => { - const testConfigService = new TestConfigurationService(); - testConfigService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); - instaService = workbenchInstantiationService({ - contextKeyService: () => disposables.add(new ContextKeyService(testConfigService)), - configurationService: () => testConfigService - }, disposables); - - const chatService = new MockChatService(); - instaService.stub(IChatService, chatService); - - 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; - disposables.add(toolService.registerToolData(testTool1)); - - const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(testTool2)); - - const runCommandsTool = { id: 'runCommands', displayName: 'runCommands', canBeReferencedInPrompt: true, toolReferenceName: 'runCommands', modelDescription: 'Run Commands Tool', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(runCommandsTool)); - - 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, - ]; - - 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', - agentInstructions: { content: 'Beast mode instructions', toolReferences: [] }, - source: { storage: PromptsStorage.local } - }); - instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); - - const parser = new PromptFileParser(); - instaService.stub(IPromptsService, { - getParsedPromptFile(model: ITextModel) { - return parser.parse(model.uri, model.getValue()); - } - }); - - hoverProvider = instaService.createInstance(PromptHoverProvider); - }); - - async function getHover(content: string, line: number, column: number, promptType: PromptsType): Promise { - const languageId = getLanguageIdForPromptsType(promptType); - const model = disposables.add(createTextModel(content, languageId, undefined, URI.parse('test://test' + getPromptFileExtension(promptType)))); - const position = new Position(line, column); - const hover = await hoverProvider.provideHover(model, position, CancellationToken.None); - if (!hover || hover.contents.length === 0) { - return undefined; - } - // Return the markdown value from the first content - const firstContent = hover.contents[0]; - if (firstContent instanceof MarkdownString) { - return firstContent.value; - } - return undefined; - } - - suite('agent hovers', () => { - test('hover on target attribute shows description', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: vscode', - '---', - ].join('\n'); - const hover = await getHover(content, 3, 1, PromptsType.agent); - assert.strictEqual(hover, 'The target to which the header attributes like tools apply to. Possible values are `github-copilot` and `vscode`.'); - }); - - test('hover on model attribute with github-copilot target shows note', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: github-copilot', - 'model: MAE 4', - '---', - ].join('\n'); - const hover = await getHover(content, 4, 1, PromptsType.agent); - const expected = [ - 'Specify the model that runs this custom agent.', - '', - 'Note: This attribute is not used when target is github-copilot.' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on model attribute with vscode target shows model info', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: vscode', - 'model: MAE 4 (olama)', - '---', - ].join('\n'); - const hover = await getHover(content, 4, 1, PromptsType.agent); - const expected = [ - 'Specify the model that runs this custom agent.', - '', - '- Name: MAE 4', - '- Family: mae', - '- Vendor: olama' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on handoffs attribute with github-copilot target shows note', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: github-copilot', - 'handoffs:', - ' - label: Test', - ' agent: Default', - ' prompt: Test', - '---', - ].join('\n'); - const hover = await getHover(content, 4, 1, PromptsType.agent); - const expected = [ - 'Possible handoff actions when the agent has completed its task.', - '', - 'Note: This attribute is not used when target is github-copilot.' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on handoffs attribute with vscode target shows description', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: vscode', - 'handoffs:', - ' - label: Test', - ' agent: Default', - ' prompt: Test', - '---', - ].join('\n'); - const hover = await getHover(content, 4, 1, PromptsType.agent); - assert.strictEqual(hover, 'Possible handoff actions when the agent has completed its task.'); - }); - - test('hover on github-copilot tool shows simple description', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: github-copilot', - `tools: ['shell', 'edit', 'search']`, - '---', - ].join('\n'); - // Hover on 'shell' tool - const hoverShell = await getHover(content, 4, 10, PromptsType.agent); - assert.strictEqual(hoverShell, 'Execute shell commands'); - - // Hover on 'edit' tool - const hoverEdit = await getHover(content, 4, 20, PromptsType.agent); - assert.strictEqual(hoverEdit, 'Edit files'); - - // Hover on 'search' tool - const hoverSearch = await getHover(content, 4, 28, PromptsType.agent); - assert.strictEqual(hoverSearch, 'Search in files'); - }); - - test('hover on github-copilot tool with target undefined', async () => { - const content = [ - '---', - 'name: "Test"', - 'description: "Test"', - `tools: ['shell', 'edit', 'search']`, - '---', - ].join('\n'); - // Hover on 'shell' tool - const hoverShell = await getHover(content, 4, 10, PromptsType.agent); - assert.strictEqual(hoverShell, 'Run Commands Tool'); - - // Hover on 'edit' tool - const hoverEdit = await getHover(content, 4, 20, PromptsType.agent); - assert.strictEqual(hoverEdit, 'Edit files'); - - // Hover on 'search' tool - const hoverSearch = await getHover(content, 4, 28, PromptsType.agent); - assert.strictEqual(hoverSearch, 'Search in files'); - }); - - test('hover on vscode tool shows detailed description', async () => { - const content = [ - '---', - 'description: "Test"', - 'target: vscode', - `tools: ['tool1', 'tool2']`, - '---', - ].join('\n'); - // Hover on 'tool1' - const hover = await getHover(content, 4, 10, PromptsType.agent); - assert.strictEqual(hover, 'Test Tool 1'); - }); - - test('hover on description attribute', async () => { - const content = [ - '---', - 'description: "Test agent"', - 'target: vscode', - '---', - ].join('\n'); - const hover = await getHover(content, 2, 1, PromptsType.agent); - assert.strictEqual(hover, 'The description of the custom agent, what it does and when to use it.'); - }); - - test('hover on argument-hint attribute', async () => { - const content = [ - '---', - 'description: "Test"', - 'argument-hint: "test hint"', - '---', - ].join('\n'); - const hover = await getHover(content, 3, 1, PromptsType.agent); - assert.strictEqual(hover, 'The argument-hint describes what inputs the custom agent expects or supports.'); - }); - - test('hover on name attribute', async () => { - const content = [ - '---', - 'name: "My Agent"', - 'description: "Test agent"', - 'target: vscode', - '---', - ].join('\n'); - const hover = await getHover(content, 2, 1, PromptsType.agent); - assert.strictEqual(hover, 'The name of the agent as shown in the UI.'); - }); - }); - - suite('prompt hovers', () => { - test('hover on model attribute shows model info', async () => { - const content = [ - '---', - 'description: "Test"', - 'model: MAE 4 (olama)', - '---', - ].join('\n'); - const hover = await getHover(content, 3, 1, PromptsType.prompt); - const expected = [ - 'The model to use in this prompt.', - '', - '- Name: MAE 4', - '- Family: mae', - '- Vendor: olama' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on tools attribute shows tool description', async () => { - const content = [ - '---', - 'description: "Test"', - `tools: ['tool1']`, - '---', - ].join('\n'); - const hover = await getHover(content, 3, 10, PromptsType.prompt); - assert.strictEqual(hover, 'Test Tool 1'); - }); - - test('hover on agent attribute shows agent info', async () => { - const content = [ - '---', - 'description: "Test"', - 'agent: BeastMode', - '---', - ].join('\n'); - const hover = await getHover(content, 3, 1, PromptsType.prompt); - const expected = [ - 'The agent to use when running this prompt.', - '', - '**Built-in agents:**', - '- `agent`: Describe what to build next', - '- `ask`: Explore and understand your code', - '- `edit`: Edit or refactor selected code', - '', - '**Custom agents:**', - '- `BeastMode`: Custom agent' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on name attribute', async () => { - const content = [ - '---', - 'name: "My Prompt"', - 'description: "Test prompt"', - '---', - ].join('\n'); - const hover = await getHover(content, 2, 1, PromptsType.prompt); - assert.strictEqual(hover, 'The name of the prompt. This is also the name of the slash command that will run this prompt.'); - }); - }); - - suite('instructions hovers', () => { - test('hover on description attribute', async () => { - const content = [ - '---', - 'description: "Test instruction"', - 'applyTo: "**/*.ts"', - '---', - ].join('\n'); - const hover = await getHover(content, 2, 1, PromptsType.instructions); - assert.strictEqual(hover, '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.'); - }); - - test('hover on applyTo attribute', async () => { - const content = [ - '---', - 'description: "Test"', - 'applyTo: "**/*.ts"', - '---', - ].join('\n'); - const hover = await getHover(content, 3, 1, PromptsType.instructions); - const expected = [ - '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.', - 'Example: `**/*.ts`, `**/*.js`, `client/**`' - ].join('\n'); - assert.strictEqual(hover, expected); - }); - - test('hover on name attribute', async () => { - const content = [ - '---', - 'name: "My Instructions"', - 'description: "Test instruction"', - 'applyTo: "**/*.ts"', - '---', - ].join('\n'); - const hover = await getHover(content, 2, 1, PromptsType.instructions); - assert.strictEqual(hover, 'The name of the instruction file as shown in the UI. If not set, the name is derived from the file name.'); - }); - }); -}); 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 deleted file mode 100644 index f26c1634353..00000000000 --- a/src/vs/workbench/contrib/chat/test/browser/promptSytntax/promptValidator.test.ts +++ /dev/null @@ -1,895 +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 { ResourceSet } from '../../../../../../base/common/map.js'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; -import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; -import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { ILabelService } from '../../../../../../platform/label/common/label.js'; -import { IMarkerData, MarkerSeverity } from '../../../../../../platform/markers/common/markers.js'; -import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { LanguageModelToolsService } from '../../../browser/languageModelToolsService.js'; -import { ChatMode, CustomChatMode, IChatModeService } from '../../../common/chatModes.js'; -import { IChatService } from '../../../common/chatService.js'; -import { ChatConfiguration } from '../../../common/constants.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../../common/languageModelToolsService.js'; -import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../common/languageModels.js'; -import { getPromptFileExtension } from '../../../common/promptSyntax/config/promptFileLocations.js'; -import { PromptValidator } from '../../../common/promptSyntax/languageProviders/promptValidator.js'; -import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; -import { PromptFileParser } from '../../../common/promptSyntax/promptFileParser.js'; -import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js'; -import { MockChatModeService } from '../../common/mockChatModeService.js'; -import { MockChatService } from '../../common/mockChatService.js'; - -suite('PromptValidator', () => { - const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instaService: TestInstantiationService; - - const existingRef1 = URI.parse('myFs://test/reference1.md'); - const existingRef2 = URI.parse('myFs://test/reference2.md'); - - setup(async () => { - - const testConfigService = new TestConfigurationService(); - 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; - disposables.add(toolService.registerToolData(testTool1)); - const testTool2 = { id: 'testTool2', displayName: 'tool2', canBeReferencedInPrompt: true, toolReferenceName: 'tool2', modelDescription: 'Test Tool 2', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(testTool2)); - const runCommandsTool = { id: 'runCommands', displayName: 'runCommands', canBeReferencedInPrompt: true, toolReferenceName: 'runCommands', modelDescription: 'Run Commands Tool', source: ToolDataSource.External, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(runCommandsTool)); - - const myExtSource = { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('My.extension') } satisfies ToolDataSource; - const testTool3 = { id: 'testTool3', displayName: 'tool3', canBeReferencedInPrompt: true, toolReferenceName: 'tool3', modelDescription: 'Test Tool 3', source: myExtSource, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(testTool3)); - - const prExtSource = { type: 'extension', label: 'GitHub Pull Request Extension', extensionId: new ExtensionIdentifier('github.vscode-pull-request-github') } satisfies ToolDataSource; - const prExtTool1 = { id: 'suggestFix', canBeReferencedInPrompt: true, toolReferenceName: 'suggest-fix', modelDescription: 'tool4', displayName: 'Test Tool 4', source: prExtSource, inputSchema: {} } satisfies IToolData; - disposables.add(toolService.registerToolData(prExtTool1)); - - 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', - agentInstructions: { content: 'Beast mode instructions', toolReferences: [] }, - source: { storage: PromptsStorage.local } - }); - instaService.stub(IChatModeService, new MockChatModeService({ builtin: [ChatMode.Agent, ChatMode.Ask, ChatMode.Edit], custom: [customChatMode] })); - - - const existingFiles = new ResourceSet([existingRef1, existingRef2]); - 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 PromptFileParser().parse(uri, code); - const validator = instaService.createInstance(PromptValidator); - const markers: IMarkerData[] = []; - await validator.validate(result, promptType, m => markers.push(m)); - return markers; - } - suite('agents', () => { - - test('correct agent', async () => { - const content = [ - /* 01 */'---', - /* 02 */`description: "Agent mode test"`, - /* 03 */'model: MAE 4.1', - /* 04 */`tools: ['tool1', 'tool2']`, - /* 05 */'---', - /* 06 */'This is a chat agent 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.agent); - assert.deepStrictEqual(markers, []); - }); - - test('agent 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.agent); - assert.deepStrictEqual( - markers.map(m => ({ severity: m.severity, message: m.message })), - [ - { severity: MarkerSeverity.Error, message: `The 'description' attribute should not be empty.` }, - { severity: MarkerSeverity.Warning, 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.agent); - 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.agent); - 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.agent); - assert.deepStrictEqual( - markers.map(m => ({ severity: m.severity, message: m.message })), - [ - { severity: MarkerSeverity.Info, message: `Tool or toolset 'tool3' has been renamed, use 'my.extension/tool3' instead.` }, - ] - ); - }); - - test('unknown attribute in agent file', async () => { - const content = [ - '---', - 'description: "Test"', - `applyTo: '*.ts'`, // not allowed in agent file - '---', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual( - markers.map(m => ({ severity: m.severity, message: m.message })), - [ - { severity: MarkerSeverity.Warning, message: `Attribute 'applyTo' is not supported in VS Code agent files. Supported: argument-hint, description, handoffs, model, name, target, tools.` }, - ] - ); - }); - - test('tools with invalid handoffs', async () => { - { - const content = [ - '---', - 'description: "Test"', - `handoffs: next`, - '---', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`The 'handoffs' attribute must be an array.`]); - } - { - const content = [ - '---', - 'description: "Test"', - `handoffs:`, - ` - label: '123'`, - '---', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`Missing required properties 'agent', 'prompt' in handoff object.`]); - } - { - const content = [ - '---', - 'description: "Test"', - `handoffs:`, - ` - label: '123'`, - ` agent: ''`, - ` prompt: ''`, - ` send: true`, - '---', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`The 'agent' property in a handoff must be a non-empty string.`]); - } - { - const content = [ - '---', - 'description: "Test"', - `handoffs:`, - ` - label: '123'`, - ` agent: 'Cool'`, - ` prompt: ''`, - ` send: true`, - '---', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`Unknown agent 'Cool'. Available agents: agent, ask, edit, BeastMode.`]); - } - }); - - test('agent with handoffs attribute', async () => { - const content = [ - '---', - 'description: \"Test agent with handoffs\"', - `handoffs:`, - ' - label: Test Prompt', - ' agent: agent', - ' prompt: Add tests for this code', - ' - label: Optimize Performance', - ' agent: agent', - ' prompt: Optimize for performance', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Expected no validation issues for handoffs attribute'); - }); - - test('github-copilot agent with supported attributes', async () => { - const content = [ - '---', - 'name: "GitHub_Copilot_Custom_Agent"', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - `tools: ['shell', 'edit', 'search', 'custom-agent']`, - 'mcp-servers: []', - '---', - 'Body with #search and #edit references', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Expected no validation issues for github-copilot target'); - }); - - test('github-copilot agent warns about model and handoffs attributes', async () => { - const content = [ - '---', - 'name: "GitHubAgent"', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - 'model: MAE 4.1', - `tools: ['shell', 'edit']`, - `handoffs:`, - ' - label: Test', - ' agent: Default', - ' prompt: Test', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - const messages = markers.map(m => m.message); - assert.deepStrictEqual(messages, [ - 'Attribute \'model\' is not supported in custom GitHub Copilot agent files. Supported: description, mcp-servers, name, target, tools.', - 'Attribute \'handoffs\' is not supported in custom GitHub Copilot agent files. Supported: description, mcp-servers, name, target, tools.', - ], 'Model and handoffs are not validated for github-copilot target'); - }); - - test('github-copilot agent does not validate variable references', async () => { - const content = [ - '---', - 'name: "GitHubAgent"', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - `tools: ['shell', 'edit']`, - '---', - 'Body with #unknownTool reference', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - // Variable references should not be validated for github-copilot target - assert.deepStrictEqual(markers, [], 'Variable references are not validated for github-copilot target'); - }); - - test('github-copilot agent rejects unsupported attributes', async () => { - const content = [ - '---', - 'name: "GitHubAgent"', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - 'argument-hint: "test hint"', - `tools: ['shell']`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); - assert.ok(markers[0].message.includes(`Attribute 'argument-hint' is not supported`), 'Expected warning about unsupported attribute'); - }); - - test('vscode target agent validates normally', async () => { - const content = [ - '---', - 'description: "VS Code agent"', - 'target: vscode', - 'model: MAE 4.1', - `tools: ['tool1', 'tool2']`, - '---', - 'Body with #tool1', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'VS Code target should validate normally'); - }); - - test('vscode target agent warns about unknown tools', async () => { - const content = [ - '---', - 'description: "VS Code agent"', - 'target: vscode', - `tools: ['tool1', 'unknownTool']`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Warning); - assert.strictEqual(markers[0].message, `Unknown tool 'unknownTool'.`); - }); - - test('vscode target agent with mcp-servers and github-tools', async () => { - const content = [ - '---', - 'description: "VS Code agent"', - 'target: vscode', - `tools: ['tool1', 'shell']`, - `mcp-servers: {}`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - const messages = markers.map(m => m.message); - assert.deepStrictEqual(messages, [ - 'Attribute \'mcp-servers\' is ignored when running locally in VS Code.', - 'Unknown tool \'shell\'.', - ]); - }); - - test('undefined target with mcp-servers and github-tools', async () => { - const content = [ - '---', - 'description: "VS Code agent"', - `tools: ['tool1', 'shell']`, - `mcp-servers: {}`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - const messages = markers.map(m => m.message); - assert.deepStrictEqual(messages, [ - 'Attribute \'mcp-servers\' is ignored when running locally in VS Code.', - ]); - }); - - test('default target (no target specified) validates as vscode', async () => { - const content = [ - '---', - 'description: "Agent without target"', - 'model: MAE 4.1', - `tools: ['tool1']`, - 'argument-hint: "test hint"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - // Should validate normally as if target was vscode - assert.deepStrictEqual(markers, [], 'Agent without target should validate as vscode'); - }); - - test('name attribute validation', async () => { - // Valid name - { - const content = [ - '---', - 'name: "MyAgent"', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); - } - - // Empty name - { - const content = [ - '---', - 'name: ""', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); - } - - // Non-string name - { - const content = [ - '---', - 'name: 123', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute must be a string.`); - } - - // Invalid characters in name - { - const content = [ - '---', - 'name: "My@Agent!"', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } - - // Valid name with allowed characters - { - const content = [ - '---', - 'name: "My_Agent-2.0"', - 'description: "Test agent"', - 'target: vscode', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Name with allowed characters should be valid'); - } - }); - - test('github-copilot target requires name attribute', async () => { - // Missing name with github-copilot target - { - const content = [ - '---', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - `tools: ['shell']`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.strictEqual(markers.length, 0); - } - - // Valid name with github-copilot target - { - const content = [ - '---', - 'name: "GitHubAgent"', - 'description: "GitHub Copilot agent"', - 'target: github-copilot', - `tools: ['shell']`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Valid github-copilot agent with name should not produce errors'); - } - - // Missing name with vscode target (should be optional) - { - const content = [ - '---', - 'description: "VS Code agent"', - 'target: vscode', - `tools: ['tool1']`, - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.agent); - assert.deepStrictEqual(markers, [], 'Name should be optional for vscode target'); - } - }); - }); - - 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'); - }); - - test('name attribute validation in instructions', async () => { - // Valid name - { - const content = [ - '---', - 'name: "MyInstructions"', - 'description: "Test instructions"', - 'applyTo: "**/*.ts"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.instructions); - assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); - } - - // Empty name - { - const content = [ - '---', - 'name: ""', - 'description: "Test instructions"', - 'applyTo: "**/*.ts"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.instructions); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); - } - - // Invalid characters in name - { - const content = [ - '---', - 'name: "My Instructions#"', - 'description: "Test instructions"', - 'applyTo: "**/*.ts"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.instructions); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } - }); - }); - - 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) - 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 agent BeastMode and tools', async () => { - // Explicit custom agent should be recognized; BeastMode kind comes from setup; ensure tools accepted - const content = [ - '---', - 'description: "Prompt custom mode"', - 'agent: BeastMode', - `tools: ['tool1']`, - '---', - 'Body' - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.deepStrictEqual(markers, []); - }); - - 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.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`The 'mode' attribute has been deprecated. Please rename it to 'agent'.`]); - - }); - - test('prompt with custom mode an agent', async () => { - // Explicit custom mode should be recognized; BeastMode kind comes from setup; ensure tools accepted - const content = [ - '---', - 'description: "Prompt custom mode"', - 'mode: BeastMode', - `agent: agent`, - '---', - 'Body' - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.strictEqual(markers.length, 1); - assert.deepStrictEqual(markers.map(m => m.message), [`The 'mode' attribute has been deprecated. The 'agent' attribute is used instead.`]); - - }); - - test('prompt with unknown agent Ask', async () => { - const content = [ - '---', - 'description: "Prompt unknown agent Ask"', - 'agent: 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 agent 'Ask'. Available agents: agent, ask, edit, BeastMode.`); - }); - - test('prompt with agent edit', async () => { - const content = [ - '---', - 'description: "Prompt edit mode with tool"', - 'agent: 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 when using agents. Attribute will be ignored.`); - }); - - test('name attribute validation in prompts', async () => { - // Valid name - { - const content = [ - '---', - 'name: "MyPrompt"', - 'description: "Test prompt"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.deepStrictEqual(markers, [], 'Valid name should not produce errors'); - } - - // Empty name - { - const content = [ - '---', - 'name: ""', - 'description: "Test prompt"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute must not be empty.`); - } - - // Invalid characters in name - { - const content = [ - '---', - 'name: "My Prompt!"', - 'description: "Test prompt"', - '---', - 'Body', - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.strictEqual(markers.length, 1); - assert.strictEqual(markers[0].severity, MarkerSeverity.Error); - assert.strictEqual(markers[0].message, `The 'name' attribute can only consist of letters, digits, underscores, hyphens, and periods.`); - } - }); - }); - - 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 http link', async () => { - const content = [ - '---', - 'description: "HTTP Link"', - '---', - 'Here is a [http link](http://example.com).' - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - assert.deepStrictEqual(markers, [], 'Expected no validation issues'); - }); - - test('body with url link', async () => { - const nonExistingRef = existingRef1.with({ path: '/nonexisting' }); - const content = [ - '---', - 'description: "URL Links"', - '---', - `Here is a [url link](${existingRef1.toString()}).`, - `Here is a [url link](${nonExistingRef.toString()}).` - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - const messages = markers.map(m => m.message).sort(); - assert.deepStrictEqual(messages, [ - `File 'myFs://test/nonexisting' not found at '/nonexisting'.`, - ]); - }); - - test('body with unknown tool variable reference warns', async () => { - const content = [ - '---', - 'description: "Unknown tool var"', - '---', - 'This line references known #tool:tool1 and unknown #tool: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.Warning); - assert.strictEqual(markers[0].message, `Unknown tool or toolset 'toolX'.`); - }); - - test('body with tool not present in tools list', async () => { - const content = [ - '---', - 'tools: []', - '---', - 'I need', - '#tool:ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes', - '#tool:github.vscode-pull-request-github/suggest-fix', - '#tool:openSimpleBrowser', - ].join('\n'); - const markers = await validate(content, PromptsType.prompt); - const actual = markers.sort((a, b) => a.startLineNumber - b.startLineNumber).map(m => ({ message: m.message, startColumn: m.startColumn, endColumn: m.endColumn })); - assert.deepEqual(actual, [ - { message: `Unknown tool or toolset 'ms-azuretools.vscode-azure-github-copilot/azure_recommend_custom_modes'.`, startColumn: 7, endColumn: 77 }, - { message: `Tool or toolset 'github.vscode-pull-request-github/suggest-fix' also needs to be enabled in the header.`, startColumn: 7, endColumn: 52 }, - { message: `Unknown tool or toolset 'openSimpleBrowser'.`, startColumn: 7, endColumn: 24 }, - ]); - }); - - }); - -}); diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsConfirmationService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts similarity index 97% rename from src/vs/workbench/contrib/chat/test/browser/languageModelToolsConfirmationService.test.ts rename to src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts index 61be238530b..653cc47feb7 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsConfirmationService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsConfirmationService.test.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { IStorageService, InMemoryStorageService } from '../../../../../platform/storage/common/storage.js'; -import { LanguageModelToolsConfirmationService } from '../../browser/languageModelToolsConfirmationService.js'; -import { ToolConfirmKind } from '../../common/chatService.js'; -import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationRef } from '../../common/languageModelToolsConfirmationService.js'; -import { ToolDataSource } from '../../common/languageModelToolsService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IStorageService, InMemoryStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { LanguageModelToolsConfirmationService } from '../../../browser/tools/languageModelToolsConfirmationService.js'; +import { ToolConfirmKind } from '../../../common/chatService/chatService.js'; +import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationRef } from '../../../common/tools/languageModelToolsConfirmationService.js'; +import { ToolDataSource } from '../../../common/tools/languageModelToolsService.js'; suite('LanguageModelToolsConfirmationService', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts new file mode 100644 index 00000000000..60e8ff96020 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/tools/languageModelToolsService.test.ts @@ -0,0 +1,3723 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Barrier } from '../../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { CancellationError, isCancellationError } from '../../../../../../base/common/errors.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IAccessibilityService } from '../../../../../../platform/accessibility/common/accessibility.js'; +import { TestAccessibilityService } from '../../../../../../platform/accessibility/test/common/testAccessibilityService.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { ConfigurationTarget, IConfigurationChangeEvent } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ContextKeyService } from '../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { ContextKeyEqualsExpr, ContextKeyExpr, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../browser/tools/languageModelToolsService.js'; +import { ChatModel, IChatModel } from '../../../common/model/chatModel.js'; +import { IChatService, IChatToolInputInvocationData, IChatToolInvocation, ToolConfirmKind } from '../../../common/chatService/chatService.js'; +import { ChatConfiguration } from '../../../common/constants.js'; +import { SpecedToolAliases, isToolResultInputOutputDetails, IToolData, IToolImpl, IToolInvocation, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { MockChatService } from '../../common/chatService/mockChatService.js'; +import { ChatToolInvocation } from '../../../common/model/chatProgressTypes/chatToolInvocation.js'; +import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; +import { ILanguageModelToolsConfirmationService } from '../../../common/tools/languageModelToolsConfirmationService.js'; +import { MockLanguageModelToolsConfirmationService } from '../../common/tools/mockLanguageModelToolsConfirmationService.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { ILanguageModelChatMetadata } from '../../../common/languageModels.js'; + +// --- Test helpers to reduce repetition and improve readability --- + +class TestAccessibilitySignalService implements Partial { + public signalPlayedCalls: { signal: AccessibilitySignal; options?: any }[] = []; + + async playSignal(signal: AccessibilitySignal, options?: any): Promise { + this.signalPlayedCalls.push({ signal, options }); + } + + reset() { + this.signalPlayedCalls = []; + } +} + +class TestTelemetryService implements Partial { + public events: Array<{ eventName: string; data: any }> = []; + + publicLog2, T extends Record>(eventName: string, data?: E): void { + this.events.push({ eventName, data }); + } + + reset() { + this.events = []; + } +} + +function registerToolForTest(service: LanguageModelToolsService, store: any, id: string, impl: IToolImpl, data?: Partial) { + const toolData: IToolData = { + id, + modelDescription: data?.modelDescription ?? 'Test Tool', + displayName: data?.displayName ?? 'Test Tool', + source: ToolDataSource.Internal, + ...data, + }; + store.add(service.registerTool(toolData, impl)); + return { + id, + makeDto: (parameters: any, context?: { sessionId: string }, callId: string = '1'): IToolInvocation => ({ + callId, + toolId: id, + tokenBudget: 100, + parameters, + context: context ? { + sessionId: context.sessionId, + sessionResource: LocalChatSessionUri.forSession(context.sessionId), + } : undefined, + }), + }; +} + +function stubGetSession(chatService: MockChatService, sessionId: string, options?: { requestId?: string; capture?: { invocation?: any } }): IChatModel { + const requestId = options?.requestId ?? 'requestId'; + const capture = options?.capture; + const fakeModel = { + sessionId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + getRequests: () => [{ id: requestId, modelId: 'test-model' }], + } as ChatModel; + chatService.addSession(fakeModel); + chatService.appendProgress = (request, progress) => { + if (capture) { capture.invocation = progress; } + }; + + return fakeModel; +} + +async function waitForPublishedInvocation(capture: { invocation?: any }, tries = 5): Promise { + for (let i = 0; i < tries && !capture.invocation; i++) { + await Promise.resolve(); + } + return capture.invocation; +} + +suite('LanguageModelToolsService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let contextKeyService: IContextKeyService; + let service: LanguageModelToolsService; + let chatService: MockChatService; + let configurationService: TestConfigurationService; + + setup(() => { + configurationService = new TestConfigurationService(); + configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(configurationService)), + configurationService: () => configurationService + }, store); + contextKeyService = instaService.get(IContextKeyService); + chatService = new MockChatService(); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + service = store.add(instaService.createInstance(LanguageModelToolsService)); + }); + + function setupToolsForTest(service: LanguageModelToolsService, store: any) { + + // Create a variety of tools and tool sets for testing + // Some with toolReferenceName, some without, some from extensions, mcp and user defined + + const tool1: IToolData = { + id: 'tool1', + toolReferenceName: 'tool1RefName', + modelDescription: 'Test Tool 1', + displayName: 'Tool1 Display Name', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(tool1)); + + const tool2: IToolData = { + id: 'tool2', + modelDescription: 'Test Tool 2', + displayName: 'Tool2 Display Name', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(tool2)); + + /** Extension Tool 1 */ + + const extTool1: IToolData = { + id: 'extTool1', + toolReferenceName: 'extTool1RefName', + modelDescription: 'Test Extension Tool 1', + displayName: 'ExtTool1 Display Name', + source: { type: 'extension', label: 'My Extension', extensionId: new ExtensionIdentifier('my.extension') }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(extTool1)); + + /** Internal Tool Set with internalToolSetTool1 */ + + const internalToolSetTool1: IToolData = { + id: 'internalToolSetTool1', + toolReferenceName: 'internalToolSetTool1RefName', + modelDescription: 'Test Internal Tool Set 1', + displayName: 'InternalToolSet1 Display Name', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(internalToolSetTool1)); + + const internalToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'internalToolSet', + 'internalToolSetRefName', + { description: 'Test Set' } + )); + store.add(internalToolSet.addTool(internalToolSetTool1)); + + /** User Tool Set with tool1 */ + + const userToolSet = store.add(service.createToolSet( + { type: 'user', label: 'User', file: URI.file('/test/userToolSet.json') }, + 'userToolSet', + 'userToolSetRefName', + { description: 'Test Set' } + )); + store.add(userToolSet.addTool(tool2)); + + /** MCP tool in a MCP tool set */ + + const mcpDataSource: ToolDataSource = { type: 'mcp', label: 'My MCP Server', serverLabel: 'MCP Server', instructions: undefined, collectionId: 'testMCPCollection', definitionId: 'testMCPDefId' }; + const mcpTool1: IToolData = { + id: 'mcpTool1', + toolReferenceName: 'mcpTool1RefName', + modelDescription: 'Test MCP Tool 1', + displayName: 'McpTool1 Display Name', + source: mcpDataSource, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(mcpTool1)); + + const mcpToolSet = store.add(service.createToolSet( + mcpDataSource, + 'mcpToolSet', + 'mcpToolSetRefName', + { description: 'MCP Test ToolSet' } + )); + store.add(mcpToolSet.addTool(mcpTool1)); + } + + + test('registerToolData', () => { + const toolData: IToolData = { + id: 'testTool', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const disposable = service.registerToolData(toolData); + assert.strictEqual(service.getTool('testTool')?.id, 'testTool'); + disposable.dispose(); + assert.strictEqual(service.getTool('testTool'), undefined); + }); + + test('registerToolImplementation', () => { + const toolData: IToolData = { + id: 'testTool', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolData)); + + const toolImpl: IToolImpl = { + invoke: async () => ({ content: [{ kind: 'text', value: 'result' }] }), + }; + + store.add(service.registerToolImplementation('testTool', toolImpl)); + assert.strictEqual(service.getTool('testTool')?.id, 'testTool'); + }); + + test('getTools', () => { + contextKeyService.createKey('testKey', true); + const toolData1: IToolData = { + id: 'testTool1', + modelDescription: 'Test Tool 1', + when: ContextKeyEqualsExpr.create('testKey', false), + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const toolData2: IToolData = { + id: 'testTool2', + modelDescription: 'Test Tool 2', + when: ContextKeyEqualsExpr.create('testKey', true), + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const toolData3: IToolData = { + id: 'testTool3', + modelDescription: 'Test Tool 3', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolData1)); + store.add(service.registerToolData(toolData2)); + store.add(service.registerToolData(toolData3)); + + const tools = Array.from(service.getTools(undefined)); + assert.strictEqual(tools.length, 2); + assert.strictEqual(tools[0].id, 'testTool2'); + assert.strictEqual(tools[1].id, 'testTool3'); + }); + + test('getToolByName', () => { + contextKeyService.createKey('testKey', true); + const toolData1: IToolData = { + id: 'testTool1', + toolReferenceName: 'testTool1', + modelDescription: 'Test Tool 1', + when: ContextKeyEqualsExpr.create('testKey', false), + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const toolData2: IToolData = { + id: 'testTool2', + toolReferenceName: 'testTool2', + modelDescription: 'Test Tool 2', + when: ContextKeyEqualsExpr.create('testKey', true), + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const toolData3: IToolData = { + id: 'testTool3', + toolReferenceName: 'testTool3', + modelDescription: 'Test Tool 3', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolData1)); + store.add(service.registerToolData(toolData2)); + store.add(service.registerToolData(toolData3)); + + // getToolByName searches all tools regardless of when clause + assert.strictEqual(service.getToolByName('testTool1')?.id, 'testTool1'); + assert.strictEqual(service.getToolByName('testTool2')?.id, 'testTool2'); + assert.strictEqual(service.getToolByName('testTool3')?.id, 'testTool3'); + }); + + test('invokeTool', async () => { + const toolData: IToolData = { + id: 'testTool', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolData)); + + const toolImpl: IToolImpl = { + invoke: async (invocation) => { + assert.strictEqual(invocation.callId, '1'); + assert.strictEqual(invocation.toolId, 'testTool'); + assert.deepStrictEqual(invocation.parameters, { a: 1 }); + return { content: [{ kind: 'text', value: 'result' }] }; + } + }; + + store.add(service.registerToolImplementation('testTool', toolImpl)); + + const dto: IToolInvocation = { + callId: '1', + toolId: 'testTool', + tokenBudget: 100, + parameters: { + a: 1 + }, + context: undefined, + }; + + const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); + assert.strictEqual(result.content[0].value, 'result'); + }); + + test('invocation parameters are overridden by input toolSpecificData', async () => { + const rawInput = { b: 2 }; + const tool = registerToolForTest(service, store, 'testToolInputOverride', { + prepareToolInvocation: async () => ({ + toolSpecificData: { kind: 'input', rawInput } satisfies IChatToolInputInvocationData, + confirmationMessages: { + title: 'a', + message: 'b', + } + }), + invoke: async (invocation) => { + // The service should replace parameters with rawInput and strip toolSpecificData + assert.deepStrictEqual(invocation.parameters, rawInput); + assert.strictEqual(invocation.toolSpecificData, undefined); + return { content: [{ kind: 'text', value: 'ok' }] }; + }, + }); + + const sessionId = 'sessionId'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture }); + const dto = tool.makeDto({ a: 1 }, { sessionId }); + + const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await invokeP; + assert.strictEqual(result.content[0].value, 'ok'); + }); + + test('chat invocation injects input toolSpecificData for confirmation when alwaysDisplayInputOutput', async () => { + const toolData: IToolData = { + id: 'testToolDisplayIO', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + alwaysDisplayInputOutput: true, + }; + + const tool = registerToolForTest(service, store, 'testToolDisplayIO', { + prepareToolInvocation: async () => ({ + confirmationMessages: { title: 'Confirm', message: 'Proceed?' } + }), + invoke: async () => ({ content: [{ kind: 'text', value: 'done' }] }), + }, toolData); + + const sessionId = 'sessionId-io'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-io', capture }); + + const dto = tool.makeDto({ a: 1 }, { sessionId }); + + const invokeP = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published, 'expected ChatToolInvocation to be published'); + assert.strictEqual(published.toolId, tool.id); + // The service should have injected input toolSpecificData with the raw parameters + assert.strictEqual(published.toolSpecificData?.kind, 'input'); + assert.deepStrictEqual(published.toolSpecificData?.rawInput, dto.parameters); + + // Confirm to let invoke proceed + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await invokeP; + assert.strictEqual(result.content[0].value, 'done'); + }); + + test('chat invocation waits for user confirmation before invoking', async () => { + const toolData: IToolData = { + id: 'testToolConfirm', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + let invoked = false; + const tool = registerToolForTest(service, store, toolData.id, { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Confirm', message: 'Go?' } }), + invoke: async () => { + invoked = true; + return { content: [{ kind: 'text', value: 'ran' }] }; + }, + }, toolData); + + const sessionId = 'sessionId-confirm'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-confirm', capture }); + + const dto = tool.makeDto({ x: 1 }, { sessionId }); + + const promise = service.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + assert.ok(published, 'expected ChatToolInvocation to be published'); + assert.strictEqual(invoked, false, 'invoke should not run before confirmation'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(invoked, true, 'invoke should have run after confirmation'); + assert.strictEqual(result.content[0].value, 'ran'); + }); + + test('cancel tool call', async () => { + const toolBarrier = new Barrier(); + const tool = registerToolForTest(service, store, 'testTool', { + invoke: async (invocation, countTokens, progress, cancelToken) => { + assert.strictEqual(invocation.callId, '1'); + assert.strictEqual(invocation.toolId, 'testTool'); + assert.deepStrictEqual(invocation.parameters, { a: 1 }); + await toolBarrier.wait(); + if (cancelToken.isCancellationRequested) { + throw new CancellationError(); + } else { + throw new Error('Tool call should be cancelled'); + } + } + }); + + const sessionId = 'sessionId'; + const requestId = 'requestId'; + const dto = tool.makeDto({ a: 1 }, { sessionId }); + stubGetSession(chatService, sessionId, { requestId }); + const toolPromise = service.invokeTool(dto, async () => 0, CancellationToken.None); + service.cancelToolCallsForRequest(requestId); + toolBarrier.open(); + await assert.rejects(toolPromise, err => { + return isCancellationError(err); + }, 'Expected tool call to be cancelled'); + }); + + test('toFullReferenceNames', () => { + setupToolsForTest(service, store); + + const tool1 = service.getToolByFullReferenceName('tool1RefName'); + const extTool1 = service.getToolByFullReferenceName('my.extension/extTool1RefName'); + const mcpToolSet = service.getToolByFullReferenceName('mcpToolSetRefName/*'); + const mcpTool1 = service.getToolByFullReferenceName('mcpToolSetRefName/mcpTool1RefName'); + const internalToolSet = service.getToolByFullReferenceName('internalToolSetRefName'); + const internalTool = service.getToolByFullReferenceName('internalToolSetRefName/internalToolSetTool1RefName'); + const userToolSet = service.getToolSet('userToolSet'); + const unknownTool = { id: 'unregisteredTool', toolReferenceName: 'unregisteredToolRefName', modelDescription: 'Unregistered Tool', displayName: 'Unregistered Tool', source: ToolDataSource.Internal, canBeReferencedInPrompt: true } satisfies IToolData; + const unknownToolSet = service.createToolSet(ToolDataSource.Internal, 'unknownToolSet', 'unknownToolSetRefName', { description: 'Unknown Test Set' }); + unknownToolSet.dispose(); // unregister the set + assert.ok(tool1); + assert.ok(extTool1); + assert.ok(mcpTool1); + assert.ok(mcpToolSet); + assert.ok(internalToolSet); + assert.ok(internalTool); + assert.ok(userToolSet); + + // Test with some enabled tool + { + // creating a map by hand is a no-go, we just do it for this test + const map = new Map([[tool1, true], [extTool1, true], [mcpToolSet, true], [mcpTool1, true]]); + const fullReferenceNames = service.toFullReferenceNames(map); + const expectedFullReferenceNames = ['tool1RefName', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*']; + assert.deepStrictEqual(fullReferenceNames.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Test with user data + { + // creating a map by hand is a no-go, we just do it for this test + const map = new Map([[tool1, true], [userToolSet, true], [internalToolSet, false], [internalTool, true]]); + const fullReferenceNames = service.toFullReferenceNames(map); + const expectedFullReferenceNames = ['tool1RefName', 'internalToolSetRefName/internalToolSetTool1RefName']; + assert.deepStrictEqual(fullReferenceNames.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Test with unknown tool and tool set + { + // creating a map by hand is a no-go, we just do it for this test + const map = new Map([[unknownTool, true], [unknownToolSet, true], [internalToolSet, true], [internalTool, true]]); + const fullReferenceNames = service.toFullReferenceNames(map); + const expectedFullReferenceNames = ['internalToolSetRefName']; + assert.deepStrictEqual(fullReferenceNames.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + }); + + test('toToolAndToolSetEnablementMap', () => { + setupToolsForTest(service, store); + + const allFullReferenceNames = [ + 'tool1RefName', + 'Tool2 Display Name', + 'my.extension/extTool1RefName', + 'mcpToolSetRefName/*', + 'mcpToolSetRefName/mcpTool1RefName', + 'internalToolSetRefName', + 'internalToolSetRefName/internalToolSetTool1RefName', + 'vscode', + 'execute', + 'read', + 'agent' + ]; + const numOfTools = allFullReferenceNames.length + 1; // +1 for userToolSet which has no full reference name but is a tool set + + const tool1 = service.getToolByFullReferenceName('tool1RefName'); + const tool2 = service.getToolByFullReferenceName('Tool2 Display Name'); + const extTool1 = service.getToolByFullReferenceName('my.extension/extTool1RefName'); + const mcpToolSet = service.getToolByFullReferenceName('mcpToolSetRefName/*'); + const mcpTool1 = service.getToolByFullReferenceName('mcpToolSetRefName/mcpTool1RefName'); + const internalToolSet = service.getToolByFullReferenceName('internalToolSetRefName'); + const internalTool = service.getToolByFullReferenceName('internalToolSetRefName/internalToolSetTool1RefName'); + const userToolSet = service.getToolSet('userToolSet'); + const vscodeToolSet = service.getToolSet('vscode'); + const executeToolSet = service.getToolSet('execute'); + const readToolSet = service.getToolSet('read'); + const agentToolSet = service.getToolSet('agent'); + assert.ok(tool1); + assert.ok(tool2); + assert.ok(extTool1); + assert.ok(mcpTool1); + assert.ok(mcpToolSet); + assert.ok(internalToolSet); + assert.ok(internalTool); + assert.ok(userToolSet); + assert.ok(vscodeToolSet); + assert.ok(executeToolSet); + assert.ok(readToolSet); + assert.ok(agentToolSet); + // Test with enabled tool + { + const fullReferenceNames = ['tool1RefName']; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 1, 'Expected 1 tool to be enabled'); + assert.strictEqual(result1.get(tool1), true, 'tool1 should be enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + assert.deepStrictEqual(fullReferenceNames1.sort(), fullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + + } + // Test with multiple enabled tools + { + const fullReferenceNames = ['my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName/internalToolSetTool1RefName']; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 4, 'Expected 4 tools to be enabled'); + assert.strictEqual(result1.get(extTool1), true, 'extTool1 should be enabled'); + assert.strictEqual(result1.get(mcpToolSet), true, 'mcpToolSet should be enabled'); + assert.strictEqual(result1.get(mcpTool1), true, 'mcpTool1 should be enabled because the set is enabled'); + assert.strictEqual(result1.get(internalTool), true, 'internalTool should be enabled because the set is enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + assert.deepStrictEqual(fullReferenceNames1.sort(), fullReferenceNames.sort(), 'toFullReferenceNames should return the expected names'); + } + // Test with all enabled tools, redundant names + { + const result1 = service.toToolAndToolSetEnablementMap(allFullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 12, 'Expected 12 tools to be enabled'); // +4 including the vscode, execute, read, agent toolsets + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + const expectedFullReferenceNames = ['tool1RefName', 'Tool2 Display Name', 'my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName', 'vscode', 'execute', 'read', 'agent']; + assert.deepStrictEqual(fullReferenceNames1.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Test with no enabled tools + { + const fullReferenceNames: string[] = []; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 0, 'Expected 0 tools to be enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + assert.deepStrictEqual(fullReferenceNames1.sort(), fullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Test with unknown tool + { + const fullReferenceNames: string[] = ['unknownToolRefName']; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 0, 'Expected 0 tools to be enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + assert.deepStrictEqual(fullReferenceNames1.sort(), [], 'toFullReferenceNames should return no enabled names'); + } + // Test with legacy tool names + { + const fullReferenceNames: string[] = ['extTool1RefName', 'mcpToolSetRefName', 'internalToolSetTool1RefName']; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 4, 'Expected 4 tools to be enabled'); + assert.strictEqual(result1.get(extTool1), true, 'extTool1 should be enabled'); + assert.strictEqual(result1.get(mcpToolSet), true, 'mcpToolSet should be enabled'); + assert.strictEqual(result1.get(mcpTool1), true, 'mcpTool1 should be enabled because the set is enabled'); + assert.strictEqual(result1.get(internalTool), true, 'internalTool should be enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + const expectedFullReferenceNames: string[] = ['my.extension/extTool1RefName', 'mcpToolSetRefName/*', 'internalToolSetRefName/internalToolSetTool1RefName']; + assert.deepStrictEqual(fullReferenceNames1.sort(), expectedFullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Test with tool in user tool set + { + const fullReferenceNames = ['Tool2 Display Name']; + const result1 = service.toToolAndToolSetEnablementMap(fullReferenceNames, undefined, undefined); + assert.strictEqual(result1.size, numOfTools, `Expected ${numOfTools} tools and tool sets`); + assert.strictEqual([...result1.entries()].filter(([_, enabled]) => enabled).length, 2, 'Expected 1 tool and user tool set to be enabled'); + assert.strictEqual(result1.get(tool2), true, 'tool2 should be enabled'); + assert.strictEqual(result1.get(userToolSet), true, 'userToolSet should be enabled'); + + const fullReferenceNames1 = service.toFullReferenceNames(result1); + assert.deepStrictEqual(fullReferenceNames1.sort(), fullReferenceNames.sort(), 'toFullReferenceNames should return the original enabled names'); + + } + }); + + 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.getFullReferenceName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, undefined); + + assert.strictEqual(result.get(toolData1), true, 'individual tool should be enabled'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), enabledNames.sort(), 'toFullReferenceNames should return the original enabled names'); + }); + + test('toToolAndToolSetEnablementMap with tool sets', () => { + // Register individual tools + const toolData1: IToolData = { + id: 'tool1', + toolReferenceName: 'refTool1', + modelDescription: 'Test Tool 1', + displayName: 'Test Tool 1', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + + const toolData2: IToolData = { + id: 'tool2', + modelDescription: 'Test Tool 2', + displayName: 'Test Tool 2', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + + store.add(service.registerToolData(toolData1)); + store.add(service.registerToolData(toolData2)); + + // Create a tool set + const toolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'testToolSet', + 'refToolSet', + { description: 'Test Tool Set' } + )); + + // Add tools to the tool set + const toolSetTool1: IToolData = { + id: 'toolSetTool1', + modelDescription: 'Tool Set Tool 1', + displayName: 'Tool Set Tool 1', + source: ToolDataSource.Internal, + }; + + const toolSetTool2: IToolData = { + id: 'toolSetTool2', + modelDescription: 'Tool Set Tool 2', + displayName: 'Tool Set Tool 2', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolSetTool1)); + store.add(service.registerToolData(toolSetTool2)); + store.add(toolSet.addTool(toolSetTool1)); + store.add(toolSet.addTool(toolSetTool2)); + + // Test enabling the tool set + const enabledNames = [toolSet, toolData1].map(t => service.getFullReferenceName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, undefined); + + 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 fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), enabledNames.sort(), 'toFullReferenceNames should return the original enabled 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)); + + const unregisteredToolData: IToolData = { + id: 'toolX', + toolReferenceName: 'refToolX', + modelDescription: 'Test Tool X', + displayName: 'Test Tool X', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + + // Test with non-existent tool names + const enabledNames = [toolData, unregisteredToolData].map(t => service.getFullReferenceName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, undefined); + + assert.strictEqual(result.get(toolData), true, 'existing tool should be enabled'); + // Non-existent tools should not appear in the result map + assert.strictEqual(result.get(unregisteredToolData), undefined, 'non-existent tool should not be in result'); + + const fullReferenceNames = service.toFullReferenceNames(result); + const expectedNames = [service.getFullReferenceName(toolData)]; // Only the existing tool + assert.deepStrictEqual(fullReferenceNames.sort(), expectedNames.sort(), 'toFullReferenceNames should return the original enabled names'); + + }); + + + test('toToolAndToolSetEnablementMap with legacy names', () => { + // Test that legacy tool reference names and legacy toolset names work correctly + + // Create a tool with legacy reference names + const toolWithLegacy: IToolData = { + id: 'newTool', + toolReferenceName: 'newToolRef', + modelDescription: 'New Tool', + displayName: 'New Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + legacyToolReferenceFullNames: ['oldToolName', 'deprecatedToolName'] + }; + store.add(service.registerToolData(toolWithLegacy)); + + // Create a tool set with legacy names + const toolSetWithLegacy = store.add(service.createToolSet( + ToolDataSource.Internal, + 'newToolSet', + 'newToolSetRef', + { description: 'New Tool Set', legacyFullNames: ['oldToolSet', 'deprecatedToolSet'] } + )); + + // Create a tool in the toolset + const toolInSet: IToolData = { + id: 'toolInSet', + toolReferenceName: 'toolInSetRef', + modelDescription: 'Tool In Set', + displayName: 'Tool In Set', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(toolInSet)); + store.add(toolSetWithLegacy.addTool(toolInSet)); + + // Test 1: Using legacy tool reference name should enable the tool + { + const result = service.toToolAndToolSetEnablementMap(['oldToolName'], undefined, undefined); + assert.strictEqual(result.get(toolWithLegacy), true, 'tool should be enabled via legacy name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolRef'], 'should return current full reference name, not legacy'); + } + + // Test 2: Using another legacy tool reference name should also work + { + const result = service.toToolAndToolSetEnablementMap(['deprecatedToolName'], undefined, undefined); + assert.strictEqual(result.get(toolWithLegacy), true, 'tool should be enabled via another legacy name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolRef'], 'should return current full reference name, not legacy'); + } + + // Test 3: Using legacy toolset name should enable the entire toolset + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolSetWithLegacy), true, 'toolset should be enabled via legacy name'); + assert.strictEqual(result.get(toolInSet), true, 'tool in set should be enabled when set is enabled via legacy name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolSetRef'], 'should return current full reference name, not legacy'); + } + + // Test 4: Using deprecated toolset name should also work + { + const result = service.toToolAndToolSetEnablementMap(['deprecatedToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolSetWithLegacy), true, 'toolset should be enabled via another legacy name'); + assert.strictEqual(result.get(toolInSet), true, 'tool in set should be enabled when set is enabled via legacy name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolSetRef'], 'should return current full reference name, not legacy'); + } + + // Test 5: Mix of current and legacy names + { + const result = service.toToolAndToolSetEnablementMap(['newToolRef', 'oldToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolWithLegacy), true, 'tool should be enabled via current name'); + assert.strictEqual(result.get(toolSetWithLegacy), true, 'toolset should be enabled via legacy name'); + assert.strictEqual(result.get(toolInSet), true, 'tool in set should be enabled'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), ['newToolRef', 'newToolSetRef'].sort(), 'should return current full reference names'); + } + + // Test 6: Using legacy names and current names together (redundant but should work) + { + const result = service.toToolAndToolSetEnablementMap(['newToolRef', 'oldToolName', 'deprecatedToolName'], undefined, undefined); + assert.strictEqual(result.get(toolWithLegacy), true, 'tool should be enabled (redundant legacy names should not cause issues)'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolRef'], 'should return single current full reference name'); + } + }); + + test('toToolAndToolSetEnablementMap with orphaned toolset in legacy names', () => { + // Test that when a tool has a legacy name with a toolset prefix, but that toolset no longer exists, + // we can enable the tool by either the full legacy name OR just the orphaned toolset name + + // Create a tool that used to be in 'oldToolSet/oldToolName' but now is just 'newToolRef' + const toolWithOrphanedToolSet: IToolData = { + id: 'migratedTool', + toolReferenceName: 'newToolRef', + modelDescription: 'Migrated Tool', + displayName: 'Migrated Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + legacyToolReferenceFullNames: ['oldToolSet/oldToolName'] + }; + store.add(service.registerToolData(toolWithOrphanedToolSet)); + + // Test 1: Using the full legacy name should enable the tool + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet/oldToolName'], undefined, undefined); + assert.strictEqual(result.get(toolWithOrphanedToolSet), true, 'tool should be enabled via full legacy name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolRef'], 'should return current full reference name'); + } + + // Test 2: Using just the orphaned toolset name should also enable the tool + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolWithOrphanedToolSet), true, 'tool should be enabled via orphaned toolset name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames, ['newToolRef'], 'should return current full reference name'); + } + + // Test 3: Multiple tools from the same orphaned toolset + const anotherToolFromOrphanedSet: IToolData = { + id: 'anotherMigratedTool', + toolReferenceName: 'anotherNewToolRef', + modelDescription: 'Another Migrated Tool', + displayName: 'Another Migrated Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + legacyToolReferenceFullNames: ['oldToolSet/anotherOldToolName'] + }; + store.add(service.registerToolData(anotherToolFromOrphanedSet)); + + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolWithOrphanedToolSet), true, 'first tool should be enabled via orphaned toolset name'); + assert.strictEqual(result.get(anotherToolFromOrphanedSet), true, 'second tool should also be enabled via orphaned toolset name'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), ['newToolRef', 'anotherNewToolRef'].sort(), 'should return both current full reference names'); + } + + // Test 4: Orphaned toolset name should NOT enable tools that weren't in that toolset + const unrelatedTool: IToolData = { + id: 'unrelatedTool', + toolReferenceName: 'unrelatedToolRef', + modelDescription: 'Unrelated Tool', + displayName: 'Unrelated Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + legacyToolReferenceFullNames: ['differentToolSet/oldName'] + }; + store.add(service.registerToolData(unrelatedTool)); + + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet'], undefined, undefined); + assert.strictEqual(result.get(toolWithOrphanedToolSet), true, 'tool from oldToolSet should be enabled'); + assert.strictEqual(result.get(anotherToolFromOrphanedSet), true, 'another tool from oldToolSet should be enabled'); + assert.strictEqual(result.get(unrelatedTool), false, 'tool from different toolset should NOT be enabled'); + + const fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), ['newToolRef', 'anotherNewToolRef'].sort(), 'should only return tools from oldToolSet'); + } + + // Test 5: If a toolset with the same name exists, it should take precedence over orphaned toolset mapping + const newToolSetWithSameName = store.add(service.createToolSet( + ToolDataSource.Internal, + 'recreatedToolSet', + 'oldToolSet', // Same name as the orphaned toolset + { description: 'Recreated Tool Set' } + )); + + const toolInRecreatedSet: IToolData = { + id: 'toolInRecreatedSet', + toolReferenceName: 'toolInRecreatedSetRef', + modelDescription: 'Tool In Recreated Set', + displayName: 'Tool In Recreated Set', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(toolInRecreatedSet)); + store.add(newToolSetWithSameName.addTool(toolInRecreatedSet)); + + { + const result = service.toToolAndToolSetEnablementMap(['oldToolSet'], undefined, undefined); + // Now 'oldToolSet' should enable BOTH the recreated toolset AND the tools with legacy names pointing to oldToolSet + assert.strictEqual(result.get(newToolSetWithSameName), true, 'recreated toolset should be enabled'); + assert.strictEqual(result.get(toolInRecreatedSet), true, 'tool in recreated set should be enabled'); + // The tools with legacy toolset names should ALSO be enabled because their legacy names match + assert.strictEqual(result.get(toolWithOrphanedToolSet), true, 'tool with legacy toolset should still be enabled'); + assert.strictEqual(result.get(anotherToolFromOrphanedSet), true, 'another tool with legacy toolset should still be enabled'); + + const fullReferenceNames = service.toFullReferenceNames(result); + // Should return the toolset name plus the individual tools that were enabled via legacy names + assert.deepStrictEqual(fullReferenceNames.sort(), ['oldToolSet', 'newToolRef', 'anotherNewToolRef'].sort(), 'should return toolset and individual tools'); + } + }); + + test('toToolAndToolSetEnablementMap map Github to VSCode tools', () => { + const runInTerminalToolData: IToolData = { + id: 'runInTerminalId', + toolReferenceName: 'runInTerminal', + modelDescription: 'runInTerminal Description', + displayName: 'runInTerminal displayName', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: false, + }; + + store.add(service.registerToolData(runInTerminalToolData)); + store.add(service.executeToolSet.addTool(runInTerminalToolData)); + + + const runSubagentToolData: IToolData = { + id: 'runSubagentId', + toolReferenceName: 'runSubagent', + modelDescription: 'runSubagent Description', + displayName: 'runSubagent displayName', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: false, + }; + + store.add(service.registerToolData(runSubagentToolData)); + store.add(service.agentToolSet.addTool(runSubagentToolData)); + + const githubMcpDataSource: ToolDataSource = { type: 'mcp', label: 'Github', serverLabel: 'Github MCP Server', instructions: undefined, collectionId: 'githubMCPCollection', definitionId: 'githubMCPDefId' }; + const githubMcpTool1: IToolData = { + id: 'create_branch', + toolReferenceName: 'create_branch', + modelDescription: 'Test Github MCP Tool 1', + displayName: 'Create Branch', + source: githubMcpDataSource, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(githubMcpTool1)); + + const githubMcpToolSet = store.add(service.createToolSet( + githubMcpDataSource, + 'githubMcpToolSet', + 'github/github-mcp-server', + { description: 'Github MCP Test ToolSet' } + )); + store.add(githubMcpToolSet.addTool(githubMcpTool1)); + + assert.equal(githubMcpToolSet.referenceName, 'github', 'github/github-mcp-server will be normalized to github'); + + const playwrightMcpDataSource: ToolDataSource = { type: 'mcp', label: 'playwright', serverLabel: 'playwright MCP Server', instructions: undefined, collectionId: 'playwrightMCPCollection', definitionId: 'playwrightMCPDefId' }; + const playwrightMcpTool1: IToolData = { + id: 'browser_click', + toolReferenceName: 'browser_click', + modelDescription: 'Test playwright MCP Tool 1', + displayName: 'Create Branch', + source: playwrightMcpDataSource, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(playwrightMcpTool1)); + + const playwrightMcpToolSet = store.add(service.createToolSet( + playwrightMcpDataSource, + 'playwrightMcpToolSet', + 'microsoft/playwright-mcp', + { description: 'playwright MCP Test ToolSet' } + )); + store.add(playwrightMcpToolSet.addTool(playwrightMcpTool1)); + + const deprecated = service.getDeprecatedFullReferenceNames(); + const deprecatesTo = (key: string): string[] | undefined => { + const values = deprecated.get(key); + return values ? Array.from(values).sort() : undefined; + }; + + assert.equal(playwrightMcpToolSet.referenceName, 'playwright', 'microsoft/playwright-mcp will be normalized to playwright'); + + { + const toolNames = ['custom-agent', 'shell']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(service.executeToolSet), true, 'execute should be enabled'); + assert.strictEqual(result.get(service.agentToolSet), true, 'agent should be enabled'); + + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, [SpecedToolAliases.agent, SpecedToolAliases.execute].sort(), 'toFullReferenceNames should return the VS Code tool names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [service.agentToolSet, service.executeToolSet]); + + assert.deepStrictEqual(deprecatesTo('custom-agent'), [SpecedToolAliases.agent], 'customAgent should map to agent'); + assert.deepStrictEqual(deprecatesTo('shell'), [SpecedToolAliases.execute], 'shell is now execute'); + } + { + const toolNames = ['github/*', 'playwright/*']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpToolSet), true, 'githubMcpToolSet should be enabled'); + assert.strictEqual(result.get(playwrightMcpToolSet), true, 'playwrightMcpToolSet should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/*', 'playwright/*'], 'toFullReferenceNames should return the VS Code tool names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpToolSet, playwrightMcpToolSet]); + + assert.deepStrictEqual(deprecatesTo('github/*'), undefined, 'github/* is fine'); + assert.deepStrictEqual(deprecatesTo('playwright/*'), undefined, 'playwright/* is fine'); + } + + { + // the speced names should work and not be altered + const toolNames = ['github/create_branch', 'playwright/browser_click']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); + assert.strictEqual(result.get(playwrightMcpTool1), true, 'playwrightMcpTool1 should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/create_branch', 'playwright/browser_click'], 'toFullReferenceNames should return the speced names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpTool1, playwrightMcpTool1]); + + assert.deepStrictEqual(deprecatesTo('github/create_branch'), undefined, 'github/create_branch is fine'); + assert.deepStrictEqual(deprecatesTo('playwright/browser_click'), undefined, 'playwright/browser_click is fine'); + } + + { + // using the old MCP full names should also work + const toolNames = ['github/github-mcp-server/*', 'microsoft/playwright-mcp/*']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpToolSet), true, 'githubMcpToolSet should be enabled'); + assert.strictEqual(result.get(playwrightMcpToolSet), true, 'playwrightMcpToolSet should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/*', 'playwright/*'], 'toFullReferenceNames should return the speced names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpToolSet, playwrightMcpToolSet]); + + assert.deepStrictEqual(deprecatesTo('github/github-mcp-server/*'), ['github/*']); + assert.deepStrictEqual(deprecatesTo('microsoft/playwright-mcp/*'), ['playwright/*']); + } + { + // using the old MCP full names should also work + const toolNames = ['github/github-mcp-server/create_branch', 'microsoft/playwright-mcp/browser_click']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); + assert.strictEqual(result.get(playwrightMcpTool1), true, 'playwrightMcpTool1 should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/create_branch', 'playwright/browser_click'], 'toFullReferenceNames should return the speced names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpTool1, playwrightMcpTool1]); + + assert.deepStrictEqual(deprecatesTo('github/github-mcp-server/create_branch'), ['github/create_branch']); + assert.deepStrictEqual(deprecatesTo('microsoft/playwright-mcp/browser_click'), ['playwright/browser_click']); + } + + { + // using the latest MCP full names should also work + const toolNames = ['io.github.github/github-mcp-server/*', 'com.microsoft/playwright-mcp/*']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpToolSet), true, 'githubMcpToolSet should be enabled'); + assert.strictEqual(result.get(playwrightMcpToolSet), true, 'playwrightMcpToolSet should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/*', 'playwright/*'], 'toFullReferenceNames should return the speced names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpToolSet, playwrightMcpToolSet]); + + assert.deepStrictEqual(deprecatesTo('io.github.github/github-mcp-server/*'), ['github/*']); + assert.deepStrictEqual(deprecatesTo('com.microsoft/playwright-mcp/*'), ['playwright/*']); + } + + { + // using the latest MCP full names should also work + const toolNames = ['io.github.github/github-mcp-server/create_branch', 'com.microsoft/playwright-mcp/browser_click']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); + assert.strictEqual(result.get(playwrightMcpTool1), true, 'playwrightMcpTool1 should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/create_branch', 'playwright/browser_click'], 'toFullReferenceNames should return the speced names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpTool1, playwrightMcpTool1]); + + assert.deepStrictEqual(deprecatesTo('io.github.github/github-mcp-server/create_branch'), ['github/create_branch']); + assert.deepStrictEqual(deprecatesTo('com.microsoft/playwright-mcp/browser_click'), ['playwright/browser_click']); + } + + { + // using the old MCP full names should also work + const toolNames = ['github-mcp-server/create_branch']; + const result = service.toToolAndToolSetEnablementMap(toolNames, undefined, undefined); + + assert.strictEqual(result.get(githubMcpTool1), true, 'githubMcpTool1 should be enabled'); + const fullReferenceNames = service.toFullReferenceNames(result).sort(); + assert.deepStrictEqual(fullReferenceNames, ['github/create_branch'], 'toFullReferenceNames should return the VS Code tool names'); + + assert.deepStrictEqual(toolNames.map(name => service.getToolByFullReferenceName(name)), [githubMcpTool1]); + + assert.deepStrictEqual(deprecatesTo('github-mcp-server/create_branch'), ['github/create_branch']); + } + + }); + + test('accessibility signal for tool confirmation', async () => { + // Create a test configuration service with proper settings + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration('chat.tools.global.autoApprove', false); + testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); + + // Create a test accessibility service that simulates screen reader being enabled + const testAccessibilityService = new class extends TestAccessibilityService { + override isScreenReaderOptimized(): boolean { return true; } + }(); + + // Create a test accessibility signal service that tracks calls + const testAccessibilitySignalService = new TestAccessibilitySignalService(); + + // Create a new service instance with the test services + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(IAccessibilityService, testAccessibilityService); + instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + const toolData: IToolData = { + id: 'testAccessibilityTool', + modelDescription: 'Test Accessibility Tool', + displayName: 'Test Accessibility Tool', + source: ToolDataSource.Internal, + }; + + const tool = registerToolForTest(testService, store, toolData.id, { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Accessibility Test', message: 'Testing accessibility signal' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }), + }, toolData); + + const sessionId = 'sessionId-accessibility'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-accessibility', capture }); + + const dto = tool.makeDto({ param: 'value' }, { sessionId }); + + const promise = testService.invokeTool(dto, async () => 0, CancellationToken.None); + const published = await waitForPublishedInvocation(capture); + + assert.ok(published, 'expected ChatToolInvocation to be published'); + assert.ok(published.confirmationMessages, 'should have confirmation messages'); + + // The accessibility signal should have been played + assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'accessibility signal should have been played once'); + const signalCall = testAccessibilitySignalService.signalPlayedCalls[0]; + assert.strictEqual(signalCall.signal, AccessibilitySignal.chatUserActionRequired, 'correct signal should be played'); + assert.ok(signalCall.options?.customAlertMessage.includes('Accessibility Test'), 'alert message should include tool title'); + assert.ok(signalCall.options?.customAlertMessage.includes('Chat confirmation required'), 'alert message should include confirmation text'); + + // Complete the invocation + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'executed'); + }); + + test('accessibility signal respects autoApprove configuration', async () => { + // Create a test configuration service with auto-approve enabled + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true); + testConfigService.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); + + // Create a test accessibility service that simulates screen reader being enabled + const testAccessibilityService = new class extends TestAccessibilityService { + override isScreenReaderOptimized(): boolean { return true; } + }(); + + // Create a test accessibility signal service that tracks calls + const testAccessibilitySignalService = new TestAccessibilitySignalService(); + + // Create a new service instance with the test services + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(IAccessibilityService, testAccessibilityService); + instaService.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + const toolData: IToolData = { + id: 'testAutoApproveTool', + modelDescription: 'Test Auto Approve Tool', + displayName: 'Test Auto Approve Tool', + source: ToolDataSource.Internal, + }; + + const tool = registerToolForTest(testService, store, toolData.id, { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Approve Test', message: 'Testing auto approve' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] }), + }, toolData); + + const sessionId = 'sessionId-auto-approve'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'requestId-auto-approve', capture }); + + const dto = tool.makeDto({ config: 'test' }, { sessionId }); + + // When auto-approve is enabled, tool should complete without user intervention + const result = await testService.invokeTool(dto, async () => 0, CancellationToken.None); + + // Verify the tool completed and no accessibility signal was played + assert.strictEqual(result.content[0].value, 'auto approved'); + assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'accessibility signal should not be played when auto-approve is enabled'); + }); + + test('shouldAutoConfirm with basic configuration', async () => { + // Test basic shouldAutoConfirm behavior with simple configuration + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration('chat.tools.global.autoApprove', true); // Global enabled + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Register a tool that should be auto-approved + const autoTool = registerToolForTest(testService, store, 'autoTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'auto approved' }] }) + }); + + const sessionId = 'test-basic-config'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be auto-approved (global config = true) + const result = await testService.invokeTool( + autoTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'auto approved'); + }); + + test('shouldAutoConfirm with per-tool configuration object', async () => { + // Test per-tool configuration: { toolId: true/false } + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration('chat.tools.global.autoApprove', { + 'approvedTool': true, + 'deniedTool': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool explicitly approved + const approvedTool = registerToolForTest(testService, store, 'approvedTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should auto-approve' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'approved' }] }) + }); + + const sessionId = 'test-per-tool'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Approved tool should auto-approve + const approvedResult = await testService.invokeTool( + approvedTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(approvedResult.content[0].value, 'approved'); + + // Test that non-specified tools require confirmation (default behavior) + const unspecifiedTool = registerToolForTest(testService, store, 'unspecifiedTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Should require confirmation' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'unspecified' }] }) + }); + + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture }); + const unspecifiedPromise = testService.invokeTool( + unspecifiedTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'unspecified tool should require confirmation'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const unspecifiedResult = await unspecifiedPromise; + assert.strictEqual(unspecifiedResult.content[0].value, 'unspecified'); + }); + + test('eligibleForAutoApproval setting controls tool eligibility', async () => { + // Test the new eligibleForAutoApproval setting + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'eligibleToolRef': true, + 'ineligibleToolRef': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool explicitly marked as eligible (using toolReferenceName) - no confirmation needed + const eligibleTool = registerToolForTest(testService, store, 'eligibleTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'eligible tool ran' }] }) + }, { + toolReferenceName: 'eligibleToolRef' + }); + + const sessionId = 'test-eligible'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Eligible tool should not get default confirmation messages injected + const eligibleResult = await testService.invokeTool( + eligibleTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(eligibleResult.content[0].value, 'eligible tool ran'); + + // Tool explicitly marked as ineligible (using toolReferenceName) - must require confirmation + const ineligibleTool = registerToolForTest(testService, store, 'ineligibleTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'ineligible requires confirmation' }] }) + }, { + toolReferenceName: 'ineligibleToolRef' + }); + + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture }); + const ineligiblePromise = testService.invokeTool( + ineligibleTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'ineligible tool should require confirmation'); + assert.ok(published?.confirmationMessages?.title, 'should have default confirmation title'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const ineligibleResult = await ineligiblePromise; + assert.strictEqual(ineligibleResult.content[0].value, 'ineligible requires confirmation'); + + // Tool not specified should default to eligible - no confirmation needed + const unspecifiedTool = registerToolForTest(testService, store, 'unspecifiedTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'unspecified defaults to eligible' }] }) + }, { + toolReferenceName: 'unspecifiedToolRef' + }); + + const unspecifiedResult = await testService.invokeTool( + unspecifiedTool.makeDto({ test: 3 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(unspecifiedResult.content[0].value, 'unspecified defaults to eligible'); + }); + + test('tool content formatting with alwaysDisplayInputOutput', async () => { + // Test ensureToolDetails, formatToolInput, and toolResultToIO + const toolData: IToolData = { + id: 'formatTool', + modelDescription: 'Format Test Tool', + displayName: 'Format Test Tool', + source: ToolDataSource.Internal, + alwaysDisplayInputOutput: true + }; + + const tool = registerToolForTest(service, store, toolData.id, { + prepareToolInvocation: async () => ({}), + invoke: async (invocation) => ({ + content: [ + { kind: 'text', value: 'Text result' }, + { kind: 'data', value: { data: VSBuffer.fromByteArray([1, 2, 3]), mimeType: 'application/octet-stream' } } + ] + }) + }, toolData); + + const input = { a: 1, b: 'test', c: [1, 2, 3] }; + const result = await service.invokeTool( + tool.makeDto(input), + async () => 0, + CancellationToken.None + ); + + // Should have tool result details because alwaysDisplayInputOutput = true + assert.ok(result.toolResultDetails, 'should have toolResultDetails'); + const details = result.toolResultDetails; + assert.ok(isToolResultInputOutputDetails(details)); + + // Test formatToolInput - should be formatted JSON + const expectedInputJson = JSON.stringify(input, undefined, 2); + assert.strictEqual(details.input, expectedInputJson, 'input should be formatted JSON'); + + // Test toolResultToIO - should convert different content types + assert.strictEqual(details.output.length, 2, 'should have 2 output items'); + + // Text content + const textOutput = details.output[0]; + assert.strictEqual(textOutput.type, 'embed'); + assert.strictEqual(textOutput.isText, true); + assert.strictEqual(textOutput.value, 'Text result'); + + // Data content (base64 encoded) + const dataOutput = details.output[1]; + assert.strictEqual(dataOutput.type, 'embed'); + assert.strictEqual(dataOutput.mimeType, 'application/octet-stream'); + assert.strictEqual(dataOutput.value, 'AQID'); // base64 of [1,2,3] + }); + + test('tool error handling and telemetry', async () => { + const testTelemetryService = new TestTelemetryService(); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(configurationService)), + configurationService: () => configurationService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ITelemetryService, testTelemetryService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Test successful invocation telemetry + const successTool = registerToolForTest(testService, store, 'successTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'success' }] }) + }); + + const sessionId = 'telemetry-test'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + await testService.invokeTool( + successTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + // Check success telemetry + const successEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked'); + assert.strictEqual(successEvents.length, 1, 'should have success telemetry event'); + assert.strictEqual(successEvents[0].data.result, 'success'); + assert.strictEqual(successEvents[0].data.toolId, 'successTool'); + assert.strictEqual(successEvents[0].data.chatSessionId, sessionId); + + testTelemetryService.reset(); + + // Test error telemetry + const errorTool = registerToolForTest(testService, store, 'errorTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => { throw new Error('Tool error'); } + }); + + stubGetSession(chatService, sessionId + '2', { requestId: 'req2' }); + + try { + await testService.invokeTool( + errorTool.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), + async () => 0, + CancellationToken.None + ); + assert.fail('Should have thrown'); + } catch (err) { + // Expected + } + + // Check error telemetry + const errorEvents = testTelemetryService.events.filter(e => e.eventName === 'languageModelToolInvoked'); + assert.strictEqual(errorEvents.length, 1, 'should have error telemetry event'); + assert.strictEqual(errorEvents[0].data.result, 'error'); + assert.strictEqual(errorEvents[0].data.toolId, 'errorTool'); + }); + + test('call tracking and cleanup', async () => { + // Test that cancelToolCallsForRequest method exists and can be called + // (The detailed cancellation behavior is already tested in "cancel tool call" test) + const sessionId = 'tracking-session'; + const requestId = 'tracking-request'; + stubGetSession(chatService, sessionId, { requestId }); + + // Just verify the method exists and doesn't throw + assert.doesNotThrow(() => { + service.cancelToolCallsForRequest(requestId); + }, 'cancelToolCallsForRequest should not throw'); + + // Verify calling with non-existent request ID doesn't throw + assert.doesNotThrow(() => { + service.cancelToolCallsForRequest('non-existent-request'); + }, 'cancelToolCallsForRequest with non-existent ID should not throw'); + }); + + test('accessibility signal with different settings combinations', async () => { + const testAccessibilitySignalService = new TestAccessibilitySignalService(); + + // Test case 1: Sound enabled, announcement disabled, screen reader off + const testConfigService1 = new TestConfigurationService(); + testConfigService1.setUserConfiguration('chat.tools.global.autoApprove', false); + testConfigService1.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'on', announcement: 'off' }); + + const testAccessibilityService1 = new class extends TestAccessibilityService { + override isScreenReaderOptimized(): boolean { return false; } + }(); + + const instaService1 = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService1)), + configurationService: () => testConfigService1 + }, store); + instaService1.stub(IChatService, chatService); + instaService1.stub(IAccessibilityService, testAccessibilityService1); + instaService1.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); + instaService1.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService1 = store.add(instaService1.createInstance(LanguageModelToolsService)); + + const tool1 = registerToolForTest(testService1, store, 'soundOnlyTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Sound Test', message: 'Testing sound only' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) + }); + + const sessionId1 = 'sound-test'; + const capture1: { invocation?: any } = {}; + stubGetSession(chatService, sessionId1, { requestId: 'req1', capture: capture1 }); + + const promise1 = testService1.invokeTool(tool1.makeDto({ test: 1 }, { sessionId: sessionId1 }), async () => 0, CancellationToken.None); + const published1 = await waitForPublishedInvocation(capture1); + + // Signal should be played (sound=on, no screen reader requirement) + assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'sound should be played when sound=on'); + const call1 = testAccessibilitySignalService.signalPlayedCalls[0]; + assert.strictEqual(call1.options?.modality, undefined, 'should use default modality for sound'); + + IChatToolInvocation.confirmWith(published1, { type: ToolConfirmKind.UserAction }); + await promise1; + + testAccessibilitySignalService.reset(); + + // Test case 2: Sound auto, announcement auto, screen reader on + const testConfigService2 = new TestConfigurationService(); + testConfigService2.setUserConfiguration('chat.tools.global.autoApprove', false); + testConfigService2.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'auto', announcement: 'auto' }); + + const testAccessibilityService2 = new class extends TestAccessibilityService { + override isScreenReaderOptimized(): boolean { return true; } + }(); + + const instaService2 = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService2)), + configurationService: () => testConfigService2 + }, store); + instaService2.stub(IChatService, chatService); + instaService2.stub(IAccessibilityService, testAccessibilityService2); + instaService2.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); + instaService2.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService2 = store.add(instaService2.createInstance(LanguageModelToolsService)); + + const tool2 = registerToolForTest(testService2, store, 'autoScreenReaderTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Auto Test', message: 'Testing auto with screen reader' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) + }); + + const sessionId2 = 'auto-sr-test'; + const capture2: { invocation?: any } = {}; + stubGetSession(chatService, sessionId2, { requestId: 'req2', capture: capture2 }); + + const promise2 = testService2.invokeTool(tool2.makeDto({ test: 2 }, { sessionId: sessionId2 }), async () => 0, CancellationToken.None); + const published2 = await waitForPublishedInvocation(capture2); + + // Signal should be played (both sound and announcement enabled for screen reader) + assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 1, 'signal should be played with screen reader optimization'); + const call2 = testAccessibilitySignalService.signalPlayedCalls[0]; + assert.ok(call2.options?.customAlertMessage, 'should have custom alert message'); + assert.strictEqual(call2.options?.userGesture, true, 'should mark as user gesture'); + + IChatToolInvocation.confirmWith(published2, { type: ToolConfirmKind.UserAction }); + await promise2; + + testAccessibilitySignalService.reset(); + + // Test case 3: Sound off, announcement off - no signal + const testConfigService3 = new TestConfigurationService(); + testConfigService3.setUserConfiguration('chat.tools.global.autoApprove', false); + testConfigService3.setUserConfiguration('accessibility.signals.chatUserActionRequired', { sound: 'off', announcement: 'off' }); + + const testAccessibilityService3 = new class extends TestAccessibilityService { + override isScreenReaderOptimized(): boolean { return true; } + }(); + + const instaService3 = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService3)), + configurationService: () => testConfigService3 + }, store); + instaService3.stub(IChatService, chatService); + instaService3.stub(IAccessibilityService, testAccessibilityService3); + instaService3.stub(IAccessibilitySignalService, testAccessibilitySignalService as unknown as IAccessibilitySignalService); + instaService3.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService3 = store.add(instaService3.createInstance(LanguageModelToolsService)); + + const tool3 = registerToolForTest(testService3, store, 'offTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Off Test', message: 'Testing off settings' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'executed' }] }) + }); + + const sessionId3 = 'off-test'; + const capture3: { invocation?: any } = {}; + stubGetSession(chatService, sessionId3, { requestId: 'req3', capture: capture3 }); + + const promise3 = testService3.invokeTool(tool3.makeDto({ test: 3 }, { sessionId: sessionId3 }), async () => 0, CancellationToken.None); + const published3 = await waitForPublishedInvocation(capture3); + + // No signal should be played + assert.strictEqual(testAccessibilitySignalService.signalPlayedCalls.length, 0, 'no signal should be played when both sound and announcement are off'); + + IChatToolInvocation.confirmWith(published3, { type: ToolConfirmKind.UserAction }); + await promise3; + }); + + test('createToolSet and getToolSet', () => { + const toolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'testToolSetId', + 'testToolSetName', + { icon: undefined, description: 'Test tool set' } + )); + + // Should be able to retrieve by ID + const retrieved = service.getToolSet('testToolSetId'); + assert.ok(retrieved); + assert.strictEqual(retrieved.id, 'testToolSetId'); + assert.strictEqual(retrieved.referenceName, 'testToolSetName'); + + // Should not find non-existent tool set + assert.strictEqual(service.getToolSet('nonExistentId'), undefined); + + // Dispose should remove it + toolSet.dispose(); + assert.strictEqual(service.getToolSet('testToolSetId'), undefined); + }); + + test('getToolSetByName', () => { + store.add(service.createToolSet( + ToolDataSource.Internal, + 'toolSet1', + 'refName1' + )); + + store.add(service.createToolSet( + ToolDataSource.Internal, + 'toolSet2', + 'refName2' + )); + + // Should find by reference name + assert.strictEqual(service.getToolSetByName('refName1')?.id, 'toolSet1'); + assert.strictEqual(service.getToolSetByName('refName2')?.id, 'toolSet2'); + + // Should not find non-existent name + assert.strictEqual(service.getToolSetByName('nonExistentName'), undefined); + }); + + test('getTools with includeDisabled parameter', () => { + // Test the includeDisabled parameter behavior with context keys + contextKeyService.createKey('testKey', false); + const disabledTool: IToolData = { + id: 'disabledTool', + modelDescription: 'Disabled Tool', + displayName: 'Disabled Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('testKey', true), // Will be disabled since testKey is false + }; + + const enabledTool: IToolData = { + id: 'enabledTool', + modelDescription: 'Enabled Tool', + displayName: 'Enabled Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(disabledTool)); + store.add(service.registerToolData(enabledTool)); + + const enabledTools = Array.from(service.getTools(undefined)); + assert.strictEqual(enabledTools.length, 1, 'Should only return enabled tools'); + assert.strictEqual(enabledTools[0].id, 'enabledTool'); + + const allTools = Array.from(service.getAllToolsIncludingDisabled()); + assert.strictEqual(allTools.length, 2, 'getAllToolsIncludingDisabled should return all tools'); + }); + + test('tool registration duplicate error', () => { + const toolData: IToolData = { + id: 'duplicateTool', + modelDescription: 'Duplicate Tool', + displayName: 'Duplicate Tool', + source: ToolDataSource.Internal, + }; + + // First registration should succeed + store.add(service.registerToolData(toolData)); + + // Second registration should throw + assert.throws(() => { + service.registerToolData(toolData); + }, /Tool "duplicateTool" is already registered/); + }); + + test('tool implementation registration without data throws', () => { + const toolImpl: IToolImpl = { + invoke: async () => ({ content: [] }), + }; + + // Should throw when registering implementation for non-existent tool + assert.throws(() => { + service.registerToolImplementation('nonExistentTool', toolImpl); + }, /Tool "nonExistentTool" was not contributed/); + }); + + test('tool implementation duplicate registration throws', () => { + const toolData: IToolData = { + id: 'testTool', + modelDescription: 'Test Tool', + displayName: 'Test Tool', + source: ToolDataSource.Internal, + }; + + const toolImpl1: IToolImpl = { + invoke: async () => ({ content: [] }), + }; + + const toolImpl2: IToolImpl = { + invoke: async () => ({ content: [] }), + }; + + store.add(service.registerToolData(toolData)); + store.add(service.registerToolImplementation('testTool', toolImpl1)); + + // Second implementation should throw + assert.throws(() => { + service.registerToolImplementation('testTool', toolImpl2); + }, /Tool "testTool" already has an implementation/); + }); + + test('invokeTool with unknown tool throws', async () => { + const dto: IToolInvocation = { + callId: '1', + toolId: 'unknownTool', + tokenBudget: 100, + parameters: {}, + context: undefined, + }; + + await assert.rejects( + service.invokeTool(dto, async () => 0, CancellationToken.None), + /Tool unknownTool was not contributed/ + ); + }); + + test('invokeTool without implementation activates extension and throws if still not found', async () => { + const toolData: IToolData = { + id: 'extensionActivationTool', + modelDescription: 'Extension Tool', + displayName: 'Extension Tool', + source: ToolDataSource.Internal, + }; + + store.add(service.registerToolData(toolData)); + + const dto: IToolInvocation = { + callId: '1', + toolId: 'extensionActivationTool', + tokenBudget: 100, + parameters: {}, + context: undefined, + }; + + // Should throw after attempting extension activation + await assert.rejects( + service.invokeTool(dto, async () => 0, CancellationToken.None), + /Tool extensionActivationTool does not have an implementation registered/ + ); + }); + + test('invokeTool without context (non-chat scenario)', async () => { + const tool = registerToolForTest(service, store, 'nonChatTool', { + invoke: async (invocation) => { + assert.strictEqual(invocation.context, undefined); + return { content: [{ kind: 'text', value: 'non-chat result' }] }; + } + }); + + const dto = tool.makeDto({ test: 1 }); // No context + + const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); + assert.strictEqual(result.content[0].value, 'non-chat result'); + }); + + test('invokeTool with unknown chat session throws', async () => { + const tool = registerToolForTest(service, store, 'unknownSessionTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'should not reach' }] }) + }); + + const dto = tool.makeDto({ test: 1 }, { sessionId: 'unknownSession' }); + + // Test that it throws, regardless of exact error message + let threwError = false; + try { + await service.invokeTool(dto, async () => 0, CancellationToken.None); + } catch (err) { + threwError = true; + // Verify it's one of the expected error types + assert.ok( + err instanceof Error && ( + err.message.includes('Tool called for unknown chat session') || + err.message.includes('getRequests is not a function') + ), + `Unexpected error: ${err.message}` + ); + } + assert.strictEqual(threwError, true, 'Should have thrown an error'); + }); + + test('tool error with alwaysDisplayInputOutput includes details', async () => { + const toolData: IToolData = { + id: 'errorToolWithIO', + modelDescription: 'Error Tool With IO', + displayName: 'Error Tool With IO', + source: ToolDataSource.Internal, + alwaysDisplayInputOutput: true + }; + + const tool = registerToolForTest(service, store, toolData.id, { + invoke: async () => { throw new Error('Tool execution failed'); } + }, toolData); + + const input = { param: 'testValue' }; + + try { + await service.invokeTool( + tool.makeDto(input), + async () => 0, + CancellationToken.None + ); + assert.fail('Should have thrown'); + } catch (err: any) { + // The error should bubble up, but we need to check if toolResultError is set + // This tests the internal error handling path + assert.strictEqual(err.message, 'Tool execution failed'); + } + }); + + test('context key changes trigger tool updates', async () => { + let changeEventFired = false; + const disposable = service.onDidChangeTools(() => { + changeEventFired = true; + }); + store.add(disposable); + + // Create a tool with a context key dependency + contextKeyService.createKey('dynamicKey', false); + const toolData: IToolData = { + id: 'contextTool', + modelDescription: 'Context Tool', + displayName: 'Context Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicKey', true), + }; + + store.add(service.registerToolData(toolData)); + + // Change the context key value + contextKeyService.createKey('dynamicKey', true); + + // Wait a bit for the scheduler + await new Promise(resolve => setTimeout(resolve, 800)); + + assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when context keys change'); + }); + + test('configuration changes trigger tool updates', async () => { + return runWithFakedTimers({}, async () => { + let changeEventFired = false; + const disposable = service.onDidChangeTools(() => { + changeEventFired = true; + }); + store.add(disposable); + + // Change the correct configuration key + configurationService.setUserConfiguration('chat.extensionTools.enabled', false); + // Fire the configuration change event manually + configurationService.onDidChangeConfigurationEmitter.fire({ + affectsConfiguration: () => true, + affectedKeys: new Set(['chat.extensionTools.enabled']), + change: null!, + source: ConfigurationTarget.USER + } satisfies IConfigurationChangeEvent); + + // Wait a bit for the scheduler + await new Promise(resolve => setTimeout(resolve, 800)); + + assert.strictEqual(changeEventFired, true, 'onDidChangeTools should fire when configuration changes'); + }); + }); + + test('toToolAndToolSetEnablementMap with MCP toolset enables contained tools', () => { + // Create MCP toolset + const mcpToolSet = store.add(service.createToolSet( + { type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' }, + 'mcpSet', + 'mcpSetRef' + )); + + const mcpTool: IToolData = { + id: 'mcpTool', + modelDescription: 'MCP Tool', + displayName: 'MCP Tool', + source: { type: 'mcp', label: 'testServer', serverLabel: 'testServer', instructions: undefined, collectionId: 'testCollection', definitionId: 'testDef' }, + canBeReferencedInPrompt: true, + toolReferenceName: 'mcpToolRef' + }; + + store.add(service.registerToolData(mcpTool)); + store.add(mcpToolSet.addTool(mcpTool)); + + // Enable the MCP toolset + { + const enabledNames = [mcpToolSet].map(t => service.getFullReferenceName(t)); + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, undefined); + + 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 fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), enabledNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + // Enable a tool from the MCP toolset + { + const enabledNames = [mcpTool].map(t => service.getFullReferenceName(t, mcpToolSet)); + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, undefined); + + 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 fullReferenceNames = service.toFullReferenceNames(result); + assert.deepStrictEqual(fullReferenceNames.sort(), enabledNames.sort(), 'toFullReferenceNames should return the original enabled names'); + } + + }); + + test('shouldAutoConfirm with workspace-specific tool configuration', async () => { + const testConfigService = new TestConfigurationService(); + // Configure per-tool settings at different scopes + testConfigService.setUserConfiguration('chat.tools.global.autoApprove', { 'workspaceTool': true }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + const workspaceTool = registerToolForTest(testService, store, 'workspaceTool', { + prepareToolInvocation: async () => ({ confirmationMessages: { title: 'Test', message: 'Workspace tool' } }), + invoke: async () => ({ content: [{ kind: 'text', value: 'workspace result' }] }) + }, { runsInWorkspace: true }); + + const sessionId = 'workspace-test'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Should auto-approve based on user configuration + const result = await testService.invokeTool( + workspaceTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'workspace result'); + }); + + test('getFullReferenceNames', () => { + setupToolsForTest(service, store); + + const fullReferenceNames = Array.from(service.getFullReferenceNames()).sort(); + + const expectedNames = [ + 'tool1RefName', + 'Tool2 Display Name', + 'my.extension/extTool1RefName', + 'mcpToolSetRefName/*', + 'mcpToolSetRefName/mcpTool1RefName', + 'internalToolSetRefName', + 'internalToolSetRefName/internalToolSetTool1RefName', + 'vscode', + 'execute', + 'read', + 'agent' + ].sort(); + + assert.deepStrictEqual(fullReferenceNames, expectedNames, 'getFullReferenceNames should return correct full reference names'); + }); + + test('getDeprecatedFullReferenceNames', () => { + setupToolsForTest(service, store); + + const deprecatedNames = service.getDeprecatedFullReferenceNames(); + + // Tools in internal tool sets should have their full reference names with toolset prefix, tools sets keep their name + assert.deepStrictEqual(deprecatedNames.get('internalToolSetTool1RefName'), new Set(['internalToolSetRefName/internalToolSetTool1RefName'])); + assert.strictEqual(deprecatedNames.get('internalToolSetRefName'), undefined); + + // For extension tools, the full reference name includes the extension ID + assert.deepStrictEqual(deprecatedNames.get('extTool1RefName'), new Set(['my.extension/extTool1RefName'])); + + // For MCP tool sets, the full reference name includes the /* suffix + assert.deepStrictEqual(deprecatedNames.get('mcpToolSetRefName'), new Set(['mcpToolSetRefName/*'])); + assert.deepStrictEqual(deprecatedNames.get('mcpTool1RefName'), new Set(['mcpToolSetRefName/mcpTool1RefName'])); + + // Internal tool sets and user tools sets and tools without namespace changes should not appear + assert.strictEqual(deprecatedNames.get('Tool2 Display Name'), undefined); + assert.strictEqual(deprecatedNames.get('tool1RefName'), undefined); + assert.strictEqual(deprecatedNames.get('userToolSetRefName'), undefined); + }); + + test('getToolByFullReferenceName', () => { + setupToolsForTest(service, store); + + // Test finding tools by their full reference names + const tool1 = service.getToolByFullReferenceName('tool1RefName'); + assert.ok(tool1); + assert.strictEqual(tool1.id, 'tool1'); + + const tool2 = service.getToolByFullReferenceName('Tool2 Display Name'); + assert.ok(tool2); + assert.strictEqual(tool2.id, 'tool2'); + + const extTool = service.getToolByFullReferenceName('my.extension/extTool1RefName'); + assert.ok(extTool); + assert.strictEqual(extTool.id, 'extTool1'); + + const mcpTool = service.getToolByFullReferenceName('mcpToolSetRefName/mcpTool1RefName'); + assert.ok(mcpTool); + assert.strictEqual(mcpTool.id, 'mcpTool1'); + + + const mcpToolSet = service.getToolByFullReferenceName('mcpToolSetRefName/*'); + assert.ok(mcpToolSet); + assert.strictEqual(mcpToolSet.id, 'mcpToolSet'); + + const internalToolSet = service.getToolByFullReferenceName('internalToolSetRefName/internalToolSetTool1RefName'); + assert.ok(internalToolSet); + assert.strictEqual(internalToolSet.id, 'internalToolSetTool1'); + + // Test finding tools within tool sets + const toolInSet = service.getToolByFullReferenceName('internalToolSetRefName'); + assert.ok(toolInSet); + assert.strictEqual(toolInSet!.id, 'internalToolSet'); + + }); + + test('eligibleForAutoApproval setting can be configured via policy', async () => { + // Test that policy configuration works for eligibleForAutoApproval + // Policy values should be JSON strings for object-type settings + const testConfigService = new TestConfigurationService(); + + // Simulate policy configuration (would come from policy file) + const policyValue = { + 'toolA': true, + 'toolB': false + }; + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, policyValue); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool A is eligible (true in policy) + const toolA = registerToolForTest(testService, store, 'toolA', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'toolA executed' }] }) + }, { + toolReferenceName: 'toolA' + }); + + // Tool B is ineligible (false in policy) + const toolB = registerToolForTest(testService, store, 'toolB', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'toolB executed' }] }) + }, { + toolReferenceName: 'toolB' + }); + + const sessionId = 'test-policy'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool A should execute without confirmation (eligible) + const resultA = await testService.invokeTool( + toolA.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(resultA.content[0].value, 'toolA executed'); + + // Tool B should require confirmation (ineligible) + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture }); + const promiseB = testService.invokeTool( + toolB.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'toolB should require confirmation due to policy'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const resultB = await promiseB; + assert.strictEqual(resultB.content[0].value, 'toolB executed'); + }); + + test('eligibleForAutoApproval with legacy tool reference names - eligible', async () => { + // Test backwards compatibility: configuring a legacy name as eligible should work + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'oldToolName': true // Using legacy name + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool has been renamed but has legacy name + const renamedTool = registerToolForTest(testService, store, 'renamedTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'tool executed via legacy name' }] }) + }, { + toolReferenceName: 'newToolName', + legacyToolReferenceFullNames: ['oldToolName'] + }); + + const sessionId = 'test-legacy-eligible'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible even though we configured the legacy name + const result = await testService.invokeTool( + renamedTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'tool executed via legacy name'); + }); + + test('eligibleForAutoApproval with legacy tool reference names - ineligible', async () => { + // Test backwards compatibility: configuring a legacy name as ineligible should work + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'deprecatedToolName': false // Using legacy name + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool has been renamed but has legacy name + const renamedTool = registerToolForTest(testService, store, 'renamedTool2', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'tool requires confirmation' }] }) + }, { + toolReferenceName: 'modernToolName', + legacyToolReferenceFullNames: ['deprecatedToolName'] + }); + + const sessionId = 'test-legacy-ineligible'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible and require confirmation + const promise = testService.invokeTool( + renamedTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation when legacy name is ineligible'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'tool requires confirmation'); + }); + + test('eligibleForAutoApproval with multiple legacy names', async () => { + // Test that any of the legacy names can be used in the configuration + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'secondLegacyName': true // Using the second legacy name + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool has multiple legacy names + const multiLegacyTool = registerToolForTest(testService, store, 'multiLegacyTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'multi legacy executed' }] }) + }, { + toolReferenceName: 'currentToolName', + legacyToolReferenceFullNames: ['firstLegacyName', 'secondLegacyName', 'thirdLegacyName'] + }); + + const sessionId = 'test-multi-legacy'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible via second legacy name + const result = await testService.invokeTool( + multiLegacyTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result.content[0].value, 'multi legacy executed'); + }); + + test('eligibleForAutoApproval current name takes precedence over legacy names', async () => { + // Test forward compatibility: current name in config should take precedence + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'currentName': false, // Current name says ineligible + 'oldName': true // Legacy name says eligible + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + const tool = registerToolForTest(testService, store, 'precedenceTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'precedence test' }] }) + }, { + toolReferenceName: 'currentName', + legacyToolReferenceFullNames: ['oldName'] + }); + + const sessionId = 'test-precedence'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Current name should take precedence, so tool should be ineligible + const promise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'current name should take precedence over legacy name'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'precedence test'); + }); + + test('eligibleForAutoApproval with legacy full reference names from toolsets', async () => { + // Test legacy names that include toolset prefixes (e.g., 'oldToolSet/oldToolName') + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'oldToolSet/oldToolName': false // Legacy full reference name from old toolset + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool was in an old toolset but now standalone + const migratedTool = registerToolForTest(testService, store, 'migratedTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'migrated tool' }] }) + }, { + toolReferenceName: 'standaloneToolName', + legacyToolReferenceFullNames: ['oldToolSet/oldToolName'] + }); + + const sessionId = 'test-fullReferenceName-legacy'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible based on legacy full reference name + const promise = testService.invokeTool( + migratedTool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should be ineligible via legacy full reference name'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'migrated tool'); + }); + + test('eligibleForAutoApproval mixed current and legacy names', async () => { + // Test realistic migration scenario with mixed current and legacy names + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'modernTool': true, // Current name + 'legacyToolOld': false, // Legacy name + 'unchangedTool': true // Tool that never changed + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Modern tool with current name + const tool1 = registerToolForTest(testService, store, 'tool1', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'modern executed' }] }) + }, { + toolReferenceName: 'modernTool' + }); + + // Renamed tool with legacy name + const tool2 = registerToolForTest(testService, store, 'tool2', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'legacy needs confirmation' }] }) + }, { + toolReferenceName: 'legacyToolNew', + legacyToolReferenceFullNames: ['legacyToolOld'] + }); + + // Unchanged tool + const tool3 = registerToolForTest(testService, store, 'tool3', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'unchanged executed' }] }) + }, { + toolReferenceName: 'unchangedTool' + }); + + const sessionId = 'test-mixed'; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool 1 should be eligible (current name) + const result1 = await testService.invokeTool( + tool1.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result1.content[0].value, 'modern executed'); + + // Tool 2 should be ineligible (legacy name) + const capture2: { invocation?: any } = {}; + stubGetSession(chatService, sessionId + '2', { requestId: 'req2', capture: capture2 }); + const promise2 = testService.invokeTool( + tool2.makeDto({ test: 2 }, { sessionId: sessionId + '2' }), + async () => 0, + CancellationToken.None + ); + const published2 = await waitForPublishedInvocation(capture2); + assert.ok(published2?.confirmationMessages, 'tool2 should require confirmation via legacy name'); + + IChatToolInvocation.confirmWith(published2, { type: ToolConfirmKind.UserAction }); + const result2 = await promise2; + assert.strictEqual(result2.content[0].value, 'legacy needs confirmation'); + + // Tool 3 should be eligible (unchanged) + const result3 = await testService.invokeTool( + tool3.makeDto({ test: 3 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + assert.strictEqual(result3.content[0].value, 'unchanged executed'); + }); + + test('eligibleForAutoApproval with namespaced legacy names - full tool name eligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitTools/gitCommit': true + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + const tool = registerToolForTest(testService, store, 'gitCommitTool', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit executed' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['gitTools/gitCommit'] + }); + + const sessionId = 'test-extension-prefix'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible via legacy extension-prefixed name + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.strictEqual(published, undefined, 'tool should not require confirmation when legacy trimmed name is eligible'); + assert.strictEqual(result.content[0].value, 'commit executed'); + }); + + test('eligibleForAutoApproval with namespaced and renamed toolname - just last segment eligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitCommit': true + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool2', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit executed' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['gitTools/gitCommit'] + }); + + const sessionId = 'test-renamed-prefix'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1' }); + + // Tool should be eligible via legacy extension-prefixed name + const result = await testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + + const published = await waitForPublishedInvocation(capture); + assert.strictEqual(published, undefined, 'tool should not require confirmation when legacy trimmed name is eligible'); + assert.strictEqual(result.content[0].value, 'commit executed'); + }); + + test('eligibleForAutoApproval with namespaced legacy names - full tool name ineligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitTools/gitCommit': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool3', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit blocked' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['something/random', 'gitTools/bar', 'gitTools/gitCommit'] + }); + + const sessionId = 'test-extension-prefix-blocked'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible via legacy extension-prefixed name + const promise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation when legacy full name is ineligible'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'commit blocked'); + }); + + test('eligibleForAutoApproval with namespaced and renamed toolname - just last segment ineligible', async () => { + const testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(ChatConfiguration.EligibleForAutoApproval, { + 'gitCommit': false + }); + + const instaService = workbenchInstantiationService({ + contextKeyService: () => store.add(new ContextKeyService(testConfigService)), + configurationService: () => testConfigService + }, store); + instaService.stub(IChatService, chatService); + instaService.stub(ILanguageModelToolsConfirmationService, new MockLanguageModelToolsConfirmationService()); + const testService = store.add(instaService.createInstance(LanguageModelToolsService)); + + // Tool that was previously namespaced under extension but is now internal + const tool = registerToolForTest(testService, store, 'gitCommitTool4', { + prepareToolInvocation: async () => ({}), + invoke: async () => ({ content: [{ kind: 'text', value: 'commit blocked' }] }) + }, { + toolReferenceName: 'commit', + legacyToolReferenceFullNames: ['something/random', 'gitTools/bar', 'gitTools/gitCommit'] + }); + + const sessionId = 'test-renamed-prefix-blocked'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId: 'req1', capture }); + + // Tool should be ineligible via trimmed legacy name + const promise = testService.invokeTool( + tool.makeDto({ test: 1 }, { sessionId }), + async () => 0, + CancellationToken.None + ); + const published = await waitForPublishedInvocation(capture); + assert.ok(published?.confirmationMessages, 'tool should require confirmation when legacy trimmed name is ineligible'); + assert.strictEqual(published?.confirmationMessages?.allowAutoConfirm, false, 'should not allow auto confirm'); + + IChatToolInvocation.confirmWith(published, { type: ToolConfirmKind.UserAction }); + const result = await promise; + assert.strictEqual(result.content[0].value, 'commit blocked'); + }); + + test('beginToolCall creates streaming tool invocation', () => { + const tool = registerToolForTest(service, store, 'streamingTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'result' }] }), + handleToolStream: async () => ({ invocationMessage: 'Processing...' }), + }); + + const sessionId = 'streaming-session'; + const requestId = 'streaming-request'; + stubGetSession(chatService, sessionId, { requestId }); + + const invocation = service.beginToolCall({ + toolCallId: 'call-123', + toolId: tool.id, + chatRequestId: requestId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + }); + + assert.ok(invocation, 'beginToolCall should return an invocation'); + assert.strictEqual(invocation.toolId, tool.id); + }); + + test('beginToolCall returns undefined for unknown tool', () => { + const invocation = service.beginToolCall({ + toolCallId: 'call-unknown', + toolId: 'nonExistentTool', + }); + + assert.strictEqual(invocation, undefined, 'beginToolCall should return undefined for unknown tools'); + }); + + test('updateToolStream calls handleToolStream on tool implementation', async () => { + let handleToolStreamCalled = false; + let receivedRawInput: unknown; + + const tool = registerToolForTest(service, store, 'streamHandlerTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'result' }] }), + handleToolStream: async (context) => { + handleToolStreamCalled = true; + receivedRawInput = context.rawInput; + return { invocationMessage: 'Processing...' }; + }, + }); + + const sessionId = 'stream-handler-session'; + const requestId = 'stream-handler-request'; + stubGetSession(chatService, sessionId, { requestId }); + + const invocation = service.beginToolCall({ + toolCallId: 'call-stream', + toolId: tool.id, + chatRequestId: requestId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + }); + + assert.ok(invocation, 'should create invocation'); + + // Update the stream with partial input + const partialInput = { partial: 'data' }; + await service.updateToolStream('call-stream', partialInput, CancellationToken.None); + + assert.strictEqual(handleToolStreamCalled, true, 'handleToolStream should be called'); + assert.deepStrictEqual(receivedRawInput, partialInput, 'should receive the partial input'); + }); + + test('updateToolStream does nothing for unknown tool call', async () => { + // Should not throw + await service.updateToolStream('unknown-call-id', { data: 'test' }, CancellationToken.None); + }); + + test('toToolAndToolSetEnablementMap with model metadata filters tools', () => { + // This test verifies that when a tool's models selector matches the provided model, + // it's included in the enablement map. + + // Tool that requires gpt-4 family (matches provided model) + const gpt4ToolDef: IToolData = { + id: 'gpt4Tool', + toolReferenceName: 'gpt4ToolRef', + modelDescription: 'GPT-4 Tool', + displayName: 'GPT-4 Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + models: [{ family: 'gpt-4' }], + }; + + // Tool with no models selector (available for all models) + const anyModelToolDef: IToolData = { + id: 'anyModelTool', + toolReferenceName: 'anyModelToolRef', + modelDescription: 'Any Model Tool', + displayName: 'Any Model Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + }; + + // Tool that requires claude family (won't match) + const claudeToolDef: IToolData = { + id: 'claudeTool', + toolReferenceName: 'claudeToolRef', + modelDescription: 'Claude Tool', + displayName: 'Claude Tool', + source: ToolDataSource.Internal, + canBeReferencedInPrompt: true, + models: [{ family: 'claude-3' }], + }; + + store.add(service.registerToolData(gpt4ToolDef)); + store.add(service.registerToolData(anyModelToolDef)); + store.add(service.registerToolData(claudeToolDef)); + + // Get the tools from the service + const gpt4Tool = service.getTool('gpt4Tool'); + const anyModelTool = service.getTool('anyModelTool'); + const claudeTool = service.getTool('claudeTool'); + assert.ok(gpt4Tool && anyModelTool && claudeTool, 'tools should be registered'); + + // Provide model metadata for gpt-4 family + const modelMetadata = { id: 'gpt-4-turbo', vendor: 'openai', family: 'gpt-4', version: '1.0' } as ILanguageModelChatMetadata; + const enabledNames = ['gpt4ToolRef', 'anyModelToolRef', 'claudeToolRef']; + const result = service.toToolAndToolSetEnablementMap(enabledNames, undefined, modelMetadata); + + // gpt4Tool should be enabled (model matches) + assert.strictEqual(result.get(gpt4Tool), true, 'gpt4Tool should be enabled'); + // anyModelTool should be enabled (no model restriction) + assert.strictEqual(result.get(anyModelTool), true, 'anyModelTool should be enabled'); + // claudeTool should NOT be in the enablement map (filtered out by model) + assert.strictEqual(result.has(claudeTool), false, 'claudeTool should be filtered out by model'); + }); + + test('observeTools returns tools filtered by context', async () => { + return runWithFakedTimers({}, async () => { + contextKeyService.createKey('featureEnabled', true); + + const enabledTool: IToolData = { + id: 'enabledObsTool', + modelDescription: 'Enabled Tool', + displayName: 'Enabled Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('featureEnabled', true), + }; + + const disabledTool: IToolData = { + id: 'disabledObsTool', + modelDescription: 'Disabled Tool', + displayName: 'Disabled Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('featureEnabled', false), + }; + + store.add(service.registerToolData(enabledTool)); + store.add(service.registerToolData(disabledTool)); + + const toolsObs = service.observeTools(undefined); + + // Read current value directly + const tools = toolsObs.get(); + + assert.strictEqual(tools.length, 1, 'should only include enabled tool'); + assert.strictEqual(tools[0].id, 'enabledObsTool'); + }); + }); + + test('invokeTool with chatStreamToolCallId correlates with pending streaming call', async () => { + const tool = registerToolForTest(service, store, 'correlatedTool', { + invoke: async () => ({ content: [{ kind: 'text', value: 'correlated result' }] }), + handleToolStream: async () => ({ invocationMessage: 'Processing...' }), + }); + + const sessionId = 'correlated-session'; + const requestId = 'correlated-request'; + const capture: { invocation?: any } = {}; + stubGetSession(chatService, sessionId, { requestId, capture }); + + // Start a streaming tool call + const streamingInvocation = service.beginToolCall({ + toolCallId: 'stream-call-id', + toolId: tool.id, + chatRequestId: requestId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + }); + + assert.ok(streamingInvocation, 'should create streaming invocation'); + + // Now invoke the tool with a different callId but matching chatStreamToolCallId + const dto: IToolInvocation = { + callId: 'different-call-id', + toolId: tool.id, + tokenBudget: 100, + parameters: { test: 1 }, + context: { + sessionId, + sessionResource: LocalChatSessionUri.forSession(sessionId), + }, + chatStreamToolCallId: 'stream-call-id', // This should correlate + }; + + const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); + assert.strictEqual(result.content[0].value, 'correlated result'); + }); + + test('getAllToolsIncludingDisabled returns tools regardless of when clause', () => { + contextKeyService.createKey('featureFlag', false); + + const enabledTool: IToolData = { + id: 'enabledTool', + modelDescription: 'Enabled Tool', + displayName: 'Enabled Tool', + source: ToolDataSource.Internal, + }; + + const disabledTool: IToolData = { + id: 'disabledTool', + modelDescription: 'Disabled Tool', + displayName: 'Disabled Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('featureFlag', true), // Will be disabled + }; + + store.add(service.registerToolData(enabledTool)); + store.add(service.registerToolData(disabledTool)); + + // getAllToolsIncludingDisabled should return both tools + const allTools = Array.from(service.getAllToolsIncludingDisabled()); + assert.strictEqual(allTools.length, 2, 'getAllToolsIncludingDisabled should return all tools'); + assert.ok(allTools.some(t => t.id === 'enabledTool'), 'should include enabled tool'); + assert.ok(allTools.some(t => t.id === 'disabledTool'), 'should include disabled tool'); + + // getTools should only return tools matching when clause + const enabledTools = Array.from(service.getTools(undefined)); + assert.strictEqual(enabledTools.length, 1, 'getTools should only return matching tools'); + assert.strictEqual(enabledTools[0].id, 'enabledTool'); + }); + + test('getTools filters by model id using models property', () => { + const gpt4Tool: IToolData = { + id: 'gpt4Tool', + modelDescription: 'GPT-4 Tool', + displayName: 'GPT-4 Tool', + source: ToolDataSource.Internal, + models: [{ id: 'gpt-4-turbo' }], + }; + + const claudeTool: IToolData = { + id: 'claudeTool', + modelDescription: 'Claude Tool', + displayName: 'Claude Tool', + source: ToolDataSource.Internal, + models: [{ id: 'claude-3-opus' }], + }; + + const universalTool: IToolData = { + id: 'universalTool', + modelDescription: 'Universal Tool', + displayName: 'Universal Tool', + source: ToolDataSource.Internal, + // No models - available for all models + }; + + store.add(service.registerToolData(gpt4Tool)); + store.add(service.registerToolData(claudeTool)); + store.add(service.registerToolData(universalTool)); + + // Mock model metadata with id 'gpt-4-turbo' + const modelMetadata = { id: 'gpt-4-turbo', vendor: 'openai', family: 'gpt-4', version: '1.0' } as ILanguageModelChatMetadata; + const tools = Array.from(service.getTools(modelMetadata)); + + assert.strictEqual(tools.length, 2, 'should return 2 tools'); + assert.ok(tools.some(t => t.id === 'gpt4Tool'), 'should include GPT-4 tool'); + assert.ok(tools.some(t => t.id === 'universalTool'), 'should include universal tool'); + assert.ok(!tools.some(t => t.id === 'claudeTool'), 'should NOT include Claude tool'); + }); + + test('getTools filters by model vendor using models property', () => { + const anthropicTool: IToolData = { + id: 'anthropicTool', + modelDescription: 'Anthropic Tool', + displayName: 'Anthropic Tool', + source: ToolDataSource.Internal, + models: [{ vendor: 'anthropic' }], + }; + + const openaiTool: IToolData = { + id: 'openaiTool', + modelDescription: 'OpenAI Tool', + displayName: 'OpenAI Tool', + source: ToolDataSource.Internal, + models: [{ vendor: 'openai' }], + }; + + store.add(service.registerToolData(anthropicTool)); + store.add(service.registerToolData(openaiTool)); + + // Mock model metadata with vendor 'anthropic' + const modelMetadata = { id: 'claude-3', vendor: 'anthropic', family: 'claude-3', version: '1.0' } as ILanguageModelChatMetadata; + const tools = Array.from(service.getTools(modelMetadata)); + + assert.strictEqual(tools.length, 1, 'should return 1 tool'); + assert.strictEqual(tools[0].id, 'anthropicTool', 'should include Anthropic tool'); + }); + + test('getTools filters by model family using models property', () => { + const gpt4FamilyTool: IToolData = { + id: 'gpt4FamilyTool', + modelDescription: 'GPT-4 Family Tool', + displayName: 'GPT-4 Family Tool', + source: ToolDataSource.Internal, + models: [{ family: 'gpt-4' }], + }; + + const gpt35FamilyTool: IToolData = { + id: 'gpt35FamilyTool', + modelDescription: 'GPT-3.5 Family Tool', + displayName: 'GPT-3.5 Family Tool', + source: ToolDataSource.Internal, + models: [{ family: 'gpt-3.5' }], + }; + + store.add(service.registerToolData(gpt4FamilyTool)); + store.add(service.registerToolData(gpt35FamilyTool)); + + // Mock model metadata with family 'gpt-4' + const modelMetadata = { id: 'gpt-4-turbo', vendor: 'openai', family: 'gpt-4', version: '1.0' } as ILanguageModelChatMetadata; + const tools = Array.from(service.getTools(modelMetadata)); + + assert.strictEqual(tools.length, 1, 'should return 1 tool'); + assert.strictEqual(tools[0].id, 'gpt4FamilyTool', 'should include GPT-4 family tool'); + }); + + test('getTools with undefined model skips model filtering', () => { + const gpt4Tool: IToolData = { + id: 'gpt4Tool', + modelDescription: 'GPT-4 Tool', + displayName: 'GPT-4 Tool', + source: ToolDataSource.Internal, + models: [{ id: 'gpt-4-turbo' }], + }; + + const claudeTool: IToolData = { + id: 'claudeTool', + modelDescription: 'Claude Tool', + displayName: 'Claude Tool', + source: ToolDataSource.Internal, + models: [{ id: 'claude-3-opus' }], + }; + + store.add(service.registerToolData(gpt4Tool)); + store.add(service.registerToolData(claudeTool)); + + // When model is undefined, all tools should be returned (model filtering skipped) + const tools = Array.from(service.getTools(undefined)); + + assert.strictEqual(tools.length, 2, 'should return all tools when model is undefined'); + assert.ok(tools.some(t => t.id === 'gpt4Tool'), 'should include GPT-4 tool'); + assert.ok(tools.some(t => t.id === 'claudeTool'), 'should include Claude tool'); + }); + + test('getTool returns tool regardless of when clause', () => { + contextKeyService.createKey('someFlag', false); + + const disabledTool: IToolData = { + id: 'disabledLookupTool', + modelDescription: 'Disabled Lookup Tool', + displayName: 'Disabled Lookup Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('someFlag', true), // Disabled + }; + + store.add(service.registerToolData(disabledTool)); + + // getTool should still find the tool by ID + const tool = service.getTool('disabledLookupTool'); + assert.ok(tool, 'getTool should return tool even when disabled'); + assert.strictEqual(tool.id, 'disabledLookupTool'); + }); + + test('getToolByName returns tool regardless of when clause', () => { + contextKeyService.createKey('anotherFlag', false); + + const disabledTool: IToolData = { + id: 'disabledNamedTool', + toolReferenceName: 'disabledNamedToolRef', + modelDescription: 'Disabled Named Tool', + displayName: 'Disabled Named Tool', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('anotherFlag', true), // Disabled + }; + + store.add(service.registerToolData(disabledTool)); + + // getToolByName should still find the tool by reference name + const tool = service.getToolByName('disabledNamedToolRef'); + assert.ok(tool, 'getToolByName should return tool even when disabled'); + assert.strictEqual(tool.id, 'disabledNamedTool'); + }); + + test('IToolData models property stores selector information', () => { + const toolWithModels: IToolData = { + id: 'modelSpecificTool', + modelDescription: 'Model Specific Tool', + displayName: 'Model Specific Tool', + source: ToolDataSource.Internal, + models: [ + { vendor: 'openai', family: 'gpt-4' }, + { vendor: 'anthropic', family: 'claude-3' }, + ], + }; + + store.add(service.registerToolData(toolWithModels)); + + const tool = service.getTool('modelSpecificTool'); + assert.ok(tool, 'tool should be registered'); + assert.ok(tool.models, 'tool should have models property'); + assert.strictEqual(tool.models.length, 2, 'tool should have 2 model selectors'); + assert.deepStrictEqual(tool.models[0], { vendor: 'openai', family: 'gpt-4' }); + assert.deepStrictEqual(tool.models[1], { vendor: 'anthropic', family: 'claude-3' }); + }); + + test('tools with extension tools disabled setting are filtered', () => { + // Create a tool from an extension + const extensionTool: IToolData = { + id: 'extensionTool', + modelDescription: 'Extension Tool', + displayName: 'Extension Tool', + source: { type: 'extension', label: 'Test Extension', extensionId: new ExtensionIdentifier('test.extension') }, + }; + + store.add(service.registerToolData(extensionTool)); + + // With extension tools enabled (default in setup) + let tools = Array.from(service.getTools(undefined)); + assert.ok(tools.some(t => t.id === 'extensionTool'), 'extension tool should be included when enabled'); + + // Disable extension tools + configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, false); + + tools = Array.from(service.getTools(undefined)); + assert.ok(!tools.some(t => t.id === 'extensionTool'), 'extension tool should be excluded when disabled'); + + // Re-enable for cleanup + configurationService.setUserConfiguration(ChatConfiguration.ExtensionToolsEnabled, true); + }); + + test('observeTools changes when context key changes', async () => { + return runWithFakedTimers({}, async () => { + const testCtxKey = contextKeyService.createKey('dynamicTestKey', 'value1'); + + const tool1: IToolData = { + id: 'dynamicTool1', + modelDescription: 'Dynamic Tool 1', + displayName: 'Dynamic Tool 1', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), + }; + + const tool2: IToolData = { + id: 'dynamicTool2', + modelDescription: 'Dynamic Tool 2', + displayName: 'Dynamic Tool 2', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), + }; + + store.add(service.registerToolData(tool1)); + store.add(service.registerToolData(tool2)); + + const toolsObs = service.observeTools(undefined); + + // Initial state: value1 matches tool1 + let tools = toolsObs.get(); + assert.strictEqual(tools.length, 1, 'should have 1 tool initially'); + assert.strictEqual(tools[0].id, 'dynamicTool1', 'should be dynamicTool1'); + + // Change context key to value2 + testCtxKey.set('value2'); + + // Wait for scheduler to trigger + await new Promise(resolve => setTimeout(resolve, 800)); + + // Now tool2 should be available + tools = toolsObs.get(); + assert.strictEqual(tools.length, 1, 'should have 1 tool after change'); + assert.strictEqual(tools[0].id, 'dynamicTool2', 'should be dynamicTool2 after context change'); + }); + }); + + test('isPermitted allows tools in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'read' toolset (permitted) + const readTool: IToolData = { + id: 'readToolInSet', + toolReferenceName: 'readToolRef', + modelDescription: 'Read Tool in Set', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneTool', + toolReferenceName: 'standaloneRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - should include the tool in the read toolset but not the standalone tool + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolInSet'), 'Tool in read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolIds.includes('standaloneTool'), 'Standalone tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows all tools when agent mode is enabled', () => { + // Enable agent mode (default) + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'read' toolset + const readTool: IToolData = { + id: 'readToolEnabled', + toolReferenceName: 'readToolEnabledRef', + modelDescription: 'Read Tool', + displayName: 'Read Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(readTool)); + store.add(service.readToolSet.addTool(readTool)); + + // Create standalone tool not in any permitted toolset + const standaloneTool: IToolData = { + id: 'standaloneToolEnabled', + toolReferenceName: 'standaloneEnabledRef', + modelDescription: 'Standalone Tool', + displayName: 'Standalone Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(standaloneTool)); + + // Get tools - both should be available when agent mode is enabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('readToolEnabled'), 'Tool in read toolset should be permitted when agent mode is enabled'); + assert.ok(toolIds.includes('standaloneToolEnabled'), 'Standalone tool should be permitted when agent mode is enabled'); + }); + + test('isPermitted filters toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a custom internal toolset that is NOT in the permitted list + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customToolSet', + 'customToolSetRef', + { description: 'Custom Tool Set' } + )); + + const customTool: IToolData = { + id: 'customToolInSet', + toolReferenceName: 'customToolRef', + modelDescription: 'Custom Tool', + displayName: 'Custom Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(customTool)); + store.add(customToolSet.addTool(customTool)); + + // Get toolsets - read/search/web should be available, custom should not + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(toolSetIds.includes('read'), 'read toolset should be permitted when agent mode is disabled'); + assert.ok(!toolSetIds.includes('customToolSet'), 'custom toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows execute toolset tools when agent mode is enabled', () => { + // Enable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, true); + + // Create tool in the 'execute' toolset (only permitted when agent mode is enabled) + const executeTool: IToolData = { + id: 'executeToolInSet', + toolReferenceName: 'executeToolRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should be available when agent mode is enabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('executeToolInSet'), 'Tool in execute toolset should be permitted when agent mode is enabled'); + }); + + test('isPermitted blocks execute toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'execute' toolset (NOT permitted when agent mode is disabled) + const executeTool: IToolData = { + id: 'executeToolBlocked', + toolReferenceName: 'executeToolBlockedRef', + modelDescription: 'Execute Tool', + displayName: 'Execute Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(executeTool)); + store.add(service.executeToolSet.addTool(executeTool)); + + // Get tools - execute tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('executeToolBlocked'), 'Tool in execute toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted allows search toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'search' toolset (permitted when agent mode is disabled) + const searchToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'search', + SpecedToolAliases.search, + { description: 'Search Tool Set' } + )); + + const searchTool: IToolData = { + id: 'searchToolInSet', + toolReferenceName: 'searchToolRef', + modelDescription: 'Search Tool', + displayName: 'Search Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(searchTool)); + store.add(searchToolSet.addTool(searchTool)); + + // Get tools - search tool should be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('searchToolInSet'), 'Tool in search toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows web toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a 'web' toolset (permitted when agent mode is disabled) + const webToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'web', + SpecedToolAliases.web, + { description: 'Web Tool Set' } + )); + + const webTool: IToolData = { + id: 'webToolInSet', + toolReferenceName: 'webToolRef', + modelDescription: 'Web Tool', + displayName: 'Web Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(webTool)); + store.add(webToolSet.addTool(webTool)); + + // Get tools - web tool should be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('webToolInSet'), 'Tool in web toolset should be permitted when agent mode is disabled'); + }); + + test('isPermitted allows vscode_fetchWebPage_internal special case when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Register the special-cased fetch tool (not added to any toolset) + const fetchTool: IToolData = { + id: 'vscode_fetchWebPage_internal', + toolReferenceName: 'fetchWebPage', + modelDescription: 'Fetch Web Page', + displayName: 'Fetch Web Page', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(fetchTool)); + + // Get tools - this special tool should be available even when not in a toolset + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('vscode_fetchWebPage_internal'), 'vscode_fetchWebPage_internal should be permitted as special case when agent mode is disabled'); + }); + + test('isPermitted blocks extension tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create extension tool not in any permitted toolset + const extensionTool: IToolData = { + id: 'extensionToolBlocked', + toolReferenceName: 'extensionToolRef', + modelDescription: 'Extension Tool', + displayName: 'Extension Tool', + source: { type: 'extension', label: 'Test Extension', extensionId: new ExtensionIdentifier('test.extension') }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(extensionTool)); + + // Get tools - extension tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('extensionToolBlocked'), 'Extension tool not in permitted toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks MCP tools not in permitted toolsets when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create MCP toolset (not in permitted list) + const mcpToolSet = store.add(service.createToolSet( + { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + 'mcpToolSetBlocked', + 'mcpToolSetBlockedRef', + { description: 'MCP Tool Set' } + )); + + const mcpTool: IToolData = { + id: 'mcpToolBlocked', + toolReferenceName: 'mcpToolRef', + modelDescription: 'MCP Tool', + displayName: 'MCP Tool', + source: { type: 'mcp', label: 'Test MCP', serverLabel: 'Test MCP Server', instructions: undefined, collectionId: 'testMcp', definitionId: 'testMcpDef' }, + canBeReferencedInPrompt: true, + }; + store.add(service.registerToolData(mcpTool)); + store.add(mcpToolSet.addTool(mcpTool)); + + // Get tools - MCP tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('mcpToolBlocked'), 'MCP tool should NOT be permitted when agent mode is disabled'); + + // Get toolsets - MCP toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('mcpToolSetBlocked'), 'MCP toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted blocks agent toolset tools when agent mode is disabled', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create tool in the 'agent' toolset (NOT permitted when agent mode is disabled) + const agentTool: IToolData = { + id: 'agentToolBlocked', + toolReferenceName: 'agentToolBlockedRef', + modelDescription: 'Agent Tool', + displayName: 'Agent Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(agentTool)); + store.add(service.agentToolSet.addTool(agentTool)); + + // Get tools - agent tool should NOT be available when agent mode is disabled + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(!toolIds.includes('agentToolBlocked'), 'Tool in agent toolset should NOT be permitted when agent mode is disabled'); + + // Get toolsets - agent toolset should NOT be available + const toolSets = Array.from(service.toolSets.get()); + const toolSetIds = Array.from(toolSets).map(ts => ts.id); + + assert.ok(!toolSetIds.includes('agent'), 'agent toolset should NOT be permitted when agent mode is disabled'); + }); + + test('isPermitted includes tool in multiple toolsets if one is permitted', () => { + // Disable agent mode + configurationService.setUserConfiguration(ChatConfiguration.AgentEnabled, false); + + // Create a tool that is added to both a permitted toolset (read) and a non-permitted toolset + const multiSetTool: IToolData = { + id: 'multiSetTool', + toolReferenceName: 'multiSetToolRef', + modelDescription: 'Multi Set Tool', + displayName: 'Multi Set Tool', + source: ToolDataSource.Internal, + }; + store.add(service.registerToolData(multiSetTool)); + + // Add to read toolset (permitted) + store.add(service.readToolSet.addTool(multiSetTool)); + + // Also create and add to a non-permitted toolset + const customToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'customMultiSet', + 'customMultiSetRef', + { description: 'Custom Multi Set' } + )); + store.add(customToolSet.addTool(multiSetTool)); + + // Get tools - tool should be available because it's in the 'read' toolset + const tools = Array.from(service.getTools(undefined)); + const toolIds = tools.map(t => t.id); + + assert.ok(toolIds.includes('multiSetTool'), 'Tool should be permitted if it belongs to at least one permitted toolset'); + }); + + suite('ToolSet when clause filtering (issue #291154)', () => { + test('ToolSet.getTools filters tools by when clause', () => { + // Create a context key for testing + contextKeyService.createKey('testFeatureEnabled', false); + + // Create tools with different when clauses + const toolWithWhenTrue: IToolData = { + id: 'toolWithWhenTrue', + modelDescription: 'Tool with when true', + displayName: 'Tool with when true', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('testFeatureEnabled', true), + }; + + const toolWithWhenFalse: IToolData = { + id: 'toolWithWhenFalse', + modelDescription: 'Tool with when false', + displayName: 'Tool with when false', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('testFeatureEnabled', false), + }; + + const toolWithoutWhen: IToolData = { + id: 'toolWithoutWhen', + modelDescription: 'Tool without when', + displayName: 'Tool without when', + source: ToolDataSource.Internal, + }; + + // Create a tool set and add the tools + const testToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'testToolSet', + 'testToolSetRef', + { description: 'Test Tool Set' } + )); + + store.add(service.registerToolData(toolWithWhenTrue)); + store.add(service.registerToolData(toolWithWhenFalse)); + store.add(service.registerToolData(toolWithoutWhen)); + + store.add(testToolSet.addTool(toolWithWhenTrue)); + store.add(testToolSet.addTool(toolWithWhenFalse)); + store.add(testToolSet.addTool(toolWithoutWhen)); + + // Get tools from the tool set + const tools = Array.from(testToolSet.getTools()); + const toolIds = tools.map(t => t.id); + + // Since testFeatureEnabled is false, only tools with when=false or no when clause should be available + assert.ok(toolIds.includes('toolWithWhenFalse'), 'Tool with when=false should be in tool set when context key is false'); + assert.ok(toolIds.includes('toolWithoutWhen'), 'Tool without when clause should be in tool set'); + assert.ok(!toolIds.includes('toolWithWhenTrue'), 'Tool with when=true should NOT be in tool set when context key is false'); + }); + + test('ToolSet.getTools updates when context key changes', async () => { + return runWithFakedTimers({}, async () => { + // Create a context key for testing + const testKey = contextKeyService.createKey('dynamicTestKey', 'value1'); + + // Create tools with when clauses + const toolWithValue1: IToolData = { + id: 'toolWithValue1', + modelDescription: 'Tool with value1', + displayName: 'Tool with value1', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value1'), + }; + + const toolWithValue2: IToolData = { + id: 'toolWithValue2', + modelDescription: 'Tool with value2', + displayName: 'Tool with value2', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('dynamicTestKey', 'value2'), + }; + + // Create a tool set and add the tools + const dynamicToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'dynamicToolSet', + 'dynamicToolSetRef', + { description: 'Dynamic Tool Set' } + )); + + store.add(service.registerToolData(toolWithValue1)); + store.add(service.registerToolData(toolWithValue2)); + + store.add(dynamicToolSet.addTool(toolWithValue1)); + store.add(dynamicToolSet.addTool(toolWithValue2)); + + // Initial state: value1 is set + let tools = Array.from(dynamicToolSet.getTools()); + let toolIds = tools.map(t => t.id); + + assert.strictEqual(tools.length, 1, 'Should have 1 tool initially'); + assert.strictEqual(toolIds[0], 'toolWithValue1', 'Should be toolWithValue1'); + + // Change context key to value2 + testKey.set('value2'); + + // Wait for scheduler to trigger + await new Promise(resolve => setTimeout(resolve, 800)); + + // Now toolWithValue2 should be available + tools = Array.from(dynamicToolSet.getTools()); + toolIds = tools.map(t => t.id); + + assert.strictEqual(tools.length, 1, 'Should have 1 tool after change'); + assert.strictEqual(toolIds[0], 'toolWithValue2', 'Should be toolWithValue2 after context change'); + }); + }); + + test('ToolSet.getTools with complex when expressions', () => { + // Create multiple context keys for testing complex expressions + contextKeyService.createKey('featureA', true); + contextKeyService.createKey('featureB', false); + contextKeyService.createKey('featureC', true); + + const toolWithAnd: IToolData = { + id: 'toolWithAnd', + modelDescription: 'Tool with AND expression', + displayName: 'Tool with AND', + source: ToolDataSource.Internal, + when: ContextKeyExpr.and( + ContextKeyExpr.has('featureA'), + ContextKeyExpr.has('featureC') + ), + }; + + const toolWithOr: IToolData = { + id: 'toolWithOr', + modelDescription: 'Tool with OR expression', + displayName: 'Tool with OR', + source: ToolDataSource.Internal, + when: ContextKeyExpr.or( + ContextKeyExpr.has('featureA'), + ContextKeyExpr.has('featureC') + ), + }; + + const toolWithNot: IToolData = { + id: 'toolWithNot', + modelDescription: 'Tool with NOT expression', + displayName: 'Tool with NOT', + source: ToolDataSource.Internal, + when: ContextKeyExpr.not('featureB'), + }; + + // Create a tool set and add the tools + const complexToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'complexToolSet', + 'complexToolSetRef', + { description: 'Complex Tool Set' } + )); + + store.add(service.registerToolData(toolWithAnd)); + store.add(service.registerToolData(toolWithOr)); + store.add(service.registerToolData(toolWithNot)); + + store.add(complexToolSet.addTool(toolWithAnd)); + store.add(complexToolSet.addTool(toolWithOr)); + store.add(complexToolSet.addTool(toolWithNot)); + + // Get tools from the tool set + const tools = Array.from(complexToolSet.getTools()); + const toolIds = tools.map(t => t.id); + + // featureA=true, featureB=false, featureC=true + // toolWithAnd: has('featureA') AND has('featureC') = true + // toolWithOr: has('featureA') OR has('featureC') = true + // toolWithNot: NOT has('featureB') = true + assert.ok(toolIds.includes('toolWithAnd'), 'Tool with AND should be in tool set (has(featureA) AND has(featureC) = true)'); + assert.ok(toolIds.includes('toolWithOr'), 'Tool with OR should be in tool set (has(featureA) OR has(featureC) = true)'); + assert.ok(toolIds.includes('toolWithNot'), 'Tool with NOT should be in tool set (NOT has(featureB) = true)'); + }); + + test('ToolSet.getTools filters nested tool sets by when clause', () => { + // Create a context key for testing + contextKeyService.createKey('nestedFeature', false); + + // Create tools in parent tool set + const parentTool: IToolData = { + id: 'parentTool', + modelDescription: 'Parent Tool', + displayName: 'Parent Tool', + source: ToolDataSource.Internal, + }; + + // Create tools in child tool set with when clause + const childToolWithWhen: IToolData = { + id: 'childToolWithWhen', + modelDescription: 'Child Tool with When', + displayName: 'Child Tool with When', + source: ToolDataSource.Internal, + when: ContextKeyEqualsExpr.create('nestedFeature', true), + }; + + const childToolWithoutWhen: IToolData = { + id: 'childToolWithoutWhen', + modelDescription: 'Child Tool without When', + displayName: 'Child Tool without When', + source: ToolDataSource.Internal, + }; + + // Create parent tool set + const parentToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'parentToolSet', + 'parentToolSetRef', + { description: 'Parent Tool Set' } + )); + + // Create child tool set + const childToolSet = store.add(service.createToolSet( + ToolDataSource.Internal, + 'childToolSet', + 'childToolSetRef', + { description: 'Child Tool Set' } + )); + + store.add(service.registerToolData(parentTool)); + store.add(service.registerToolData(childToolWithWhen)); + store.add(service.registerToolData(childToolWithoutWhen)); + + store.add(parentToolSet.addTool(parentTool)); + store.add(parentToolSet.addToolSet(childToolSet)); + store.add(childToolSet.addTool(childToolWithWhen)); + store.add(childToolSet.addTool(childToolWithoutWhen)); + + // Get tools from the parent tool set + const tools = Array.from(parentToolSet.getTools()); + const toolIds = tools.map(t => t.id); + + // Should include parent tool, child tool without when, but not child tool with when + assert.ok(toolIds.includes('parentTool'), 'Parent tool should be in tool set'); + assert.ok(toolIds.includes('childToolWithoutWhen'), 'Child tool without when should be in tool set'); + assert.ok(!toolIds.includes('childToolWithWhen'), 'Child tool with when=true should NOT be in tool set when context key is false'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_CDATA.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_html_comments.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_invalid_HTML.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_invalid_HTML_with_attributes.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_mixed_valid_and_invalid_HTML.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_remote_images_are_disallowed.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_self-closing_elements.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.1.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_self-closing_elements.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_self-closing_elements.1.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_self-closing_elements.1.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_simple.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_simple.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_simple.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_supportHtml_with_one-line_markdown.1.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap b/src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/browser/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap rename to src/vs/workbench/contrib/chat/test/browser/widget/__snapshots__/ChatMarkdownRenderer_valid_HTML.0.snap diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts new file mode 100644 index 00000000000..e4bd7c5285a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatAttachmentsContentPart.test.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { ChatAttachmentsContentPart } from '../../../../browser/widget/chatContentParts/chatAttachmentsContentPart.js'; +import { IChatRequestVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; + +suite('ChatAttachmentsContentPart', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let disposables: DisposableStore; + let instantiationService: ReturnType; + + setup(() => { + disposables = store.add(new DisposableStore()); + instantiationService = workbenchInstantiationService(undefined, store); + }); + + teardown(() => { + disposables.dispose(); + }); + + function createFileEntry(name: string, uri?: URI): IChatRequestVariableEntry { + const fileUri = uri ?? URI.file(`/test/${name}`); + return { + kind: 'file', + id: `file-${name}`, + name, + fullName: fileUri.path, + value: fileUri + }; + } + + function createImageEntry(name: string, buffer: Uint8Array, mimeType: string = 'image/png'): IChatRequestVariableEntry { + return { + kind: 'image', + id: `image-${name}`, + name, + value: buffer, + mimeType, + isURL: false, + references: [{ kind: 'reference', reference: URI.file(`/test/${name}`) }] + }; + } + + suite('updateVariables', () => { + test('should update variables and re-render', () => { + const initialVariables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + createFileEntry('file2.ts') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables: initialVariables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + // Initial state should have 2 attachments + const initialAttachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(initialAttachments.length, 2, 'Should have 2 initial attachments'); + + // Update with new variables + const newVariables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + createFileEntry('file2.ts'), + createFileEntry('file3.ts') + ]; + + part.updateVariables(newVariables); + + // Should now have 3 attachments + const updatedAttachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(updatedAttachments.length, 3, 'Should have 3 attachments after update'); + }); + + test('should handle updating from file to image', () => { + const initialVariables: IChatRequestVariableEntry[] = [ + createFileEntry('image.png') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables: initialVariables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + // Initial state should have 1 file attachment + assert.strictEqual(part.domNode!.querySelectorAll('.chat-attached-context-attachment').length, 1); + + // Update with image entry (simulating lazy load completion) + const imageBuffer = new Uint8Array([0x89, 0x50, 0x4E, 0x47]); // PNG header + const newVariables: IChatRequestVariableEntry[] = [ + createImageEntry('image.png', imageBuffer) + ]; + + part.updateVariables(newVariables); + + // Should still have 1 attachment (now as image) + const updatedAttachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(updatedAttachments.length, 1, 'Should have 1 attachment after update'); + }); + + test('should preserve contextMenuHandler after update', () => { + const initialVariables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables: initialVariables } + )); + + const handler = () => { /* handler logic */ }; + part.contextMenuHandler = handler; + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + // Update with new variables + const newVariables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + createFileEntry('file2.ts') + ]; + + part.updateVariables(newVariables); + + // The handler property should be preserved (updateVariables doesn't clear it) + assert.strictEqual(part.contextMenuHandler, handler, 'contextMenuHandler should be preserved after update'); + }); + + test('should handle empty variables array', () => { + const initialVariables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables: initialVariables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + assert.strictEqual(part.domNode!.querySelectorAll('.chat-attached-context-attachment').length, 1); + + // Update with empty array + part.updateVariables([]); + + // Should have no attachments + const updatedAttachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(updatedAttachments.length, 0, 'Should have 0 attachments after clearing'); + }); + + test('should handle updating same variables (no-op)', () => { + const variables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + createFileEntry('file2.ts') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + // Update with same variables (different array, same content) + part.updateVariables([...variables]); + + // Should re-render (we don't optimize for same content) + const updatedAttachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(updatedAttachments.length, 2, 'Should still have 2 attachments'); + }); + }); + + suite('basic rendering', () => { + test('should render file attachments', () => { + const variables: IChatRequestVariableEntry[] = [ + createFileEntry('file1.ts'), + createFileEntry('file2.ts') + ]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables } + )); + + mainWindow.document.body.appendChild(part.domNode!); + disposables.add(toDisposable(() => part.domNode?.remove())); + + const attachments = part.domNode!.querySelectorAll('.chat-attached-context-attachment'); + assert.strictEqual(attachments.length, 2, 'Should render 2 file attachments'); + }); + + test('should have chat-attached-context class on domNode', () => { + const variables: IChatRequestVariableEntry[] = [createFileEntry('file.ts')]; + + const part = store.add(instantiationService.createInstance( + ChatAttachmentsContentPart, + { variables } + )); + + assert.ok(part.domNode!.classList.contains('chat-attached-context'), 'Should have chat-attached-context class'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.ts new file mode 100644 index 00000000000..94f5c182311 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatInlineAnchorWidget.test.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 assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { renderFileWidgets } from '../../../../browser/widget/chatContentParts/chatInlineAnchorWidget.js'; +import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; +import { IChatMarkdownAnchorService } from '../../../../browser/widget/chatContentParts/chatMarkdownAnchorService.js'; + +suite('ChatInlineAnchorWidget Metadata Validation', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let disposables: DisposableStore; + let instantiationService: ReturnType; + let mockAnchorService: IChatMarkdownAnchorService; + + setup(() => { + disposables = store.add(new DisposableStore()); + instantiationService = workbenchInstantiationService(undefined, store); + + // Mock the anchor service + mockAnchorService = { + _serviceBrand: undefined, + register: () => ({ dispose: () => { } }), + lastFocusedAnchor: undefined + }; + + instantiationService.stub(IChatMarkdownAnchorService, mockAnchorService); + }); + + function createTestElement(linkText: string, href: string = 'file:///test.txt'): HTMLElement { + const container = mainWindow.document.createElement('div'); + const anchor = mainWindow.document.createElement('a'); + anchor.textContent = linkText; + anchor.setAttribute('data-href', href); + container.appendChild(anchor); + return container; + } + + test('renders widget for link with vscodeLinkType query parameter', () => { + const element = createTestElement('mySkill', 'file:///test.txt?vscodeLinkType=skill'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered for link with vscodeLinkType query parameter'); + }); + + test('renders widget for empty link text', () => { + const element = createTestElement(''); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered for empty link text'); + }); + + test('renders widget for vscodeLinkType=file', () => { + const element = createTestElement('document.txt', 'file:///path/to/document.txt?vscodeLinkType=file'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered for vscodeLinkType=file'); + }); + + test('does not render widget for link without vscodeLinkType query parameter', () => { + const element = createTestElement('regular link text', 'file:///test.txt'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(!widget, 'Widget should not be rendered for link without vscodeLinkType query parameter'); + }); + + test('does not render widget when URI scheme is missing', () => { + const element = createTestElement('mySkill', ''); // Empty href + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(!widget, 'Widget should not be rendered when URI scheme is missing'); + }); + + test('renders widget with various vscodeLinkType values', () => { + const element = createTestElement('customName', 'file:///test.txt?vscodeLinkType=custom'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered for any vscodeLinkType value'); + }); + + test('handles vscodeLinkType with other query parameters', () => { + const element = createTestElement('skillName', 'file:///test.txt?other=value&vscodeLinkType=skill&another=param'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered when vscodeLinkType is among multiple query parameters'); + }); + + test('handles multiple links in same element', () => { + const container = mainWindow.document.createElement('div'); + + // Add link with vscodeLinkType query parameter + const validAnchor = mainWindow.document.createElement('a'); + validAnchor.textContent = 'validSkill'; + validAnchor.setAttribute('data-href', 'file:///valid.txt?vscodeLinkType=skill'); + container.appendChild(validAnchor); + + // Add link without vscodeLinkType query parameter + const invalidAnchor = mainWindow.document.createElement('a'); + invalidAnchor.textContent = 'regular text'; + invalidAnchor.setAttribute('data-href', 'file:///invalid.txt'); + container.appendChild(invalidAnchor); + + // Add empty link text + const emptyAnchor = mainWindow.document.createElement('a'); + emptyAnchor.textContent = ''; + emptyAnchor.setAttribute('data-href', 'file:///empty.txt'); + container.appendChild(emptyAnchor); + + renderFileWidgets(container, instantiationService, mockAnchorService, disposables); + + const widgets = container.querySelectorAll('.chat-inline-anchor-widget'); + assert.strictEqual(widgets.length, 2, 'Should render widgets for link with vscodeLinkType and empty link text only'); + }); + + test('uses link text as fileName in metadata', () => { + const element = createTestElement('myCustomFileName', 'file:///test.txt?vscodeLinkType=skill'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(widget, 'Widget should be rendered'); + // The link text becomes the fileName which is used as the label + const labelElement = widget?.querySelector('.icon-label'); + assert.ok(labelElement?.textContent?.includes('myCustomFileName'), 'Label should contain the link text as fileName'); + }); + + test('does not render widget for malformed URI', () => { + const element = createTestElement('mySkill', '://malformed-uri-without-scheme'); + renderFileWidgets(element, instantiationService, mockAnchorService, disposables); + + const widget = element.querySelector('.chat-inline-anchor-widget'); + assert.ok(!widget, 'Widget should not be rendered for malformed URI'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts new file mode 100644 index 00000000000..c83ae7241ff --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatQuestionCarouselPart.test.ts @@ -0,0 +1,589 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { mainWindow } from '../../../../../../../base/browser/window.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { ChatQuestionCarouselPart, IChatQuestionCarouselOptions } from '../../../../browser/widget/chatContentParts/chatQuestionCarouselPart.js'; +import { IChatQuestionCarousel } from '../../../../common/chatService/chatService.js'; +import { IChatContentPartRenderContext } from '../../../../browser/widget/chatContentParts/chatContentParts.js'; + +function createMockCarousel(questions: IChatQuestionCarousel['questions'], allowSkip: boolean = true): IChatQuestionCarousel { + return { + kind: 'questionCarousel', + questions, + allowSkip, + }; +} + +function createMockContext(): IChatContentPartRenderContext { + return {} as IChatContentPartRenderContext; +} + +suite('ChatQuestionCarouselPart', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let widget: ChatQuestionCarouselPart; + let submittedAnswers: Map | undefined | null = null; + + function createWidget(carousel: IChatQuestionCarousel): ChatQuestionCarouselPart { + const instantiationService = workbenchInstantiationService(undefined, store); + const options: IChatQuestionCarouselOptions = { + onSubmit: (answers) => { + submittedAnswers = answers; + } + }; + widget = store.add(instantiationService.createInstance(ChatQuestionCarouselPart, carousel, createMockContext(), options)); + mainWindow.document.body.appendChild(widget.domNode); + return widget; + } + + teardown(() => { + if (widget?.domNode?.parentNode) { + widget.domNode.parentNode.removeChild(widget.domNode); + } + submittedAnswers = null; + }); + + suite('Basic Rendering', () => { + test('renders carousel container with proper structure', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ]); + createWidget(carousel); + + assert.ok(widget.domNode.classList.contains('chat-question-carousel-container')); + assert.ok(widget.domNode.querySelector('.chat-question-header-row')); + assert.ok(widget.domNode.querySelector('.chat-question-carousel-content')); + assert.ok(widget.domNode.querySelector('.chat-question-carousel-nav')); + }); + + test('renders question title', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'What is your name?', message: 'What is your name?' } + ]); + createWidget(carousel); + + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title); + // Title includes progress prefix like "(1/1) What is your name?" + assert.ok(title?.textContent?.includes('What is your name?')); + }); + + test('renders question title when message is not provided', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Fallback title text' } + ]); + createWidget(carousel); + + const title = widget.domNode.querySelector('.chat-question-title'); + assert.ok(title, 'title element should exist when only title is provided'); + // Title should fall back to title property when message is not provided + assert.ok(title?.textContent?.includes('Fallback title text')); + }); + + test('renders progress indicator correctly', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1', message: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2', message: 'Question 2' }, + { id: 'q3', type: 'text', title: 'Question 3', message: 'Question 3' } + ]); + createWidget(carousel); + + // Progress is shown in the step indicator in the footer as "1/3" + const stepIndicator = widget.domNode.querySelector('.chat-question-step-indicator'); + assert.ok(stepIndicator); + assert.ok(stepIndicator?.textContent?.includes('1')); + assert.ok(stepIndicator?.textContent?.includes('3')); + }); + }); + + suite('Question Types', () => { + test('renders text input for text type questions', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Enter your name' } + ]); + createWidget(carousel); + + const inputContainer = widget.domNode.querySelector('.chat-question-input-container'); + assert.ok(inputContainer); + const inputBox = inputContainer?.querySelector('.monaco-inputbox input'); + assert.ok(inputBox, 'Should have an input box for text questions'); + }); + + test('renders list items for singleSelect type questions', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' } + ] + } + ]); + createWidget(carousel); + + const listItems = widget.domNode.querySelectorAll('.chat-question-list-item'); + assert.strictEqual(listItems.length, 2, 'Should have 2 list items'); + }); + + test('renders list items with checkboxes for multiSelect type questions', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'multiSelect', + title: 'Choose multiple', + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' }, + { id: 'c', label: 'Option C', value: 'c' } + ] + } + ]); + createWidget(carousel); + + const listItems = widget.domNode.querySelectorAll('.chat-question-list-item.multi-select'); + assert.strictEqual(listItems.length, 3, 'Should have 3 list items for multiSelect'); + const checkboxes = widget.domNode.querySelectorAll('.chat-question-list-checkbox'); + assert.strictEqual(checkboxes.length, 3, 'Should have 3 checkboxes'); + }); + + test('freeform textarea is always rendered for singleSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'a' } + ] + } + ]); + createWidget(carousel); + + const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); + assert.ok(freeformTextarea, 'Freeform textarea should always be rendered for singleSelect'); + }); + + test('freeform textarea is always rendered for multiSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'multiSelect', + title: 'Choose multiple', + options: [ + { id: 'a', label: 'Option A', value: 'a' } + ] + } + ]); + createWidget(carousel); + + const freeformTextarea = widget.domNode.querySelector('.chat-question-freeform-textarea'); + assert.ok(freeformTextarea, 'Freeform textarea should always be rendered for multiSelect'); + }); + + test('default options are pre-selected for singleSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' } + ], + defaultValue: 'b' + } + ]); + createWidget(carousel); + + const listItems = widget.domNode.querySelectorAll('.chat-question-list-item') as NodeListOf; + assert.strictEqual(listItems[0].classList.contains('selected'), false); + assert.strictEqual(listItems[1].classList.contains('selected'), true, 'Default option should be selected'); + }); + + test('default options are pre-selected for multiSelect', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'multiSelect', + title: 'Choose multiple', + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' }, + { id: 'c', label: 'Option C', value: 'c' } + ], + defaultValue: ['a', 'c'] + } + ]); + createWidget(carousel); + + const listItems = widget.domNode.querySelectorAll('.chat-question-list-item') as NodeListOf; + assert.strictEqual(listItems[0].classList.contains('checked'), true, 'First default option should be checked'); + assert.strictEqual(listItems[1].classList.contains('checked'), false); + assert.strictEqual(listItems[2].classList.contains('checked'), true, 'Third default option should be checked'); + }); + }); + + suite('Navigation', () => { + test('previous button is disabled on first question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' }, + { id: 'q2', type: 'text', title: 'Question 2' } + ]); + createWidget(carousel); + + // Use dedicated class selectors for stability + const prevButton = widget.domNode.querySelector('.chat-question-nav-prev') as HTMLButtonElement; + assert.ok(prevButton, 'Previous button should exist'); + assert.ok(prevButton.classList.contains('disabled') || prevButton.disabled, 'Previous button should be disabled on first question'); + }); + + test('next button shows submit icon on last question', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Only Question' } + ]); + createWidget(carousel); + + // Use dedicated class selector for stability + const nextButton = widget.domNode.querySelector('.chat-question-nav-next') as HTMLElement; + assert.ok(nextButton, 'Next button should exist'); + assert.strictEqual(nextButton.title, 'Submit', 'Next button should have Submit title on last question'); + }); + }); + + suite('Skip Functionality', () => { + test('skip succeeds when allowSkip is true and returns defaults', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1', defaultValue: 'default answer' } + ], true); + createWidget(carousel); + + const result = widget.skip(); + assert.strictEqual(result, true, 'skip() should return true when allowSkip is true'); + assert.ok(submittedAnswers instanceof Map, 'Skip should call onSubmit with a Map'); + assert.strictEqual(submittedAnswers?.get('q1'), 'default answer', 'Skip should return default values'); + }); + + test('skip fails when allowSkip is false', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], false); + createWidget(carousel); + + const result = widget.skip(); + assert.strictEqual(result, false, 'skip() should return false when allowSkip is false'); + assert.strictEqual(submittedAnswers, null, 'onSubmit should not have been called'); + }); + + test('skip can only be called once', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], true); + createWidget(carousel); + + widget.skip(); + submittedAnswers = null; // reset + const result = widget.skip(); + assert.strictEqual(result, false, 'Second skip() should return false'); + assert.strictEqual(submittedAnswers, null, 'onSubmit should not be called again'); + }); + }); + + suite('Ignore Functionality', () => { + test('ignore succeeds when allowSkip is true and returns undefined', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], true); + createWidget(carousel); + + const result = widget.ignore(); + assert.strictEqual(result, true, 'ignore() should return true when allowSkip is true'); + assert.strictEqual(submittedAnswers, undefined, 'Ignore should call onSubmit with undefined'); + }); + + test('ignore fails when allowSkip is false', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], false); + createWidget(carousel); + + const result = widget.ignore(); + assert.strictEqual(result, false, 'ignore() should return false when allowSkip is false'); + assert.strictEqual(submittedAnswers, null, 'onSubmit should not have been called'); + }); + + test('ignore can only be called once', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], true); + createWidget(carousel); + + widget.ignore(); + submittedAnswers = null; // reset + const result = widget.ignore(); + assert.strictEqual(result, false, 'Second ignore() should return false'); + assert.strictEqual(submittedAnswers, null, 'onSubmit should not be called again'); + }); + + test('skip and ignore are mutually exclusive', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], true); + createWidget(carousel); + + widget.skip(); + submittedAnswers = null; // reset + const result = widget.ignore(); + assert.strictEqual(result, false, 'ignore() should return false after skip()'); + assert.strictEqual(submittedAnswers, null, 'onSubmit should not be called again'); + }); + }); + + suite('Accessibility', () => { + test('navigation area has proper role and aria-label', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ]); + createWidget(carousel); + + const nav = widget.domNode.querySelector('.chat-question-carousel-nav'); + assert.strictEqual(nav?.getAttribute('role'), 'navigation'); + assert.ok(nav?.getAttribute('aria-label'), 'Navigation should have aria-label'); + }); + + test('single select list has proper role and aria-label', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'a' }, + { id: 'b', label: 'Option B', value: 'b' } + ] + } + ]); + createWidget(carousel); + + const list = widget.domNode.querySelector('.chat-question-list'); + assert.strictEqual(list?.getAttribute('role'), 'listbox'); + assert.strictEqual(list?.getAttribute('aria-label'), 'Choose one'); + }); + + test('list items have proper role and aria-selected', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'a' } + ] + } + ]); + createWidget(carousel); + + const listItem = widget.domNode.querySelector('.chat-question-list-item') as HTMLElement; + assert.ok(listItem, 'List item should exist'); + assert.strictEqual(listItem.getAttribute('role'), 'option'); + assert.ok(listItem.id, 'List item should have an id'); + assert.strictEqual(listItem.getAttribute('aria-selected'), 'false', 'Unselected item should have aria-selected=false'); + }); + }); + + suite('hasSameContent', () => { + test('returns true for same carousel instance', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ]); + createWidget(carousel); + + assert.strictEqual(widget.hasSameContent(carousel, [], {} as never), true); + }); + + test('returns false for different content type', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ]); + createWidget(carousel); + + const differentContent = { kind: 'markdown' as const } as never; + assert.strictEqual(widget.hasSameContent(differentContent, [], {} as never), false); + }); + }); + + suite('Auto-Approve (Yolo Mode)', () => { + test('skip returns default values for text questions', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1', defaultValue: 'default text' } + ], true); + createWidget(carousel); + + widget.skip(); + assert.ok(submittedAnswers instanceof Map); + assert.strictEqual(submittedAnswers?.get('q1'), 'default text'); + }); + + test('skip returns default values for singleSelect questions', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'singleSelect', + title: 'Choose one', + options: [ + { id: 'a', label: 'Option A', value: 'value_a' }, + { id: 'b', label: 'Option B', value: 'value_b' } + ], + defaultValue: 'b' + } + ], true); + createWidget(carousel); + + widget.skip(); + assert.ok(submittedAnswers instanceof Map); + // singleSelect always returns structured format with freeformValue + const answer = submittedAnswers?.get('q1') as { selectedValue: unknown; freeformValue: unknown }; + assert.strictEqual(answer.selectedValue, 'value_b'); + assert.strictEqual(answer.freeformValue, undefined); + }); + + test('skip returns default values for multiSelect questions', () => { + const carousel = createMockCarousel([ + { + id: 'q1', + type: 'multiSelect', + title: 'Choose multiple', + options: [ + { id: 'a', label: 'Option A', value: 'value_a' }, + { id: 'b', label: 'Option B', value: 'value_b' }, + { id: 'c', label: 'Option C', value: 'value_c' } + ], + defaultValue: ['a', 'c'] + } + ], true); + createWidget(carousel); + + widget.skip(); + assert.ok(submittedAnswers instanceof Map); + // multiSelect always returns structured format with freeformValue + const answer = submittedAnswers?.get('q1') as { selectedValues: unknown[]; freeformValue: unknown }; + assert.ok(Array.isArray(answer.selectedValues)); + assert.strictEqual(answer.selectedValues.length, 2); + assert.ok(answer.selectedValues.includes('value_a')); + assert.ok(answer.selectedValues.includes('value_c')); + assert.strictEqual(answer.freeformValue, undefined); + }); + + test('skip returns defaults for multiple questions', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Text Question', defaultValue: 'text default' }, + { + id: 'q2', + type: 'singleSelect', + title: 'Single Select', + options: [ + { id: 'opt1', label: 'First', value: 'first_value' } + ], + defaultValue: 'opt1' + } + ], true); + createWidget(carousel); + + widget.skip(); + assert.ok(submittedAnswers instanceof Map); + assert.strictEqual(submittedAnswers?.get('q1'), 'text default'); + // singleSelect always returns structured format with freeformValue + const answer = submittedAnswers?.get('q2') as { selectedValue: unknown; freeformValue: unknown }; + assert.strictEqual(answer.selectedValue, 'first_value'); + assert.strictEqual(answer.freeformValue, undefined); + }); + + test('skip returns empty map when no defaults are provided', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question without default' } + ], true); + createWidget(carousel); + + widget.skip(); + assert.ok(submittedAnswers instanceof Map); + assert.strictEqual(submittedAnswers?.size, 0, 'Should return empty map when no defaults'); + }); + }); + + suite('Used Carousel Summary', () => { + test('shows summary with answers after skip()', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1', defaultValue: 'default answer' } + ], true); + createWidget(carousel); + + widget.skip(); + + assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); + const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); + assert.ok(summary, 'Should show summary container after skip'); + const summaryItem = summary?.querySelector('.chat-question-summary-item'); + assert.ok(summaryItem, 'Should have summary item for the question'); + const summaryValue = summaryItem?.querySelector('.chat-question-summary-answer-title'); + assert.ok(summaryValue?.textContent?.includes('default answer'), 'Summary should show the default answer'); + }); + + test('shows skipped message after ignore()', () => { + const carousel = createMockCarousel([ + { id: 'q1', type: 'text', title: 'Question 1' } + ], true); + createWidget(carousel); + + widget.ignore(); + + assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); + const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); + assert.ok(summary, 'Should show summary container after ignore'); + const skippedMessage = summary?.querySelector('.chat-question-summary-skipped'); + assert.ok(skippedMessage, 'Should show skipped message when ignored'); + }); + + test('renders summary when constructed with isUsed and data', () => { + const carousel: IChatQuestionCarousel = { + kind: 'questionCarousel', + questions: [ + { id: 'q1', type: 'text', title: 'Question 1' } + ], + allowSkip: true, + isUsed: true, + data: { q1: 'saved answer' } + }; + createWidget(carousel); + + assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); + const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); + assert.ok(summary, 'Should show summary container when isUsed is true'); + const summaryValue = summary?.querySelector('.chat-question-summary-answer-title'); + assert.ok(summaryValue?.textContent?.includes('saved answer'), 'Summary should show saved answer from data'); + }); + + test('shows skipped message when constructed with isUsed but no data', () => { + const carousel: IChatQuestionCarousel = { + kind: 'questionCarousel', + questions: [ + { id: 'q1', type: 'text', title: 'Question 1' } + ], + allowSkip: true, + isUsed: true + }; + createWidget(carousel); + + assert.ok(widget.domNode.classList.contains('chat-question-carousel-used'), 'Should have used class'); + const summary = widget.domNode.querySelector('.chat-question-carousel-summary'); + assert.ok(summary, 'Should show summary container'); + const skippedMessage = summary?.querySelector('.chat-question-summary-skipped'); + assert.ok(skippedMessage, 'Should show skipped message when no data'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts new file mode 100644 index 00000000000..6844fd2cee0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatSubagentContentPart.test.ts @@ -0,0 +1,1368 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { isHTMLElement } from '../../../../../../../base/browser/dom.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../../../base/common/observable.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { ChatSubagentContentPart } from '../../../../browser/widget/chatContentParts/chatSubagentContentPart.js'; +import { IChatMarkdownContent, IChatSubagentToolInvocationData, IChatToolInvocation, IChatToolInvocationSerialized, ToolConfirmKind } from '../../../../common/chatService/chatService.js'; +import { IChatContentPartRenderContext } from '../../../../browser/widget/chatContentParts/chatContentParts.js'; +import { IChatResponseViewModel } from '../../../../common/model/chatViewModel.js'; +import { IChatMarkdownAnchorService } from '../../../../browser/widget/chatContentParts/chatMarkdownAnchorService.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../../../../base/browser/markdownRenderer.js'; +import { IMarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { EditorPool, DiffEditorPool } from '../../../../browser/widget/chatContentParts/chatContentCodePools.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { ChatTreeItem } from '../../../../browser/chat.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { RunSubagentTool } from '../../../../common/tools/builtinTools/runSubagentTool.js'; +import { CollapsibleListPool } from '../../../../browser/widget/chatContentParts/chatReferencesContentPart.js'; +import { ToolDataSource } from '../../../../common/tools/languageModelToolsService.js'; + +suite('ChatSubagentContentPart', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + type ToolInvocationParameters = IChatToolInvocation.State extends { parameters: infer P } ? P : never; + + let disposables: DisposableStore; + let instantiationService: ReturnType; + let mockMarkdownRenderer: IMarkdownRenderer; + let mockAnchorService: IChatMarkdownAnchorService; + let mockHoverService: IHoverService; + let mockListPool: CollapsibleListPool; + let mockEditorPool: EditorPool; + let mockCodeBlockModelCollection: CodeBlockModelCollection; + let announcedToolProgressKeys: Set; + + function createMockRenderContext(isComplete: boolean = false): IChatContentPartRenderContext { + const mockElement: Partial = { + isComplete, + id: 'test-response-id', + sessionResource: URI.parse('chat-session://test/session1'), + get model() { return {} as IChatResponseViewModel['model']; } + }; + + return { + element: mockElement as ChatTreeItem, + elementIndex: 0, + container: mainWindow.document.createElement('div'), + content: [], + contentIndex: 0, + editorPool: mockEditorPool, + codeBlockStartIndex: 0, + treeStartIndex: 0, + diffEditorPool: {} as DiffEditorPool, + codeBlockModelCollection: mockCodeBlockModelCollection, + currentWidth: observableValue('currentWidth', 500), + onDidChangeVisibility: Event.None + }; + } + + function createState(stateType: IChatToolInvocation.StateKind, parameters?: ToolInvocationParameters): IChatToolInvocation.State { + switch (stateType) { + case IChatToolInvocation.StateKind.Streaming: + return { + type: IChatToolInvocation.StateKind.Streaming, + partialInput: observableValue('partialInput', {}), + streamingMessage: observableValue('streamingMessage', undefined) + }; + case IChatToolInvocation.StateKind.Completed: + return { + type: IChatToolInvocation.StateKind.Completed, + parameters, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + resultDetails: undefined, + postConfirmed: undefined, + contentForModel: [{ kind: 'text', value: 'test result' }] + }; + case IChatToolInvocation.StateKind.Executing: + return { + type: IChatToolInvocation.StateKind.Executing, + parameters, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + progress: observableValue('progress', { message: undefined, progress: undefined }) + }; + case IChatToolInvocation.StateKind.WaitingForConfirmation: + return { + type: IChatToolInvocation.StateKind.WaitingForConfirmation, + parameters, + confirmationMessages: { + title: 'Confirm action', + message: 'Are you sure you want to proceed?' + }, + confirm: () => { } + }; + case IChatToolInvocation.StateKind.WaitingForPostApproval: + return { + type: IChatToolInvocation.StateKind.WaitingForPostApproval, + parameters, + confirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + resultDetails: undefined, + contentForModel: [{ kind: 'text', value: 'test result' }], + confirm: () => { } + }; + case IChatToolInvocation.StateKind.Cancelled: + return { + type: IChatToolInvocation.StateKind.Cancelled, + parameters, + reason: ToolConfirmKind.Denied + }; + } + } + + function createMockToolInvocation(options: { + toolId?: string; + toolCallId?: string; + subAgentInvocationId?: string; + toolSpecificData?: IChatSubagentToolInvocationData; + stateType?: IChatToolInvocation.StateKind; + parameters?: ToolInvocationParameters; + invocationMessage?: string; + } = {}): IChatToolInvocation { + const stateType = options.stateType ?? IChatToolInvocation.StateKind.Streaming; + const stateValue = createState(stateType, options.parameters); + const toolCallId = options.toolCallId ?? 'tool-call-' + Math.random().toString(36).substring(7); + + const toolInvocation: IChatToolInvocation = { + presentation: undefined, + toolSpecificData: options.toolSpecificData ?? { + kind: 'subagent', + description: 'Test subagent description', + agentName: 'TestAgent', + prompt: 'Test prompt' + }, + originMessage: undefined, + invocationMessage: options.invocationMessage ?? 'Running subagent...', + pastTenseMessage: undefined, + source: ToolDataSource.Internal, + toolId: options.toolId ?? RunSubagentTool.Id, + toolCallId: toolCallId, + subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + state: observableValue('state', stateValue), + kind: 'toolInvocation', + toJSON: () => createMockSerializedToolInvocation({ + toolId: options.toolId ?? RunSubagentTool.Id, + subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + toolSpecificData: options.toolSpecificData, + isComplete: stateType === IChatToolInvocation.StateKind.Completed + }) + }; + + return toolInvocation; + } + + function createMockSerializedToolInvocation(options: { + toolId?: string; + subAgentInvocationId?: string; + toolSpecificData?: IChatSubagentToolInvocationData; + isComplete?: boolean; + } = {}): IChatToolInvocationSerialized { + return { + presentation: undefined, + toolSpecificData: options.toolSpecificData ?? { + kind: 'subagent', + description: 'Test subagent description', + agentName: 'TestAgent', + prompt: 'Test prompt', + result: 'Test result text' + }, + originMessage: undefined, + invocationMessage: 'Running subagent...', + pastTenseMessage: undefined, + resultDetails: undefined, + isConfirmed: { type: ToolConfirmKind.ConfirmationNotNeeded }, + isComplete: options.isComplete ?? true, + toolCallId: options.subAgentInvocationId ?? 'test-tool-call-id', + toolId: options.toolId ?? RunSubagentTool.Id, + source: ToolDataSource.Internal, + subAgentInvocationId: options.subAgentInvocationId ?? 'test-subagent-id', + kind: 'toolInvocationSerialized' + }; + } + + setup(() => { + disposables = store.add(new DisposableStore()); + instantiationService = workbenchInstantiationService(undefined, store); + + // Create a mock markdown renderer + mockMarkdownRenderer = { + render: (_markdown: IMarkdownString, _options?: MarkdownRenderOptions, outElement?: HTMLElement): IRenderedMarkdown => { + const element = outElement ?? mainWindow.document.createElement('div'); + const content = typeof _markdown === 'string' ? _markdown : (_markdown.value ?? ''); + element.textContent = content; + return { + element, + dispose: () => { } + }; + } + }; + + // Mock the anchor service + mockAnchorService = { + _serviceBrand: undefined, + register: () => ({ dispose: () => { } }), + lastFocusedAnchor: undefined + }; + instantiationService.stub(IChatMarkdownAnchorService, mockAnchorService); + + // Mock hover service + mockHoverService = { + _serviceBrand: undefined, + showDelayedHover: () => undefined, + setupDelayedHover: () => ({ dispose: () => { } }), + setupDelayedHoverAtMouse: () => ({ dispose: () => { } }), + showInstantHover: () => undefined, + hideHover: () => { }, + showAndFocusLastHover: () => { }, + setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }), + showManagedHover: () => { } + }; + instantiationService.stub(IHoverService, mockHoverService); + + // Mock list pool and editor pool + mockListPool = {} as CollapsibleListPool; + mockEditorPool = {} as EditorPool; + mockCodeBlockModelCollection = {} as CodeBlockModelCollection; + announcedToolProgressKeys = new Set(); + }); + + teardown(() => { + disposables.dispose(); + }); + + function createPart( + toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized, + context: IChatContentPartRenderContext, + idOverride?: string + ): ChatSubagentContentPart { + const part = store.add(instantiationService.createInstance( + ChatSubagentContentPart, + idOverride ?? toolInvocation.subAgentInvocationId!, + toolInvocation, + context, + mockMarkdownRenderer, + mockListPool, + mockEditorPool, + () => 500, + mockCodeBlockModelCollection, + announcedToolProgressKeys + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add({ dispose: () => part.domNode.remove() }); + + return part; + } + + + function getCollapseButton(part: ChatSubagentContentPart): HTMLElement | undefined { + const label = part.domNode.firstElementChild; + if (!isHTMLElement(label)) { + return undefined; + } + + const button = label.firstElementChild; + return isHTMLElement(button) ? button : undefined; + } + + function getCollapseButtonLabel(button: HTMLElement): HTMLElement | undefined { + const label = button.lastElementChild; + return isHTMLElement(label) ? label : undefined; + } + + function getCollapseButtonIcon(button: HTMLElement): HTMLElement | undefined { + const icon = button.firstElementChild; + return isHTMLElement(icon) ? icon : undefined; + } + + function getWrapperElement(part: ChatSubagentContentPart): HTMLElement | undefined { + const wrapper = part.domNode.lastElementChild; + return isHTMLElement(wrapper) ? wrapper : undefined; + } + + suite('Basic rendering', () => { + test('should create subagent part with correct classes', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + assert.ok(part.domNode.classList.contains('chat-thinking-box'), 'Should have chat-thinking-box class'); + assert.ok(part.domNode.classList.contains('chat-subagent-part'), 'Should have chat-subagent-part class'); + assert.ok(part.domNode.classList.contains('chat-thinking-fixed-mode'), 'Should have chat-thinking-fixed-mode class'); + }); + + test('should start collapsed', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed by default'); + }); + }); + + suite('Title extraction', () => { + test('should extract title with agent name from toolSpecificData', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Searching the codebase', + agentName: 'CodeSearchAgent', + prompt: 'Search for authentication' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const labelElement = getCollapseButtonLabel(button); + const buttonText = labelElement?.textContent ?? button.textContent ?? ''; + assert.ok(buttonText.includes('CodeSearchAgent'), 'Title should include agent name'); + assert.ok(buttonText.includes('Searching the codebase'), 'Title should include description'); + }); + + test('should use default prefix when no agent name is provided', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task' + // no agentName + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const labelElement = getCollapseButtonLabel(button); + const buttonText = labelElement?.textContent ?? button.textContent ?? ''; + assert.ok(buttonText.includes('Subagent:'), 'Title should use default Subagent prefix'); + }); + }); + + suite('State management', () => { + test('should start as active', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + assert.strictEqual(part.getIsActive(), true, 'Should start as active'); + }); + + test('markAsInactive should update isActive state', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + part.markAsInactive(); + + assert.strictEqual(part.getIsActive(), false, 'Should be inactive after markAsInactive'); + }); + + test('markAsInactive should remove streaming class', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand to trigger wrapper creation + const button = getCollapseButton(part); + button?.click(); + + part.markAsInactive(); + + const wrapper = getWrapperElement(part); + if (wrapper) { + assert.strictEqual(wrapper.classList.contains('chat-thinking-streaming'), false, + 'Streaming class should be removed after markAsInactive'); + } + }); + + test('markAsInactive should collapse the part', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // First expand + const button = getCollapseButton(part); + button?.click(); + + // Verify expanded + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false); + + part.markAsInactive(); + + // Should collapse when inactive + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed after markAsInactive'); + }); + + test('finalizeTitle should update button icon to check', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + part.finalizeTitle(); + + // The button should now show a check icon + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const iconElement = getCollapseButtonIcon(button); + assert.ok(iconElement?.classList.contains('codicon-check'), 'Should have check icon after finalization'); + }); + }); + + suite('Serialized invocation', () => { + test('should handle serialized tool invocation', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'FinishedAgent', + prompt: 'Original prompt', + result: 'Task completed successfully' + } + }); + const context = createMockRenderContext(true); // isComplete = true + + const part = createPart(serializedInvocation, context); + + // Should already be inactive since it's serialized + assert.strictEqual(part.getIsActive(), false, 'Serialized invocation should be inactive'); + }); + }); + + suite('hasSameContent', () => { + test('should return true for tool invocation with same subAgentInvocationId', () => { + const toolInvocation = createMockToolInvocation({ subAgentInvocationId: 'subagent-123' }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const otherInvocation = createMockToolInvocation({ + toolId: 'some-tool', + subAgentInvocationId: 'subagent-123' + }); + + const result = part.hasSameContent(otherInvocation, [], context.element); + assert.strictEqual(result, true, 'Should match tool invocation with same subAgentInvocationId'); + }); + + test('should return false for tool invocation with different subAgentInvocationId', () => { + const toolInvocation = createMockToolInvocation({ subAgentInvocationId: 'subagent-123' }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const otherInvocation = createMockToolInvocation({ + toolId: 'some-tool', + subAgentInvocationId: 'subagent-456' + }); + + const result = part.hasSameContent(otherInvocation, [], context.element); + assert.strictEqual(result, false, 'Should not match tool invocation with different subAgentInvocationId'); + }); + + test('should return true for runSubagent tool using toolCallId as effective ID', () => { + const sharedToolCallId = 'shared-tool-call-id'; + const toolInvocation = createMockToolInvocation({ + toolId: RunSubagentTool.Id, + toolCallId: sharedToolCallId, + subAgentInvocationId: 'call-abc' + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context, toolInvocation.toolCallId); + + const otherInvocation = createMockToolInvocation({ + toolId: RunSubagentTool.Id, + toolCallId: sharedToolCallId, + subAgentInvocationId: 'call-abc' + }); + + const result = part.hasSameContent(otherInvocation, [], context.element); + assert.strictEqual(result, true, 'Should match runSubagent tool using toolCallId as effective ID'); + }); + + test('should return true for markdownContent (allowing grouping)', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'test' } + }; + + const result = part.hasSameContent(markdownContent, [], context.element); + assert.strictEqual(result, true, 'Should match markdownContent to allow grouping'); + }); + }); + + suite('Streaming behavior', () => { + test('should show loading spinner while streaming', () => { + const toolInvocation = createMockToolInvocation({ + stateType: IChatToolInvocation.StateKind.Streaming + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Should have loading spinner icon while streaming + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const loadingIcon = getCollapseButtonIcon(button); + assert.ok(loadingIcon?.classList.contains('codicon-loading'), 'Should have loading spinner while streaming'); + }); + }); + + suite('Expand/collapse', () => { + test('should toggle expansion when button is clicked', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Initially collapsed + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed')); + + // Click to expand + const button = getCollapseButton(part); + assert.ok(button, 'Should have expand button'); + button.click(); + + // Should be expanded + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should be expanded after clicking button'); + + // Click again to collapse + button.click(); + + // Should be collapsed again + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), + 'Should be collapsed after clicking button again'); + }); + + test('should have proper aria-expanded attribute', () => { + const toolInvocation = createMockToolInvocation(); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + const button = getCollapseButton(part); + assert.ok(button, 'Button should exist'); + assert.strictEqual(button.getAttribute('aria-expanded'), 'false', 'Should have aria-expanded="false" when collapsed'); + + // Expand + button.click(); + + assert.strictEqual(button.getAttribute('aria-expanded'), 'true', 'Should have aria-expanded="true" when expanded'); + }); + }); + + suite('Lazy rendering', () => { + test('should defer prompt/result rendering until expanded when initially complete', () => { + const serializedInvocation = createMockSerializedToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Completed task', + agentName: 'FinishedAgent', + prompt: 'Original prompt for the task', + result: 'Task completed successfully' + } + }); + const context = createMockRenderContext(true); // isComplete = true + + const part = createPart(serializedInvocation, context); + + // Content should be collapsed - no wrapper content initially visible + // Just verify that the domNode has the collapsed class + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed initially'); + + // Expand to trigger lazy rendering + const button = getCollapseButton(part); + assert.ok(button, 'Expand button should exist'); + button.click(); + + // After expanding, the content containers should be rendered + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded'); + + // Verify prompt and result sections exist in the expanded content + const wrapperContent = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapperContent, 'Wrapper content should exist after expand'); + + // Check that sections were inserted + const sections = wrapperContent.querySelectorAll('.chat-subagent-section'); + assert.ok(sections.length >= 2, 'Should have prompt and result sections after expand'); + }); + + test('should not render wrapper content while subagent is running (truly collapsed)', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Running task', + agentName: 'RunningAgent', + prompt: 'Prompt text' + }, + stateType: IChatToolInvocation.StateKind.Streaming + }); + const context = createMockRenderContext(false); // Not complete + + const part = createPart(toolInvocation, context); + + // Should be collapsed with just the title visible + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed while running'); + + // Wrapper content should not be initialized yet (lazy) + const wrapperContent = part.domNode.querySelector('.chat-used-context-list'); + assert.strictEqual(wrapperContent, null, 'Wrapper content should not be rendered while running and collapsed'); + }); + + test('should show prompt on expand when no tool items yet', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Starting task', + agentName: 'RunningAgent', + prompt: 'This is the prompt to execute' + }, + stateType: IChatToolInvocation.StateKind.Streaming + }); + const context = createMockRenderContext(false); // Not complete + + const part = createPart(toolInvocation, context); + + // Initially collapsed with no content + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should be collapsed initially'); + let wrapperContent = part.domNode.querySelector('.chat-used-context-list'); + assert.strictEqual(wrapperContent, null, 'Wrapper should not exist initially'); + + // Expand + const button = getCollapseButton(part); + assert.ok(button, 'Expand button should exist'); + button.click(); + + // Wrapper should now exist and be visible + wrapperContent = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapperContent, 'Wrapper should exist after expand'); + + // Prompt section should be rendered + const promptSection = wrapperContent.querySelector('.chat-subagent-section'); + assert.ok(promptSection, 'Prompt section should be visible after expand'); + }); + }); + + suite('Current running tool in title', () => { + test('should update title with current running tool invocation message', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Add a child tool invocation + const childTool = createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId, + stateType: IChatToolInvocation.StateKind.Executing, + invocationMessage: 'Reading config.ts' + }); + + part.appendToolInvocation(childTool, 0); + + // The title should include the current running tool message + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const labelElement = getCollapseButtonLabel(button); + const buttonText = labelElement?.textContent ?? button.textContent ?? ''; + assert.ok(buttonText.includes('Reading config.ts'), 'Title should include current running tool message'); + }); + + test('should show latest tool when multiple tools are added', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Add first tool + const firstTool = createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId, + stateType: IChatToolInvocation.StateKind.Executing, + invocationMessage: 'Reading file1.ts' + }); + part.appendToolInvocation(firstTool, 0); + + // Add second tool + const secondTool = createMockToolInvocation({ + toolId: 'searchFiles', + subAgentInvocationId: toolInvocation.subAgentInvocationId, + stateType: IChatToolInvocation.StateKind.Executing, + invocationMessage: 'Searching for patterns' + }); + part.appendToolInvocation(secondTool, 1); + + const button = getCollapseButton(part); + assert.ok(button, 'Should have collapse button'); + const labelElement = getCollapseButtonLabel(button); + const buttonText = labelElement?.textContent ?? button.textContent ?? ''; + // Should show the latest tool message + assert.ok(buttonText.includes('Searching for patterns'), 'Title should include latest tool message'); + }); + + test('should keep showing running tool when another tool completes', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Add first tool (will complete) + const firstToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const firstTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: firstToolState, + invocationMessage: 'Reading file1.ts' + }; + part.trackToolState(firstTool); + + // Add second tool (will keep running) + const secondToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const secondTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'searchFiles', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: secondToolState, + invocationMessage: 'Searching for patterns' + }; + part.trackToolState(secondTool); + + // Verify title shows second tool + const button = getCollapseButton(part); + assert.ok(button, 'Button should exist'); + const labelElement = getCollapseButtonLabel(button); + let buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Searching for patterns'), 'Title should show second tool'); + + // Complete the first tool + firstToolState.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + + // Title should still show the second tool (which is still running and owns the title) + buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Searching for patterns'), 'Title should still show second tool after first completes'); + }); + + test('should keep title when tool is cancelled', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Add a tool that will be cancelled + const toolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: toolState, + invocationMessage: 'Reading file.ts' + }; + part.trackToolState(childTool); + + // Verify title includes tool message + const button = getCollapseButton(part); + assert.ok(button, 'Button should exist'); + const labelElement = getCollapseButtonLabel(button); + let buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Reading file.ts'), 'Title should include tool message while running'); + + // Cancel the tool + toolState.set(createState(IChatToolInvocation.StateKind.Cancelled), undefined); + + // Title should still include the tool message (persists like thinking part) + buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Reading file.ts'), + 'Title should still include tool message after cancellation'); + }); + + test('should keep showing last tool message when that tool completes', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // First tool starts + const firstToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const firstTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: firstToolState, + invocationMessage: 'Reading file1.ts' + }; + part.trackToolState(firstTool); + + // Verify title shows first tool + const button = getCollapseButton(part); + assert.ok(button, 'Button should exist'); + const labelElement = getCollapseButtonLabel(button); + let buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Reading file1.ts'), 'Title should show first tool'); + + // Second tool starts and becomes the current title + const secondToolState = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const secondTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'searchFiles', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: secondToolState, + invocationMessage: 'Searching for patterns' + }; + part.trackToolState(secondTool); + + // Verify title shows second tool + buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Searching for patterns'), 'Title should show second tool'); + + // Second tool completes + secondToolState.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + + // Title should still show second tool (persists like thinking part) + buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Searching for patterns'), + 'Title should still show last tool message after completion'); + }); + }); + + suite('appendMarkdownItem', () => { + test('should append markdown item to expanded subagent part', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-id', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part first + const button = getCollapseButton(part); + button?.click(); + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded'); + + // Create a mock markdown content with edit pill + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Edited file.ts' } + }; + + // Create a mock DOM node for the markdown + const markdownDomNode = mainWindow.document.createElement('div'); + markdownDomNode.className = 'chat-codeblock-button'; + markdownDomNode.textContent = 'file.ts'; + + let disposeCallCount = 0; + const mockDisposable = { dispose: () => { disposeCallCount++; } }; + + // Append markdown item + part.appendMarkdownItem( + () => ({ domNode: markdownDomNode, disposable: mockDisposable }), + 'codeblock-123', + markdownContent, + undefined + ); + + // Verify the markdown was appended + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + const appendedElement = wrapper.querySelector('.chat-codeblock-button'); + assert.ok(appendedElement, 'Appended markdown element should exist in wrapper'); + assert.strictEqual(appendedElement.textContent, 'file.ts', 'Should have correct content'); + }); + + test('should not render markdown item when part is collapsed', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-defer', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Part is collapsed by default + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed'); + + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Deferred edit' } + }; + + let factoryCalled = false; + const markdownDomNode = mainWindow.document.createElement('div'); + markdownDomNode.className = 'deferred-edit'; + markdownDomNode.textContent = 'deferred.ts'; + + const mockDisposable = { dispose: () => { } }; + + // Append markdown item while collapsed - factory should not be called + part.appendMarkdownItem( + () => { + factoryCalled = true; + return { domNode: markdownDomNode, disposable: mockDisposable }; + }, + 'codeblock-deferred', + markdownContent, + undefined + ); + + // Factory should not be called when collapsed + assert.strictEqual(factoryCalled, false, 'Factory should not be called when collapsed'); + }); + + test('should append multiple markdown items with same codeblock ID', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-dedup', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part + const button = getCollapseButton(part); + button?.click(); + + const markdownContent: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'Same codeblock' } + }; + + const sharedCodeblockId = 'codeblock-same-id'; + + // Append first item + const firstNode = mainWindow.document.createElement('div'); + firstNode.className = 'first-item'; + firstNode.textContent = 'first item content'; + part.appendMarkdownItem( + () => ({ domNode: firstNode, disposable: { dispose: () => { } } }), + sharedCodeblockId, + markdownContent, + undefined + ); + + // Append second item with same codeblock ID + const secondNode = mainWindow.document.createElement('div'); + secondNode.className = 'second-item'; + secondNode.textContent = 'second item content'; + part.appendMarkdownItem( + () => ({ domNode: secondNode, disposable: { dispose: () => { } } }), + sharedCodeblockId, + markdownContent, + undefined + ); + + // Both items are added (no built-in deduplication by codeblock ID) + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + const firstItems = wrapper.querySelectorAll('.first-item'); + const secondItems = wrapper.querySelectorAll('.second-item'); + // Implementation does not deduplicate - both items exist + assert.strictEqual(firstItems.length, 1, 'First item should exist'); + assert.strictEqual(secondItems.length, 1, 'Second item should exist'); + }); + + test('should handle multiple different codeblock IDs', () => { + const toolInvocation = createMockToolInvocation({ + subAgentInvocationId: 'test-subagent-multi', + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Expand the part + const button = getCollapseButton(part); + button?.click(); + + // Append first item + const firstNode = mainWindow.document.createElement('div'); + firstNode.className = 'item-one'; + firstNode.textContent = 'first item content'; + part.appendMarkdownItem( + () => ({ domNode: firstNode, disposable: { dispose: () => { } } }), + 'codeblock-1', + { kind: 'markdownContent', content: { value: 'First' } }, + undefined + ); + + // Append second item with different ID + const secondNode = mainWindow.document.createElement('div'); + secondNode.className = 'item-two'; + secondNode.textContent = 'second item content'; + part.appendMarkdownItem( + () => ({ domNode: secondNode, disposable: { dispose: () => { } } }), + 'codeblock-2', + { kind: 'markdownContent', content: { value: 'Second' } }, + undefined + ); + + // Both should exist + const wrapper = getWrapperElement(part); + assert.ok(wrapper, 'Wrapper should exist'); + assert.ok(wrapper.querySelector('.item-one'), 'First item should exist'); + assert.ok(wrapper.querySelector('.item-two'), 'Second item should exist'); + }); + }); + + suite('Auto-expand on confirmation', () => { + test('should auto-expand when tool state becomes WaitingForConfirmation', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Verify initially collapsed + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed'); + + // Create a tool invocation that starts in executing state, then changes to WaitingForConfirmation + const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable, + invocationMessage: 'Reading file' + }; + + // Track this tool's state (this registers observers) + part.trackToolState(childTool); + + // Should still be collapsed since tool is executing, not waiting for confirmation + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should still be collapsed when tool is executing'); + + // Now change state to WaitingForConfirmation + stateObservable.set(createState(IChatToolInvocation.StateKind.WaitingForConfirmation), undefined); + + // Should auto-expand when tool needs confirmation + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should auto-expand when tool needs confirmation'); + }); + + test('should auto-collapse when confirmation is addressed', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Create a tool invocation that is waiting for confirmation + const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'runInTerminal', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable, + invocationMessage: 'Run npm install' + }; + + // Track this tool's state + part.trackToolState(childTool); + + // Should be expanded now + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should be expanded when waiting for confirmation'); + + // Now simulate confirmation being addressed (tool moves to executing) + stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined); + + // Should auto-collapse after confirmation is addressed + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), + 'Should auto-collapse after confirmation is addressed'); + }); + + test('should not auto-collapse if user manually expanded', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // User manually expands + const button = getCollapseButton(part); + button?.click(); + + // Should be expanded + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, 'Should be expanded after user click'); + + // Create a tool that goes through confirmation cycle + const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'runInTerminal', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable, + invocationMessage: 'Run npm install' + }; + + // Track this tool's state + part.trackToolState(childTool); + + // Confirm the tool (move to executing) + stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined); + + // Since user manually expanded, it should stay expanded + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should stay expanded when user manually expanded'); + }); + + test('should respect manual expansion after auto-expand', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Verify initially collapsed + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should start collapsed'); + + // Create a tool that needs confirmation + const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.WaitingForConfirmation)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'runInTerminal', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable, + invocationMessage: 'Run npm install' + }; + + part.trackToolState(childTool); + + // Should auto-expand + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should auto-expand for confirmation'); + + // User manually collapses + const button = getCollapseButton(part); + button?.click(); + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user click'); + + // User manually expands again + button?.click(); + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should expand after second user click'); + + // Confirm the tool (move to executing) + stateObservable.set(createState(IChatToolInvocation.StateKind.Executing), undefined); + + // Since user manually re-expanded after auto-expand, should stay expanded + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should stay expanded when user manually re-expanded after auto-expand'); + }); + + test('should resume auto-collapse after user manually expands then collapses', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // First confirmation cycle - user manually expands + const stateObservable1 = observableValue('state1', createState(IChatToolInvocation.StateKind.WaitingForConfirmation)); + const childTool1: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'runInTerminal', + toolCallId: 'tool1', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable1, + invocationMessage: 'First tool' + }; + + part.trackToolState(childTool1); + + // Should auto-expand for first confirmation + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should auto-expand for first confirmation'); + + // User manually collapses + const button = getCollapseButton(part); + button?.click(); + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user click'); + + // User manually expands (this sets userManuallyExpanded = true) + button?.click(); + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should expand after user re-expands'); + + // Complete first tool (should not auto-collapse since user manually expanded) + stateObservable1.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should stay expanded after first tool completes (user manually expanded)'); + + // User manually collapses again (this resets userManuallyExpanded) + button?.click(); + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), 'Should collapse after user manually collapses'); + + // Second confirmation cycle - should auto-collapse now since userManuallyExpanded was reset + const stateObservable2 = observableValue('state2', createState(IChatToolInvocation.StateKind.WaitingForConfirmation)); + const childTool2: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'runInTerminal', + toolCallId: 'tool2', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable2, + invocationMessage: 'Second tool' + }; + + part.trackToolState(childTool2); + + // Should auto-expand for second confirmation + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should auto-expand for second confirmation'); + + // Complete second tool - should auto-collapse since userManuallyExpanded was reset by the earlier collapse + stateObservable2.set(createState(IChatToolInvocation.StateKind.Executing), undefined); + assert.ok(part.domNode.classList.contains('chat-used-context-collapsed'), + 'Should auto-collapse after second confirmation is addressed (userManuallyExpanded was reset)'); + }); + + test('should clear current running tool message when tool completes', () => { + const toolInvocation = createMockToolInvocation({ + toolSpecificData: { + kind: 'subagent', + description: 'Working on task', + agentName: 'TestAgent' + } + }); + const context = createMockRenderContext(false); + + const part = createPart(toolInvocation, context); + + // Create a tool that will complete + const stateObservable = observableValue('state', createState(IChatToolInvocation.StateKind.Executing)); + const childTool: IChatToolInvocation = { + ...createMockToolInvocation({ + toolId: 'readFile', + subAgentInvocationId: toolInvocation.subAgentInvocationId + }), + state: stateObservable, + invocationMessage: 'Reading config.ts' + }; + + part.trackToolState(childTool); + + // Verify title includes tool message + const button = getCollapseButton(part); + assert.ok(button, 'Button should exist'); + const labelElement = getCollapseButtonLabel(button); + let buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Reading config.ts'), 'Title should include tool message while running'); + + // Complete the tool + stateObservable.set(createState(IChatToolInvocation.StateKind.Completed), undefined); + + // Title should still include the tool message (persists like thinking part) + buttonText = labelElement?.textContent ?? button?.textContent ?? ''; + assert.ok(buttonText.includes('Reading config.ts'), + 'Title should still include tool message after completion'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts new file mode 100644 index 00000000000..ba9914a7d04 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTerminalToolProgressPart.test.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from '../../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { TerminalToolAutoExpand, TerminalToolAutoExpandTimeout } from '../../../../browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; + +suite('ChatTerminalToolProgressPart Auto-Expand Logic', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + // Mocked events + let onCommandExecuted: Emitter; + let onCommandFinished: Emitter; + let onWillData: Emitter; + + // State tracking + let isExpanded: boolean; + let userToggledOutput: boolean; + let hasRealOutputValue: boolean; + + function shouldAutoExpand(): boolean { + return !isExpanded && !userToggledOutput; + } + + function hasRealOutput(): boolean { + return hasRealOutputValue; + } + + function setupAutoExpandLogic(): void { + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Use the real TerminalToolAutoExpand class + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand, + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => { + isExpanded = true; + })); + } + + setup(() => { + onCommandExecuted = store.add(new Emitter()); + onCommandFinished = store.add(new Emitter()); + onWillData = store.add(new Emitter()); + + isExpanded = false; + userToggledOutput = false; + hasRealOutputValue = false; + }); + + test('fast command without data should not auto-expand (finishes before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand for fast command without data'); + })); + + test('fast command with quick data should not auto-expand (data + finish before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when command finishes within timeout of first data'); + })); + + test('long-running command with data should auto-expand (data received, command still running after timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when command still running after first data timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command with data but no real output should NOT auto-expand (like sleep with shell sequences)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // Shell integration sequences, not real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Shell integration data arrives (not real output) + onWillData.fire('shell-sequence'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data is shell sequences, not real output'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data should NOT auto-expand if no real output (like sleep)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // No real output like `sleep 1` + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when no real output even after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data SHOULD auto-expand if real output exists', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output in buffer + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when real output exists after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('data arriving after command finish should not trigger expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes and finishes immediately + onCommandExecuted.fire(undefined); + onCommandFinished.fire(undefined); + + // Data arrives after command finished + onWillData.fire('late output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data arrives after command finished'); + })); + + test('user toggled output prevents auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + userToggledOutput = true; + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when user has manually toggled output'); + onCommandFinished.fire(undefined); + })); + + test('already expanded output prevents additional auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + isExpanded = true; + + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Track if event was fired + let eventFired = false; + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand: () => !isExpanded && !userToggledOutput, + hasRealOutput: () => hasRealOutputValue, + })); + store.add(autoExpand.onDidRequestExpand(() => { + eventFired = true; + })); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(eventFired, false, 'Should NOT fire expand event when already expanded'); + onCommandFinished.fire(undefined); + })); + + test('data arriving cancels no-data timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Would have expanded if no-data timeout fired + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives (cancels no-data timeout) + onWillData.fire('output'); + + // Command finishes immediately after data (before data timeout would fire) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'No-data timeout should be cancelled when data arrives'); + })); + + test('multiple data events only trigger one timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Multiple data events + onWillData.fire('output 1'); + onWillData.fire('output 2'); + onWillData.fire('output 3'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand exactly once after first data'); + onCommandFinished.fire(undefined); + })); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts new file mode 100644 index 00000000000..cd8d46a2abc --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatThinkingContentPart.test.ts @@ -0,0 +1,1178 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { $ } from '../../../../../../../base/browser/dom.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { DisposableStore, toDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { observableValue } from '../../../../../../../base/common/observable.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ChatThinkingContentPart } from '../../../../browser/widget/chatContentParts/chatThinkingContentPart.js'; +import { IChatMarkdownContent, IChatThinkingPart } from '../../../../common/chatService/chatService.js'; +import { IChatContentPartRenderContext } from '../../../../browser/widget/chatContentParts/chatContentParts.js'; +import { IChatRendererContent, IChatResponseViewModel } from '../../../../common/model/chatViewModel.js'; +import { IChatMarkdownAnchorService } from '../../../../browser/widget/chatContentParts/chatMarkdownAnchorService.js'; +import { IMarkdownRenderer } from '../../../../../../../platform/markdown/browser/markdownRenderer.js'; +import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../../../../base/browser/markdownRenderer.js'; +import { IMarkdownString } from '../../../../../../../base/common/htmlContent.js'; +import { ThinkingDisplayMode } from '../../../../common/constants.js'; +import { CodeBlockModelCollection } from '../../../../common/widget/codeBlockModelCollection.js'; +import { EditorPool, DiffEditorPool } from '../../../../browser/widget/chatContentParts/chatContentCodePools.js'; +import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js'; +import { ChatTreeItem } from '../../../../browser/chat.js'; +import { ILanguageModelsService } from '../../../../common/languageModels.js'; +import { URI } from '../../../../../../../base/common/uri.js'; + +suite('ChatThinkingContentPart', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let disposables: DisposableStore; + let instantiationService: ReturnType; + let mockConfigurationService: TestConfigurationService; + let mockMarkdownRenderer: IMarkdownRenderer; + let mockAnchorService: IChatMarkdownAnchorService; + let mockHoverService: IHoverService; + let mockLanguageModelsService: ILanguageModelsService; + + function createMockRenderContext(isComplete: boolean = false): IChatContentPartRenderContext { + const mockElement: Partial = { + isComplete, + id: 'test-response-id', + sessionResource: URI.parse('chat-session://test/session1'), + get model() { return {} as IChatResponseViewModel['model']; } + }; + + return { + element: mockElement as ChatTreeItem, + elementIndex: 0, + container: mainWindow.document.createElement('div'), + content: [], + contentIndex: 0, + editorPool: {} as EditorPool, + codeBlockStartIndex: 0, + treeStartIndex: 0, + diffEditorPool: {} as DiffEditorPool, + codeBlockModelCollection: {} as CodeBlockModelCollection, + currentWidth: observableValue('currentWidth', 500), + onDidChangeVisibility: Event.None + }; + } + + function createThinkingPart(value?: string, id?: string): IChatThinkingPart { + return { + kind: 'thinking', + value: value ?? '', + id: id ?? 'test-thinking-id' + }; + } + + setup(() => { + disposables = store.add(new DisposableStore()); + instantiationService = workbenchInstantiationService(undefined, store); + + mockConfigurationService = new TestConfigurationService(); + instantiationService.stub(IConfigurationService, mockConfigurationService); + + // Create a mock markdown renderer + mockMarkdownRenderer = { + render: (_markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IRenderedMarkdown => { + const element = outElement ?? mainWindow.document.createElement('div'); + const content = typeof _markdown === 'string' ? _markdown : (_markdown.value ?? ''); + element.textContent = content; + return { + element, + dispose: () => { } + }; + } + }; + + // Mock the anchor service + mockAnchorService = { + _serviceBrand: undefined, + register: () => toDisposable(() => { }), + lastFocusedAnchor: undefined + }; + instantiationService.stub(IChatMarkdownAnchorService, mockAnchorService); + + // Mock hover service + mockHoverService = { + _serviceBrand: undefined, + showHover: () => undefined, + showDelayedHover: () => undefined, + showAndFocusLastHover: () => { }, + hideHover: () => { }, + setupDelayedHover: () => toDisposable(() => { }), + setupManagedHover: () => ({ dispose: () => { }, show: () => { }, hide: () => { }, update: () => { } }), + showManagedHover: () => undefined, + isHovered: () => false, + } as unknown as IHoverService; + instantiationService.stub(IHoverService, mockHoverService); + + // Mock language models service + mockLanguageModelsService = { + _serviceBrand: undefined, + onDidChangeLanguageModels: Event.None, + getLanguageModelIds: () => [], + lookupLanguageModel: () => undefined, + selectLanguageModels: async () => [], + registerLanguageModelChat: () => toDisposable(() => { }), + sendChatRequest: async () => ({ stream: (async function* () { })(), result: Promise.resolve({}) }), + computeTokenLength: async () => 0 + } as unknown as ILanguageModelsService; + instantiationService.stub(ILanguageModelsService, mockLanguageModelsService); + }); + + teardown(() => { + disposables.dispose(); + }); + + suite('ThinkingDisplayMode.Collapsed', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('should start collapsed', () => { + const content = createThinkingPart('**Analyzing code**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), true, 'Should be collapsed by default'); + }); + + test('should have chat-thinking-box class', () => { + const content = createThinkingPart('**Processing**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + assert.ok(part.domNode.classList.contains('chat-thinking-box'), 'Should have chat-thinking-box class'); + }); + + test('should extract title from bold markdown', () => { + const content = createThinkingPart('**Reading configuration files**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + const button = part.domNode.querySelector('.chat-used-context-label .monaco-button'); + assert.ok(button, 'Should have collapse button'); + // The title should contain the extracted text + const labelElement = button.querySelector('.icon-label'); + assert.ok(labelElement?.textContent?.includes('Reading configuration files') || button.textContent?.includes('Reading configuration files'), + 'Title should contain extracted text'); + }); + + test('lazy rendering - should not render content until expanded', () => { + const content = createThinkingPart('**Initial thinking content**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // In collapsed mode, content wrapper should not be initialized + const contentList = part.domNode.querySelector('.chat-used-context-list'); + assert.strictEqual(contentList, null, 'Content should not be rendered when collapsed'); + }); + + test('lazy rendering - should render content when expanded', () => { + const content = createThinkingPart('**Thinking content to render**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Click the button to expand + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + assert.ok(button, 'Should have expand button'); + button.click(); + + // Now content should be rendered + const contentList = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(contentList, 'Content should be rendered after expanding'); + }); + }); + + suite('ThinkingDisplayMode.CollapsedPreview', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.CollapsedPreview); + }); + + test('should start expanded when streaming (not complete)', () => { + const content = createThinkingPart('**Analyzing**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // In CollapsedPreview mode, should be expanded while streaming + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false, + 'Should be expanded during streaming in CollapsedPreview mode'); + }); + + test('should be collapsed when complete', () => { + const content = createThinkingPart('**Completed task**'); + const context = createMockRenderContext(true); // isComplete = true + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + true // streamingCompleted + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // When complete, should be collapsed + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), true, + 'Should be collapsed when complete'); + }); + + test('should be collapsed when streamingCompleted is true even if element.isComplete is false (look-ahead completion)', () => { + // This tests the scenario where we know the thinking part is complete + // based on look-ahead (subsequent non-pinnable parts exist), but the + // overall response is still in progress + const content = createThinkingPart('**Finished analyzing**'); + const context = createMockRenderContext(false); // element.isComplete = false + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + true // streamingCompleted = true (look-ahead detected this thinking is done) + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Even though element.isComplete is false, this thinking part should be + // collapsed because streamingCompleted is true (determined by look-ahead) + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), true, + 'Should be collapsed when streamingCompleted is true, even if element.isComplete is false'); + }); + + test('should use lazy rendering when streamingCompleted is true even if element.isComplete is false', () => { + // Verify lazy rendering is triggered when streamingCompleted=true and element.isComplete=false + const content = createThinkingPart('**Looking ahead completed**'); + const context = createMockRenderContext(false); // element.isComplete = false + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + true // streamingCompleted = true + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Content should not be rendered because it's collapsed (lazy rendering) + const contentList = part.domNode.querySelector('.chat-used-context-list'); + assert.strictEqual(contentList, null, 'Content should not be rendered when streamingCompleted=true (collapsed = lazy)'); + }); + }); + + suite('ThinkingDisplayMode.FixedScrolling', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.FixedScrolling); + }); + + test('should have fixed mode class', () => { + const content = createThinkingPart('**Scrolling content**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + assert.ok(part.domNode.classList.contains('chat-thinking-fixed-mode'), + 'Should have fixed mode class'); + }); + + test('should init content early (eager rendering)', () => { + const content = createThinkingPart('**Fixed scrolling content**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Fixed mode should initialize content immediately (eager rendering) + // The scrollable element should be present + const scrollableContent = part.domNode.querySelector('.monaco-scrollable-element'); + assert.ok(scrollableContent, 'Should have scrollable element in fixed mode (eager rendering)'); + }); + + test('should create scrollable container', () => { + const content = createThinkingPart('**Content with scrolling**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + const scrollable = part.domNode.querySelector('.monaco-scrollable-element'); + assert.ok(scrollable, 'Should have scrollable container'); + }); + }); + + suite('Thinking content updates', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('updateThinking should update content', () => { + const content = createThinkingPart('**Initial**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // First expand to render content + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Update the thinking content + const updatedContent = createThinkingPart('**Updated thinking**', content.id); + part.updateThinking(updatedContent); + + // Verify the content was updated + const thinkingItem = part.domNode.querySelector('.chat-thinking-item'); + assert.ok(thinkingItem, 'Should have thinking item'); + }); + + test('should track multiple title extractions', () => { + const content = createThinkingPart('**First title**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Expand first + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Update with new title + part.updateThinking(createThinkingPart('**Second title**', content.id)); + part.updateThinking(createThinkingPart('**Third title**', content.id)); + + // The part should track these titles for finalization + assert.ok(part.domNode, 'Part should still be valid'); + }); + }); + + suite('Tool invocation appending', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('appendItem should use lazy rendering when collapsed', () => { + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + let factoryCalled = false; + const factory = () => { + factoryCalled = true; + return { + domNode: $('div.test-tool-item'), + disposable: undefined + }; + }; + + // Append item while collapsed + part.appendItem(factory, 'test-tool-id'); + + // Factory should NOT be called yet due to lazy rendering + assert.strictEqual(factoryCalled, false, 'Factory should not be called when collapsed (lazy rendering)'); + }); + + test('appendItem should render immediately when expanded', () => { + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Expand first + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + let factoryCalled = false; + const factory = () => { + factoryCalled = true; + const div = $('div.test-tool-item'); + div.textContent = 'Test tool content'; + return { domNode: div }; + }; + + // Append item while expanded + part.appendItem(factory, 'test-tool-id'); + + // Factory should be called immediately when expanded + assert.strictEqual(factoryCalled, true, 'Factory should be called immediately when expanded'); + }); + + test('lazy items should materialize when first expanded', () => { + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + let factoryCalled = false; + const factory = () => { + factoryCalled = true; + const div = $('div.test-tool-item'); + div.textContent = 'Lazy content'; + return { domNode: div }; + }; + + // Append item while collapsed + part.appendItem(factory, 'test-tool-id'); + assert.strictEqual(factoryCalled, false, 'Factory should not be called yet'); + + // Now expand + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Factory should now be called + assert.strictEqual(factoryCalled, true, 'Factory should be called after expanding'); + }); + + test('removeLazyItem should remove pending lazy items', () => { + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + let factoryCalled = false; + const factory = () => { + factoryCalled = true; + return { domNode: $('div.test-tool-item') }; + }; + + // Append and then remove + part.appendItem(factory, 'test-tool-to-remove'); + const removed = part.removeLazyItem('test-tool-to-remove'); + + assert.strictEqual(removed, true, 'Should successfully remove the lazy item'); + assert.strictEqual(factoryCalled, false, 'Factory should never have been called'); + }); + + test('lazy items should preserve append order when mixing tool and markdown items', () => { + // This test verifies that when tool invocations and markdown items are appended + // in a specific order while collapsed, the DOM order matches the append order + // when expanded. This catches the bug where markdown items render before + // tool items because markdown isn't lazy. + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + const appendOrder: string[] = []; + + // Append in order: tool1, markdown, tool2 + // Tool 1 + part.appendItem(() => { + appendOrder.push('tool1'); + const div = $('div.test-item'); + div.setAttribute('data-order', 'tool1'); + div.textContent = 'Tool 1'; + return { domNode: div }; + }, 'tool-1'); + + // Markdown content (simulated - no toolInvocationId means it's markdown-like) + const markdownItem: IChatMarkdownContent = { + kind: 'markdownContent', + content: { value: 'test markdown' } + }; + part.appendItem(() => { + appendOrder.push('markdown'); + const div = $('div.test-item'); + div.setAttribute('data-order', 'markdown'); + div.textContent = 'Markdown content'; + return { domNode: div }; + }, undefined, markdownItem); + + // Tool 2 + part.appendItem(() => { + appendOrder.push('tool2'); + const div = $('div.test-item'); + div.setAttribute('data-order', 'tool2'); + div.textContent = 'Tool 2'; + return { domNode: div }; + }, 'tool-2'); + + // Nothing should have rendered yet + assert.strictEqual(appendOrder.length, 0, 'No items should be rendered while collapsed'); + + // Now expand to trigger lazy rendering + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // All items should now be rendered + assert.strictEqual(appendOrder.length, 3, 'All 3 items should be rendered after expanding'); + + // Verify the render order matches append order + assert.deepStrictEqual(appendOrder, ['tool1', 'markdown', 'tool2'], + 'Items should render in the same order they were appended (tool1, markdown, tool2)'); + + // Also verify the DOM order + const wrapper = part.domNode.querySelector('.chat-used-context-list'); + const toolWrappers = wrapper?.querySelectorAll('.chat-thinking-tool-wrapper'); + assert.ok(toolWrappers, 'Should have tool wrappers'); + assert.strictEqual(toolWrappers?.length, 3, 'Should have 3 tool wrappers'); + + const domOrder = Array.from(toolWrappers!).map(el => { + const testItem = el.querySelector('.test-item'); + return testItem?.getAttribute('data-order'); + }); + + assert.deepStrictEqual(domOrder, ['tool1', 'markdown', 'tool2'], + 'DOM order should match append order (tool1, markdown, tool2)'); + }); + + test('setupThinkingContainer should preserve order with lazy tool items', () => { + // This test reproduces the bug where markdown parts added via setupThinkingContainer + // render before tool parts because setupThinkingContainer doesn't use lazy rendering. + // Expected behavior: tool1, thinking2, tool2 in DOM order + // Bug behavior: thinking2 renders before tool1 because its not lazy + const initialContent = createThinkingPart('**Initial thinking**', 'thinking-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + initialContent, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Append tool1 while collapsed (lazy) + let tool1Rendered = false; + part.appendItem(() => { + tool1Rendered = true; + const div = $('div.test-item'); + div.setAttribute('data-test-id', 'tool1'); + div.textContent = 'Tool 1'; + return { domNode: div }; + }, 'tool-1'); + + // Now setupThinkingContainer is called for a new thinking section + // This simulates what happens when a new thinking part arrives during streaming + const newThinkingContent = createThinkingPart('**Second thinking section**', 'thinking-2'); + part.setupThinkingContainer(newThinkingContent); + + // Append tool2 while collapsed (lazy) + let tool2Rendered = false; + part.appendItem(() => { + tool2Rendered = true; + const div = $('div.test-item'); + div.setAttribute('data-test-id', 'tool2'); + div.textContent = 'Tool 2'; + return { domNode: div }; + }, 'tool-2'); + + // Tools should not have rendered yet + assert.strictEqual(tool1Rendered, false, 'Tool 1 should not render while collapsed'); + assert.strictEqual(tool2Rendered, false, 'Tool 2 should not render while collapsed'); + + // Now expand + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Everything should render now + assert.strictEqual(tool1Rendered, true, 'Tool 1 should render after expand'); + assert.strictEqual(tool2Rendered, true, 'Tool 2 should render after expand'); + + // Get all rendered items and check their order + const wrapper = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapper, 'Should have wrapper'); + + // The children should be in order: initial-thinking, tool1-wrapper, thinking2, tool2-wrapper + // Get all direct children to check order + const children = Array.from(wrapper!.children); + + // Find indices of our items + const tool1Index = children.findIndex(el => + el.classList.contains('chat-thinking-tool-wrapper') && + el.querySelector('[data-test-id="tool1"]') + ); + const tool2Index = children.findIndex(el => + el.classList.contains('chat-thinking-tool-wrapper') && + el.querySelector('[data-test-id="tool2"]') + ); + + // Find thinking containers (they have class chat-thinking-item) + const thinkingItems = children.filter(el => el.classList.contains('chat-thinking-item')); + + // We should have 2 thinking items (initial and the one from setupThinkingContainer) + // and 2 tool wrappers + assert.ok(thinkingItems.length >= 1, 'Should have at least one thinking item'); + assert.ok(tool1Index >= 0, 'Should find tool1'); + assert.ok(tool2Index >= 0, 'Should find tool2'); + + // The key assertion: tool1 should come before tool2 in DOM order + // and any thinking content between them should also be in order + assert.ok(tool1Index < tool2Index, + `Tool1 (index ${tool1Index}) should come before Tool2 (index ${tool2Index}) in DOM order`); + }); + + test('markdown via updateThinking should preserve order with lazy tool items (BUG: markdown renders before tools)', () => { + // This test exposes the lazy rendering bug where markdown content from updateThinking/ + // setupThinkingContainer gets rendered immediately and placed in DOM before tool items. + // + // The bug flow: + // 1. Tool1 arrives → appendItem() → stored in lazyItems (not rendered yet) + // 2. Thinking/markdown arrives → setupThinkingContainer() → textContainer created, + // updateThinking() → renderMarkdown() renders IMMEDIATELY into textContainer + // 3. Tool2 arrives → appendItem() → stored in lazyItems (not rendered yet) + // 4. User expands → initContent() creates wrapper, adds textContainer FIRST, + // then materializes lazyItems (tools) + // + // Result: DOM order is [markdown, tool1, tool2] instead of [tool1, markdown, tool2] + const initialContent = createThinkingPart('', 'thinking-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + initialContent, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Step 1: Tool1 arrives while collapsed - should be lazy + part.appendItem(() => { + const div = $('div.test-item'); + div.setAttribute('data-test-id', 'tool1'); + div.setAttribute('data-order', '1'); + div.textContent = 'Tool 1'; + return { domNode: div }; + }, 'tool-1'); + + // Step 2: New thinking section arrives - this uses setupThinkingContainer + updateThinking + // In the bug, this creates textContainer and renders markdown immediately + const thinkingContent = createThinkingPart('**Analyzing the codebase**', 'thinking-2'); + part.setupThinkingContainer(thinkingContent); + + // Step 3: Tool2 arrives while collapsed - should be lazy + part.appendItem(() => { + const div = $('div.test-item'); + div.setAttribute('data-test-id', 'tool2'); + div.setAttribute('data-order', '3'); + div.textContent = 'Tool 2'; + return { domNode: div }; + }, 'tool-2'); + + // Now expand to trigger lazy rendering + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Get the wrapper and check DOM order + const wrapper = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapper, 'Should have wrapper after expanding'); + + const children = Array.from(wrapper!.children); + + // Find indices + const tool1Index = children.findIndex(el => + el.querySelector('[data-test-id="tool1"]') + ); + const tool2Index = children.findIndex(el => + el.querySelector('[data-test-id="tool2"]') + ); + const markdownIndex = children.findIndex(el => + el.classList.contains('chat-thinking-item') && el.classList.contains('markdown-content') + ); + + assert.ok(tool1Index >= 0, `Should find tool1 in DOM (found at index ${tool1Index})`); + assert.ok(tool2Index >= 0, `Should find tool2 in DOM (found at index ${tool2Index})`); + assert.ok(markdownIndex >= 0, `Should find markdown in DOM (found at index ${markdownIndex})`); + + // The key assertion: order should match arrival order (tool1, markdown, tool2) + // BUG: Currently markdown is always first because it's not lazy + assert.ok(tool1Index < markdownIndex, + `BUG: Tool1 (index ${tool1Index}) should come BEFORE markdown (index ${markdownIndex}) ` + + `because tool1 was appended first. Current DOM order indicates markdown is eagerly ` + + `placed first regardless of arrival order.`); + assert.ok(markdownIndex < tool2Index, + `Markdown (index ${markdownIndex}) should come before Tool2 (index ${tool2Index})`); + }); + + test('lazy thinking items should show updated content after streaming updates', () => { + // This test exposes the bug where streaming updates to thinking content are lost + // when the thinking part is collapsed. + // + // Bug flow: + // 1. setupThinkingContainer(content1) creates lazy item with content1 + // 2. updateThinking(content2) is called with updated streaming content + // - this.content is updated to content2 + // - this.currentThinkingValue is updated + // - but the lazy item still stores content1 + // 3. User expands: + // - initContent creates a NEW textContainer with currentThinkingValue (latest) + // - materializeLazyItem appends ANOTHER container from lazy item with stale content + // + // Result: Duplicate thinking containers, one with correct content, one with stale + const initialContent = createThinkingPart('', 'thinking-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + initialContent, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Step 1: New thinking section arrives while collapsed + const thinkingContent1 = createThinkingPart('**Starting analysis**', 'thinking-2'); + part.setupThinkingContainer(thinkingContent1); + + // Step 2: Streaming continues - more content arrives via updateThinking + const thinkingContent2 = createThinkingPart('**Starting analysis** Looking at the code structure...', 'thinking-2'); + part.updateThinking(thinkingContent2); + + // Step 3: Even more streaming content + const thinkingContent3 = createThinkingPart('**Starting analysis** Looking at the code structure... Found the issue in the parser module.', 'thinking-2'); + part.updateThinking(thinkingContent3); + + // Now expand to trigger lazy rendering + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Get the rendered content + const wrapper = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapper, 'Should have wrapper after expanding'); + + // Get ALL thinking items - the bug creates duplicate containers + const thinkingItems = wrapper!.querySelectorAll('.chat-thinking-item.markdown-content'); + + // BUG: There should only be ONE thinking item, but the bug causes TWO: + // 1. One from initContent with correct current content + // 2. One from materializeLazyItem with stale content + assert.strictEqual(thinkingItems.length, 1, + `BUG: Should have exactly 1 thinking item, but got ${thinkingItems.length}. ` + + `materializeLazyItem creates a duplicate container from the lazy item. ` + + `Items: ${Array.from(thinkingItems).map(i => `"${i.textContent}"`).join(', ')}`); + + // Also verify the single item has the latest content + if (thinkingItems.length === 1) { + const renderedText = thinkingItems[0].textContent || ''; + assert.ok( + renderedText.includes('Found the issue in the parser module'), + `Content should show latest streaming update. Got: "${renderedText}"` + ); + } + }); + + test('lazy thinking items should work without streaming updates after setupThinkingContainer', () => { + // Edge case: setupThinkingContainer is called but no subsequent updateThinking arrives + // In this case, the lazy item's content should be used when materializing + const initialContent = createThinkingPart('', 'thinking-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + initialContent, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Only call setupThinkingContainer, no subsequent updateThinking + const thinkingContent = createThinkingPart('**Analyzing files**', 'thinking-2'); + part.setupThinkingContainer(thinkingContent); + + // Expand to trigger lazy rendering + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + button?.click(); + + // Get the rendered content + const wrapper = part.domNode.querySelector('.chat-used-context-list'); + assert.ok(wrapper, 'Should have wrapper after expanding'); + + const thinkingItems = wrapper!.querySelectorAll('.chat-thinking-item.markdown-content'); + assert.strictEqual(thinkingItems.length, 1, 'Should have exactly 1 thinking item'); + + // The content should be the one from setupThinkingContainer + const renderedText = thinkingItems[0].textContent || ''; + assert.ok( + renderedText.includes('Analyzing files'), + `Content should show setupThinkingContainer content. Got: "${renderedText}"` + ); + }); + }); + + suite('State management', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('markAsInactive should update isActive state', () => { + const content = createThinkingPart('**Active thinking**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + assert.strictEqual(part.getIsActive(), true, 'Should start as active'); + + part.markAsInactive(); + + assert.strictEqual(part.getIsActive(), false, 'Should be inactive after markAsInactive'); + }); + + test('collapseContent should collapse the part', () => { + const content = createThinkingPart('**Content**'); + const context = createMockRenderContext(false); + + // Use CollapsedPreview to start expanded + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.CollapsedPreview); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Should be expanded initially + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), false); + + part.collapseContent(); + + assert.strictEqual(part.domNode.classList.contains('chat-used-context-collapsed'), true, + 'Should be collapsed after collapseContent'); + }); + + test('finalizeTitleIfDefault should update button icon to check', () => { + const content = createThinkingPart('**Working**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + part.finalizeTitleIfDefault(); + + // The button should now show a check icon + const iconElement = part.domNode.querySelector('.codicon-check'); + assert.ok(iconElement, 'Should have check icon after finalization'); + }); + }); + + suite('hasSameContent', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('should return true for tool invocations', () => { + const content = createThinkingPart('**Working**', 'id-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + const toolInvocation = { + kind: 'toolInvocation' as const, + toolId: 'test-tool', + invocationMessage: 'Testing', + resultDetails: [], + isConfirmed: undefined, + pastTenseMessage: undefined, + isComplete: true, + isCanceled: false + } as unknown as IChatRendererContent; + + const result = part.hasSameContent(toolInvocation, [], context.element); + assert.strictEqual(result, true, 'Should accept tool invocations as same content'); + }); + + test('should return true for markdown content', () => { + const content = createThinkingPart('**Working**', 'id-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + const markdownContent = { + kind: 'markdownContent' as const, + content: { value: 'test' } + } as unknown as IChatRendererContent; + + const result = part.hasSameContent(markdownContent, [], context.element); + assert.strictEqual(result, true, 'Should accept markdown content as same content'); + }); + + test('should return false for different thinking part with same id', () => { + const content = createThinkingPart('**Working**', 'id-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + const otherThinking: IChatRendererContent = createThinkingPart('**Different**', 'id-1'); + + // When the id is the same, hasSameContent returns true (other.id !== this.id is false) + const result = part.hasSameContent(otherThinking, [], context.element); + assert.strictEqual(result, false, 'Should return false for thinking part with same id'); + }); + + test('should return true for thinking part with different id', () => { + const content = createThinkingPart('**Working**', 'id-1'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + const otherThinking: IChatRendererContent = createThinkingPart('**Different**', 'id-2'); + + const result = part.hasSameContent(otherThinking, [], context.element); + assert.strictEqual(result, true, 'Should return true for thinking part with different id'); + }); + }); + + suite('DOM structure', () => { + setup(() => { + mockConfigurationService.setUserConfiguration('chat.agent.thinkingStyle', ThinkingDisplayMode.Collapsed); + }); + + test('should have proper aria-expanded attribute', () => { + const content = createThinkingPart('**Content**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + const button = part.domNode.querySelector('.monaco-button') as HTMLElement; + assert.ok(button, 'Button should exist'); + assert.strictEqual(button.getAttribute('aria-expanded'), 'false', 'Should have aria-expanded="false" when collapsed'); + + // Expand + button.click(); + + assert.strictEqual(button.getAttribute('aria-expanded'), 'true', 'Should have aria-expanded="true" when expanded'); + }); + + test('should show loading spinner while streaming', () => { + const content = createThinkingPart('**Streaming content**'); + const context = createMockRenderContext(false); + + const part = store.add(instantiationService.createInstance( + ChatThinkingContentPart, + content, + context, + mockMarkdownRenderer, + false // not streaming completed + )); + + mainWindow.document.body.appendChild(part.domNode); + disposables.add(toDisposable(() => part.domNode.remove())); + + // Should have loading spinner icon + const loadingIcon = part.domNode.querySelector('.codicon-loading'); + assert.ok(loadingIcon, 'Should have loading spinner while streaming'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts similarity index 86% rename from src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts rename to src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts index f69c5a308d1..517e57bc3b3 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatTodoListWidget.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/chatTodoListWidget.test.ts @@ -3,18 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; -import { Event } from '../../../../../base/common/event.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ChatTodoListWidget } from '../../browser/chatContentParts/chatTodoListWidget.js'; -import { IChatTodo, IChatTodoListService } from '../../common/chatTodoListService.js'; -import { mainWindow } from '../../../../../base/browser/window.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ChatTodoListWidget } from '../../../../browser/widget/chatContentParts/chatTodoListWidget.js'; +import { IChatTodo, IChatTodoListService } from '../../../../common/tools/chatTodoListService.js'; +import { mainWindow } from '../../../../../../../base/browser/window.js'; +import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { URI } from '../../../../../../../base/common/uri.js'; const testSessionUri = URI.parse('chat-session://test/session1'); @@ -25,7 +23,7 @@ suite('ChatTodoListWidget Accessibility', () => { const sampleTodos: IChatTodo[] = [ { id: 1, title: 'First task', status: 'not-started' }, - { id: 2, title: 'Second task', status: 'in-progress', description: 'This is a task description' }, + { id: 2, title: 'Second task', status: 'in-progress' }, { id: 3, title: 'Third task', status: 'completed' } ]; @@ -39,7 +37,7 @@ suite('ChatTodoListWidget Accessibility', () => { }; // Mock the configuration service - const mockConfigurationService = new TestConfigurationService({ 'chat.todoListTool.descriptionField': true }); + const mockConfigurationService = new TestConfigurationService(); const instantiationService = workbenchInstantiationService(undefined, store); instantiationService.stub(IChatTodoListService, mockTodoListService); @@ -84,11 +82,10 @@ suite('ChatTodoListWidget Accessibility', () => { assert.ok(firstItem.getAttribute('aria-label')?.includes('First task')); assert.ok(firstItem.getAttribute('aria-label')?.includes('not started')); - // Check second item (in-progress with description) + // Check second item (in-progress) const secondItem = todoItems[1] as HTMLElement; assert.ok(secondItem.getAttribute('aria-label')?.includes('Second task')); assert.ok(secondItem.getAttribute('aria-label')?.includes('in progress')); - assert.ok(secondItem.getAttribute('aria-label')?.includes('This is a task description')); // Check third item (completed) const thirdItem = todoItems[2] as HTMLElement; @@ -139,12 +136,11 @@ suite('ChatTodoListWidget Accessibility', () => { assert.ok(firstAriaLabel?.includes('First task'), 'First item aria-label should include title'); assert.ok(firstAriaLabel?.includes('not started'), 'First item aria-label should include status'); - // Check second item (in-progress with description) - aria-label should include title, status, and description + // Check second item (in-progress) - aria-label should include title and status const secondItem = todoItems[1] as HTMLElement; const secondAriaLabel = secondItem.getAttribute('aria-label'); assert.ok(secondAriaLabel?.includes('Second task'), 'Second item aria-label should include title'); assert.ok(secondAriaLabel?.includes('in progress'), 'Second item aria-label should include status'); - assert.ok(secondAriaLabel?.includes('This is a task description'), 'Second item aria-label should include description'); // Check third item (completed) - aria-label should include title and status const thirdItem = todoItems[2] as HTMLElement; @@ -162,7 +158,7 @@ suite('ChatTodoListWidget Accessibility', () => { setTodos: (sessionResource: URI, todos: IChatTodo[]) => { } }; - const emptyConfigurationService = new TestConfigurationService({ 'chat.todoListTool.descriptionField': true }); + const emptyConfigurationService = new TestConfigurationService(); const instantiationService = workbenchInstantiationService(undefined, store); instantiationService.stub(IChatTodoListService, emptyTodoListService); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts new file mode 100644 index 00000000000..67bafff5cba --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatContentParts/terminalToolAutoExpand.test.ts @@ -0,0 +1,360 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter } from '../../../../../../../base/common/event.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { TerminalToolAutoExpand, TerminalToolAutoExpandTimeout } from '../../../../browser/widget/chatContentParts/toolInvocationParts/terminalToolAutoExpand.js'; +import type { ICommandDetectionCapability } from '../../../../../../../platform/terminal/common/capabilities/capabilities.js'; + +suite('TerminalToolAutoExpand', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + // Mocked events + let onCommandExecuted: Emitter; + let onCommandFinished: Emitter; + let onWillData: Emitter; + + // State tracking + let isExpanded: boolean; + let userToggledOutput: boolean; + let hasRealOutputValue: boolean; + + function shouldAutoExpand(): boolean { + return !isExpanded && !userToggledOutput; + } + + function hasRealOutput(): boolean { + return hasRealOutputValue; + } + + function setupAutoExpandLogic(): void { + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Use the real TerminalToolAutoExpand class + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand, + hasRealOutput, + })); + store.add(autoExpand.onDidRequestExpand(() => { + isExpanded = true; + })); + } + + setup(() => { + onCommandExecuted = store.add(new Emitter()); + onCommandFinished = store.add(new Emitter()); + onWillData = store.add(new Emitter()); + + isExpanded = false; + userToggledOutput = false; + hasRealOutputValue = false; + }); + + test('fast command without data should not auto-expand (finishes before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand for fast command without data'); + })); + + test('fast command with quick data should not auto-expand (data + finish before timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Command finishes quickly (before timeout) + onCommandFinished.fire(undefined); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when command finishes within timeout of first data'); + })); + + test('long-running command with data should auto-expand (data received, command still running after timeout)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when command still running after first data timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command with data but no real output should NOT auto-expand (like sleep with shell sequences)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // Shell integration sequences, not real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Shell integration data arrives (not real output) + onWillData.fire('shell-sequence'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data is shell sequences, not real output'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data should NOT auto-expand if no real output (like sleep)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // No real output like `sleep 1` + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when no real output even after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('long-running command without data SHOULD auto-expand if real output exists', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output in buffer + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, true, 'Should expand when real output exists after timeout'); + + onCommandFinished.fire(undefined); + })); + + test('data arriving after command finish should not trigger expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + setupAutoExpandLogic(); + + // Command executes and finishes immediately + onCommandExecuted.fire(undefined); + onCommandFinished.fire(undefined); + + // Data arrives after command finished + onWillData.fire('late output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when data arrives after command finished'); + })); + + test('user toggled output prevents auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + userToggledOutput = true; + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when user has manually toggled output'); + onCommandFinished.fire(undefined); + })); + + test('already expanded output prevents additional auto-expand', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + isExpanded = true; + + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Track if event was fired + let eventFired = false; + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand: () => !isExpanded && !userToggledOutput, + hasRealOutput: () => hasRealOutputValue, + })); + store.add(autoExpand.onDidRequestExpand(() => { + eventFired = true; + })); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Wait past all timeouts (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(eventFired, false, 'Should NOT fire expand event when already expanded'); + onCommandFinished.fire(undefined); + })); + + test('data arriving with real output cancels no-data timeout (DataEvent path succeeds)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Real output exists + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives with real output + onWillData.fire('output'); + + // Wait for DataEvent timeout to fire (50ms) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 10); + + // Should have expanded via DataEvent path + assert.strictEqual(isExpanded, true, 'Should expand via DataEvent path when real output exists'); + + // Command finishes later + onCommandFinished.fire(undefined); + })); + + test('data arriving without real output does NOT cancel no-data timeout (NoData path can still expand)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = false; // No real output initially (shell sequences) + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives (shell integration sequences, not real output) + onWillData.fire('shell-sequence'); + + // Wait for DataEvent timeout to fire - should NOT expand since no real output + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 10); + assert.strictEqual(isExpanded, false, 'Should NOT expand when DataEvent fires without real output'); + + // Now real output appears during the NoData timeout window (after DataEvent timeout but before NoData timeout completes) + hasRealOutputValue = true; + + // Wait for NoData timeout to fire (500ms from command executed) + await timeout(TerminalToolAutoExpandTimeout.NoData - TerminalToolAutoExpandTimeout.DataEvent); + + // Should have expanded via NoData path + assert.strictEqual(isExpanded, true, 'NoData path should still expand when real output appears later'); + + onCommandFinished.fire(undefined); + })); + + test('quick finish after data prevents expansion even with real output', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Data arrives + onWillData.fire('output'); + + // Command finishes immediately after data (before any timeout fires) + onCommandFinished.fire(undefined); + + // Wait past all timeouts + await timeout(TerminalToolAutoExpandTimeout.NoData + 100); + + assert.strictEqual(isExpanded, false, 'Should NOT expand when command finishes before timeouts'); + })); + + test('multiple data events only trigger one timeout', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + hasRealOutputValue = true; // Has real output + setupAutoExpandLogic(); + + // Command executes + onCommandExecuted.fire(undefined); + + // Multiple data events + onWillData.fire('output 1'); + onWillData.fire('output 2'); + onWillData.fire('output 3'); + + // Wait for timeout to fire (faked timers advance instantly) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 100); + + assert.strictEqual(isExpanded, true, 'Should expand exactly once after first data'); + onCommandFinished.fire(undefined); + })); + + test('progress bar output detected via multiple data events (receivedDataCount > 1)', () => runWithFakedTimers({ useFakeTimers: true }, async () => { + // Simulates progress bars that update on the same line - cursor doesn't move past marker + // but multiple data events indicate real output + let dataEventCount = 0; + + // Create a mock command detection capability + const mockCommandDetection = { + onCommandExecuted: onCommandExecuted.event, + onCommandFinished: onCommandFinished.event, + } as Pick as ICommandDetectionCapability; + + // Track data events to simulate receivedDataCount logic + store.add(onWillData.event(() => { + dataEventCount++; + })); + + const autoExpand = store.add(new TerminalToolAutoExpand({ + commandDetection: mockCommandDetection, + onWillData: onWillData.event, + shouldAutoExpand: () => !isExpanded && !userToggledOutput, + // Simulate: cursor hasn't moved past marker, but multiple data events = real output + hasRealOutput: () => dataEventCount > 1, + })); + store.add(autoExpand.onDidRequestExpand(() => { + isExpanded = true; + })); + + // Command executes + onCommandExecuted.fire(undefined); + + // First data event (shell sequence) - hasRealOutput returns false (dataEventCount = 1) + onWillData.fire('shell-sequence'); + + // Wait for DataEvent timeout - should NOT expand yet (hasRealOutput = false) + await timeout(TerminalToolAutoExpandTimeout.DataEvent + 10); + assert.strictEqual(isExpanded, false, 'Should NOT expand after first data event'); + + // Second data event (progress bar update) - hasRealOutput returns true (dataEventCount = 2) + onWillData.fire('progress'); + + // Wait for NoData timeout - should expand via NoData path + await timeout(TerminalToolAutoExpandTimeout.NoData); + assert.strictEqual(isExpanded, true, 'Should expand when multiple data events detected as real output'); + + onCommandFinished.fire(undefined); + })); +}); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownRenderer.test.ts similarity index 90% rename from src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts rename to src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownRenderer.test.ts index 1c26d03fe08..4666ec04907 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatMarkdownRenderer.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownRenderer.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ChatContentMarkdownRenderer } from '../../browser/chatContentMarkdownRenderer.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ChatContentMarkdownRenderer } from '../../../browser/widget/chatContentMarkdownRenderer.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; suite('ChatMarkdownRenderer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/chat/test/browser/chatSelectedTools.test.ts b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts similarity index 75% rename from src/vs/workbench/contrib/chat/test/browser/chatSelectedTools.test.ts rename to src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts index 4ba6023fbdf..9860fdcf379 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatSelectedTools.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/widget/input/chatSelectedTools.test.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { ContextKeyService } from '../../../../../platform/contextkey/browser/contextKeyService.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { LanguageModelToolsService } from '../../browser/languageModelToolsService.js'; -import { IChatService } from '../../common/chatService.js'; -import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js'; -import { MockChatService } from '../common/mockChatService.js'; -import { ChatSelectedTools } from '../../browser/chatSelectedTools.js'; -import { constObservable } from '../../../../../base/common/observable.js'; -import { Iterable } from '../../../../../base/common/iterator.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; -import { timeout } from '../../../../../base/common/async.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ChatMode } from '../../common/chatModes.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ContextKeyService } from '../../../../../../../platform/contextkey/browser/contextKeyService.js'; +import { workbenchInstantiationService } from '../../../../../../test/browser/workbenchTestServices.js'; +import { LanguageModelToolsService } from '../../../../browser/tools/languageModelToolsService.js'; +import { IChatService } from '../../../../common/chatService/chatService.js'; +import { ILanguageModelToolsService, IToolData, ToolDataSource, ToolSet } from '../../../../common/tools/languageModelToolsService.js'; +import { MockChatService } from '../../../common/chatService/mockChatService.js'; +import { ChatSelectedTools } from '../../../../browser/widget/input/chatSelectedTools.js'; +import { constObservable } from '../../../../../../../base/common/observable.js'; +import { Iterable } from '../../../../../../../base/common/iterator.js'; +import { DisposableStore } from '../../../../../../../base/common/lifecycle.js'; +import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTravelScheduler.js'; +import { timeout } from '../../../../../../../base/common/async.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ChatMode } from '../../../../common/chatModes.js'; suite('ChatSelectedTools', () => { @@ -40,7 +40,7 @@ suite('ChatSelectedTools', () => { store.add(instaService); toolsService = instaService.get(ILanguageModelToolsService); - selectedTools = store.add(instaService.createInstance(ChatSelectedTools, constObservable(ChatMode.Agent))); + selectedTools = store.add(instaService.createInstance(ChatSelectedTools, constObservable(ChatMode.Agent), constObservable(undefined))); }); teardown(function () { @@ -95,14 +95,14 @@ suite('ChatSelectedTools', () => { store.add(toolset.addTool(toolData2)); store.add(toolset.addTool(toolData3)); - assert.strictEqual(Iterable.length(toolsService.getTools()), 3); + assert.strictEqual(Iterable.length(toolsService.getTools(undefined)), 3); const size = Iterable.length(toolset.getTools()); assert.strictEqual(size, 3); await timeout(1000); // UGLY the tools service updates its state sync but emits the event async (750ms) delay. This affects the observable that depends on the event - assert.strictEqual(selectedTools.entriesMap.get().size, 4); // 1 toolset, 3 tools + assert.strictEqual(selectedTools.entriesMap.get().size, 8); // 1 toolset (+4 vscode, execute, read, agent toolsets), 3 tools const toSet = new Map([[toolData1, true], [toolData2, false], [toolData3, false], [toolset, false]]); selectedTools.set(toSet, false); @@ -159,14 +159,14 @@ suite('ChatSelectedTools', () => { store.add(toolset.addTool(toolData2)); store.add(toolset.addTool(toolData3)); - assert.strictEqual(Iterable.length(toolsService.getTools()), 3); + assert.strictEqual(Iterable.length(toolsService.getTools(undefined)), 3); const size = Iterable.length(toolset.getTools()); assert.strictEqual(size, 3); await timeout(1000); // UGLY the tools service updates its state sync but emits the event async (750ms) delay. This affects the observable that depends on the event - assert.strictEqual(selectedTools.entriesMap.get().size, 4); // 1 toolset, 3 tools + assert.strictEqual(selectedTools.entriesMap.get().size, 8); // 1 toolset (+4 vscode, execute, read, agent toolsets), 3 tools // Toolset is checked, tools 2 and 3 are unchecked const toSet = new Map([[toolData1, true], [toolData2, false], [toolData3, false], [toolset, true]]); diff --git a/src/vs/workbench/contrib/chat/test/browser/widget/mockChatWidget.ts b/src/vs/workbench/contrib/chat/test/browser/widget/mockChatWidget.ts new file mode 100644 index 00000000000..2014bd9b9d4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/widget/mockChatWidget.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IChatWidget, IChatWidgetService } from '../../../browser/chat.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; + +export class MockChatWidgetService implements IChatWidgetService { + readonly onDidAddWidget: Event = Event.None; + readonly onDidBackgroundSession: Event = Event.None; + + readonly _serviceBrand: undefined; + + /** + * Returns the most recently focused widget if any. + */ + readonly lastFocusedWidget: IChatWidget | undefined; + + getWidgetByInputUri(uri: URI): IChatWidget | undefined { + return undefined; + } + + getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined { + return undefined; + } + + getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray { + return []; + } + + revealWidget(preserveFocus?: boolean): Promise { + return Promise.resolve(undefined); + } + + reveal(widget: IChatWidget, preserveFocus?: boolean): Promise { + return Promise.resolve(true); + } + + getAllWidgets(): ReadonlyArray { + throw new Error('Method not implemented.'); + } + + openSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } + + register(newWidget: IChatWidget): IDisposable { + return Disposable.None; + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap deleted file mode 100644 index 404cd0188d2..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.0.snap +++ /dev/null @@ -1,8 +0,0 @@ -{ - requesterUsername: "test", - requesterAvatarIconUri: undefined, - responderUsername: "", - responderAvatarIconUri: undefined, - initialLocation: "panel", - requests: [ ] -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap deleted file mode 100644 index 2abf0a346b4..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.0.snap +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - resolvedContent: { }, - content: "text", - kind: "asyncContent" - }, - { - resolvedContent: { }, - content: "text2", - kind: "asyncContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap deleted file mode 100644 index 9a2a0ee7bb2..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_async_content.1.snap +++ /dev/null @@ -1,26 +0,0 @@ -[ - { - content: { - value: "resolved", - isTrusted: false, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - }, - { - kind: "treeData", - treeData: { - label: "label", - uri: { - scheme: "https", - authority: "microsoft.com", - path: "/", - query: "", - fragment: "", - _formatted: null, - _fsPath: null - } - } - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap deleted file mode 100644 index b923c5ded84..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_content__markdown.0.snap +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - content: { - value: "textmarkdown", - isTrusted: { enabledCommands: [ ] }, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap deleted file mode 100644 index 0643f935d8f..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__content.0.snap +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - content: { - value: "markdowntext", - isTrusted: false, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap deleted file mode 100644 index a7cb1f9bd3a..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_markdown__markdown.0.snap +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - content: { - value: "markdown1markdown2", - isTrusted: { enabledCommands: [ ] }, - supportThemeIcons: false, - supportHtml: false - }, - kind: "markdownContent" - } -] \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/annotations.test.ts b/src/vs/workbench/contrib/chat/test/common/annotations.test.ts deleted file mode 100644 index 9f57b0ad6b4..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/annotations.test.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 { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IChatMarkdownContent } from '../../common/chatService.js'; -import { annotateSpecialMarkdownContent, extractVulnerabilitiesFromText } from '../../common/annotations.js'; - -function content(str: string): IChatMarkdownContent { - return { kind: 'markdownContent', content: new MarkdownString(str) }; -} - -suite('Annotations', function () { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('extractVulnerabilitiesFromText', () => { - test('single line', async () => { - const before = 'some code '; - const vulnContent = 'content with vuln'; - const after = ' after'; - const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, content(after)]); - await assertSnapshot(annotatedResult); - - const markdown = annotatedResult[0] as IChatMarkdownContent; - const result = extractVulnerabilitiesFromText(markdown.content.value); - await assertSnapshot(result); - }); - - test('multiline', async () => { - const before = 'some code\nover\nmultiple lines '; - const vulnContent = 'content with vuln\nand\nnewlines'; - const after = 'more code\nwith newline'; - const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, content(after)]); - await assertSnapshot(annotatedResult); - - const markdown = annotatedResult[0] as IChatMarkdownContent; - const result = extractVulnerabilitiesFromText(markdown.content.value); - await assertSnapshot(result); - }); - - test('multiple vulns', async () => { - const before = 'some code\nover\nmultiple lines '; - const vulnContent = 'content with vuln\nand\nnewlines'; - const after = 'more code\nwith newline'; - const annotatedResult = annotateSpecialMarkdownContent([ - content(before), - { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, - content(after), - { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, - ]); - await assertSnapshot(annotatedResult); - - const markdown = annotatedResult[0] as IChatMarkdownContent; - const result = extractVulnerabilitiesFromText(markdown.content.value); - await assertSnapshot(result); - }); - }); -}); 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 0ac313b9177..5f1d2940776 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/chatModeService.test.ts @@ -8,17 +8,19 @@ import { timeout } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; import { IStorageService } from '../../../../../platform/storage/common/storage.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; -import { IChatAgentService } from '../../common/chatAgents.js'; +import { IChatAgentService } from '../../common/participants/chatAgents.js'; import { ChatMode, ChatModeService } from '../../common/chatModes.js'; import { ChatModeKind } from '../../common/constants.js'; import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { MockPromptsService } from './mockPromptsService.js'; +import { MockPromptsService } from './promptSyntax/service/mockPromptsService.js'; class TestChatAgentService implements Partial { _serviceBrand: undefined; @@ -47,6 +49,7 @@ suite('ChatModeService', () => { let promptsService: MockPromptsService; let chatAgentService: TestChatAgentService; let storageService: TestStorageService; + let configurationService: TestConfigurationService; let chatModeService: ChatModeService; setup(async () => { @@ -54,12 +57,14 @@ suite('ChatModeService', () => { promptsService = new MockPromptsService(); chatAgentService = new TestChatAgentService(); storageService = testDisposables.add(new TestStorageService()); + configurationService = new TestConfigurationService(); instantiationService.stub(IPromptsService, promptsService); instantiationService.stub(IChatAgentService, chatAgentService); instantiationService.stub(IStorageService, storageService); instantiationService.stub(ILogService, new NullLogService()); instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IConfigurationService, configurationService); chatModeService = testDisposables.add(instantiationService.createInstance(ChatModeService)); }); @@ -79,7 +84,7 @@ suite('ChatModeService', () => { }); test('should adjust builtin modes based on tools agent availability', () => { - // With tools agent + // Agent mode should always be present regardless of tools agent availability chatAgentService.setHasToolsAgent(true); let agents = chatModeService.getModes(); assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Agent)); @@ -89,7 +94,7 @@ suite('ChatModeService', () => { agents = chatModeService.getModes(); assert.strictEqual(agents.builtin.find(agent => agent.id === ChatModeKind.Agent), undefined); - // But Ask and Edit modes should always be present + // Ask and Edit modes should always be present assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Ask)); assert.ok(agents.builtin.find(agent => agent.id === ChatModeKind.Edit)); }); @@ -113,7 +118,8 @@ suite('ChatModeService', () => { description: 'A test custom mode', tools: ['tool1', 'tool2'], agentInstructions: { content: 'Custom mode body', toolReferences: [] }, - source: workspaceSource + source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; promptsService.setCustomModes([customMode]); @@ -150,6 +156,7 @@ suite('ChatModeService', () => { tools: [], agentInstructions: { content: 'Custom mode body', toolReferences: [] }, source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; promptsService.setCustomModes([customMode]); @@ -168,6 +175,7 @@ suite('ChatModeService', () => { tools: [], agentInstructions: { content: 'Findable mode body', toolReferences: [] }, source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; promptsService.setCustomModes([customMode]); @@ -190,8 +198,9 @@ suite('ChatModeService', () => { description: 'Initial description', tools: ['tool1'], agentInstructions: { content: 'Initial body', toolReferences: [] }, - model: 'gpt-4', + model: ['gpt-4'], source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; promptsService.setCustomModes([initialMode]); @@ -207,7 +216,7 @@ suite('ChatModeService', () => { description: 'Updated description', tools: ['tool1', 'tool2'], agentInstructions: { content: 'Updated body', toolReferences: [] }, - model: 'Updated model' + model: ['Updated model'] }; promptsService.setCustomModes([updatedMode]); @@ -223,7 +232,7 @@ suite('ChatModeService', () => { assert.strictEqual(updatedCustomMode.description.get(), 'Updated description'); assert.deepStrictEqual(updatedCustomMode.customTools?.get(), ['tool1', 'tool2']); assert.deepStrictEqual(updatedCustomMode.modeInstructions?.get(), { content: 'Updated body', toolReferences: [] }); - assert.strictEqual(updatedCustomMode.model?.get(), 'Updated model'); + assert.deepStrictEqual(updatedCustomMode.model?.get(), ['Updated model']); assert.deepStrictEqual(updatedCustomMode.source, workspaceSource); }); @@ -235,6 +244,7 @@ suite('ChatModeService', () => { tools: [], agentInstructions: { content: 'Mode 1 body', toolReferences: [] }, source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; const mode2: ICustomAgent = { @@ -244,6 +254,7 @@ suite('ChatModeService', () => { tools: [], agentInstructions: { content: 'Mode 2 body', toolReferences: [] }, source: workspaceSource, + visibility: { userInvokable: true, agentInvokable: true } }; // Add both modes @@ -261,4 +272,5 @@ suite('ChatModeService', () => { assert.strictEqual(modes.custom.length, 1); assert.strictEqual(modes.custom[0].id, mode1.uri.toString()); }); + }); diff --git a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts deleted file mode 100644 index 5f5d94305fa..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/chatModel.test.ts +++ /dev/null @@ -1,260 +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 { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IStorageService } from '../../../../../platform/storage/common/storage.js'; -import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; -import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; -import { ChatAgentService, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatModel, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, normalizeSerializableChatData, Response } from '../../common/chatModel.js'; -import { ChatRequestTextPart } from '../../common/chatParserTypes.js'; -import { ChatAgentLocation } from '../../common/constants.js'; - -suite('ChatModel', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instantiationService: TestInstantiationService; - - setup(async () => { - instantiationService = testDisposables.add(new TestInstantiationService()); - instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); - instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IContextKeyService, new MockContextKeyService()); - instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); - instantiationService.stub(IConfigurationService, new TestConfigurationService()); - }); - - test('removeRequest', async () => { - const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); - - const text = 'hello'; - model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); - const requests = model.getRequests(); - assert.strictEqual(requests.length, 1); - - model.removeRequest(requests[0].id); - assert.strictEqual(model.getRequests().length, 0); - }); - - test('adoptRequest', async function () { - const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: true })); - const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); - - const text = 'hello'; - const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); - - assert.strictEqual(model1.getRequests().length, 1); - assert.strictEqual(model2.getRequests().length, 0); - assert.ok(request1.session === model1); - assert.ok(request1.response?.session === model1); - - model2.adoptRequest(request1); - - assert.strictEqual(model1.getRequests().length, 0); - assert.strictEqual(model2.getRequests().length, 1); - assert.ok(request1.session === model2); - assert.ok(request1.response?.session === model2); - - model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); - - assert.strictEqual(request1.response.response.toString(), 'Hello'); - }); - - test('addCompleteRequest', async function () { - const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); - - const text = 'hello'; - const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, undefined, true); - - assert.strictEqual(request1.isCompleteAddedRequest, true); - assert.strictEqual(request1.response!.isCompleteAddedRequest, true); - assert.strictEqual(request1.shouldBeRemovedOnSend, undefined); - assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined); - }); -}); - -suite('Response', () => { - const store = ensureNoDisposablesAreLeakedInTestSuite(); - - test('mergeable markdown', async () => { - const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' }); - response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); - await assertSnapshot(response.value); - - assert.strictEqual(response.toString(), 'markdown1markdown2'); - }); - - test('not mergeable markdown', async () => { - const response = store.add(new Response([])); - const md1 = new MarkdownString('markdown1'); - md1.supportHtml = true; - response.updateContent({ content: md1, kind: 'markdownContent' }); - response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); - await assertSnapshot(response.value); - }); - - test('inline reference', async () => { - const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('text before '), kind: 'markdownContent' }); - response.updateContent({ inlineReference: URI.parse('https://microsoft.com/'), kind: 'inlineReference' }); - response.updateContent({ content: new MarkdownString(' text after'), kind: 'markdownContent' }); - await assertSnapshot(response.value); - - assert.strictEqual(response.toString(), 'text before https://microsoft.com/ text after'); - - }); - - test('consolidated edit summary', async () => { - const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('Some content before edits'), kind: 'markdownContent' }); - response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true }); - response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true }); - response.updateContent({ content: new MarkdownString('Some content after edits'), kind: 'markdownContent' }); - - // Should have single "Made changes." at the end instead of multiple entries - const responseString = response.toString(); - const madeChangesCount = (responseString.match(/Made changes\./g) || []).length; - assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message'); - assert.ok(responseString.includes('Some content before edits'), 'Should include content before edits'); - assert.ok(responseString.includes('Some content after edits'), 'Should include content after edits'); - assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."'); - }); - - test('no edit summary when no edits', async () => { - const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('Some content'), kind: 'markdownContent' }); - response.updateContent({ content: new MarkdownString('More content'), kind: 'markdownContent' }); - - // Should not have "Made changes." when there are no edit groups - const responseString = response.toString(); - assert.ok(!responseString.includes('Made changes.'), 'Should not include "Made changes." when no edits present'); - assert.strictEqual(responseString, 'Some contentMore content'); - }); - - test('consolidated edit summary with clear operation', async () => { - const response = store.add(new Response([])); - response.updateContent({ content: new MarkdownString('Initial content'), kind: 'markdownContent' }); - response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true }); - response.updateContent({ kind: 'clearToPreviousToolInvocation', reason: 1 }); - response.updateContent({ content: new MarkdownString('Content after clear'), kind: 'markdownContent' }); - response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true }); - - // Should only show "Made changes." for edits after the clear operation - const responseString = response.toString(); - const madeChangesCount = (responseString.match(/Made changes\./g) || []).length; - assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message after clear'); - assert.ok(responseString.includes('Content after clear'), 'Should include content after clear'); - assert.ok(!responseString.includes('Initial content'), 'Should not include content before clear'); - assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."'); - }); -}); - -suite('normalizeSerializableChatData', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - test('v1', () => { - const v1Data: ISerializableChatData1 = { - creationDate: Date.now(), - initialLocation: undefined, - isImported: false, - requesterAvatarIconUri: undefined, - requesterUsername: 'me', - requests: [], - responderAvatarIconUri: undefined, - responderUsername: 'bot', - sessionId: 'session1', - }; - - const newData = normalizeSerializableChatData(v1Data); - assert.strictEqual(newData.creationDate, v1Data.creationDate); - assert.strictEqual(newData.lastMessageDate, v1Data.creationDate); - assert.strictEqual(newData.version, 3); - assert.ok('customTitle' in newData); - }); - - test('v2', () => { - const v2Data: ISerializableChatData2 = { - version: 2, - creationDate: 100, - lastMessageDate: Date.now(), - initialLocation: undefined, - isImported: false, - requesterAvatarIconUri: undefined, - requesterUsername: 'me', - requests: [], - responderAvatarIconUri: undefined, - responderUsername: 'bot', - sessionId: 'session1', - computedTitle: 'computed title' - }; - - const newData = normalizeSerializableChatData(v2Data); - assert.strictEqual(newData.version, 3); - assert.strictEqual(newData.creationDate, v2Data.creationDate); - assert.strictEqual(newData.lastMessageDate, v2Data.lastMessageDate); - assert.strictEqual(newData.customTitle, v2Data.computedTitle); - }); - - test('old bad data', () => { - const v1Data: ISerializableChatData1 = { - // Testing the scenario where these are missing - sessionId: undefined!, - creationDate: undefined!, - - initialLocation: undefined, - isImported: false, - requesterAvatarIconUri: undefined, - requesterUsername: 'me', - requests: [], - responderAvatarIconUri: undefined, - responderUsername: 'bot', - }; - - const newData = normalizeSerializableChatData(v1Data); - assert.strictEqual(newData.version, 3); - assert.ok(newData.creationDate > 0); - assert.ok(newData.lastMessageDate > 0); - assert.ok(newData.sessionId); - }); - - test('v3 with bug', () => { - const v3Data: ISerializableChatData3 = { - // Test case where old data was wrongly normalized and these fields were missing - creationDate: undefined!, - lastMessageDate: undefined!, - - version: 3, - initialLocation: undefined, - isImported: false, - requesterAvatarIconUri: undefined, - requesterUsername: 'me', - requests: [], - responderAvatarIconUri: undefined, - responderUsername: 'bot', - sessionId: 'session1', - customTitle: 'computed title' - }; - - const newData = normalizeSerializableChatData(v3Data); - assert.strictEqual(newData.version, 3); - assert.ok(newData.creationDate > 0); - assert.ok(newData.lastMessageDate > 0); - assert.ok(newData.sessionId); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService.test.ts deleted file mode 100644 index 04862001c12..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/chatService.test.ts +++ /dev/null @@ -1,368 +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 { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IStorageService } from '../../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { InMemoryTestFileService, mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../common/chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from '../../common/chatEditingService.js'; -import { IChatModel, ISerializableChatData } from '../../common/chatModel.js'; -import { IChatFollowup, IChatService } from '../../common/chatService.js'; -import { ChatService } from '../../common/chatServiceImpl.js'; -import { ChatSlashCommandService, IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; -import { MockChatService } from './mockChatService.js'; -import { MockChatVariablesService } from './mockChatVariables.js'; - -const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; -const chatAgentWithUsedContext: IChatAgent = { - id: chatAgentWithUsedContextId, - name: chatAgentWithUsedContextId, - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionPublisherId: '', - extensionDisplayName: '', - locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - async invoke(request, progress, history, token) { - progress([{ - documents: [ - { - uri: URI.file('/test/path/to/file'), - version: 3, - ranges: [ - new Range(1, 1, 2, 2) - ] - } - ], - kind: 'usedContext' - }]); - - return { metadata: { metadataKey: 'value' } }; - }, - async provideFollowups(sessionId, token) { - return [{ kind: 'reply', message: 'Something else', agentId: '', tooltip: 'a tooltip' } satisfies IChatFollowup]; - }, -}; - -const chatAgentWithMarkdownId = 'ChatProviderWithMarkdown'; -const chatAgentWithMarkdown: IChatAgent = { - id: chatAgentWithMarkdownId, - name: chatAgentWithMarkdownId, - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionPublisherId: '', - extensionDisplayName: '', - locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - async invoke(request, progress, history, token) { - progress([{ kind: 'markdownContent', content: new MarkdownString('test') }]); - return { metadata: { metadataKey: 'value' } }; - }, - async provideFollowups(sessionId, token) { - return []; - }, -}; - -function getAgentData(id: string): IChatAgentData { - return { - name: id, - id: id, - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - extensionPublisherId: '', - publisherDisplayName: '', - extensionDisplayName: '', - locations: [ChatAgentLocation.Chat], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }; -} - -suite('ChatService', () => { - const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); - - let instantiationService: TestInstantiationService; - let testFileService: InMemoryTestFileService; - - let chatAgentService: IChatAgentService; - - setup(async () => { - instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection( - [IChatVariablesService, new MockChatVariablesService()], - [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()], - [IMcpService, new TestMcpService()], - ))); - instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); - instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(ITelemetryService, NullTelemetryService); - instantiationService.stub(IExtensionService, new TestExtensionService()); - instantiationService.stub(IContextKeyService, new MockContextKeyService()); - instantiationService.stub(IViewsService, new TestExtensionService()); - instantiationService.stub(IWorkspaceContextService, new TestContextService()); - instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); - instantiationService.stub(IConfigurationService, new TestConfigurationService()); - instantiationService.stub(IChatService, new MockChatService()); - instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/path/to/workspaceStorage') }); - instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None }); - instantiationService.stub(IChatEditingService, new class extends mock() { - override startOrContinueGlobalEditingSession(): Promise { - return Promise.resolve(Disposable.None as IChatEditingSession); - } - }); - - // Configure test file service with tracking and in-memory storage - testFileService = testDisposables.add(new InMemoryTestFileService()); - instantiationService.stub(IFileService, testFileService); - - chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); - instantiationService.stub(IChatAgentService, chatAgentService); - - const agent: IChatAgentImplementation = { - async invoke(request, progress, history, token) { - return {}; - }, - }; - testDisposables.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true })); - testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, getAgentData(chatAgentWithUsedContextId))); - testDisposables.add(chatAgentService.registerAgent(chatAgentWithMarkdownId, getAgentData(chatAgentWithMarkdownId))); - testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); - chatAgentService.updateAgent('testAgent', { requester: { name: 'test' } }); - }); - - test('retrieveSession', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - const session1 = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0); - - const session2 = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0); - - // Clear sessions to trigger persistence to file service - await testService.clearSession(session1.sessionResource); - await testService.clearSession(session2.sessionResource); - - // Verify that sessions were written to the file service - assert.strictEqual(testFileService.writeOperations.length, 2, 'Should have written 2 sessions to file service'); - - const session1WriteOp = testFileService.writeOperations.find((op: { resource: URI; content: string }) => - op.content.includes('request 1')); - const session2WriteOp = testFileService.writeOperations.find((op: { resource: URI; content: string }) => - op.content.includes('request 2')); - - assert.ok(session1WriteOp, 'Session 1 should have been written to file service'); - assert.ok(session2WriteOp, 'Session 2 should have been written to file service'); - - // Create a new service instance to simulate app restart - const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); - - // Retrieve sessions and verify they're loaded from file service - const retrieved1 = testDisposables.add((await testService2.getOrRestoreSession(session1.sessionResource))!); - const retrieved2 = testDisposables.add((await testService2.getOrRestoreSession(session2.sessionResource))!); - - assert.ok(retrieved1, 'Should retrieve session 1'); - assert.ok(retrieved2, 'Should retrieve session 2'); - assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1'); - assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2'); - }); - - test('addCompleteRequest', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - - const model = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - await testService.addCompleteRequest(model.sessionResource, 'test request', undefined, 0, { message: 'test response' }); - assert.strictEqual(model.getRequests().length, 1); - assert.ok(model.getRequests()[0].response); - assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response'); - }); - - test('sendRequest fails', async () => { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - - const model = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - const response = await testService.sendRequest(model.sessionResource, `@${chatAgentWithUsedContextId} test request`); - assert(response); - await response.responseCompletePromise; - - await assertSnapshot(toSnapshotExportData(model)); - }); - - test('history', async () => { - const historyLengthAgent: IChatAgentImplementation = { - async invoke(request, progress, history, token) { - return { - metadata: { historyLength: history.length } - }; - }, - }; - - testDisposables.add(chatAgentService.registerAgent('defaultAgent', { ...getAgentData('defaultAgent'), isDefault: true })); - testDisposables.add(chatAgentService.registerAgent('agent2', getAgentData('agent2'))); - testDisposables.add(chatAgentService.registerAgentImplementation('defaultAgent', historyLengthAgent)); - testDisposables.add(chatAgentService.registerAgentImplementation('agent2', historyLengthAgent)); - - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - const model = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - - // Send a request to default agent - const response = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'defaultAgent' }); - assert(response); - await response.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - assert.strictEqual(model.getRequests()[0].response?.result?.metadata?.historyLength, 0); - - // Send a request to agent2- it can't see the default agent's message - const response2 = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'agent2' }); - assert(response2); - await response2.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 2); - assert.strictEqual(model.getRequests()[1].response?.result?.metadata?.historyLength, 0); - - // Send a request to defaultAgent - the default agent can see agent2's message - const response3 = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'defaultAgent' }); - assert(response3); - await response3.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 3); - assert.strictEqual(model.getRequests()[2].response?.result?.metadata?.historyLength, 2); - }); - - test('can serialize', async () => { - testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); - chatAgentService.updateAgent(chatAgentWithUsedContextId, { requester: { name: 'test' } }); - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - - const model = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - assert.strictEqual(model.getRequests().length, 0); - - await assertSnapshot(toSnapshotExportData(model)); - - const response = await testService.sendRequest(model.sessionResource, `@${chatAgentWithUsedContextId} test request`); - assert(response); - await response.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 1); - - const response2 = await testService.sendRequest(model.sessionResource, `test request 2`); - assert(response2); - await response2.responseCompletePromise; - assert.strictEqual(model.getRequests().length, 2); - - await assertSnapshot(toSnapshotExportData(model)); - }); - - test('can deserialize', async () => { - let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); - - // create the first service, send request, get response, and serialize the state - { // serapate block to not leak variables in outer scope - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - - const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - assert.strictEqual(chatModel1.getRequests().length, 0); - - const response = await testService.sendRequest(chatModel1.sessionResource, `@${chatAgentWithUsedContextId} test request`); - assert(response); - - await response.responseCompletePromise; - - serializedChatData = JSON.parse(JSON.stringify(chatModel1)); - } - - // try deserializing the state into a new service - - const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); - - const chatModel2 = testService2.loadSessionFromContent(serializedChatData); - assert(chatModel2); - - await assertSnapshot(toSnapshotExportData(chatModel2)); - chatModel2.dispose(); - }); - - test('can deserialize with response', async () => { - let serializedChatData: ISerializableChatData; - testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithMarkdownId, chatAgentWithMarkdown)); - - { - const testService = testDisposables.add(instantiationService.createInstance(ChatService)); - - const chatModel1 = testDisposables.add(testService.startSession(ChatAgentLocation.Chat, CancellationToken.None)); - assert.strictEqual(chatModel1.getRequests().length, 0); - - const response = await testService.sendRequest(chatModel1.sessionResource, `@${chatAgentWithUsedContextId} test request`); - assert(response); - - await response.responseCompletePromise; - - serializedChatData = JSON.parse(JSON.stringify(chatModel1)); - } - - // try deserializing the state into a new service - - const testService2 = testDisposables.add(instantiationService.createInstance(ChatService)); - - const chatModel2 = testService2.loadSessionFromContent(serializedChatData); - assert(chatModel2); - - await assertSnapshot(toSnapshotExportData(chatModel2)); - chatModel2.dispose(); - }); -}); - - -function toSnapshotExportData(model: IChatModel) { - const exp = model.toExport(); - return { - ...exp, - requests: exp.requests.map(r => { - return { - ...r, - timestamp: undefined, - requestId: undefined, // id contains a random part - responseId: undefined, // id contains a random part - }; - }) - }; -} diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap similarity index 94% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap rename to src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap index 84d93d91194..643e3727914 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize.0.snap @@ -1,8 +1,5 @@ { - requesterUsername: "test", - requesterAvatarIconUri: undefined, responderUsername: "", - responderAvatarIconUri: undefined, initialLocation: "panel", requests: [ { @@ -57,14 +54,7 @@ }, variableData: { variables: [ ] }, response: [ ], - responseId: undefined, shouldBeRemovedOnSend: undefined, - result: { metadata: { metadataKey: "value" } }, - responseMarkdownInfo: undefined, - followups: undefined, - isCanceled: false, - vote: undefined, - voteDownReason: undefined, agent: { name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", @@ -81,6 +71,20 @@ slashCommands: [ ], disambiguation: [ ] }, + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined, + modelId: undefined, + responseId: undefined, + result: { metadata: { metadataKey: "value" } }, + responseMarkdownInfo: undefined, + followups: undefined, + modelState: { + value: 1, + completedAt: undefined + }, + vote: undefined, + voteDownReason: undefined, slashCommand: undefined, usedContext: { documents: [ @@ -101,10 +105,7 @@ }, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined, - confirmation: undefined, - editedFileEvents: undefined, - modelId: undefined + timeSpentWaiting: 0 } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap similarity index 94% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap rename to src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap index dcb2adcc070..f0e423320dd 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -1,8 +1,5 @@ { - requesterUsername: "test", - requesterAvatarIconUri: undefined, responderUsername: "", - responderAvatarIconUri: undefined, initialLocation: "panel", requests: [ { @@ -57,14 +54,7 @@ }, variableData: { variables: [ ] }, response: [ ], - responseId: undefined, shouldBeRemovedOnSend: undefined, - result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, - responseMarkdownInfo: undefined, - followups: undefined, - isCanceled: false, - vote: undefined, - voteDownReason: undefined, agent: { name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", @@ -81,14 +71,25 @@ slashCommands: [ ], disambiguation: [ ] }, + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined, + modelId: undefined, + responseId: undefined, + result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + responseMarkdownInfo: undefined, + followups: undefined, + modelState: { + value: 3, + completedAt: undefined + }, + vote: undefined, + voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined, - confirmation: undefined, - editedFileEvents: undefined, - modelId: undefined + timeSpentWaiting: 0 } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.0.snap new file mode 100644 index 00000000000..8bbe4ed0340 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.0.snap @@ -0,0 +1,5 @@ +{ + responderUsername: "", + initialLocation: "panel", + requests: [ ] +} \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap similarity index 92% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap rename to src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap index 22c4eb64b28..3e5c32aa740 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_can_serialize.1.snap @@ -1,8 +1,5 @@ { - requesterUsername: "test", - requesterAvatarIconUri: undefined, responderUsername: "", - responderAvatarIconUri: undefined, initialLocation: "panel", requests: [ { @@ -33,7 +30,7 @@ extensionDisplayName: "", locations: [ "panel" ], modes: [ "ask" ], - metadata: { requester: { name: "test" } }, + metadata: { }, slashCommands: [ ], disambiguation: [ ] }, @@ -58,21 +55,7 @@ }, variableData: { variables: [ ] }, response: [ ], - responseId: undefined, shouldBeRemovedOnSend: undefined, - result: { metadata: { metadataKey: "value" } }, - responseMarkdownInfo: undefined, - followups: [ - { - kind: "reply", - message: "Something else", - agentId: "", - tooltip: "a tooltip" - } - ], - isCanceled: false, - vote: undefined, - voteDownReason: undefined, agent: { name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", @@ -86,10 +69,31 @@ extensionDisplayName: "", locations: [ "panel" ], modes: [ "ask" ], - metadata: { requester: { name: "test" } }, + metadata: { }, slashCommands: [ ], disambiguation: [ ] }, + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined, + modelId: undefined, + responseId: undefined, + result: { metadata: { metadataKey: "value" } }, + responseMarkdownInfo: undefined, + followups: [ + { + kind: "reply", + message: "Something else", + agentId: "", + tooltip: "a tooltip" + } + ], + modelState: { + value: 1, + completedAt: undefined + }, + vote: undefined, + voteDownReason: undefined, slashCommand: undefined, usedContext: { documents: [ @@ -110,10 +114,7 @@ }, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined, - confirmation: undefined, - editedFileEvents: undefined, - modelId: undefined + timeSpentWaiting: 0 }, { requestId: undefined, @@ -138,14 +139,7 @@ }, variableData: { variables: [ ] }, response: [ ], - responseId: undefined, shouldBeRemovedOnSend: undefined, - result: { }, - responseMarkdownInfo: undefined, - followups: [ ], - isCanceled: false, - vote: undefined, - voteDownReason: undefined, agent: { name: "testAgent", id: "testAgent", @@ -159,19 +153,30 @@ extensionDisplayName: "", locations: [ "panel" ], modes: [ "ask" ], - metadata: { requester: { name: "test" } }, + metadata: { }, slashCommands: [ ], disambiguation: [ ], isDefault: true }, + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined, + modelId: undefined, + responseId: undefined, + result: { }, + responseMarkdownInfo: undefined, + followups: [ ], + modelState: { + value: 1, + completedAt: undefined + }, + vote: undefined, + voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined, - confirmation: undefined, - editedFileEvents: undefined, - modelId: undefined + timeSpentWaiting: 0 } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap similarity index 94% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap rename to src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap index c3a5982cd8f..9a7073212a2 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/chatService/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -1,8 +1,5 @@ { - requesterUsername: "test", - requesterAvatarIconUri: undefined, responderUsername: "", - responderAvatarIconUri: undefined, initialLocation: "panel", requests: [ { @@ -58,14 +55,7 @@ }, variableData: { variables: [ ] }, response: [ ], - responseId: undefined, shouldBeRemovedOnSend: undefined, - result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, - responseMarkdownInfo: undefined, - followups: undefined, - isCanceled: false, - vote: undefined, - voteDownReason: undefined, agent: { name: "ChatProviderWithUsedContext", id: "ChatProviderWithUsedContext", @@ -83,14 +73,25 @@ slashCommands: [ ], disambiguation: [ ] }, + timestamp: undefined, + confirmation: undefined, + editedFileEvents: undefined, + modelId: undefined, + responseId: undefined, + result: { errorDetails: { message: "No activated agent with id \"ChatProviderWithUsedContext\"" } }, + responseMarkdownInfo: undefined, + followups: undefined, + modelState: { + value: 3, + completedAt: undefined + }, + vote: undefined, + voteDownReason: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], codeCitations: [ ], - timestamp: undefined, - confirmation: undefined, - editedFileEvents: undefined, - modelId: undefined + timeSpentWaiting: 0 } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts new file mode 100644 index 00000000000..4b1c94e37e3 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatService/chatService.test.ts @@ -0,0 +1,442 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Event } from '../../../../../../base/common/event.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { constObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { NullTelemetryService } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IUserDataProfilesService, toUserDataProfile } from '../../../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { IWorkbenchAssignmentService } from '../../../../../services/assignment/common/assignmentService.js'; +import { NullWorkbenchAssignmentService } from '../../../../../services/assignment/test/common/nullAssignmentService.js'; +import { IExtensionService, nullExtensionDescription } from '../../../../../services/extensions/common/extensions.js'; +import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { IViewsService } from '../../../../../services/views/common/viewsService.js'; +import { InMemoryTestFileService, mock, TestContextService, TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { IMcpService } from '../../../../mcp/common/mcpTypes.js'; +import { TestMcpService } from '../../../../mcp/test/common/testMcpService.js'; +import { ChatAgentService, IChatAgent, IChatAgentData, IChatAgentImplementation, IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { IChatEditingService, IChatEditingSession } from '../../../common/editing/chatEditingService.js'; +import { ChatModel, IChatModel, ISerializableChatData } from '../../../common/model/chatModel.js'; +import { IChatFollowup, IChatModelReference, IChatService } from '../../../common/chatService/chatService.js'; +import { ChatService } from '../../../common/chatService/chatServiceImpl.js'; +import { ChatSlashCommandService, IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js'; +import { IChatVariablesService } from '../../../common/attachments/chatVariables.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { MockChatService } from './mockChatService.js'; +import { MockChatVariablesService } from '../mockChatVariables.js'; + +const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext'; +const chatAgentWithUsedContext: IChatAgent = { + id: chatAgentWithUsedContextId, + name: chatAgentWithUsedContextId, + extensionId: nullExtensionDescription.identifier, + extensionVersion: undefined, + publisherDisplayName: '', + extensionPublisherId: '', + extensionDisplayName: '', + locations: [ChatAgentLocation.Chat], + modes: [ChatModeKind.Ask], + metadata: {}, + slashCommands: [], + disambiguation: [], + async invoke(request, progress, history, token) { + progress([{ + documents: [ + { + uri: URI.file('/test/path/to/file'), + version: 3, + ranges: [ + new Range(1, 1, 2, 2) + ] + } + ], + kind: 'usedContext' + }]); + + return { metadata: { metadataKey: 'value' } }; + }, + async provideFollowups(sessionId, token) { + return [{ kind: 'reply', message: 'Something else', agentId: '', tooltip: 'a tooltip' } satisfies IChatFollowup]; + }, +}; + +const chatAgentWithMarkdownId = 'ChatProviderWithMarkdown'; +const chatAgentWithMarkdown: IChatAgent = { + id: chatAgentWithMarkdownId, + name: chatAgentWithMarkdownId, + extensionId: nullExtensionDescription.identifier, + extensionVersion: undefined, + publisherDisplayName: '', + extensionPublisherId: '', + extensionDisplayName: '', + locations: [ChatAgentLocation.Chat], + modes: [ChatModeKind.Ask], + metadata: {}, + slashCommands: [], + disambiguation: [], + async invoke(request, progress, history, token) { + progress([{ kind: 'markdownContent', content: new MarkdownString('test') }]); + return { metadata: { metadataKey: 'value' } }; + }, + async provideFollowups(sessionId, token) { + return []; + }, +}; + +function getAgentData(id: string): IChatAgentData { + return { + name: id, + id: id, + extensionId: nullExtensionDescription.identifier, + extensionVersion: undefined, + extensionPublisherId: '', + publisherDisplayName: '', + extensionDisplayName: '', + locations: [ChatAgentLocation.Chat], + modes: [ChatModeKind.Ask], + metadata: {}, + slashCommands: [], + disambiguation: [], + }; +} + +suite('ChatService', () => { + const testDisposables = new DisposableStore(); + + let instantiationService: TestInstantiationService; + let testFileService: InMemoryTestFileService; + + let chatAgentService: IChatAgentService; + const testServices: ChatService[] = []; + + /** + * Ensure we wait for model disposals from all created ChatServices + */ + function createChatService(): ChatService { + const service = testDisposables.add(instantiationService.createInstance(ChatService)); + testServices.push(service); + return service; + } + + function startSessionModel(service: IChatService, location: ChatAgentLocation = ChatAgentLocation.Chat): IChatModelReference { + const ref = testDisposables.add(service.startSession(location)); + return ref; + } + + async function getOrRestoreModel(service: IChatService, resource: URI): Promise { + const ref = await service.getOrRestoreSession(resource); + if (!ref) { + return undefined; + } + return testDisposables.add(ref).object; + } + + setup(async () => { + instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection( + [IChatVariablesService, new MockChatVariablesService()], + [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()], + [IMcpService, new TestMcpService()], + ))); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) }); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IViewsService, new TestExtensionService()); + instantiationService.stub(IWorkspaceContextService, new TestContextService()); + instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IChatService, new MockChatService()); + instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/path/to/workspaceStorage') }); + instantiationService.stub(ILifecycleService, { onWillShutdown: Event.None }); + instantiationService.stub(IChatEditingService, new class extends mock() { + override startOrContinueGlobalEditingSession(): IChatEditingSession { + return { + state: constObservable('idle'), + requestDisablement: observableValue('requestDisablement', []), + entries: constObservable([]), + dispose: () => { } + } as unknown as IChatEditingSession; + } + }); + + // Configure test file service with tracking and in-memory storage + testFileService = testDisposables.add(new InMemoryTestFileService()); + instantiationService.stub(IFileService, testFileService); + + chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService)); + instantiationService.stub(IChatAgentService, chatAgentService); + + const agent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + return {}; + }, + }; + testDisposables.add(chatAgentService.registerAgent('testAgent', { ...getAgentData('testAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContextId, getAgentData(chatAgentWithUsedContextId))); + testDisposables.add(chatAgentService.registerAgent(chatAgentWithMarkdownId, getAgentData(chatAgentWithMarkdownId))); + testDisposables.add(chatAgentService.registerAgentImplementation('testAgent', agent)); + chatAgentService.updateAgent('testAgent', {}); + }); + + teardown(async () => { + testDisposables.clear(); + await Promise.all(testServices.map(s => s.waitForModelDisposals())); + testServices.length = 0; + }); + ensureNoDisposablesAreLeakedInTestSuite(); + + test('retrieveSession', async () => { + const testService = createChatService(); + // Don't add refs to testDisposables so we can control disposal + const session1Ref = testService.startSession(ChatAgentLocation.Chat); + const session1 = session1Ref.object as ChatModel; + session1.addRequest({ parts: [], text: 'request 1' }, { variables: [] }, 0); + + const session2Ref = testService.startSession(ChatAgentLocation.Chat); + const session2 = session2Ref.object as ChatModel; + session2.addRequest({ parts: [], text: 'request 2' }, { variables: [] }, 0); + + // Dispose refs to trigger persistence to file service + session1Ref.dispose(); + session2Ref.dispose(); + + // Wait for async persistence to complete + await testService.waitForModelDisposals(); + + // Verify that sessions were written to the file service + assert.strictEqual(testFileService.writeOperations.length, 2, 'Should have written 2 sessions to file service'); + + const session1WriteOp = testFileService.writeOperations.find((op: { resource: URI; content: string }) => + op.content.includes('request 1')); + const session2WriteOp = testFileService.writeOperations.find((op: { resource: URI; content: string }) => + op.content.includes('request 2')); + + assert.ok(session1WriteOp, 'Session 1 should have been written to file service'); + assert.ok(session2WriteOp, 'Session 2 should have been written to file service'); + + // Create a new service instance to simulate app restart + const testService2 = createChatService(); + + // Retrieve sessions and verify they're loaded from file service + const retrieved1 = await getOrRestoreModel(testService2, session1.sessionResource); + const retrieved2 = await getOrRestoreModel(testService2, session2.sessionResource); + + assert.ok(retrieved1, 'Should retrieve session 1'); + assert.ok(retrieved2, 'Should retrieve session 2'); + assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1'); + assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2'); + }); + + test('addCompleteRequest', async () => { + const testService = createChatService(); + + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + assert.strictEqual(model.getRequests().length, 0); + + await testService.addCompleteRequest(model.sessionResource, 'test request', undefined, 0, { message: 'test response' }); + assert.strictEqual(model.getRequests().length, 1); + assert.ok(model.getRequests()[0].response); + assert.strictEqual(model.getRequests()[0].response?.response.toString(), 'test response'); + }); + + test('sendRequest fails', async () => { + const testService = createChatService(); + + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + const response = await testService.sendRequest(model.sessionResource, `@${chatAgentWithUsedContextId} test request`); + assert(response); + await response.responseCompletePromise; + + await assertSnapshot(toSnapshotExportData(model)); + }); + + test('history', async () => { + const historyLengthAgent: IChatAgentImplementation = { + async invoke(request, progress, history, token) { + return { + metadata: { historyLength: history.length } + }; + }, + }; + + testDisposables.add(chatAgentService.registerAgent('defaultAgent', { ...getAgentData('defaultAgent'), isDefault: true })); + testDisposables.add(chatAgentService.registerAgent('agent2', getAgentData('agent2'))); + testDisposables.add(chatAgentService.registerAgentImplementation('defaultAgent', historyLengthAgent)); + testDisposables.add(chatAgentService.registerAgentImplementation('agent2', historyLengthAgent)); + + const testService = createChatService(); + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + + // Send a request to default agent + const response = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'defaultAgent' }); + assert(response); + await response.responseCompletePromise; + assert.strictEqual(model.getRequests().length, 1); + assert.strictEqual(model.getRequests()[0].response?.result?.metadata?.historyLength, 0); + + // Send a request to agent2- it can't see the default agent's message + const response2 = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'agent2' }); + assert(response2); + await response2.responseCompletePromise; + assert.strictEqual(model.getRequests().length, 2); + assert.strictEqual(model.getRequests()[1].response?.result?.metadata?.historyLength, 0); + + // Send a request to defaultAgent - the default agent can see agent2's message + const response3 = await testService.sendRequest(model.sessionResource, `test request`, { agentId: 'defaultAgent' }); + assert(response3); + await response3.responseCompletePromise; + assert.strictEqual(model.getRequests().length, 3); + assert.strictEqual(model.getRequests()[2].response?.result?.metadata?.historyLength, 2); + }); + + test('can serialize', async () => { + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); + chatAgentService.updateAgent(chatAgentWithUsedContextId, {}); + const testService = createChatService(); + + const modelRef = testDisposables.add(startSessionModel(testService)); + const model = modelRef.object; + assert.strictEqual(model.getRequests().length, 0); + + await assertSnapshot(toSnapshotExportData(model)); + + const response = await testService.sendRequest(model.sessionResource, `@${chatAgentWithUsedContextId} test request`); + assert(response); + await response.responseCompletePromise; + assert.strictEqual(model.getRequests().length, 1); + + const response2 = await testService.sendRequest(model.sessionResource, `test request 2`); + assert(response2); + await response2.responseCompletePromise; + assert.strictEqual(model.getRequests().length, 2); + + await assertSnapshot(toSnapshotExportData(model)); + }); + + test('can deserialize', async () => { + let serializedChatData: ISerializableChatData; + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithUsedContextId, chatAgentWithUsedContext)); + + // create the first service, send request, get response, and serialize the state + { // serapate block to not leak variables in outer scope + const testService = createChatService(); + + const chatModel1Ref = testDisposables.add(startSessionModel(testService)); + const chatModel1 = chatModel1Ref.object; + assert.strictEqual(chatModel1.getRequests().length, 0); + + const response = await testService.sendRequest(chatModel1.sessionResource, `@${chatAgentWithUsedContextId} test request`); + assert(response); + + await response.responseCompletePromise; + + serializedChatData = JSON.parse(JSON.stringify(chatModel1)); + } + + // try deserializing the state into a new service + + const testService2 = createChatService(); + + const chatModel2Ref = testService2.loadSessionFromContent(serializedChatData); + assert(chatModel2Ref); + testDisposables.add(chatModel2Ref); + const chatModel2 = chatModel2Ref.object; + + await assertSnapshot(toSnapshotExportData(chatModel2)); + }); + + test('can deserialize with response', async () => { + let serializedChatData: ISerializableChatData; + testDisposables.add(chatAgentService.registerAgentImplementation(chatAgentWithMarkdownId, chatAgentWithMarkdown)); + + { + const testService = createChatService(); + + const chatModel1Ref = testDisposables.add(startSessionModel(testService)); + const chatModel1 = chatModel1Ref.object; + assert.strictEqual(chatModel1.getRequests().length, 0); + + const response = await testService.sendRequest(chatModel1.sessionResource, `@${chatAgentWithUsedContextId} test request`); + assert(response); + + await response.responseCompletePromise; + + serializedChatData = JSON.parse(JSON.stringify(chatModel1)); + } + + // try deserializing the state into a new service + + const testService2 = createChatService(); + + const chatModel2Ref = testService2.loadSessionFromContent(serializedChatData); + assert(chatModel2Ref); + testDisposables.add(chatModel2Ref); + const chatModel2 = chatModel2Ref.object; + + await assertSnapshot(toSnapshotExportData(chatModel2)); + }); + + test('onDidDisposeSession', async () => { + const testService = createChatService(); + const modelRef = testService.startSession(ChatAgentLocation.Chat); + const model = modelRef.object; + + let disposed = false; + testDisposables.add(testService.onDidDisposeSession(e => { + for (const resource of e.sessionResource) { + if (resource.toString() === model.sessionResource.toString()) { + disposed = true; + } + } + })); + + modelRef.dispose(); + await testService.waitForModelDisposals(); + assert.strictEqual(disposed, true); + }); +}); + + +function toSnapshotExportData(model: IChatModel) { + const exp = model.toExport(); + return { + ...exp, + requests: exp.requests.map(r => { + return { + ...r, + modelState: { + ...r.modelState, + completedAt: undefined + }, + timestamp: undefined, + requestId: undefined, // id contains a random part + responseId: undefined, // id contains a random part + }; + }) + }; +} diff --git a/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts new file mode 100644 index 00000000000..5f104554cab --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/chatService/mockChatService.ts @@ -0,0 +1,153 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { ResourceMap } from '../../../../../../base/common/map.js'; +import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../../common/model/chatModel.js'; +import { IParsedChatRequest } from '../../../common/requestParser/chatParserTypes.js'; +import { IChatCompleteResponse, IChatDetail, IChatModelReference, IChatProgress, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionStartOptions, IChatUserActionEvent } from '../../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; + +export class MockChatService implements IChatService { + chatModels: IObservable> = observableValue('chatModels', []); + requestInProgressObs = observableValue('name', false); + edits2Enabled: boolean = false; + _serviceBrand: undefined; + editingSessions = []; + transferredSessionResource: URI | undefined; + readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None; + readonly onDidCreateModel: Event = Event.None; + + private sessions = new ResourceMap(); + + setSaveModelsEnabled(enabled: boolean): void { + + } + isEnabled(location: ChatAgentLocation): boolean { + throw new Error('Method not implemented.'); + } + hasSessions(): boolean { + throw new Error('Method not implemented.'); + } + getProviderInfos(): IChatProviderInfo[] { + throw new Error('Method not implemented.'); + } + startSession(location: ChatAgentLocation, options?: IChatSessionStartOptions): IChatModelReference { + throw new Error('Method not implemented.'); + } + addSession(session: IChatModel): void { + this.sessions.set(session.sessionResource, session); + } + getSession(sessionResource: URI): IChatModel | undefined { + // eslint-disable-next-line local/code-no-dangerous-type-assertions + return this.sessions.get(sessionResource) ?? {} as IChatModel; + } + async getOrRestoreSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } + getSessionTitle(sessionResource: URI): string | undefined { + throw new Error('Method not implemented.'); + } + loadSessionFromContent(data: ISerializableChatData): IChatModelReference | undefined { + throw new Error('Method not implemented.'); + } + loadSessionForResource(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { + throw new Error('Method not implemented.'); + } + getActiveSessionReference(sessionResource: URI): IChatModelReference | undefined { + return undefined; + } + setTitle(sessionResource: URI, title: string): void { + throw new Error('Method not implemented.'); + } + appendProgress(request: IChatRequestModel, progress: IChatProgress): void { + + } + /** + * Returns whether the request was accepted. + */ + sendRequest(sessionResource: URI, message: string): Promise { + throw new Error('Method not implemented.'); + } + resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise { + throw new Error('Method not implemented.'); + } + removeRequest(sessionResource: URI, requestId: string): Promise { + throw new Error('Method not implemented.'); + } + cancelCurrentRequestForSession(sessionResource: URI): void { + throw new Error('Method not implemented.'); + } + addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void { + throw new Error('Method not implemented.'); + } + async getLocalSessionHistory(): Promise { + throw new Error('Method not implemented.'); + } + async clearAllHistoryEntries() { + throw new Error('Method not implemented.'); + } + async removeHistoryEntry(resource: URI) { + throw new Error('Method not implemented.'); + } + + readonly onDidPerformUserAction: Event = undefined!; + notifyUserAction(event: IChatUserActionEvent): void { + throw new Error('Method not implemented.'); + } + readonly onDidReceiveQuestionCarouselAnswer: Event<{ requestId: string; resolveId: string; answers: Record | undefined }> = undefined!; + notifyQuestionCarouselAnswer(requestId: string, resolveId: string, answers: Record | undefined): void { + throw new Error('Method not implemented.'); + } + readonly onDidDisposeSession: Event<{ sessionResource: URI[]; reason: 'cleared' }> = undefined!; + + async transferChatSession(transferredSessionResource: URI, toWorkspace: URI): Promise { + throw new Error('Method not implemented.'); + } + + setChatSessionTitle(sessionResource: URI, title: string): void { + throw new Error('Method not implemented.'); + } + + isEditingLocation(location: ChatAgentLocation): boolean { + throw new Error('Method not implemented.'); + } + + getChatStorageFolder(): URI { + throw new Error('Method not implemented.'); + } + + logChatIndex(): void { + throw new Error('Method not implemented.'); + } + + activateDefaultAgent(location: ChatAgentLocation): Promise { + throw new Error('Method not implemented.'); + } + + getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined { + throw new Error('Method not implemented.'); + } + + async getLiveSessionItems(): Promise { + throw new Error('Method not implemented.'); + } + getHistorySessionItems(): Promise { + throw new Error('Method not implemented.'); + } + + waitForModelDisposals(): Promise { + throw new Error('Method not implemented.'); + } + getMetadataForSession(sessionResource: URI): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts index ac50ccf476a..48e2b40d1f1 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.test.ts @@ -10,16 +10,18 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { ChatMessageRole, languageModelChatProviderExtensionPoint, LanguageModelsService, IChatMessage, IChatResponsePart } from '../../common/languageModels.js'; +import { ChatMessageRole, LanguageModelsService, IChatMessage, IChatResponsePart, ILanguageModelChatMetadata } from '../../common/languageModels.js'; import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { ExtensionsRegistry } from '../../../../services/extensions/common/extensionsRegistry.js'; -import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/modelPicker/modelPickerWidget.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../common/widget/input/modelPickerWidget.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { TestChatEntitlementService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; +import { StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; import { Event } from '../../../../../base/common/event.js'; import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; +import { ILanguageModelsConfigurationService } from '../../common/languageModelsConfiguration.js'; +import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js'; +import { TestSecretStorageService } from '../../../../../platform/secrets/test/common/testSecretStorageService.js'; suite('LanguageModels', function () { @@ -40,21 +42,20 @@ suite('LanguageModels', function () { new NullLogService(), new TestStorageService(), new MockContextKeyService(), - new TestConfigurationService(), - new TestChatEntitlementService() + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'test-vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); store.add(languageModels.registerLanguageModelProvider('test-vendor', { onDidChange: Event.None, @@ -70,7 +71,8 @@ suite('LanguageModels', function () { id: 'test-id-1', maxInputTokens: 100, maxOutputTokens: 100, - }, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, { extension: nullExtensionDescription.identifier, name: 'Pretty Name', @@ -81,7 +83,8 @@ suite('LanguageModels', function () { id: 'test-id-12', maxInputTokens: 100, maxOutputTokens: 100, - } + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata ]; const modelMetadataAndIdentifier = modelMetadata.map(m => ({ metadata: m, @@ -144,7 +147,8 @@ suite('LanguageModels', function () { maxInputTokens: 100, maxOutputTokens: 100, modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, - } + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata ]; const modelMetadataAndIdentifier = modelMetadata.map(m => ({ metadata: m, @@ -177,12 +181,9 @@ suite('LanguageModels', function () { })); // Register the extension point for the actual vendor - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'actual-vendor' }, - collector: null! - }]); + languageModels.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'actual-vendor', displayName: 'Actual Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); const models = await languageModels.selectLanguageModels({ id: 'actual-lm' }); assert.ok(models.length === 1); @@ -245,25 +246,18 @@ suite('LanguageModels - When Clause', function () { new NullLogService(), new TestStorageService(), contextKeyService, - new TestConfigurationService(), - new TestChatEntitlementService() + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + }, + new class extends mock() { }, + new TestSecretStorageService(), ); - const ext = ExtensionsRegistry.getExtensionPoints().find(e => e.name === languageModelChatProviderExtensionPoint.name)!; - - ext.acceptUsers([{ - description: { ...nullExtensionDescription }, - value: { vendor: 'visible-vendor', displayName: 'Visible Vendor' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', when: 'testKey' }, - collector: null! - }, { - description: { ...nullExtensionDescription }, - value: { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', when: 'falseKey' }, - collector: null! - }]); + languageModelsWithWhen.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'visible-vendor', displayName: 'Visible Vendor', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'conditional-vendor', displayName: 'Conditional Vendor', configuration: undefined, managementCommand: undefined, when: 'testKey' }, + { vendor: 'hidden-vendor', displayName: 'Hidden Vendor', configuration: undefined, managementCommand: undefined, when: 'falseKey' } + ], []); }); teardown(function () { @@ -289,4 +283,701 @@ suite('LanguageModels - When Clause', function () { const vendors = languageModelsWithWhen.getVendors(); assert.ok(!vendors.some(v => v.vendor === 'hidden-vendor'), 'hidden-vendor should be hidden when falseKey is false'); }); + +}); + +suite('LanguageModels - Model Picker Preferences Storage', function () { + + let languageModelsService: LanguageModelsService; + let storageService: TestStorageService; + const disposables = new DisposableStore(); + + setup(async function () { + storageService = new TestStorageService(); + + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + storageService, + new MockContextKeyService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + + // Register vendor1 used in most tests + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor1', displayName: 'Vendor 1', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + disposables.add(languageModelsService.registerLanguageModelProvider('vendor1', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'vendor1', + family: 'family1', + version: '1.0', + id: 'vendor1/model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'vendor1/model1' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Populate the model cache + await languageModelsService.selectLanguageModels({}); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onChange event when new model preferences are added', async function () { + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => firedVendorId = vendorId)); + + // Add new preferences to storage - store() automatically triggers change event synchronously + const preferences = { + 'vendor1/model1': true + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(preferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1'); + + // Verify preference was updated + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, true); + }); + + test('fires onChange event when model preferences are removed', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Remove preferences via storage API + const updatedPreferences = {}; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference removed'); + + // Verify preference was removed + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, undefined); + }); + + test('fires onChange event when model preferences are updated', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Update the preference value + const updatedPreferences = { + 'vendor1/model1': false + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify change event was fired + assert.strictEqual(firedVendorId, 'vendor1', 'Should fire change event for vendor1 when preference updated'); + + // Verify preference was updated + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, false); + }); + + test('only fires onChange event for affected vendors', async function () { + // Register vendor2 + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor2', displayName: 'Vendor 2', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + disposables.add(languageModelsService.registerLanguageModelProvider('vendor2', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'vendor2', + family: 'family2', + version: '1.0', + id: 'vendor2/model2', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: DEFAULT_MODEL_PICKER_CATEGORY, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'vendor2/model2' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + await languageModelsService.selectLanguageModels({}); + + // Set initial preferences using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + languageModelsService.updateModelPickerPreference('vendor2/model2', false); + + // Listen for change event + let firedVendorId: string | undefined; + disposables.add(languageModelsService.onDidChangeLanguageModels(vendorId => { + firedVendorId = vendorId; + })); + + // Update only vendor1 preference + const updatedPreferences = { + 'vendor1/model1': false, + 'vendor2/model2': false // unchanged + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(updatedPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify only vendor1 was affected + assert.strictEqual(firedVendorId, 'vendor1', 'Should only affect vendor1'); + + // Verify preferences were updated correctly + const model1 = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model1); + assert.strictEqual(model1.isUserSelectable, false, 'vendor1/model1 should be updated to false'); + + const model2 = languageModelsService.lookupLanguageModel('vendor2/model2'); + assert.ok(model2); + assert.strictEqual(model2.isUserSelectable, false, 'vendor2/model2 should remain false'); + }); + + test('does not fire onChange event when preferences are unchanged', async function () { + // Set initial preference using the API + languageModelsService.updateModelPickerPreference('vendor1/model1', true); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Store the same preferences again + const initialPreferences = { + 'vendor1/model1': true + }; + storageService.store('chatModelPickerPreferences', JSON.stringify(initialPreferences), StorageScope.PROFILE, StorageTarget.USER); + + // Verify no event was fired + assert.strictEqual(eventFired, false, 'Should not fire event when preferences are unchanged'); + + // Verify preference remains the same + const model = languageModelsService.lookupLanguageModel('vendor1/model1'); + assert.ok(model); + assert.strictEqual(model.isUserSelectable, true); + }); + + test('handles malformed JSON in storage gracefully', function () { + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Store empty preferences - store() automatically triggers change event + storageService.store('chatModelPickerPreferences', '{}', StorageScope.PROFILE, StorageTarget.USER); + + // Verify no event was fired - empty preferences is valid and causes no changes + assert.strictEqual(eventFired, false, 'Should not fire event for empty preferences'); + }); +}); + +suite('LanguageModels - Model Change Events', function () { + + let languageModelsService: LanguageModelsService; + let storageService: TestStorageService; + const disposables = new DisposableStore(); + + setup(async function () { + storageService = new TestStorageService(); + + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + storageService, + new MockContextKeyService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + + // Register the vendor first + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'test-vendor', displayName: 'Test Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onChange event when new models are added', async function () { + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels((vendorId) => { + resolve(vendorId); + })); + }); + + // Store a preference to trigger auto-resolution when provider is registered + storageService.store('chatModelPickerPreferences', JSON.stringify({ 'test-vendor/model1': true }), StorageScope.PROFILE, StorageTarget.USER); + + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: Event.None, + provideLanguageModelChatInfo: async () => { + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + const firedVendorId = await eventPromise; + assert.strictEqual(firedVendorId, 'test-vendor', 'Should fire event when new models are added'); + }); + + test('does not fire onChange event when models are unchanged', async function () { + const models = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => models, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + // Trigger provider change with same models + onDidChangeEmitter.fire(); + + // Call selectLanguageModels again - provider will return different models + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + assert.strictEqual(eventFired, false, 'Should not fire event when models are unchanged'); + }); + + test('fires onChange event when model metadata changes', async function () { + const initialModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let currentModels = initialModels; + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Change model metadata (e.g., maxInputTokens) + currentModels = [{ + metadata: { + ...initialModels[0].metadata, + maxInputTokens: 200 // Changed from 100 + }, + identifier: 'test-vendor/model1' + }]; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when model metadata changed'); + }); + + test('fires onChange event when models are removed', async function () { + let currentModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Remove all models + currentModels = []; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when models were removed'); + }); + + test('fires onChange event when new model is added to existing set', async function () { + let currentModels = [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + + let onDidChangeEmitter: any; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: (listener) => { + onDidChangeEmitter = { fire: () => listener() }; + return { dispose: () => { } }; + }, + provideLanguageModelChatInfo: async () => currentModels, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Create a promise that resolves when the event fires + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + resolve(); + })); + }); + + // Add a new model + currentModels = [ + ...currentModels, + { + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'test-vendor', + family: 'family2', + version: '1.0', + id: 'model2', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model2' + } + ]; + + onDidChangeEmitter.fire(); + + await eventPromise; + assert.ok(true, 'Event fired when new model was added'); + }); + + test('fires onChange event when models change without provider emitting change event', async function () { + let callCount = 0; + disposables.add(languageModelsService.registerLanguageModelProvider('test-vendor', { + onDidChange: Event.None, // Provider doesn't emit change events + provideLanguageModelChatInfo: async () => { + callCount++; + if (callCount === 1) { + // First call returns initial model + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 1', + vendor: 'test-vendor', + family: 'family1', + version: '1.0', + id: 'model1', + maxInputTokens: 100, + maxOutputTokens: 100, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model1' + }]; + } else { + // Subsequent calls return different model + return [{ + metadata: { + extension: nullExtensionDescription.identifier, + name: 'Model 2', + vendor: 'test-vendor', + family: 'family2', + version: '2.0', + id: 'model2', + maxInputTokens: 200, + maxOutputTokens: 200, + modelPickerCategory: undefined, + isDefaultForLocation: {} + } satisfies ILanguageModelChatMetadata, + identifier: 'test-vendor/model2' + }]; + } + }, + sendChatRequest: async () => { throw new Error(); }, + provideTokenCount: async () => { throw new Error(); } + })); + + // Initial resolution + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModels(() => { + eventFired = true; + })); + + // Call selectLanguageModels again - provider will return different models + await languageModelsService.selectLanguageModels({ vendor: 'test-vendor' }); + + assert.strictEqual(eventFired, true, 'Should fire event when models change even without provider change event'); + }); +}); + +suite('LanguageModels - Vendor Change Events', function () { + + let languageModelsService: LanguageModelsService; + const disposables = new DisposableStore(); + + setup(function () { + languageModelsService = new LanguageModelsService( + new class extends mock() { + override activateByEvent(name: string) { + return Promise.resolve(); + } + }, + new NullLogService(), + new TestStorageService(), + new MockContextKeyService(), + new class extends mock() { + override onDidChangeLanguageModelGroups = Event.None; + override getLanguageModelsProviderGroups() { + return []; + } + }, + new class extends mock() { }, + new TestSecretStorageService(), + ); + }); + + teardown(function () { + languageModelsService.dispose(); + disposables.clear(); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('fires onDidChangeLanguageModelVendors when a vendor is added', async function () { + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'added-vendor', displayName: 'Added Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const vendors = await eventPromise; + assert.ok(vendors.includes('added-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when a vendor is removed', async function () { + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const eventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'removed-vendor', displayName: 'Removed Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const vendors = await eventPromise; + assert.ok(vendors.includes('removed-vendor')); + }); + + test('fires onDidChangeLanguageModelVendors when multiple vendors are added and removed', async function () { + // Add multiple vendors + const addEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined }, + { vendor: 'vendor-b', displayName: 'Vendor B', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + const addedVendors = await addEventPromise; + assert.ok(addedVendors.includes('vendor-a')); + assert.ok(addedVendors.includes('vendor-b')); + + // Remove one vendor + const removeEventPromise = new Promise((resolve) => { + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(vendors => resolve(vendors))); + }); + + languageModelsService.deltaLanguageModelChatProviderDescriptors([], [ + { vendor: 'vendor-a', displayName: 'Vendor A', configuration: undefined, managementCommand: undefined, when: undefined } + ]); + + const removedVendors = await removeEventPromise; + assert.ok(removedVendors.includes('vendor-a')); + }); + + test('does not fire onDidChangeLanguageModelVendors when no vendors are added or removed', async function () { + // Add initial vendor + languageModelsService.deltaLanguageModelChatProviderDescriptors([ + { vendor: 'stable-vendor', displayName: 'Stable Vendor', configuration: undefined, managementCommand: undefined, when: undefined } + ], []); + + // Listen for change event + let eventFired = false; + disposables.add(languageModelsService.onDidChangeLanguageModelVendors(() => { + eventFired = true; + })); + + // Call with empty arrays - should not fire event + languageModelsService.deltaLanguageModelChatProviderDescriptors([], []); + + assert.strictEqual(eventFired, false, 'Should not fire event when vendor list is unchanged'); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/languageModels.ts b/src/vs/workbench/contrib/chat/test/common/languageModels.ts index c7a54d6c2e6..35b2595109d 100644 --- a/src/vs/workbench/contrib/chat/test/common/languageModels.ts +++ b/src/vs/workbench/contrib/chat/test/common/languageModels.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; +import { IStringDictionary } from '../../../../../base/common/collections.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { IChatMessage, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatResponse, ILanguageModelChatSelector, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js'; +import { IChatMessage, ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatProvider, ILanguageModelChatResponse, ILanguageModelChatSelector, ILanguageModelProviderDescriptor, ILanguageModelsGroup, ILanguageModelsService, IUserFriendlyLanguageModel } from '../../common/languageModels.js'; +import { ILanguageModelsProviderGroup } from '../../common/languageModelsConfiguration.js'; export class NullLanguageModelsService implements ILanguageModelsService { _serviceBrand: undefined; @@ -16,13 +18,17 @@ export class NullLanguageModelsService implements ILanguageModelsService { return Disposable.None; } + deltaLanguageModelChatProviderDescriptors(added: IUserFriendlyLanguageModel[], removed: IUserFriendlyLanguageModel[]): void { + } + onDidChangeLanguageModels = Event.None; + onDidChangeLanguageModelVendors = Event.None; updateModelPickerPreference(modelIdentifier: string, showInModelPicker: boolean): void { return; } - getVendors(): IUserFriendlyLanguageModel[] { + getVendors(): ILanguageModelProviderDescriptor[] { return []; } @@ -34,6 +40,10 @@ export class NullLanguageModelsService implements ILanguageModelsService { return undefined; } + lookupLanguageModelByQualifiedName(qualifiedName: string) { + return undefined; + } + getLanguageModels(): ILanguageModelChatMetadataAndIdentifier[] { return []; } @@ -46,10 +56,15 @@ export class NullLanguageModelsService implements ILanguageModelsService { return; } + getLanguageModelGroups(vendor: string): ILanguageModelsGroup[] { + return []; + } + async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { return []; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } @@ -57,4 +72,17 @@ export class NullLanguageModelsService implements ILanguageModelsService { computeTokenLength(identifier: string, message: string | IChatMessage, token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + + async configureLanguageModelsProviderGroup(vendorId: string, name?: string): Promise { + + } + + async addLanguageModelsProviderGroup(name: string, vendorId: string, configuration: IStringDictionary | undefined): Promise { + + } + + async removeLanguageModelsProviderGroup(vendorId: string, providerGroupName: string): Promise { + } + + async migrateLanguageModelsProviderGroup(languageModelsProviderGroup: ILanguageModelsProviderGroup): Promise { } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts index f3ed5ac2c9e..53e66f2b5e0 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatModeService.ts @@ -8,11 +8,13 @@ import { Event } from '../../../../../base/common/event.js'; import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js'; export class MockChatModeService implements IChatModeService { - readonly _serviceBrand: undefined; + declare readonly _serviceBrand: undefined; public readonly onDidChangeChatModes = Event.None; - constructor(private readonly _modes: { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } = { builtin: [ChatMode.Ask], custom: [] }) { } + constructor( + private readonly _modes: { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } = { builtin: [ChatMode.Ask], custom: [] } + ) { } getModes(): { builtin: readonly IChatMode[]; custom: readonly IChatMode[] } { return this._modes; @@ -25,4 +27,5 @@ export class MockChatModeService implements IChatModeService { findModeByName(name: string): IChatMode | undefined { return this._modes.builtin.find(mode => mode.name.get() === name) ?? this._modes.custom.find(mode => mode.name.get() === name); } + } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts deleted file mode 100644 index 8bcc8ef7d32..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { observableValue } from '../../../../../base/common/observable.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, ISerializableChatData } from '../../common/chatModel.js'; -import { IParsedChatRequest } from '../../common/chatParserTypes.js'; -import { IChatCompleteResponse, IChatDetail, IChatProviderInfo, IChatSendRequestData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatTransferredSessionData, IChatUserActionEvent } from '../../common/chatService.js'; -import { ChatAgentLocation } from '../../common/constants.js'; - -export class MockChatService implements IChatService { - requestInProgressObs = observableValue('name', false); - edits2Enabled: boolean = false; - _serviceBrand: undefined; - editingSessions = []; - transferredSessionData: IChatTransferredSessionData | undefined; - readonly onDidSubmitRequest: Event<{ readonly chatSessionResource: URI }> = Event.None; - - private sessions = new ResourceMap(); - - isEnabled(location: ChatAgentLocation): boolean { - throw new Error('Method not implemented.'); - } - hasSessions(): boolean { - throw new Error('Method not implemented.'); - } - getProviderInfos(): IChatProviderInfo[] { - throw new Error('Method not implemented.'); - } - startSession(location: ChatAgentLocation, token: CancellationToken): ChatModel { - throw new Error('Method not implemented.'); - } - addSession(session: IChatModel): void { - this.sessions.set(session.sessionResource, session); - } - getSession(sessionResource: URI): IChatModel | undefined { - // eslint-disable-next-line local/code-no-dangerous-type-assertions - return this.sessions.get(sessionResource) ?? {} as IChatModel; - } - getSessionByLegacyId(sessionId: string): IChatModel | undefined { - return Array.from(this.sessions.values()).find(session => session.sessionId === sessionId); - } - async getOrRestoreSession(sessionResource: URI): Promise { - throw new Error('Method not implemented.'); - } - getPersistedSessionTitle(sessionResource: URI): string | undefined { - throw new Error('Method not implemented.'); - } - loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined { - throw new Error('Method not implemented.'); - } - loadSessionForResource(resource: URI, position: ChatAgentLocation, token: CancellationToken): Promise { - throw new Error('Method not implemented.'); - } - /** - * Returns whether the request was accepted. - */ - sendRequest(sessionResource: URI, message: string): Promise { - throw new Error('Method not implemented.'); - } - resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions | undefined): Promise { - throw new Error('Method not implemented.'); - } - adoptRequest(sessionResource: URI, request: IChatRequestModel): Promise { - throw new Error('Method not implemented.'); - } - removeRequest(sessionResource: URI, requestId: string): Promise { - throw new Error('Method not implemented.'); - } - cancelCurrentRequestForSession(sessionResource: URI): void { - throw new Error('Method not implemented.'); - } - clearSession(sessionResource: URI): Promise { - throw new Error('Method not implemented.'); - } - addCompleteRequest(sessionResource: URI, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void { - throw new Error('Method not implemented.'); - } - async getLocalSessionHistory(): Promise { - throw new Error('Method not implemented.'); - } - async clearAllHistoryEntries() { - throw new Error('Method not implemented.'); - } - async removeHistoryEntry(resource: URI) { - throw new Error('Method not implemented.'); - } - - readonly onDidPerformUserAction: Event = undefined!; - notifyUserAction(event: IChatUserActionEvent): void { - throw new Error('Method not implemented.'); - } - readonly onDidDisposeSession: Event<{ sessionResource: URI; reason: 'cleared' }> = undefined!; - - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { - throw new Error('Method not implemented.'); - } - - setChatSessionTitle(sessionResource: URI, title: string): void { - throw new Error('Method not implemented.'); - } - - isEditingLocation(location: ChatAgentLocation): boolean { - throw new Error('Method not implemented.'); - } - - getChatStorageFolder(): URI { - throw new Error('Method not implemented.'); - } - - logChatIndex(): void { - throw new Error('Method not implemented.'); - } - - isPersistedSessionEmpty(sessionResource: URI): boolean { - throw new Error('Method not implemented.'); - } - - activateDefaultAgent(location: ChatAgentLocation): Promise { - throw new Error('Method not implemented.'); - } - - getChatSessionFromInternalUri(sessionResource: URI): IChatSessionContext | undefined { - throw new Error('Method not implemented.'); - } -} diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts index fa3ad78161a..80e7de23580 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatSessionsService.ts @@ -4,18 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Emitter } from '../../../../../base/common/event.js'; +import { AsyncEmitter, Emitter } from '../../../../../base/common/event.js'; import { IDisposable } from '../../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../../base/common/map.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IEditableData } from '../../../../common/views.js'; -import { IChatAgentAttachmentCapabilities, IChatAgentRequest } from '../../common/chatAgents.js'; -import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionProviderOptionGroup, IChatSessionsExtensionPoint, IChatSessionsService, SessionOptionsChangedCallback } from '../../common/chatSessionsService.js'; +import { IChatAgentAttachmentCapabilities } from '../../common/participants/chatAgents.js'; +import { IChatModel } from '../../common/model/chatModel.js'; +import { IChatService } from '../../common/chatService/chatService.js'; +import { IChatSession, IChatSessionContentProvider, IChatSessionItem, IChatSessionItemProvider, IChatSessionOptionsWillNotifyExtensionEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js'; export class MockChatSessionsService implements IChatSessionsService { _serviceBrand: undefined; + private readonly _onDidChangeSessionOptions = new Emitter(); + readonly onDidChangeSessionOptions = this._onDidChangeSessionOptions.event; private readonly _onDidChangeItemsProviders = new Emitter(); readonly onDidChangeItemsProviders = this._onDidChangeItemsProviders.event; @@ -31,13 +34,19 @@ export class MockChatSessionsService implements IChatSessionsService { private readonly _onDidChangeContentProviderSchemes = new Emitter<{ readonly added: string[]; readonly removed: string[] }>(); readonly onDidChangeContentProviderSchemes = this._onDidChangeContentProviderSchemes.event; + private readonly _onDidChangeOptionGroups = new Emitter(); + readonly onDidChangeOptionGroups = this._onDidChangeOptionGroups.event; + + private readonly _onRequestNotifyExtension = new AsyncEmitter(); + readonly onRequestNotifyExtension = this._onRequestNotifyExtension.event; + private sessionItemProviders = new Map(); private contentProviders = new Map(); private contributions: IChatSessionsExtensionPoint[] = []; private optionGroups = new Map(); private sessionOptions = new ResourceMap>(); - private editableData = new ResourceMap(); private inProgress = new Map(); + private onChange = () => { }; // For testing: allow triggering events fireDidChangeItemsProviders(provider: IChatSessionItemProvider): void { @@ -69,16 +78,16 @@ export class MockChatSessionsService implements IChatSessionsService { return this.contributions; } - setContributions(contributions: IChatSessionsExtensionPoint[]): void { - this.contributions = contributions; + getChatSessionContribution(chatSessionType: string): IChatSessionsExtensionPoint | undefined { + return this.contributions.find(contrib => contrib.type === chatSessionType); } - async hasChatSessionItemProvider(chatSessionType: string): Promise { - return this.sessionItemProviders.has(chatSessionType); + setContributions(contributions: IChatSessionsExtensionPoint[]): void { + this.contributions = contributions; } - getAllChatSessionItemProviders(): IChatSessionItemProvider[] { - return Array.from(this.sessionItemProviders.values()); + async activateChatSessionItemProvider(chatSessionType: string): Promise { + return this.sessionItemProviders.get(chatSessionType); } getIconForSessionType(chatSessionType: string): ThemeIcon | URI | undefined { @@ -98,21 +107,13 @@ export class MockChatSessionsService implements IChatSessionsService { return this.contributions.find(c => c.type === chatSessionType)?.inputPlaceholder; } - async getNewChatSessionItem(chatSessionType: string, options: { request: IChatAgentRequest; metadata?: unknown }, token: CancellationToken): Promise { - const provider = this.sessionItemProviders.get(chatSessionType); - if (!provider?.provideNewChatSessionItem) { - throw new Error(`No provider for ${chatSessionType}`); - } - return provider.provideNewChatSessionItem(options, token); - } - - getAllChatSessionItems(token: CancellationToken): Promise> { - return Promise.all(Array.from(this.sessionItemProviders.values(), async provider => { - return { + getChatSessionItems(providersToResolve: readonly string[] | undefined, token: CancellationToken): Promise> { + return Promise.all(Array.from(this.sessionItemProviders.values()) + .filter(provider => !providersToResolve || providersToResolve.includes(provider.chatSessionType)) + .map(async provider => ({ chatSessionType: provider.chatSessionType, items: await provider.provideChatSessionItems(token), - }; - })); + }))); } reportInProgress(chatSessionType: string, count: number): void { @@ -162,30 +163,8 @@ export class MockChatSessionsService implements IChatSessionsService { } } - private optionsChangeCallback?: SessionOptionsChangedCallback; - - setOptionsChangeCallback(callback: SessionOptionsChangedCallback): void { - this.optionsChangeCallback = callback; - } - - async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string }>): Promise { - await this.optionsChangeCallback?.(sessionResource, updates); - } - - async setEditableSession(sessionResource: URI, data: IEditableData | null): Promise { - if (data) { - this.editableData.set(sessionResource, data); - } else { - this.editableData.delete(sessionResource); - } - } - - getEditableData(sessionResource: URI): IEditableData | undefined { - return this.editableData.get(sessionResource); - } - - isEditable(sessionResource: URI): boolean { - return this.editableData.has(sessionResource); + async notifySessionOptionsChange(sessionResource: URI, updates: ReadonlyArray<{ optionId: string; value: string | IChatSessionProviderOptionItem }>): Promise { + await this._onRequestNotifyExtension.fireAsync({ sessionResource, updates }, CancellationToken.None); } notifySessionItemsChanged(chatSessionType: string): void { @@ -212,7 +191,31 @@ export class MockChatSessionsService implements IChatSessionsService { return this.contributions.find(c => c.type === chatSessionType)?.capabilities; } + getCustomAgentTargetForSessionType(chatSessionType: string): string | undefined { + return this.contributions.find(c => c.type === chatSessionType)?.customAgentTarget; + } + getContentProviderSchemes(): string[] { return Array.from(this.contentProviders.keys()); } + + getInProgressSessionDescription(chatModel: IChatModel): string | undefined { + return undefined; + } + + registerChatModelChangeListeners(chatService: IChatService, chatSessionType: string, onChange: () => void): IDisposable { + // Store the emitter so tests can trigger it + this.onChange = onChange; + return { + dispose: () => { + } + }; + } + + // Helper method for tests to trigger progress events + triggerProgressEvent(): void { + if (this.onChange) { + this.onChange(); + } + } } diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index b075db33b8c..7e8d3e487a2 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,8 +5,8 @@ import { ResourceMap } from '../../../../../base/common/map.js'; import { URI } from '../../../../../base/common/uri.js'; -import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; -import { IToolAndToolSetEnablementMap } from '../../common/languageModelToolsService.js'; +import { IChatVariablesService, IDynamicVariable } from '../../common/attachments/chatVariables.js'; +import { IToolAndToolSetEnablementMap } from '../../common/tools/languageModelToolsService.js'; export class MockChatVariablesService implements IChatVariablesService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsConfirmationService.ts deleted file mode 100644 index 581eb1386ed..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsConfirmationService.ts +++ /dev/null @@ -1,34 +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 { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { ConfirmedReason } from '../../common/chatService.js'; -import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationRef, ILanguageModelToolsConfirmationService } from '../../common/languageModelToolsConfirmationService.js'; -import { IToolData } from '../../common/languageModelToolsService.js'; - -export class MockLanguageModelToolsConfirmationService implements ILanguageModelToolsConfirmationService { - manageConfirmationPreferences(tools: Readonly[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void { - throw new Error('Method not implemented.'); - } - registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable { - throw new Error('Method not implemented.'); - } - resetToolAutoConfirmation(): void { - - } - getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { - return undefined; - } - getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { - return undefined; - } - getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { - return []; - } - getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { - return []; - } - declare readonly _serviceBrand: undefined; -} diff --git a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts deleted file mode 100644 index 6de4a3ac35c..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/mockLanguageModelToolsService.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { constObservable, IObservable } from '../../../../../base/common/observable.js'; -import { IProgressStep } from '../../../../../platform/progress/common/progress.js'; -import { IVariableReference } from '../../common/chatModes.js'; -import { ChatRequestToolReferenceEntry } from '../../common/chatVariableEntries.js'; -import { CountTokensCallback, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolSet } from '../../common/languageModelToolsService.js'; - -export class MockLanguageModelToolsService implements ILanguageModelToolsService { - _serviceBrand: undefined; - - constructor() { } - - readonly onDidChangeTools: Event = Event.None; - readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionId: string; toolData: IToolData }> = Event.None; - - registerToolData(toolData: IToolData): IDisposable { - return Disposable.None; - } - - resetToolAutoConfirmation(): void { - - } - - getToolPostExecutionAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never' { - return 'never'; - } - - resetToolPostExecutionAutoConfirmation(): void { - - } - - flushToolUpdates(): void { - - } - - cancelToolCallsForRequest(requestId: string): void { - - } - - setToolAutoConfirmation(toolId: string, scope: any): void { - - } - - getToolAutoConfirmation(toolId: string): 'never' { - return 'never'; - } - - registerToolImplementation(name: string, tool: IToolImpl): IDisposable { - return Disposable.None; - } - - registerTool(toolData: IToolData, tool: IToolImpl): IDisposable { - return Disposable.None; - } - - getTools(): Iterable> { - return []; - } - - getTool(id: string): IToolData | undefined { - return undefined; - } - - getToolByName(name: string, includeDisabled?: boolean): IToolData | undefined { - return undefined; - } - - acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { - - } - - async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { - return { - content: [{ kind: 'text', value: 'result' }] - }; - } - - toolSets: IObservable = constObservable([]); - - getToolSetByName(name: string): ToolSet | undefined { - return undefined; - } - - getToolSet(id: string): ToolSet | undefined { - return undefined; - } - - createToolSet(): ToolSet & IDisposable { - throw new Error('Method not implemented.'); - } - - toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[]): IToolAndToolSetEnablementMap { - throw new Error('Method not implemented.'); - } - - 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.'); - } - - mapGithubToolName(githubToolName: string): string { - 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 deleted file mode 100644 index 78cc658a8ff..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/mockPromptsService.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 { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js'; -import { PromptsType } from '../../common/promptSyntax/promptTypes.js'; -import { ParsedPromptFile } from '../../common/promptSyntax/promptFileParser.js'; -import { ICustomAgent, IPromptPath, IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js'; -import { ResourceSet } from '../../../../../base/common/map.js'; - -export class MockPromptsService implements IPromptsService { - - _serviceBrand: undefined; - - private readonly _onDidChangeCustomChatModes = new Emitter(); - readonly onDidChangeCustomAgents = this._onDidChangeCustomChatModes.event; - - private _customModes: ICustomAgent[] = []; - - setCustomModes(modes: ICustomAgent[]): void { - this._customModes = modes; - this._onDidChangeCustomChatModes.fire(); - } - - async getCustomAgents(token: CancellationToken): Promise { - return this._customModes; - } - - // Stub implementations for required interface methods - getSyntaxParserFor(_model: any): any { throw new Error('Not implemented'); } - listPromptFiles(_type: any): Promise { throw new Error('Not implemented'); } - listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { throw new Error('Not implemented'); } - getSourceFolders(_type: any): readonly any[] { throw new Error('Not implemented'); } - asPromptSlashCommand(_command: string): any { return undefined; } - resolvePromptSlashCommand(_data: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } - resolvePromptSlashCommandFromCache(command: string): ParsedPromptFile | undefined { throw new Error('Not implemented'); } - get onDidChangeParsedPromptFilesCache(): Event { throw new Error('Not implemented'); } - findPromptSlashCommands(): Promise { throw new Error('Not implemented'); } - getPromptCommandName(uri: URI): 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'); } - registerContributedFile(type: PromptsType, name: string, description: string, uri: URI, extension: IExtensionDescription): IDisposable { throw new Error('Not implemented'); } - getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } - findAgentMDsInWorkspace(token: CancellationToken): Promise { throw new Error('Not implemented'); } - listAgentMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } - listCopilotInstructionsMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } - getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } - getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } - setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } - dispose(): void { } -} diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap b/src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_inline_reference.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_inline_reference.0.snap rename to src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_inline_reference.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_mergeable_markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_mergeable_markdown.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_mergeable_markdown.0.snap rename to src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_mergeable_markdown.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_not_mergeable_markdown.0.snap b/src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_not_mergeable_markdown.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Response_not_mergeable_markdown.0.snap rename to src/vs/workbench/contrib/chat/test/common/model/__snapshots__/Response_not_mergeable_markdown.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts new file mode 100644 index 00000000000..ee07a7c00d4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModel.test.ts @@ -0,0 +1,740 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from 'sinon'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { Range } from '../../../../../../editor/common/core/range.js'; +import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; +import { TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { CellUri } from '../../../../notebook/common/notebookCommon.js'; +import { IChatRequestImplicitVariableEntry, IChatRequestStringVariableEntry, IChatRequestFileEntry, StringChatContextValue } from '../../../common/attachments/chatVariableEntries.js'; +import { ChatAgentService, IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ChatModel, IExportableChatData, ISerializableChatData1, ISerializableChatData2, ISerializableChatData3, isExportableSessionData, isSerializableSessionData, normalizeSerializableChatData, Response } from '../../../common/model/chatModel.js'; +import { ChatRequestTextPart } from '../../../common/requestParser/chatParserTypes.js'; +import { IChatService, IChatToolInvocation } from '../../../common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; +import { MockChatService } from '../chatService/mockChatService.js'; + +suite('ChatModel', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + setup(async () => { + instantiationService = testDisposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IChatService, new MockChatService()); + }); + + test('initialization with exported data only (imported)', async () => { + const exportedData: IExportableChatData = { + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: exportedData, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + assert.strictEqual(model.isImported, true); + assert.ok(model.sessionId); // Should have generated ID + assert.ok(model.timestamp > 0); // Should have generated timestamp + }); + + test('initialization with full serializable data (not imported)', async () => { + const now = Date.now(); + const serializableData: ISerializableChatData3 = { + version: 3, + sessionId: 'existing-session', + creationDate: now - 1000, + customTitle: 'My Chat', + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: serializableData, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + assert.strictEqual(model.isImported, false); + assert.strictEqual(model.sessionId, 'existing-session'); + assert.strictEqual(model.timestamp, now - 1000); + assert.strictEqual(model.customTitle, 'My Chat'); + }); + + test('initialization with invalid data', async () => { + const invalidData = { + // Missing required fields + requests: 'not-an-array' + } as unknown as IExportableChatData; + + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + { value: invalidData, serializer: undefined! }, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + // Should handle gracefully with empty state + assert.strictEqual(model.getRequests().length, 0); + assert.ok(model.sessionId); // Should have generated ID + }); + + test('initialization without data', async () => { + const model = testDisposables.add(instantiationService.createInstance( + ChatModel, + undefined, + { initialLocation: ChatAgentLocation.Chat, canUseTools: true } + )); + + assert.strictEqual(model.isImported, false); + assert.strictEqual(model.getRequests().length, 0); + assert.ok(model.sessionId); + assert.ok(model.timestamp > 0); + }); + + test('removeRequest', async () => { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + + const text = 'hello'; + model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + const requests = model.getRequests(); + assert.strictEqual(requests.length, 1); + + model.removeRequest(requests[0].id); + assert.strictEqual(model.getRequests().length, 0); + }); + + test('adoptRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.EditorInline, canUseTools: true })); + const model2 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + + assert.strictEqual(model1.getRequests().length, 1); + assert.strictEqual(model2.getRequests().length, 0); + assert.ok(request1.session === model1); + assert.ok(request1.response?.session === model1); + + model2.adoptRequest(request1); + + assert.strictEqual(model1.getRequests().length, 0); + assert.strictEqual(model2.getRequests().length, 1); + assert.ok(request1.session === model2); + assert.ok(request1.response?.session === model2); + + model2.acceptResponseProgress(request1, { content: new MarkdownString('Hello'), kind: 'markdownContent' }); + + assert.strictEqual(request1.response.response.toString(), 'Hello'); + }); + + test('addCompleteRequest', async function () { + const model1 = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + + const text = 'hello'; + const request1 = model1.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0, undefined, undefined, undefined, undefined, undefined, undefined, true); + + assert.strictEqual(request1.isCompleteAddedRequest, true); + assert.strictEqual(request1.response!.isCompleteAddedRequest, true); + assert.strictEqual(request1.shouldBeRemovedOnSend, undefined); + assert.strictEqual(request1.response!.shouldBeRemovedOnSend, undefined); + }); + + test('inputModel.toJSON filters extension-contributed contexts', async function () { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + + const fileAttachment: IChatRequestFileEntry = { + kind: 'file', + value: URI.parse('file:///test.ts'), + id: 'file-id', + name: 'test.ts', + }; + + const stringContextValue: StringChatContextValue = { + value: 'pr-content', + name: 'PR #123', + icon: Codicon.gitPullRequest, + uri: URI.parse('pr://123'), + handle: 1 + }; + + const stringAttachment: IChatRequestStringVariableEntry = { + kind: 'string', + value: 'pr-content', + id: 'string-id', + name: 'PR #123', + icon: Codicon.gitPullRequest, + uri: URI.parse('pr://123'), + handle: 1 + }; + + const implicitWithStringContext: IChatRequestImplicitVariableEntry = { + kind: 'implicit', + isFile: true, + value: stringContextValue, + uri: URI.parse('pr://123'), + isSelection: false, + enabled: true, + id: 'implicit-string-id', + name: 'PR Context', + }; + + const implicitWithUri: IChatRequestImplicitVariableEntry = { + kind: 'implicit', + isFile: true, + value: URI.parse('file:///current.ts'), + uri: URI.parse('file:///current.ts'), + isSelection: false, + enabled: true, + id: 'implicit-uri-id', + name: 'current.ts', + }; + + model.inputModel.setState({ + attachments: [fileAttachment, stringAttachment, implicitWithStringContext, implicitWithUri], + inputText: 'test' + }); + + const serialized = model.inputModel.toJSON(); + assert.ok(serialized); + + // Should filter out string attachments and implicit attachments with StringChatContextValue + // Should keep file attachments and implicit attachments with URI values + assert.deepStrictEqual(serialized.attachments, [fileAttachment, implicitWithUri]); + }); +}); + +suite('Response', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('mergeable markdown', async () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('markdown1'), kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); + await assertSnapshot(response.value); + + assert.strictEqual(response.toString(), 'markdown1markdown2'); + }); + + test('not mergeable markdown', async () => { + const response = store.add(new Response([])); + const md1 = new MarkdownString('markdown1'); + md1.supportHtml = true; + response.updateContent({ content: md1, kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('markdown2'), kind: 'markdownContent' }); + await assertSnapshot(response.value); + }); + + test('inline reference', async () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('text before '), kind: 'markdownContent' }); + response.updateContent({ inlineReference: URI.parse('https://microsoft.com/'), kind: 'inlineReference' }); + response.updateContent({ content: new MarkdownString(' text after'), kind: 'markdownContent' }); + await assertSnapshot(response.value); + + assert.strictEqual(response.toString(), 'text before https://microsoft.com/ text after'); + + }); + + test('consolidated edit summary', () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('Some content before edits'), kind: 'markdownContent' }); + response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true }); + response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true }); + response.updateContent({ content: new MarkdownString('Some content after edits'), kind: 'markdownContent' }); + + // Should have single "Made changes." at the end instead of multiple entries + const responseString = response.toString(); + const madeChangesCount = (responseString.match(/Made changes\./g) || []).length; + assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message'); + assert.ok(responseString.includes('Some content before edits'), 'Should include content before edits'); + assert.ok(responseString.includes('Some content after edits'), 'Should include content after edits'); + assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."'); + }); + + test('no edit summary when no edits', () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('Some content'), kind: 'markdownContent' }); + response.updateContent({ content: new MarkdownString('More content'), kind: 'markdownContent' }); + + // Should not have "Made changes." when there are no edit groups + const responseString = response.toString(); + assert.ok(!responseString.includes('Made changes.'), 'Should not include "Made changes." when no edits present'); + assert.strictEqual(responseString, 'Some contentMore content'); + }); + + test('consolidated edit summary with clear operation', () => { + const response = store.add(new Response([])); + response.updateContent({ content: new MarkdownString('Initial content'), kind: 'markdownContent' }); + response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file1.ts'), edits: [], state: undefined, done: true }); + response.updateContent({ kind: 'clearToPreviousToolInvocation', reason: 1 }); + response.updateContent({ content: new MarkdownString('Content after clear'), kind: 'markdownContent' }); + response.updateContent({ kind: 'textEditGroup', uri: URI.parse('file:///file2.ts'), edits: [], state: undefined, done: true }); + + // Should only show "Made changes." for edits after the clear operation + const responseString = response.toString(); + const madeChangesCount = (responseString.match(/Made changes\./g) || []).length; + assert.strictEqual(madeChangesCount, 1, 'Should have exactly one "Made changes." message after clear'); + assert.ok(responseString.includes('Content after clear'), 'Should include content after clear'); + assert.ok(!responseString.includes('Initial content'), 'Should not include content before clear'); + assert.ok(responseString.endsWith('Made changes.'), 'Should end with "Made changes."'); + }); + + test('textEdit merges edits for same URI when not done', () => { + const response = store.add(new Response([])); + const uri = URI.parse('file:///file1.ts'); + + response.updateContent({ + kind: 'textEdit', + uri, + edits: [{ range: new Range(1, 1, 1, 1), text: 'edit1' }], + done: false, + isExternalEdit: true + }); + + response.updateContent({ + kind: 'textEdit', + uri, + edits: [{ range: new Range(2, 1, 2, 1), text: 'edit2' }], + done: true + }); + + const textEditGroups = response.value.filter(p => p.kind === 'textEditGroup'); + assert.strictEqual(textEditGroups.length, 1, 'Should have exactly one textEditGroup'); + assert.strictEqual(textEditGroups[0].edits.length, 2, 'Should have two edit batches merged'); + assert.strictEqual(textEditGroups[0].done, true, 'Should be marked as done after final edit'); + assert.strictEqual(textEditGroups[0].isExternalEdit, true, 'Should preserve isExternalEdit flag from first edit'); + }); + + test('textEdit does not merge edits when previous is done', () => { + const response = store.add(new Response([])); + const uri = URI.parse('file:///file1.ts'); + + response.updateContent({ + kind: 'textEdit', + uri, + edits: [{ range: new Range(1, 1, 1, 1), text: 'edit1' }], + done: true + }); + + response.updateContent({ + kind: 'textEdit', + uri, + edits: [{ range: new Range(2, 1, 2, 1), text: 'edit2' }], + done: true + }); + + const textEditGroups = response.value.filter(p => p.kind === 'textEditGroup'); + assert.strictEqual(textEditGroups.length, 2, 'Should have two separate textEditGroups'); + }); + + test('textEdit does not merge edits for different URIs', () => { + const response = store.add(new Response([])); + + response.updateContent({ + kind: 'textEdit', + uri: URI.parse('file:///file1.ts'), + edits: [{ range: new Range(1, 1, 1, 1), text: 'edit1' }], + done: false + }); + + response.updateContent({ + kind: 'textEdit', + uri: URI.parse('file:///file2.ts'), + edits: [{ range: new Range(1, 1, 1, 1), text: 'edit2' }], + done: true + }); + + const textEditGroups = response.value.filter(p => p.kind === 'textEditGroup'); + assert.strictEqual(textEditGroups.length, 2, 'Should have two separate textEditGroups for different URIs'); + }); + + test('notebookEdit merges edits for same notebook URI when not done', () => { + const response = store.add(new Response([])); + const notebookUri = URI.parse('file:///notebook.ipynb'); + + response.updateContent({ + kind: 'notebookEdit', + uri: notebookUri, + edits: [{ editType: 1 /* CellEditType.Replace */, index: 0, count: 0, cells: [] }], + done: false, + isExternalEdit: true + }); + + response.updateContent({ + kind: 'notebookEdit', + uri: notebookUri, + edits: [{ editType: 1 /* CellEditType.Replace */, index: 1, count: 0, cells: [] }], + done: true + }); + + const notebookEditGroups = response.value.filter(p => p.kind === 'notebookEditGroup'); + assert.strictEqual(notebookEditGroups.length, 1, 'Should have exactly one notebookEditGroup'); + assert.strictEqual(notebookEditGroups[0].edits.length, 2, 'Should have two edit batches merged'); + assert.strictEqual(notebookEditGroups[0].done, true, 'Should be marked as done after final edit'); + assert.strictEqual(notebookEditGroups[0].isExternalEdit, true, 'Should preserve isExternalEdit flag from first edit'); + }); + + test('notebookEdit does not merge edits when previous is done', () => { + const response = store.add(new Response([])); + const notebookUri = URI.parse('file:///notebook.ipynb'); + + response.updateContent({ + kind: 'notebookEdit', + uri: notebookUri, + edits: [{ editType: 1 /* CellEditType.Replace */, index: 0, count: 0, cells: [] }], + done: true + }); + + response.updateContent({ + kind: 'notebookEdit', + uri: notebookUri, + edits: [{ editType: 1 /* CellEditType.Replace */, index: 1, count: 0, cells: [] }], + done: true + }); + + const notebookEditGroups = response.value.filter(p => p.kind === 'notebookEditGroup'); + assert.strictEqual(notebookEditGroups.length, 2, 'Should have two separate notebookEditGroups'); + }); + + test('notebookEdit does not merge edits for different notebook URIs', () => { + const response = store.add(new Response([])); + + response.updateContent({ + kind: 'notebookEdit', + uri: URI.parse('file:///notebook1.ipynb'), + edits: [{ editType: 1 /* CellEditType.Replace */, index: 0, count: 0, cells: [] }], + done: false + }); + + response.updateContent({ + kind: 'notebookEdit', + uri: URI.parse('file:///notebook2.ipynb'), + edits: [{ editType: 1 /* CellEditType.Replace */, index: 0, count: 0, cells: [] }], + done: true + }); + + const notebookEditGroups = response.value.filter(p => p.kind === 'notebookEditGroup'); + assert.strictEqual(notebookEditGroups.length, 2, 'Should have two separate notebookEditGroups for different URIs'); + }); + + test('textEdit to notebook cell creates notebookEditGroup', () => { + const response = store.add(new Response([])); + const notebookUri = URI.parse('file:///notebook.ipynb'); + const cellUri = CellUri.generate(notebookUri, 1); + + response.updateContent({ + kind: 'textEdit', + uri: cellUri, + edits: [{ range: new Range(1, 1, 1, 1), text: 'edit1' }], + done: true + }); + + const textEditGroups = response.value.filter(p => p.kind === 'textEditGroup'); + const notebookEditGroups = response.value.filter(p => p.kind === 'notebookEditGroup'); + assert.strictEqual(textEditGroups.length, 0, 'Should not have textEditGroup for cell edits'); + assert.strictEqual(notebookEditGroups.length, 1, 'Should have notebookEditGroup for cell edits'); + }); +}); + +suite('normalizeSerializableChatData', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('v1', () => { + const v1Data: ISerializableChatData1 = { + creationDate: Date.now(), + initialLocation: undefined, + requests: [], + responderUsername: 'bot', + sessionId: 'session1', + }; + + const newData = normalizeSerializableChatData(v1Data); + assert.strictEqual(newData.creationDate, v1Data.creationDate); + assert.strictEqual(newData.version, 3); + }); + + test('v2', () => { + const v2Data: ISerializableChatData2 = { + version: 2, + creationDate: 100, + initialLocation: undefined, + requests: [], + responderUsername: 'bot', + sessionId: 'session1', + computedTitle: 'computed title' + }; + + const newData = normalizeSerializableChatData(v2Data); + assert.strictEqual(newData.version, 3); + assert.strictEqual(newData.creationDate, v2Data.creationDate); + assert.strictEqual(newData.customTitle, v2Data.computedTitle); + }); + + test('old bad data', () => { + const v1Data: ISerializableChatData1 = { + // Testing the scenario where these are missing + sessionId: undefined!, + creationDate: undefined!, + + initialLocation: undefined, + requests: [], + responderUsername: 'bot', + }; + + const newData = normalizeSerializableChatData(v1Data); + assert.strictEqual(newData.version, 3); + assert.ok(newData.creationDate > 0); + assert.ok(newData.sessionId); + }); + + test('v3 with bug', () => { + const v3Data: ISerializableChatData3 = { + // Test case where old data was wrongly normalized and these fields were missing + creationDate: undefined!, + + version: 3, + initialLocation: undefined, + requests: [], + responderUsername: 'bot', + sessionId: 'session1', + customTitle: 'computed title' + }; + + const newData = normalizeSerializableChatData(v3Data); + assert.strictEqual(newData.version, 3); + assert.ok(newData.creationDate > 0); + assert.ok(newData.sessionId); + }); +}); + +suite('isExportableSessionData', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('valid exportable data', () => { + const validData: IExportableChatData = { + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + assert.strictEqual(isExportableSessionData(validData), true); + }); + + test('invalid - missing requests', () => { + const invalidData = { + initialLocation: ChatAgentLocation.Chat, + responderUsername: 'bot', + }; + + assert.strictEqual(isExportableSessionData(invalidData), false); + }); + + test('invalid - requests not array', () => { + const invalidData = { + initialLocation: ChatAgentLocation.Chat, + requests: 'not-an-array', + responderUsername: 'bot', + }; + + assert.strictEqual(isExportableSessionData(invalidData), false); + }); + + test('invalid - missing responderUsername', () => { + const invalidData = { + initialLocation: ChatAgentLocation.Chat, + requests: [], + }; + + assert.strictEqual(isExportableSessionData(invalidData), false); + }); + + test('invalid - responderUsername not string', () => { + const invalidData = { + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 123, + }; + + assert.strictEqual(isExportableSessionData(invalidData), false); + }); + + test('invalid - null', () => { + assert.strictEqual(isExportableSessionData(null), false); + }); + + test('invalid - undefined', () => { + assert.strictEqual(isExportableSessionData(undefined), false); + }); +}); + +suite('isSerializableSessionData', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('valid serializable data', () => { + const validData: ISerializableChatData3 = { + version: 3, + sessionId: 'session1', + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + assert.strictEqual(isSerializableSessionData(validData), true); + }); + + test('valid - with usedContext', () => { + const validData: ISerializableChatData3 = { + version: 3, + sessionId: 'session1', + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: [{ + requestId: 'req1', + message: 'test', + variableData: { variables: [] }, + response: undefined, + usedContext: { documents: [], kind: 'usedContext' } + }], + responderUsername: 'bot', + }; + + assert.strictEqual(isSerializableSessionData(validData), true); + }); + + test('invalid - missing sessionId', () => { + const invalidData = { + version: 3, + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + assert.strictEqual(isSerializableSessionData(invalidData), false); + }); + + test('invalid - missing creationDate', () => { + const invalidData = { + version: 3, + sessionId: 'session1', + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: [], + responderUsername: 'bot', + }; + + assert.strictEqual(isSerializableSessionData(invalidData), false); + }); + + test('invalid - not exportable', () => { + const invalidData = { + version: 3, + sessionId: 'session1', + creationDate: Date.now(), + customTitle: undefined, + initialLocation: ChatAgentLocation.Chat, + requests: 'not-an-array', + responderUsername: 'bot', + }; + + assert.strictEqual(isSerializableSessionData(invalidData), false); + }); +}); + +suite('ChatResponseModel', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + setup(async () => { + instantiationService = testDisposables.add(new TestInstantiationService()); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, new NullLogService()); + instantiationService.stub(IExtensionService, new TestExtensionService()); + instantiationService.stub(IContextKeyService, new MockContextKeyService()); + instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService))); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + instantiationService.stub(IChatService, new MockChatService()); + }); + + test('timestamp and confirmationAdjustedTimestamp', async () => { + const clock = sinon.useFakeTimers(); + try { + const model = testDisposables.add(instantiationService.createInstance(ChatModel, undefined, { initialLocation: ChatAgentLocation.Chat, canUseTools: true })); + const start = Date.now(); + + const text = 'hello'; + const request = model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] }, { variables: [] }, 0); + const response = request.response!; + + assert.strictEqual(response.timestamp, start); + assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start); + + // Advance time, no pending confirmation + clock.tick(1000); + assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start); + + // Add pending confirmation via tool invocation + const toolState = observableValue('state', { type: 1 /* IChatToolInvocation.StateKind.WaitingForConfirmation */, confirmationMessages: { title: 'Please confirm' } }); + const toolInvocation = { + kind: 'toolInvocation', + invocationMessage: 'calling tool', + state: toolState + } as Partial as IChatToolInvocation; + + model.acceptResponseProgress(request, toolInvocation); + + // Advance time while pending + clock.tick(2000); + // Timestamp should still be start (it includes the wait time while waiting) + assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start); + + // Resolve confirmation + toolState.set({ type: 4 /* IChatToolInvocation.StateKind.Completed */ }, undefined); + + // Now adjusted timestamp should reflect the wait time + // The wait time was 2000ms. + // confirmationAdjustedTimestamp = start + waitTime = start + 2000 + assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start + 2000); + + // Advance time again + clock.tick(1000); + assert.strictEqual(response.confirmationAdjustedTimestamp.get(), start + 2000); + + } finally { + clock.restore(); + } + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts new file mode 100644 index 00000000000..19bebd6283a --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/chatModelStore.test.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { DeferredPromise } from '../../../../../../base/common/async.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import { ChatModel } from '../../../common/model/chatModel.js'; +import { ChatModelStore, IStartSessionProps } from '../../../common/model/chatModelStore.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; +import { MockChatModel } from './mockChatModel.js'; + +suite('ChatModelStore', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + let testObject: ChatModelStore; + let createdModels: MockChatModel[]; + let willDisposePromises: DeferredPromise[]; + + setup(() => { + createdModels = []; + willDisposePromises = []; + testObject = store.add(new ChatModelStore({ + createModel: (props: IStartSessionProps) => { + const model = new MockChatModel(props.sessionResource); + createdModels.push(model); + return model as unknown as ChatModel; + }, + willDisposeModel: async (model: ChatModel) => { + const p = new DeferredPromise(); + willDisposePromises.push(p); + await p.p; + } + }, new NullLogService())); + }); + + test('create and dispose', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref = testObject.acquireOrCreate(props); + assert.strictEqual(createdModels.length, 1); + assert.strictEqual(ref.object, createdModels[0]); + + ref.dispose(); + assert.strictEqual(willDisposePromises.length, 1); + + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + assert.strictEqual(testObject.get(uri), undefined); + }); + + test('resurrection', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref1 = testObject.acquireOrCreate(props); + const model1 = ref1.object; + ref1.dispose(); + + // Model is pending disposal + assert.strictEqual(willDisposePromises.length, 1); + assert.strictEqual(testObject.get(uri), model1); + + // Acquire again - should be resurrected + const ref2 = testObject.acquireOrCreate(props); + assert.strictEqual(ref2.object, model1); + assert.strictEqual(createdModels.length, 1); + + // Finish disposal of the first ref + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + + // Model should still exist because ref2 holds it + assert.strictEqual(testObject.get(uri), model1); + + ref2.dispose(); + }); + + test('get and has', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref = testObject.acquireOrCreate(props); + assert.strictEqual(testObject.get(uri), ref.object); + assert.strictEqual(testObject.has(uri), true); + + ref.dispose(); + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + + assert.strictEqual(testObject.get(uri), undefined); + assert.strictEqual(testObject.has(uri), false); + }); + + test('acquireExisting', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + assert.strictEqual(testObject.acquireExisting(uri), undefined); + + const ref1 = testObject.acquireOrCreate(props); + const ref2 = testObject.acquireExisting(uri); + assert.ok(ref2); + assert.strictEqual(ref2.object, ref1.object); + + ref1.dispose(); + ref2.dispose(); + willDisposePromises[0].complete(); + await testObject.waitForModelDisposals(); + }); + + test('values', async () => { + const uri1 = URI.parse('test://session1'); + const uri2 = URI.parse('test://session2'); + const props1: IStartSessionProps = { + sessionResource: uri1, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + const props2: IStartSessionProps = { + sessionResource: uri2, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref1 = testObject.acquireOrCreate(props1); + const ref2 = testObject.acquireOrCreate(props2); + + const values = Array.from(testObject.values()); + assert.strictEqual(values.length, 2); + assert.ok(values.includes(ref1.object)); + assert.ok(values.includes(ref2.object)); + + ref1.dispose(); + ref2.dispose(); + willDisposePromises[0].complete(); + willDisposePromises[1].complete(); + await testObject.waitForModelDisposals(); + }); + + test('dispose store', async () => { + const uri = URI.parse('test://session'); + const props: IStartSessionProps = { + sessionResource: uri, + location: ChatAgentLocation.Chat, + canUseTools: true + }; + + const ref = testObject.acquireOrCreate(props); + const model = ref.object as unknown as MockChatModel; + testObject.dispose(); + + assert.strictEqual(model.isDisposed, true); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatSessionOperationLog.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatSessionOperationLog.test.ts new file mode 100644 index 00000000000..28e0e98d1d0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/chatSessionOperationLog.test.ts @@ -0,0 +1,579 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { VSBuffer } from '../../../../../../base/common/buffer.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import * as Adapt from '../../../common/model/objectMutationLog.js'; +import { equals } from '../../../../../../base/common/objects.js'; + +suite('ChatSessionOperationLog', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + // Test data types + interface TestItem { + id: string; + value: number; + } + + interface TestObject { + name: string; + count?: number; + items: TestItem[]; + metadata?: { tags: string[] }; + } + + // Helper to create a simple schema for testing + function createTestSchema() { + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.value()), + }); + + return Adapt.object({ + name: Adapt.t(o => o.name, Adapt.value()), + count: Adapt.t(o => o.count, Adapt.value()), + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + metadata: Adapt.v(o => o.metadata, equals), + }); + } + + // Helper to simulate file operations + function simulateFileRoundtrip(adapter: Adapt.ObjectMutationLog, initial: TestObject, updates: TestObject[]): TestObject { + let fileContent = adapter.createInitial(initial); + + for (const update of updates) { + const result = adapter.write(update); + if (result.op === 'replace') { + fileContent = result.data; + } else { + fileContent = VSBuffer.concat([fileContent, result.data]); + } + } + + // Create new adapter and read back + const reader = new Adapt.ObjectMutationLog(createTestSchema()); + return reader.read(fileContent); + } + + suite('Transform factories', () => { + test('key uses strict equality by default', () => { + const transform = Adapt.key(); + assert.strictEqual(transform.equals('a', 'a'), true); + assert.strictEqual(transform.equals('a', 'b'), false); + }); + + test('key uses custom comparator', () => { + const transform = Adapt.key<{ id: number }>((a, b) => a.id === b.id); + assert.strictEqual(transform.equals({ id: 1 }, { id: 1 }), true); + assert.strictEqual(transform.equals({ id: 1 }, { id: 2 }), false); + }); + + test('primitive uses strict equality', () => { + const transform = Adapt.value(); + assert.strictEqual(transform.equals(1, 1), true); + assert.strictEqual(transform.equals(1, 2), false); + }); + + test('primitive with custom comparator', () => { + const transform = Adapt.value((a, b) => a.toLowerCase() === b.toLowerCase()); + assert.strictEqual(transform.equals('ABC', 'abc'), true); + assert.strictEqual(transform.equals('ABC', 'def'), false); + }); + + test('object extracts and compares properties', () => { + const schema = Adapt.object<{ x: number; y: string }, { x: number; y: string }>({ + x: Adapt.t(o => o.x, Adapt.value()), + y: Adapt.t(o => o.y, Adapt.value()), + }); + + const extracted = schema.extract({ x: 1, y: 'test' }); + assert.strictEqual(extracted.x, 1); + assert.strictEqual(extracted.y, 'test'); + }); + + test('t composes getter with transform', () => { + const transform = Adapt.t( + (obj: { nested: { value: number } }) => obj.nested.value, + Adapt.value() + ); + + assert.strictEqual(transform.extract({ nested: { value: 42 } }), 42); + }); + + test('differentiated uses separate extract and equals functions', () => { + const transform = Adapt.v<{ type: string; data: number }, string>( + obj => `${obj.type}:${obj.data}`, + (a, b) => a.split(':')[0] === b.split(':')[0], // compare only the type prefix + ); + + const extracted = transform.extract({ type: 'test', data: 123 }); + assert.strictEqual(extracted, 'test:123'); + + // Same type prefix should be equal + assert.strictEqual(transform.equals('test:123', 'test:456'), true); + // Different type prefix should not be equal + assert.strictEqual(transform.equals('test:123', 'other:123'), false); + }); + }); + + suite('LogAdapter', () => { + test('createInitial creates valid log entry', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const initial: TestObject = { name: 'test', count: 0, items: [] }; + const buffer = adapter.createInitial(initial); + + const content = buffer.toString(); + const entry = JSON.parse(content.trim()); + assert.strictEqual(entry.kind, 0); // EntryKind.Initial + assert.deepStrictEqual(entry.v, initial); + }); + + test('read reconstructs initial state', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const initial: TestObject = { name: 'test', count: 5, items: [{ id: 'a', value: 1 }] }; + const buffer = adapter.createInitial(initial); + + const reader = new Adapt.ObjectMutationLog(schema); + const result = reader.read(buffer); + + assert.deepStrictEqual(result, initial); + }); + + test('write returns empty data when no changes', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 0, items: [] }; + adapter.createInitial(obj); + + const result = adapter.write(obj); + assert.strictEqual(result.op, 'append'); + assert.strictEqual(result.data.toString(), ''); + }); + + test('write detects primitive changes', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 0, items: [] }; + adapter.createInitial(obj); + + const updated = { ...obj, count: 10 }; + const result = adapter.write(updated); + + assert.strictEqual(result.op, 'append'); + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 1); // EntryKind.Set + assert.deepStrictEqual(entry.k, ['count']); + assert.strictEqual(entry.v, 10); + }); + + test('write detects array append', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 0, items: [{ id: 'a', value: 1 }] }; + adapter.createInitial(obj); + + const updated: TestObject = { ...obj, items: [...obj.items, { id: 'b', value: 2 }] }; + const result = adapter.write(updated); + + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 2); // EntryKind.Push + assert.deepStrictEqual(entry.k, ['items']); + assert.deepStrictEqual(entry.v, [{ id: 'b', value: 2 }]); + assert.strictEqual(entry.i, undefined); + }); + + test('write detects array append nested', () => { + type Item = { id: string; value: number[] }; + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.array(Adapt.value())), + }); + + type TestObject = { items: Item[] }; + const schema = Adapt.object({ + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + adapter.createInitial({ items: [{ id: 'a', value: [1, 2] }] }); + + + const result1 = adapter.write({ items: [{ id: 'a', value: [1, 2, 3] }] }); + assert.deepStrictEqual( + JSON.parse(result1.data.toString().trim()), + { kind: 2, k: ['items', 0, 'value'], v: [3] }, + ); + + const result2 = adapter.write({ items: [{ id: 'b', value: [1, 2, 3] }] }); + assert.deepStrictEqual( + JSON.parse(result2.data.toString().trim()), + { kind: 2, k: ['items'], i: 0, v: [{ id: 'b', value: [1, 2, 3] }] }, + ); + }); + + test('write detects array truncation', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 0, items: [{ id: 'a', value: 1 }, { id: 'b', value: 2 }] }; + adapter.createInitial(obj); + + const updated: TestObject = { ...obj, items: [obj.items[0]] }; + const result = adapter.write(updated); + + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 2); // EntryKind.Push + assert.deepStrictEqual(entry.k, ['items']); + assert.strictEqual(entry.i, 1); + assert.strictEqual(entry.v, undefined); + }); + + test('write detects array item modification and recurses into object', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { + name: 'test', + count: 0, + items: [{ id: 'a', value: 1 }, { id: 'b', value: 2 }, { id: 'c', value: 3 }] + }; + adapter.createInitial(obj); + + // Modify middle item - key 'id' matches, so we recurse to set the 'value' property + const updated: TestObject = { + ...obj, + items: [{ id: 'a', value: 1 }, { id: 'b', value: 999 }, { id: 'c', value: 3 }] + }; + const result = adapter.write(updated); + + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 1); // EntryKind.Set - setting individual property + assert.deepStrictEqual(entry.k, ['items', 1, 'value']); + assert.strictEqual(entry.v, 999); + }); + + test('read applies multiple entries correctly', () => { + const schema = createTestSchema(); + const initial: TestObject = { name: 'test', count: 0, items: [] }; + + // Build log manually + const entries = [ + { kind: 0, v: initial }, + { kind: 1, k: ['count'], v: 5 }, + { kind: 2, k: ['items'], v: [{ id: 'a', value: 1 }] }, + { kind: 2, k: ['items'], v: [{ id: 'b', value: 2 }] }, + ]; + const logContent = entries.map(e => JSON.stringify(e)).join('\n') + '\n'; + + const adapter = new Adapt.ObjectMutationLog(schema); + const result = adapter.read(VSBuffer.fromString(logContent)); + + assert.strictEqual(result.count, 5); + assert.strictEqual(result.items.length, 2); + assert.deepStrictEqual(result.items[0], { id: 'a', value: 1 }); + assert.deepStrictEqual(result.items[1], { id: 'b', value: 2 }); + }); + + test('roundtrip preserves data through multiple updates', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const initial: TestObject = { name: 'test', count: 0, items: [] }; + const updates: TestObject[] = [ + { name: 'test', count: 1, items: [] }, + { name: 'test', count: 1, items: [{ id: 'a', value: 10 }] }, + { name: 'test', count: 2, items: [{ id: 'a', value: 10 }, { id: 'b', value: 20 }] }, + { name: 'test', count: 2, items: [{ id: 'a', value: 10 }] }, // Remove item + ]; + + const result = simulateFileRoundtrip(adapter, initial, updates); + assert.deepStrictEqual(result, updates[updates.length - 1]); + }); + + test('compacts log when entry count exceeds threshold', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema, 3); // Compact after 3 entries + + const obj: TestObject = { name: 'test', count: 0, items: [] }; + adapter.createInitial(obj); // Entry 1 + + adapter.write({ ...obj, count: 1 }); // Entry 2 + adapter.write({ ...obj, count: 2 }); // Entry 3 + + const before = adapter.write({ ...obj, count: 3 }); + assert.strictEqual(before.op, 'append'); + + // This should trigger compaction + const result = adapter.write({ ...obj, count: 4 }); + assert.strictEqual(result.op, 'replace'); + + // Verify the compacted log only has initial entry + const lines = result.data.toString().split('\n').filter(l => l.trim()); + assert.strictEqual(lines.length, 1); + const entry = JSON.parse(lines[0]); + assert.strictEqual(entry.kind, 0); // EntryKind.Initial + }); + + test('handles deepCompare property changes', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 0, items: [], metadata: { tags: ['a'] } }; + adapter.createInitial(obj); + + const updated: TestObject = { ...obj, metadata: { tags: ['a', 'b'] } }; + const result = adapter.write(updated); + + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 1); // EntryKind.Set + assert.deepStrictEqual(entry.k, ['metadata']); + assert.deepStrictEqual(entry.v, { tags: ['a', 'b'] }); + }); + + test('handles differentiated property changes', () => { + // Schema with a differentiated transform that extracts a string + // but uses a custom equals that only checks the prefix + interface DiffObj { + data: { type: string; version: number }; + } + const schema = Adapt.object({ + data: Adapt.t( + o => o.data, + Adapt.v<{ type: string; version: number }, string>( + obj => `${obj.type}:${obj.version}`, + (a, b) => a.split(':')[0] === b.split(':')[0], // compare only the type prefix + ) + ), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + // Initial state: 'foo:1' + adapter.createInitial({ data: { type: 'foo', version: 1 } }); + + // Change type from 'foo' to 'bar' - should detect change (different prefix) + const result1 = adapter.write({ data: { type: 'bar', version: 2 } }); + assert.notStrictEqual(result1.data.toString(), '', 'different type should trigger change'); + const entry1 = JSON.parse(result1.data.toString().trim()); + assert.strictEqual(entry1.kind, 1); // EntryKind.Set + assert.deepStrictEqual(entry1.k, ['data']); + assert.strictEqual(entry1.v, 'bar:2'); + + // Change version but keep type 'bar' - should NOT detect change (same prefix) + const result2 = adapter.write({ data: { type: 'bar', version: 3 } }); + assert.strictEqual(result2.data.toString(), '', 'same type prefix should not trigger change'); + }); + + test('read throws on empty log file', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + assert.throws(() => adapter.read(VSBuffer.fromString('')), /Empty log file/); + }); + + test('write without prior read creates initial entry', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const obj: TestObject = { name: 'test', count: 5, items: [] }; + const result = adapter.write(obj); + + assert.strictEqual(result.op, 'replace'); + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 0); // EntryKind.Initial + }); + + test('sealed objects skip non-key field comparison when both are sealed', () => { + interface SealedItem { + id: string; + value: number; + isSealed: boolean; + } + + interface SealedTestObject { + items: SealedItem[]; + } + + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.value()), + isSealed: Adapt.t(i => i.isSealed, Adapt.value()), + }, { + sealed: (obj) => obj.isSealed, + }); + + const schema = Adapt.object({ + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + // Initial state with a sealed item + adapter.createInitial({ items: [{ id: 'a', value: 1, isSealed: true }] }); + + // Change value on sealed item - should NOT be detected because both are sealed + const result1 = adapter.write({ items: [{ id: 'a', value: 999, isSealed: true }] }); + assert.strictEqual(result1.data.toString(), '', 'sealed item value change should be ignored'); + }); + + test('sealed objects still detect key changes', () => { + interface SealedItem { + id: string; + value: number; + isSealed: boolean; + } + + interface SealedTestObject { + items: SealedItem[]; + } + + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.value()), + isSealed: Adapt.t(i => i.isSealed, Adapt.value()), + }, { + sealed: (obj) => obj.isSealed, + }); + + const schema = Adapt.object({ + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + // Initial state with a sealed item + adapter.createInitial({ items: [{ id: 'a', value: 1, isSealed: true }] }); + + // Change key on sealed item - SHOULD be detected (replacement) + const result = adapter.write({ items: [{ id: 'b', value: 1, isSealed: true }] }); + assert.notStrictEqual(result.data.toString(), '', 'key change should be detected even when sealed'); + + const entry = JSON.parse(result.data.toString().trim()); + assert.strictEqual(entry.kind, 2); // EntryKind.Push (array replacement) + }); + + test('sealed objects diff normally when one is not sealed', () => { + interface SealedItem { + id: string; + value: number; + isSealed: boolean; + } + + interface SealedTestObject { + items: SealedItem[]; + } + + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.value()), + isSealed: Adapt.t(i => i.isSealed, Adapt.value()), + }, { + sealed: (obj) => obj.isSealed, + }); + + const schema = Adapt.object({ + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + // Initial state with a non-sealed item + adapter.createInitial({ items: [{ id: 'a', value: 1, isSealed: false }] }); + + // Change value - should be detected since prev is not sealed + const result1 = adapter.write({ items: [{ id: 'a', value: 999, isSealed: false }] }); + assert.notStrictEqual(result1.data.toString(), '', 'non-sealed item should detect value change'); + + const entry = JSON.parse(result1.data.toString().trim()); + assert.strictEqual(entry.kind, 1); // EntryKind.Set + assert.deepStrictEqual(entry.k, ['items', 0, 'value']); + assert.strictEqual(entry.v, 999); + }); + + test('sealed transition from unsealed to sealed detects final changes', () => { + interface SealedItem { + id: string; + value: number; + isSealed: boolean; + } + + interface SealedTestObject { + items: SealedItem[]; + } + + const itemSchema = Adapt.object({ + id: Adapt.t(i => i.id, Adapt.key()), + value: Adapt.t(i => i.value, Adapt.value()), + isSealed: Adapt.t(i => i.isSealed, Adapt.value()), + }, { + sealed: (obj) => obj.isSealed, + }); + + const schema = Adapt.object({ + items: Adapt.t(o => o.items, Adapt.array(itemSchema)), + }); + + const adapter = new Adapt.ObjectMutationLog(schema); + + // Initial state with a non-sealed item + adapter.createInitial({ items: [{ id: 'a', value: 1, isSealed: false }] }); + + // Transition to sealed with value change - should detect changes since prev was not sealed + const result = adapter.write({ items: [{ id: 'a', value: 999, isSealed: true }] }); + assert.notStrictEqual(result.data.toString(), '', 'transition to sealed should detect value change'); + + // Should have two entries - one for value, one for isSealed + const lines = result.data.toString().trim().split('\n'); + assert.strictEqual(lines.length, 2, 'should have two change entries'); + }); + + test('write detects property set to undefined', () => { + const schema = createTestSchema(); + const adapter = new Adapt.ObjectMutationLog(schema); + + const initial: TestObject = { name: 'test', count: 5, items: [], metadata: { tags: ['foo'] } }; + + const result = simulateFileRoundtrip(adapter, initial, [ + { name: 'test', count: 10, items: [], metadata: { tags: ['foo'] } }, + { name: 'test', count: undefined, items: [], metadata: undefined }, + ]); + assert.deepStrictEqual(result, { name: 'test', count: undefined, items: [], metadata: undefined }); + + const result2 = simulateFileRoundtrip(adapter, initial, [ + { name: 'test', count: 10, items: [], metadata: { tags: ['foo'] } }, + { name: 'test', count: undefined, items: [], metadata: undefined }, + { name: 'test', count: 12, items: [], metadata: { tags: ['bar'] } }, + ]); + assert.deepStrictEqual(result2, { name: 'test', count: 12, items: [], metadata: { tags: ['bar'] } }); + }); + + test('delete followed by set restores property', () => { + const schema = createTestSchema(); + const initial: TestObject = { name: 'test', count: 0, items: [], metadata: { tags: ['a'] } }; + + // Build log with delete then set + const entries = [ + { kind: 0, v: initial }, + { kind: 3, k: ['metadata'] }, // Delete + { kind: 1, k: ['metadata'], v: { tags: ['b', 'c'] } }, // Set to new value + ]; + const logContent = entries.map(e => JSON.stringify(e)).join('\n') + '\n'; + + const adapter = new Adapt.ObjectMutationLog(schema); + const result = adapter.read(VSBuffer.fromString(logContent)); + + assert.deepStrictEqual(result.metadata, { tags: ['b', 'c'] }); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatSessionStore.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatSessionStore.test.ts new file mode 100644 index 00000000000..cce617aabe1 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/chatSessionStore.test.ts @@ -0,0 +1,377 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IEnvironmentService } from '../../../../../../platform/environment/common/environment.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { NullTelemetryService } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IUserDataProfilesService, toUserDataProfile } from '../../../../../../platform/userDataProfile/common/userDataProfile.js'; +import { IWorkspaceContextService, WorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js'; +import { TestWorkspace, Workspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js'; +import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { InMemoryTestFileService, TestContextService, TestLifecycleService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { ChatModel, ISerializableChatData3 } from '../../../common/model/chatModel.js'; +import { ChatSessionStore, IChatTransfer } from '../../../common/model/chatSessionStore.js'; +import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; +import { MockChatModel } from './mockChatModel.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; + +function createMockChatModel(sessionResource: URI, options?: { customTitle?: string }): ChatModel { + const sessionId = LocalChatSessionUri.parseLocalSessionId(sessionResource); + if (!sessionId) { + throw new Error('createMockChatModel requires a local session URI'); + } + const model = new MockChatModel(sessionResource); + model.sessionId = sessionId; + if (options?.customTitle) { + model.customTitle = options.customTitle; + } + // Cast to ChatModel - the mock implements enough of the interface for testing + return model as unknown as ChatModel; +} + +suite('ChatSessionStore', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let instantiationService: TestInstantiationService; + + function createChatSessionStore(isEmptyWindow: boolean = false): ChatSessionStore { + const workspace = isEmptyWindow ? new Workspace('empty-window-id', []) : TestWorkspace; + instantiationService.stub(IWorkspaceContextService, new TestContextService(workspace)); + return testDisposables.add(instantiationService.createInstance(ChatSessionStore)); + } + + setup(() => { + instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection())); + instantiationService.stub(IStorageService, testDisposables.add(new TestStorageService())); + instantiationService.stub(ILogService, NullLogService); + instantiationService.stub(ITelemetryService, NullTelemetryService); + instantiationService.stub(IFileService, testDisposables.add(new InMemoryTestFileService())); + instantiationService.stub(IEnvironmentService, { workspaceStorageHome: URI.file('/test/workspaceStorage') }); + instantiationService.stub(ILifecycleService, testDisposables.add(new TestLifecycleService())); + instantiationService.stub(IUserDataProfilesService, { defaultProfile: toUserDataProfile('default', 'Default', URI.file('/test/userdata'), URI.file('/test/cache')) }); + instantiationService.stub(IConfigurationService, new TestConfigurationService()); + }); + + test('hasSessions returns false when no sessions exist', () => { + const store = createChatSessionStore(); + + assert.strictEqual(store.hasSessions(), false); + }); + + test('getIndex returns empty index initially', async () => { + const store = createChatSessionStore(); + + const index = await store.getIndex(); + assert.deepStrictEqual(index, {}); + }); + + test('getChatStorageFolder returns correct path for workspace', () => { + const store = createChatSessionStore(false); + + const storageFolder = store.getChatStorageFolder(); + assert.ok(storageFolder.path.includes('workspaceStorage')); + assert.ok(storageFolder.path.includes('chatSessions')); + }); + + test('getChatStorageFolder returns correct path for empty window', () => { + const store = createChatSessionStore(true); + + const storageFolder = store.getChatStorageFolder(); + assert.ok(storageFolder.path.includes('emptyWindowChatSessions')); + }); + + test('isSessionEmpty returns true for non-existent session', () => { + const store = createChatSessionStore(); + + assert.strictEqual(store.isSessionEmpty('non-existent-session'), true); + }); + + test('readSession returns undefined for non-existent session', async () => { + const store = createChatSessionStore(); + + const session = await store.readSession('non-existent-session'); + assert.strictEqual(session, undefined); + }); + + test('deleteSession handles non-existent session gracefully', async () => { + const store = createChatSessionStore(); + + // Should not throw + await store.deleteSession('non-existent-session'); + + assert.strictEqual(store.hasSessions(), false); + }); + + test('storeSessions persists session to index', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + + assert.strictEqual(store.hasSessions(), true); + const index = await store.getIndex(); + assert.ok(index['session-1']); + assert.strictEqual(index['session-1'].sessionId, 'session-1'); + }); + + test('storeSessions persists custom title', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'My Custom Title' })); + + await store.storeSessions([model]); + + const index = await store.getIndex(); + assert.strictEqual(index['session-1'].title, 'My Custom Title'); + }); + + test('readSession returns stored session data', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + const session = await store.readSession('session-1'); + + assert.ok(session); + assert.strictEqual((session.value as ISerializableChatData3).sessionId, 'session-1'); + }); + + test('deleteSession removes session from index', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + + await store.storeSessions([model]); + assert.strictEqual(store.hasSessions(), true); + + await store.deleteSession('session-1'); + + assert.strictEqual(store.hasSessions(), false); + const index = await store.getIndex(); + assert.strictEqual(index['session-1'], undefined); + }); + + test('clearAllSessions removes all sessions', async () => { + const store = createChatSessionStore(); + const model1 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'))); + const model2 = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-2'))); + + await store.storeSessions([model1, model2]); + assert.strictEqual(Object.keys(await store.getIndex()).length, 2); + + await store.clearAllSessions(); + + const index = await store.getIndex(); + assert.deepStrictEqual(index, {}); + }); + + test('setSessionTitle updates existing session title', async () => { + const store = createChatSessionStore(); + const model = testDisposables.add(createMockChatModel(LocalChatSessionUri.forSession('session-1'), { customTitle: 'Original Title' })); + + await store.storeSessions([model]); + await store.setSessionTitle('session-1', 'New Title'); + + const index = await store.getIndex(); + assert.strictEqual(index['session-1'].title, 'New Title'); + }); + + test('setSessionTitle does nothing for non-existent session', async () => { + const store = createChatSessionStore(); + + // Should not throw + await store.setSessionTitle('non-existent', 'Title'); + + const index = await store.getIndex(); + assert.strictEqual(index['non-existent'], undefined); + }); + + test('multiple stores can be created with different workspaces', async () => { + const store1 = createChatSessionStore(false); + const store2 = createChatSessionStore(true); + + const folder1 = store1.getChatStorageFolder(); + const folder2 = store2.getChatStorageFolder(); + + assert.notStrictEqual(folder1.toString(), folder2.toString()); + }); + + suite('transferred sessions', () => { + function createSingleFolderWorkspace(folderUri: URI): Workspace { + const folder = new WorkspaceFolder({ uri: folderUri, index: 0, name: 'test' }); + return new Workspace('single-folder-id', [folder]); + } + + function createChatSessionStoreWithSingleFolder(folderUri: URI): ChatSessionStore { + instantiationService.stub(IWorkspaceContextService, new TestContextService(createSingleFolderWorkspace(folderUri))); + return testDisposables.add(instantiationService.createInstance(ChatSessionStore)); + } + + function createTransferData(toWorkspace: URI, sessionResource: URI, timestampInMilliseconds?: number): IChatTransfer { + return { + toWorkspace, + sessionResource, + timestampInMilliseconds: timestampInMilliseconds ?? Date.now(), + }; + } + + test('getTransferredSessionData returns undefined for empty window', () => { + const store = createChatSessionStore(true); // empty window + + const result = store.getTransferredSessionData(); + + assert.strictEqual(result, undefined); + }); + + test('getTransferredSessionData returns undefined when no transfer exists', () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + + const result = store.getTransferredSessionData(); + + assert.strictEqual(result, undefined); + }); + + test('storeTransferSession stores and retrieves transfer data', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + const result = store.getTransferredSessionData(); + assert.ok(result); + assert.strictEqual(result.toString(), sessionResource.toString()); + }); + + test('readTransferredSession returns session data', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + const sessionData = await store.readTransferredSession(sessionResource); + assert.ok(sessionData); + assert.strictEqual((sessionData.value as ISerializableChatData3).sessionId, 'transfer-session'); + }); + + test('readTransferredSession cleans up after reading', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + const transferData = createTransferData(folderUri, sessionResource); + await store.storeTransferSession(transferData, model); + + // Read the session + await store.readTransferredSession(sessionResource); + + // Transfer should be cleaned up + const result = store.getTransferredSessionData(); + assert.strictEqual(result, undefined); + }); + + test('getTransferredSessionData returns undefined for expired transfer', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + // Create transfer with timestamp 10 minutes in the past (expired) + const expiredTimestamp = Date.now() - (10 * 60 * 1000); + const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp); + await store.storeTransferSession(transferData, model); + + const result = store.getTransferredSessionData(); + assert.strictEqual(result, undefined); + }); + + test('expired transfer cleans up index and file', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const sessionResource = LocalChatSessionUri.forSession('transfer-session'); + const model = testDisposables.add(createMockChatModel(sessionResource)); + + // Create transfer with timestamp 100 minutes in the past (expired) + const expiredTimestamp = Date.now() - (100 * 60 * 1000); + const transferData = createTransferData(folderUri, sessionResource, expiredTimestamp); + await store.storeTransferSession(transferData, model); + + // Assert cleaned up + const data = store.getTransferredSessionData(); + assert.strictEqual(data, undefined); + }); + + test('readTransferredSession returns undefined for invalid session resource', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + + // Use a non-local session URI + const invalidResource = URI.parse('file:///invalid/session'); + + const result = await store.readTransferredSession(invalidResource); + assert.strictEqual(result, undefined); + }); + + test('storeTransferSession deletes preexisting transferred session file', async () => { + const folderUri = URI.file('/test/workspace'); + const store = createChatSessionStoreWithSingleFolder(folderUri); + const fileService = instantiationService.get(IFileService); + + // Store first session + const session1Resource = LocalChatSessionUri.forSession('transfer-session-1'); + const model1 = testDisposables.add(createMockChatModel(session1Resource)); + const transferData1 = createTransferData(folderUri, session1Resource); + await store.storeTransferSession(transferData1, model1); + + // Verify first session file exists + const userDataProfile = instantiationService.get(IUserDataProfilesService).defaultProfile; + const storageLocation1 = URI.joinPath( + userDataProfile.globalStorageHome, + 'transferredChatSessions', + 'transfer-session-1.json' + ); + const exists1 = await fileService.exists(storageLocation1); + assert.strictEqual(exists1, true, 'First session file should exist'); + + // Store second session for the same workspace + const session2Resource = LocalChatSessionUri.forSession('transfer-session-2'); + const model2 = testDisposables.add(createMockChatModel(session2Resource)); + const transferData2 = createTransferData(folderUri, session2Resource); + await store.storeTransferSession(transferData2, model2); + + // Verify first session file is deleted + const exists1After = await fileService.exists(storageLocation1); + assert.strictEqual(exists1After, false, 'First session file should be deleted'); + + // Verify second session file exists + const storageLocation2 = URI.joinPath( + userDataProfile.globalStorageHome, + 'transferredChatSessions', + 'transfer-session-2.json' + ); + const exists2 = await fileService.exists(storageLocation2); + assert.strictEqual(exists2, true, 'Second session file should exist'); + + // Verify only the second session is retrievable + const result = store.getTransferredSessionData(); + assert.ok(result); + assert.strictEqual(result.toString(), session2Resource.toString()); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/model/chatStreamStats.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatStreamStats.test.ts new file mode 100644 index 00000000000..7413402ed5b --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/chatStreamStats.test.ts @@ -0,0 +1,174 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { timeout } from '../../../../../../base/common/async.js'; +import { runWithFakedTimers } from '../../../../../../base/test/common/timeTravelScheduler.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../../platform/log/common/log.js'; +import { ChatStreamStatsTracker, type IChatStreamStatsInternal } from '../../../common/model/chatStreamStats.js'; + +suite('ChatStreamStatsTracker', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function createTracker(): ChatStreamStatsTracker { + return new ChatStreamStatsTracker(store.add(new NullLogService())); + } + + test('drops bootstrap once sufficient markdown streamed', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + let data = tracker.update({ totalWordCount: 10 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.totalTime, 250); + + await timeout(100); + data = tracker.update({ totalWordCount: 35 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, false); + assert.strictEqual(data.totalTime, 100); + assert.strictEqual(data.lastWordCount, 35); + })); + + test('large initial chunk uses higher bootstrap minimum', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + const data = tracker.update({ totalWordCount: 40 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.totalTime, 500); + })); + + test('ignores updates without new words', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + const first = tracker.update({ totalWordCount: 5 }); + assert.ok(first); + + await timeout(50); + const second = tracker.update({ totalWordCount: 5 }); + assert.strictEqual(second, undefined); + })); + + test('ignores zero-word totals until words arrive', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + const zero = tracker.update({ totalWordCount: 0 }); + assert.strictEqual(zero, undefined); + assert.strictEqual(tracker.internalData.lastWordCount, 0); + assert.strictEqual(tracker.internalData.totalTime, 0); + + await timeout(100); + const data = tracker.update({ totalWordCount: 12 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.totalTime, 500); + })); + + test('unchanged totals do not advance timers', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + const first = tracker.update({ totalWordCount: 6 }) as IChatStreamStatsInternal | undefined; + assert.ok(first); + const initialTotalTime = first.totalTime; + const initialLastUpdateTime = first.lastUpdateTime; + + await timeout(400); + const second = tracker.update({ totalWordCount: 6 }); + assert.strictEqual(second, undefined); + + assert.strictEqual(tracker.internalData.totalTime, initialTotalTime); + assert.strictEqual(tracker.internalData.lastUpdateTime, initialLastUpdateTime); + })); + + test('records first markdown time but keeps bootstrap active', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + + const data = tracker.update({ totalWordCount: 12 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.firstMarkdownTime, 0); + assert.strictEqual(data.totalTime, 500); + })); + + test('implied rate uses elapsed time after bootstrap drops', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + assert.ok(tracker.update({ totalWordCount: 10 })); + + await timeout(300); + const data = tracker.update({ totalWordCount: 40 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, false); + assert.strictEqual(data.totalTime, 300); + const expectedRate = 30 / 0.3; + assert.ok(Math.abs(data.impliedWordLoadRate - expectedRate) < 0.0001); + })); + + test('keeps bootstrap active until both thresholds satisfied', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + let data = tracker.update({ totalWordCount: 8 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.wordCountAtBootstrapExit, undefined); + assert.strictEqual(data.totalTime, 250); + + await timeout(200); + data = tracker.update({ totalWordCount: 12 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, false); + assert.strictEqual(data.wordCountAtBootstrapExit, 8); + assert.strictEqual(data.totalTime, 200); + })); + + test('caps interval contribution to max interval time', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + assert.ok(tracker.update({ totalWordCount: 5 })); + + await timeout(2000); + const data = tracker.update({ totalWordCount: 9 }) as IChatStreamStatsInternal | undefined; + assert.ok(data); + assert.strictEqual(data.bootstrapActive, true); + assert.strictEqual(data.totalTime, 250 + 250); + })); + + test('uses larger interval cap for large updates', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + assert.ok(tracker.update({ totalWordCount: 10 })); + + await timeout(200); + const exitData = tracker.update({ totalWordCount: 40 }) as IChatStreamStatsInternal | undefined; + assert.ok(exitData); + assert.strictEqual(exitData.bootstrapActive, false); + const baselineTotal = exitData.totalTime; + + await timeout(2000); + const postData = tracker.update({ totalWordCount: 90 }) as IChatStreamStatsInternal | undefined; + assert.ok(postData); + assert.strictEqual(postData.bootstrapActive, false); + assert.strictEqual(postData.totalTime, baselineTotal + 1000); + })); + + test('tracks words since bootstrap exit for rate calculation', () => runWithFakedTimers({ startTime: 0, useFakeTimers: true }, async () => { + const tracker = createTracker(); + assert.ok(tracker.update({ totalWordCount: 12 })); + + await timeout(200); + const exitData = tracker.update({ totalWordCount: 45 }) as IChatStreamStatsInternal | undefined; + assert.ok(exitData); + assert.strictEqual(exitData.bootstrapActive, false); + assert.strictEqual(exitData.wordCountAtBootstrapExit, 12); + assert.strictEqual(exitData.totalTime, 200); + + await timeout(200); + const postBootstrap = tracker.update({ totalWordCount: 60 }) as IChatStreamStatsInternal | undefined; + assert.ok(postBootstrap); + assert.strictEqual(postBootstrap.bootstrapActive, false); + assert.strictEqual(postBootstrap.totalTime, 400); + assert.strictEqual(postBootstrap.wordCountAtBootstrapExit, 12); + const expectedRate = (60 - 12) / 0.4; + assert.strictEqual(postBootstrap.impliedWordLoadRate, expectedRate); + })); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts b/src/vs/workbench/contrib/chat/test/common/model/chatWordCounter.test.ts similarity index 96% rename from src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts rename to src/vs/workbench/contrib/chat/test/common/model/chatWordCounter.test.ts index 3281a16c9d1..6ae135ef22c 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatWordCounter.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/model/chatWordCounter.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { getNWords, IWordCountResult } from '../../common/chatWordCounter.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { getNWords, IWordCountResult } from '../../../common/model/chatWordCounter.js'; suite('ChatWordCounter', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts b/src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts new file mode 100644 index 00000000000..026c88b2fa5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/model/mockChatModel.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from '../../../../../../base/common/event.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { IObservable, observableValue } from '../../../../../../base/common/observable.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IChatEditingSession } from '../../../common/editing/chatEditingService.js'; +import { IChatChangeEvent, IChatModel, IChatRequestModel, IChatRequestNeedsInputInfo, IExportableChatData, IExportableRepoData, IInputModel, ISerializableChatData } from '../../../common/model/chatModel.js'; +import { ChatAgentLocation } from '../../../common/constants.js'; +import { IChatSessionTiming } from '../../../common/chatService/chatService.js'; + +export class MockChatModel extends Disposable implements IChatModel { + readonly onDidDispose = this._register(new Emitter()).event; + readonly onDidChange = this._register(new Emitter()).event; + sessionId = ''; + readonly timestamp = 0; + readonly timing: IChatSessionTiming = { created: Date.now(), lastRequestStarted: undefined, lastRequestEnded: undefined }; + readonly initialLocation = ChatAgentLocation.Chat; + readonly title = ''; + readonly hasCustomTitle = false; + customTitle: string | undefined; + lastMessageDate = Date.now(); + creationDate = Date.now(); + requests: IChatRequestModel[] = []; + readonly requestInProgress = observableValue('requestInProgress', false); + readonly requestNeedsInput = observableValue('requestNeedsInput', undefined); + readonly inputPlaceholder = undefined; + readonly editingSession = undefined; + readonly checkpoint = undefined; + readonly willKeepAlive = true; + readonly responderUsername: string = 'agent'; + readonly inputModel: IInputModel = { + state: observableValue('inputModelState', undefined), + setState: () => { }, + clearState: () => { }, + toJSON: () => undefined + }; + readonly contributedChatSession = undefined; + repoData: IExportableRepoData | undefined = undefined; + isDisposed = false; + lastRequestObs: IObservable; + + constructor(readonly sessionResource: URI) { + super(); + this.lastRequest = undefined; + this.lastRequestObs = observableValue('lastRequest', undefined); + } + + readonly hasRequests = false; + readonly lastRequest: IChatRequestModel | undefined; + + override dispose() { + this.isDisposed = true; + super.dispose(); + } + + startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void { } + getRequests(): IChatRequestModel[] { return []; } + setCheckpoint(requestId: string | undefined): void { } + setRepoData(data: IExportableRepoData | undefined): void { this.repoData = data; } + toExport(): IExportableChatData { + return { + initialLocation: this.initialLocation, + requests: [], + responderUsername: '', + }; + } + toJSON(): ISerializableChatData { + return { + version: 3, + sessionId: this.sessionId, + creationDate: this.timestamp, + customTitle: this.customTitle, + initialLocation: this.initialLocation, + requests: [], + responderUsername: '', + repoData: this.repoData + }; + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts b/src/vs/workbench/contrib/chat/test/common/participants/chatAgents.test.ts similarity index 87% rename from src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts rename to src/vs/workbench/contrib/chat/test/common/participants/chatAgents.test.ts index 3c43f4e22de..98a8e87fb76 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatAgents.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/participants/chatAgents.test.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { ContextKeyExpression } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../common/chatAgents.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ContextKeyExpression } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { ChatAgentService, IChatAgentData, IChatAgentImplementation } from '../../../common/participants/chatAgents.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; const testAgentId = 'testAgent'; const testAgentData: IChatAgentData = { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts new file mode 100644 index 00000000000..2038fe08f25 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -0,0 +1,1181 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sinon from 'sinon'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { Schemas } from '../../../../../../base/common/network.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../../editor/common/services/model.js'; +import { ModelService } from '../../../../../../editor/common/services/modelService.js'; +import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { FileService } from '../../../../../../platform/files/common/fileService.js'; +import { InMemoryFileSystemProvider } from '../../../../../../platform/files/common/inMemoryFilesystemProvider.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { ILabelService } from '../../../../../../platform/label/common/label.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { ITelemetryService } from '../../../../../../platform/telemetry/common/telemetry.js'; +import { NullTelemetryService } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; +import { IWorkspaceContextService } from '../../../../../../platform/workspace/common/workspace.js'; +import { testWorkspace } from '../../../../../../platform/workspace/test/common/testWorkspace.js'; +import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; +import { TestContextService, TestUserDataProfileService } from '../../../../../test/common/workbenchTestServices.js'; +import { ChatRequestVariableSet, isPromptFileVariableEntry, isPromptTextVariableEntry, toFileVariableEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { ComputeAutomaticInstructions } from '../../../common/promptSyntax/computeAutomaticInstructions.js'; +import { PromptsConfig } from '../../../common/promptSyntax/config/config.js'; +import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../common/promptSyntax/config/promptFileLocations.js'; +import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../../common/promptSyntax/promptTypes.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; +import { PromptsService } from '../../../common/promptSyntax/service/promptsServiceImpl.js'; +import { mockFiles } from './testUtils/mockFilesystem.js'; +import { InMemoryStorageService, IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { IPathService } from '../../../../../services/path/common/pathService.js'; +import { IFileQuery, ISearchService } from '../../../../../services/search/common/search.js'; +import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; +import { ChatMode } from '../../../common/chatModes.js'; +import { ILanguageModelToolsService } from '../../../common/tools/languageModelToolsService.js'; +import { basename } from '../../../../../../base/common/resources.js'; +import { match } from '../../../../../../base/common/glob.js'; + +suite('ComputeAutomaticInstructions', () => { + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); + + let service: IPromptsService; + let instaService: TestInstantiationService; + let workspaceContextService: TestContextService; + let testConfigService: TestConfigurationService; + let fileService: IFileService; + let toolsService: ILanguageModelToolsService; + + setup(async () => { + instaService = disposables.add(new TestInstantiationService()); + instaService.stub(ILogService, new NullLogService()); + + workspaceContextService = new TestContextService(); + instaService.stub(IWorkspaceContextService, workspaceContextService); + + testConfigService = new TestConfigurationService(); + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_NESTED_AGENT_MD, false); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true }); + testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true }); + testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true }); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { '.claude/skills': true }); + + instaService.stub(IConfigurationService, testConfigService); + instaService.stub(IUserDataProfileService, new TestUserDataProfileService()); + instaService.stub(ITelemetryService, NullTelemetryService); + instaService.stub(IStorageService, InMemoryStorageService); + instaService.stub(IExtensionService, { + whenInstalledExtensionsRegistered: () => Promise.resolve(true), + activateByEvent: () => Promise.resolve() + }); + + fileService = disposables.add(instaService.createInstance(FileService)); + instaService.stub(IFileService, fileService); + + const modelService = disposables.add(instaService.createInstance(ModelService)); + instaService.stub(IModelService, modelService); + instaService.stub(ILanguageService, { + guessLanguageIdByFilepathOrFirstLine(uri: URI) { + if (uri.path.endsWith(PROMPT_FILE_EXTENSION)) { + return PROMPT_LANGUAGE_ID; + } + + if (uri.path.endsWith(INSTRUCTION_FILE_EXTENSION)) { + return INSTRUCTIONS_LANGUAGE_ID; + } + + return 'plaintext'; + } + }); + instaService.stub(ILabelService, { + getUriLabel: (uri: URI, options?: { relative?: boolean }) => { + if (options?.relative) { + return basename(uri); + } + return uri.path; + } + }); + + const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider()); + disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider)); + + const pathService = { + userHome: (): URI | Promise => { + return Promise.resolve(URI.file('/home/user')); + }, + } as IPathService; + instaService.stub(IPathService, pathService); + + instaService.stub(ISearchService, { + schemeHasFileSearchProvider: () => true, + async fileSearch(query: IFileQuery) { + const results: any[] = []; + for (const folderQuery of query.folderQueries) { + const findFilesInLocation = async (location: URI, results: URI[] = []): Promise => { + try { + const resolve = await fileService.resolve(location); + if (resolve.isFile) { + results.push(resolve.resource); + } else if (resolve.isDirectory && resolve.children) { + for (const child of resolve.children) { + await findFilesInLocation(child.resource, results); + } + } + } catch (error) { + // folder doesn't exist + } + return results; + }; + + const allFiles = await findFilesInLocation(folderQuery.folder); + for (const resource of allFiles) { + const pathMatch = query.filePattern === undefined || match(query.filePattern, resource.path); + if (pathMatch) { + results.push({ resource }); + } + } + } + return { results, messages: [] }; + } + }); + + // Mock tools service + toolsService = { + getToolByName: (name: string) => { + if (name === 'readFile') { + return { id: 'vscode_readFile', name: 'readFile' }; + } + if (name === 'runSubagent') { + return { id: 'vscode_runSubagent', name: 'runSubagent' }; + } + return undefined; + }, + getFullReferenceName: (tool: { name: string }) => tool.name, + } as unknown as ILanguageModelToolsService; + instaService.stub(ILanguageModelToolsService, toolsService); + + service = disposables.add(instaService.createInstance(PromptsService)); + instaService.stub(IPromptsService, service); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('collect', () => { + test('should collect all types of instructions', async () => { + const rootFolderName = 'collect-all-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + // Applying instruction + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'description: \'TypeScript instructions\'', + 'applyTo: "**/*.ts"', + '---', + 'TypeScript coding standards', + ] + }, + // copilot-instructions + { + path: `${rootFolder}/.github/copilot-instructions.md`, + contents: [ + 'Be helpful and friendly', + ] + }, + // AGENTS.md + { + path: `${rootFolder}/AGENTS.md`, + contents: [ + 'Agent guidelines', + ] + }, + // Attached file + { + path: `${rootFolder}/src/file.ts`, + contents: [ + 'console.log("test");', + ] + }, + ]); + { + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + const paths = instructionFiles.map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + + assert.ok(paths.includes(`${rootFolder}/.github/instructions/typescript.instructions.md`), 'Should include applying instruction'); + assert.ok(paths.includes(`${rootFolder}/.github/copilot-instructions.md`), 'Should include copilot-instructions'); + assert.ok(paths.includes(`${rootFolder}/AGENTS.md`), 'Should include AGENTS.md'); + } + { + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, false); + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + const paths = instructionFiles.map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + + assert.ok(!paths.includes(`${rootFolder}/.github/instructions/typescript.instructions.md`), 'Should not include applying instruction'); + assert.ok(paths.includes(`${rootFolder}/.github/copilot-instructions.md`), 'Should include copilot-instructions'); + assert.ok(paths.includes(`${rootFolder}/AGENTS.md`), 'Should include AGENTS.md'); + } + { + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, false); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + const paths = instructionFiles.map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + + assert.ok(paths.includes(`${rootFolder}/.github/instructions/typescript.instructions.md`), 'Should include applying instruction'); + assert.ok(!paths.includes(`${rootFolder}/.github/copilot-instructions.md`), 'Should not include copilot-instructions'); + assert.ok(paths.includes(`${rootFolder}/AGENTS.md`), 'Should include AGENTS.md'); + } + { + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, false); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + const paths = instructionFiles.map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + + assert.ok(paths.includes(`${rootFolder}/.github/instructions/typescript.instructions.md`), 'Should include applying instruction'); + assert.ok(paths.includes(`${rootFolder}/.github/copilot-instructions.md`), 'Should include copilot-instructions'); + assert.ok(!paths.includes(`${rootFolder}/AGENTS.md`), 'Should not include AGENTS.md'); + } + }); + + test('should not collect when settings are disabled', async () => { + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, false); + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, false); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, false); + + const rootFolderName = 'disabled-settings-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'TypeScript coding standards', + ] + }, + { + path: `${rootFolder}/.github/copilot-instructions.md`, + contents: ['Be helpful'], + }, + { + path: `${rootFolder}/AGENTS.md`, + contents: ['Guidelines'], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['console.log("test");'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.strictEqual(instructionFiles.length, 0, 'Should not collect any instructions when settings are disabled'); + }); + + test('should collect for edit mode even when settings disabled', async () => { + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, false); + + const rootFolderName = 'edit-mode-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'TypeScript standards', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['console.log("test");'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Edit, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.ok(instructionFiles.length > 0, 'Should collect instructions in edit mode even when setting is disabled'); + }); + }); + + suite('addApplyingInstructions', () => { + test('should match ** pattern for any file', async () => { + const rootFolderName = 'wildcard-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/all-files.instructions.md`, + contents: [ + '---', + 'applyTo: "**"', + '---', + 'Apply to all files', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.strictEqual(instructionFiles.length, 1, 'Should match ** pattern'); + }); + + test('should match specific file patterns', async () => { + const rootFolderName = 'pattern-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'TS instructions', + ] + }, + { + path: `${rootFolder}/.github/instructions/javascript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.js"', + '---', + 'JS instructions', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const paths = variables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(v => isPromptFileVariableEntry(v) ? v.value.path : undefined); + assert.ok(paths.includes(`${rootFolder}/.github/instructions/typescript.instructions.md`), 'Should match TS file'); + assert.ok(!paths.includes(`${rootFolder}/.github/instructions/javascript.instructions.md`), 'Should not match JS pattern'); + }); + + test('should handle multiple patterns separated by comma', async () => { + const rootFolderName = 'multi-pattern-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/web.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts, **/*.js, **/*.tsx"', + '---', + 'Web instructions', + ] + }, + { + path: `${rootFolder}/src/component.tsx`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/component.tsx'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.strictEqual(instructionFiles.length, 1, 'Should match one of the comma-separated patterns'); + }); + + test('should not add duplicate instructions', async () => { + const rootFolderName = 'duplicate-test'; const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'TS instructions', + ] + }, + { + path: `${rootFolder}/src/file1.ts`, + contents: ['code'], + }, + { + path: `${rootFolder}/src/file2.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file1.ts'))); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file2.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.strictEqual(instructionFiles.length, 1, 'Should add instruction only once even with multiple matching files'); + }); + + test('should handle relative glob patterns', async () => { + const rootFolderName = 'relative-pattern-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/src-files.instructions.md`, + contents: [ + '---', + 'applyTo: "src/**/*.ts"', + '---', + 'Src instructions', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const instructionFiles = variables.asArray().filter(v => isPromptFileVariableEntry(v)); + assert.strictEqual(instructionFiles.length, 1, 'Should match relative glob pattern'); + }); + }); + + suite('referenced instructions', () => { + test('should add referenced instruction files', async () => { + const rootFolderName = 'referenced-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/main.instructions.md`, + contents: [ + '---', + 'description: \'Main instructions\'', + 'applyTo: "**/*.ts"', + '---', + 'Main instructions #file:./referenced.instructions.md', + ] + }, + { + path: `${rootFolder}/.github/instructions/referenced.instructions.md`, + contents: [ + '---', + 'description: \'Referenced instructions\'', + '---', + 'Referenced content', + ] + }, + ]); + + const mainUri = URI.joinPath(rootFolderUri, '.github/instructions/main.instructions.md'); + const referencedUri = URI.joinPath(rootFolderUri, '.github/instructions/referenced.instructions.md'); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const paths = variables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(v => isPromptFileVariableEntry(v) ? v.value.path : undefined); + + assert.ok(paths.includes(mainUri.path), 'Should include main instruction'); + assert.ok(paths.includes(referencedUri.path), 'Should include referenced instruction'); + }); + + test('should not add non-workspace references', async () => { + const rootFolderName = 'non-workspace-ref-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/main.instructions.md`, + contents: [ + '---', + 'description: \'Main instructions\'', + 'applyTo: "**/*.ts"', + '---', + 'Main instructions #file:/tmp/external.md', + ] + }, + ]); + + const mainUri = URI.joinPath(rootFolderUri, '.github/instructions/main.instructions.md'); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const paths = variables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(v => isPromptFileVariableEntry(v) ? v.value.path : undefined); + + assert.ok(paths.includes(mainUri.path), 'Should include main instruction'); + assert.ok(!paths.includes('/tmp/external.md'), 'Should not include non-workspace reference'); + }); + + test('should handle nested references', async () => { + const rootFolderName = 'nested-ref-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/level1.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'Level 1 #file:./level2.instructions.md', + ] + }, + { + path: `${rootFolder}/.github/instructions/level2.instructions.md`, + contents: [ + 'Level 2 #file:./level3.instructions.md', + ] + }, + { + path: `${rootFolder}/.github/instructions/level3.instructions.md`, + contents: [ + 'Level 3', + ] + }, + ]); + + const level1Uri = URI.joinPath(rootFolderUri, '.github/instructions/level1.instructions.md'); + const level2Uri = URI.joinPath(rootFolderUri, '.github/instructions/level2.instructions.md'); + const level3Uri = URI.joinPath(rootFolderUri, '.github/instructions/level3.instructions.md'); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const paths = variables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(v => isPromptFileVariableEntry(v) ? v.value.path : undefined); + + assert.ok(paths.includes(level1Uri.path), 'Should include level 1'); + assert.ok(paths.includes(level2Uri.path), 'Should include level 2'); + assert.ok(paths.includes(level3Uri.path), 'Should include level 3'); + }); + }); + + suite('telemetry', () => { + test('should emit telemetry event with counts', async () => { + const rootFolderName = 'telemetry-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/typescript.instructions.md`, + contents: [ + '---', + 'applyTo: "**/*.ts"', + '---', + 'TS instructions [](./referenced.instructions.md)', + ] + }, + { + path: `${rootFolder}/.github/instructions/referenced.instructions.md`, + contents: ['Referenced content'], + }, + { + path: `${rootFolder}/.github/copilot-instructions.md`, + contents: ['Copilot instructions'], + }, + { + path: `${rootFolder}/AGENTS.md`, + contents: ['Agent instructions'], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const telemetryEvents: { eventName: string; data: unknown }[] = []; + const mockTelemetryService = { + publicLog2: (eventName: string, data: unknown) => { + telemetryEvents.push({ eventName, data }); + } + } as unknown as ITelemetryService; + instaService.stub(ITelemetryService, mockTelemetryService); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await contextComputer.collect(variables, CancellationToken.None); + + const telemetryEvent = telemetryEvents.find(e => e.eventName === 'instructionsCollected'); + assert.ok(telemetryEvent, 'Should emit telemetry event'); + const data = telemetryEvent.data as { + applyingInstructionsCount: number; + referencedInstructionsCount: number; + agentInstructionsCount: number; + totalInstructionsCount: number; + }; + assert.ok(data.applyingInstructionsCount >= 0, 'Should have applying count'); + assert.ok(data.referencedInstructionsCount >= 0, 'Should have referenced count'); + assert.ok(data.agentInstructionsCount >= 0, 'Should have agent count'); + assert.ok(data.totalInstructionsCount >= 0, 'Should have total count'); + }); + }); + + suite('instructions list variable', () => { + function xmlContents(text: string, tag: string): string[] { + const regex = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'g'); + const matches = []; + let match; + while ((match = regex.exec(text)) !== null) { + matches.push(match[1].trim()); + } + return matches; + } + + function getFilePath(path: string): string { + return URI.file(path).fsPath; + } + + test('should generate instructions list when readFile tool available', async () => { + const rootFolderName = 'instructions-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/test.instructions.md`, + contents: [ + '---', + 'description: \'Test instructions\'', + 'applyTo: "**/*.ts"', + '---', + 'Test content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + { 'vscode_readFile': true }, // Enable readFile tool + undefined + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for instructions list'); + + const instructionsList = xmlContents(textVariables[0].value, 'instructions'); + assert.equal(instructionsList.length, 1, 'There should be one instructions list'); + + const instructions = xmlContents(instructionsList[0], 'instruction'); + assert.equal(instructions.length, 1, 'There should be one instruction'); + + assert.equal(xmlContents(instructions[0], 'description')[0], 'Test instructions'); + assert.equal(xmlContents(instructions[0], 'file')[0], getFilePath(`${rootFolder}/.github/instructions/test.instructions.md`)); + assert.equal(xmlContents(instructions[0], 'applyTo')[0], '**/*.ts'); + }); + + test('should include agents list when runSubagent tool available', async () => { + const rootFolderName = 'agents-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Enable the config for custom agents + testConfigService.setUserConfiguration('chat.customAgentInSubagent.enabled', true); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/test-agent-1.agent.md`, + contents: [ + '---', + 'description: \'Test agent 1\'', + 'user-invokable: true', + 'disable-model-invocation: false', + '---', + 'Test agent content', + ] + }, + { + path: `${rootFolder}/.github/agents/test-agent-2.agent.md`, + contents: [ + '---', + 'description: \'Test agent 2\'', + 'user-invokable: true', + 'disable-model-invocation: true', + '---', + 'Test agent content', + ] + }, + { + path: `${rootFolder}/.github/agents/test-agent-3.agent.md`, + contents: [ + '---', + 'description: \'Test agent 3\'', + 'user-invokable: false', + 'disable-model-invocation: false', + '---', + 'Test agent content', + ] + }, + { + path: `${rootFolder}/.github/agents/test-agent-4.agent.md`, + contents: [ + '---', + 'description: \'Test agent 4\'', + 'user-invokable: false', + 'disable-model-invocation: true', + '---', + 'Test agent content', + ] + }, + { + path: `${rootFolder}/.github/agents/test-agent-5.agent.md`, + contents: [ + '---', + 'description: \'Test agent 5\'', + '---', + 'Test agent content', + ] + } + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + { 'vscode_runSubagent': true }, // Enable runSubagent tool + ['*'] // Enable all subagents + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for agents list'); + + const agentsList = xmlContents(textVariables[0].value, 'agents'); + assert.equal(agentsList.length, 1, 'There should be one agents list'); + + const agents = xmlContents(agentsList[0], 'agent'); + assert.equal(agents.length, 3, 'There should be three agents'); + + assert.equal(xmlContents(agents[0], 'description')[0], 'Test agent 1'); + assert.equal(xmlContents(agents[0], 'name')[0], `test-agent-1`); + + assert.equal(xmlContents(agents[1], 'description')[0], 'Test agent 3'); + assert.equal(xmlContents(agents[1], 'name')[0], `test-agent-3`); + + assert.equal(xmlContents(agents[2], 'description')[0], 'Test agent 5'); + assert.equal(xmlContents(agents[2], 'name')[0], `test-agent-5`); + }); + + test('should include skills list when readFile tool available', async () => { + const rootFolderName = 'skills-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Enable the config for agent skills + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.claude/skills/javascript/SKILL.md`, + contents: [ + '---', + 'name: \'javascript\'', + 'description: \'JavaScript best practices\'', + '---', + 'JavaScript skill content', + ] + }, + { + path: `${rootFolder}/.claude/skills/typescript/SKILL.md`, + contents: [ + '---', + 'name: \'typescript\'', + 'description: \'TypeScript best practices\'', + '---', + 'TypeScript skill content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + { 'vscode_readFile': true }, // Enable readFile tool + undefined + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 1, 'There should be one text variable for skills list'); + + const skillsList = xmlContents(textVariables[0].value, 'skills'); + assert.equal(skillsList.length, 1, 'There should be one skills list'); + + const skills = xmlContents(skillsList[0], 'skill'); + assert.equal(skills.length, 2, 'There should be two skills'); + + assert.equal(xmlContents(skills[0], 'description')[0], 'JavaScript best practices'); + assert.equal(xmlContents(skills[0], 'file')[0], getFilePath(`${rootFolder}/.claude/skills/javascript/SKILL.md`)); + assert.equal(xmlContents(skills[0], 'name')[0], 'javascript'); + + assert.equal(xmlContents(skills[1], 'description')[0], 'TypeScript best practices'); + assert.equal(xmlContents(skills[1], 'file')[0], getFilePath(`${rootFolder}/.claude/skills/typescript/SKILL.md`)); + assert.equal(xmlContents(skills[1], 'name')[0], 'typescript'); + }); + + test('should not include skills list when readFile tool unavailable', async () => { + const rootFolderName = 'no-skills-list-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Enable the config for agent skills + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/javascript/SKILL.md`, + contents: [ + '---', + 'description: \'JavaScript best practices\'', + '---', + 'JavaScript skill content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + undefined, // No tools available + undefined + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 0, 'There should be no text variables when readFile tool is unavailable'); + }); + + test('should not include skills list when USE_AGENT_SKILLS disabled', async () => { + const rootFolderName = 'skills-disabled-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Disable the config for agent skills + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, false); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/javascript/SKILL.md`, + contents: [ + '---', + 'description: \'JavaScript best practices\'', + '---', + 'JavaScript skill content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + { 'vscode_readFile': true }, // Enable readFile tool + undefined + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + assert.equal(textVariables.length, 0, 'There should be no text variables when readFile tool is unavailable'); + }); + + test('should include skills from home folder in skills list', async () => { + const rootFolderName = 'home-skills-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Enable the config for agent skills + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + // Disable workspace skills to isolate home folder skills + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { + '.github/skills': false, + '.claude/skills': false, + }); + + await mockFiles(fileService, [ + // Home folder skills (using the mock user home /home/user) + { + path: '/home/user/.copilot/skills/personal-skill/SKILL.md', + contents: [ + '---', + 'name: \'personal-skill\'', + 'description: \'A personal skill from home folder\'', + '---', + 'Personal skill content', + ] + }, + { + path: '/home/user/.claude/skills/claude-personal/SKILL.md', + contents: [ + '---', + 'name: \'claude-personal\'', + 'description: \'A Claude personal skill\'', + '---', + 'Claude personal skill content', + ] + }, + ]); + + const contextComputer = instaService.createInstance( + ComputeAutomaticInstructions, + ChatMode.Agent, + { 'vscode_readFile': true }, // Enable readFile tool + undefined + ); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + const textVariables = variables.asArray().filter(v => isPromptTextVariableEntry(v)); + const skillsList = xmlContents(textVariables[0].value, 'skills'); + assert.equal(skillsList.length, 1, 'There should be one skills list'); + + const skills = xmlContents(skillsList[0], 'skill'); + assert.equal(skills.length, 2, 'There should be two skills'); + + assert.equal(xmlContents(skills[0], 'description')[0], 'A personal skill from home folder'); + assert.equal(xmlContents(skills[0], 'file')[0], getFilePath(`/home/user/.copilot/skills/personal-skill/SKILL.md`)); + assert.equal(xmlContents(skills[0], 'name')[0], 'personal-skill'); + + assert.equal(xmlContents(skills[1], 'description')[0], 'A Claude personal skill'); + assert.equal(xmlContents(skills[1], 'file')[0], getFilePath(`/home/user/.claude/skills/claude-personal/SKILL.md`)); + assert.equal(xmlContents(skills[1], 'name')[0], 'claude-personal'); + }); + }); + + suite('edge cases', () => { + test('should handle empty workspace', async () => { + const rootFolderName = 'empty-workspace'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + + await contextComputer.collect(variables, CancellationToken.None); + + // Should not throw and should handle gracefully + assert.ok(true, 'Should handle empty workspace without errors'); + }); + + test('should handle malformed instruction files', async () => { + const rootFolderName = 'malformed-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/instructions/malformed.instructions.md`, + contents: [ + '---', + 'invalid yaml: [unclosed', + '---', + 'Content', + ] + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + // Should not throw + await contextComputer.collect(variables, CancellationToken.None); + assert.ok(true, 'Should handle malformed instruction files gracefully'); + }); + + test('should handle cancellation', async () => { + const rootFolderName = 'cancellation-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/src/file.ts`, + contents: ['code'], + }, + ]); + + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); + const variables = new ChatRequestVariableSet(); + variables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + // Create a cancelled token + const cancelledToken: CancellationToken = { + isCancellationRequested: true, + onCancellationRequested: Event.None + }; + + // Should handle cancellation gracefully + await contextComputer.collect(variables, cancelledToken); + assert.ok(true, 'Should handle cancellation without errors'); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/config.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/config.test.ts index 98302f3211c..36f585c0877 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/config.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/config.test.ts @@ -9,6 +9,14 @@ import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js' import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { IConfigurationOverrides, IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'; +import { IPromptSourceFolder } from '../../../../common/promptSyntax/config/promptFileLocations.js'; + +/** + * Helper to extract just the paths from IPromptSourceFolder array for testing. + */ +function getPaths(folders: IPromptSourceFolder[]): string[] { + return folders.map(f => f.path); +} /** * Mocked instance of {@link IConfigurationService}. @@ -22,7 +30,7 @@ function createMock(value: T): IConfigurationService { ); assert( - [PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY, PromptsConfig.MODE_LOCATION_KEY].includes(key), + [PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY, PromptsConfig.MODE_LOCATION_KEY, PromptsConfig.SKILLS_LOCATION_KEY].includes(key), `Unsupported configuration key '${key}'.`, ); @@ -55,6 +63,26 @@ suite('PromptsConfig', () => { ); }); + test('undefined for skill', () => { + const configService = createMock(undefined); + + assert.strictEqual( + PromptsConfig.getLocationsValue(configService, PromptsType.skill), + undefined, + 'Must read correct value for skills.', + ); + }); + + test('null for skill', () => { + const configService = createMock(null); + + assert.strictEqual( + PromptsConfig.getLocationsValue(configService, PromptsType.skill), + undefined, + 'Must read correct value for skills.', + ); + }); + suite('object', () => { test('empty', () => { assert.deepStrictEqual( @@ -157,6 +185,50 @@ suite('PromptsConfig', () => { 'Must read correct value.', ); }); + + test('skill locations - empty', () => { + assert.deepStrictEqual( + PromptsConfig.getLocationsValue(createMock({}), PromptsType.skill), + {}, + 'Must read correct value for skills.', + ); + }); + + test('skill locations - valid paths', () => { + assert.deepStrictEqual( + PromptsConfig.getLocationsValue(createMock({ + '.github/skills': true, + '.claude/skills': true, + '/custom/skills/folder': true, + './relative/skills': true, + }), PromptsType.skill), + { + '.github/skills': true, + '.claude/skills': true, + '/custom/skills/folder': true, + './relative/skills': true, + }, + 'Must read correct skill locations.', + ); + }); + + test('skill locations - filters invalid entries', () => { + assert.deepStrictEqual( + PromptsConfig.getLocationsValue(createMock({ + '.github/skills': true, + '.claude/skills': '\t\n', + '/invalid/path': '', + '': true, + './valid/skills': true, + '\n': true, + }), PromptsType.skill), + { + '.github/skills': true, + './valid/skills': true, + }, + 'Must filter invalid skill locations.', + ); + }); }); }); @@ -165,7 +237,7 @@ suite('PromptsConfig', () => { const configService = createMock(undefined); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService, PromptsType.prompt), + getPaths(PromptsConfig.promptSourceFolders(configService, PromptsType.prompt)), [], 'Must read correct value.', ); @@ -175,7 +247,7 @@ suite('PromptsConfig', () => { const configService = createMock(null); assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(configService, PromptsType.prompt), + getPaths(PromptsConfig.promptSourceFolders(configService, PromptsType.prompt)), [], 'Must read correct value.', ); @@ -184,7 +256,7 @@ suite('PromptsConfig', () => { suite('object', () => { test('empty', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({}), PromptsType.prompt), + getPaths(PromptsConfig.promptSourceFolders(createMock({}), PromptsType.prompt)), ['.github/prompts'], 'Must read correct value.', ); @@ -192,7 +264,7 @@ suite('PromptsConfig', () => { test('only valid strings', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({ + getPaths(PromptsConfig.promptSourceFolders(createMock({ '/root/.bashrc': true, '../../folder/.hidden-folder/config.xml': true, '/srv/www/Public_html/.htaccess': true, @@ -206,7 +278,7 @@ suite('PromptsConfig', () => { '/var/logs/app.01.05.error': true, '.GitHub/prompts': true, './.tempfile': true, - }), PromptsType.prompt), + }), PromptsType.prompt)), [ '.github/prompts', '/root/.bashrc', @@ -229,7 +301,7 @@ suite('PromptsConfig', () => { test('filters out non valid entries', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({ + getPaths(PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/img/logo.v2.png': true, @@ -254,7 +326,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': 2345, - }), PromptsType.prompt), + }), PromptsType.prompt)), [ '.github/prompts', '../assets/img/logo.v2.png', @@ -271,7 +343,7 @@ suite('PromptsConfig', () => { test('only invalid or false values', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({ + getPaths(PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '../assets/IMG/logo.v2.png': '', @@ -282,7 +354,7 @@ suite('PromptsConfig', () => { '/var/data/datafile.2025-02-05.json': '\n', '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': 7654, - }), PromptsType.prompt), + }), PromptsType.prompt)), [ '.github/prompts', ], @@ -292,7 +364,7 @@ suite('PromptsConfig', () => { test('filters out disabled default location', () => { assert.deepStrictEqual( - PromptsConfig.promptSourceFolders(createMock({ + getPaths(PromptsConfig.promptSourceFolders(createMock({ '/etc/hosts.backup': '\t\n\t', './run.tests.sh': '\v', '.github/prompts': false, @@ -317,7 +389,7 @@ suite('PromptsConfig', () => { '\f\f': true, '../lib/some_library.v1.0.1.so': '\r\n', '/dev/shm/.shared_resource': 853, - }), PromptsType.prompt), + }), PromptsType.prompt)), [ '../assets/img/logo.v2.png', '../.local/bin/script.sh', @@ -331,5 +403,126 @@ suite('PromptsConfig', () => { ); }); }); + + suite('skills', () => { + test('undefined returns empty array', () => { + const configService = createMock(undefined); + + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(configService, PromptsType.skill)), + [], + 'Must return empty array for undefined config.', + ); + }); + + test('null returns empty array', () => { + const configService = createMock(null); + + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(configService, PromptsType.skill)), + [], + 'Must return empty array for null config.', + ); + }); + + test('empty object returns default skill folders', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({}), PromptsType.skill)), + ['.github/skills', '.claude/skills', '~/.copilot/skills', '~/.claude/skills'], + 'Must return default skill folders.', + ); + }); + + test('includes custom skill folders', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({ + '/custom/skills': true, + './local/skills': true, + }), PromptsType.skill)), + [ + '.github/skills', + '.claude/skills', + '~/.copilot/skills', + '~/.claude/skills', + '/custom/skills', + './local/skills', + ], + 'Must include custom skill folders.', + ); + }); + + test('filters out disabled default skill folders', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({ + '.github/skills': false, + '/custom/skills': true, + }), PromptsType.skill)), + [ + '.claude/skills', + '~/.copilot/skills', + '~/.claude/skills', + '/custom/skills', + ], + 'Must filter out disabled .github/skills folder.', + ); + }); + + test('filters out all disabled default skill folders', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({ + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + '/only/custom/skills': true, + }), PromptsType.skill)), + [ + '/only/custom/skills', + ], + 'Must filter out all disabled default folders.', + ); + }); + + test('filters out invalid entries', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({ + '/valid/skills': true, + '/invalid/path': '\t\n', + '': true, + './another/valid': true, + '\n': true, + }), PromptsType.skill)), + [ + '.github/skills', + '.claude/skills', + '~/.copilot/skills', + '~/.claude/skills', + '/valid/skills', + './another/valid', + ], + 'Must filter out invalid entries.', + ); + }); + + test('includes all default folders when explicitly enabled', () => { + assert.deepStrictEqual( + getPaths(PromptsConfig.promptSourceFolders(createMock({ + '.github/skills': true, + '.claude/skills': true, + '~/.copilot/skills': true, + '~/.claude/skills': true, + '/extra/skills': true, + }), PromptsType.skill)), + [ + '.github/skills', + '.claude/skills', + '~/.copilot/skills', + '~/.claude/skills', + '/extra/skills', + ], + 'Must include all default folders.', + ); + }); + }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts new file mode 100644 index 00000000000..0a3dc060d7e --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/config/promptFileLocations.test.ts @@ -0,0 +1,166 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { getPromptFileType, getCleanPromptName, isPromptOrInstructionsFile } from '../../../../common/promptSyntax/config/promptFileLocations.js'; +import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; + +suite('promptFileLocations', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('getPromptFileType', () => { + test('.prompt.md files', () => { + const uri = URI.file('/workspace/test.prompt.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.prompt); + }); + + test('.instructions.md files', () => { + const uri = URI.file('/workspace/test.instructions.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.instructions); + }); + + test('.agent.md files', () => { + const uri = URI.file('/workspace/test.agent.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('.chatmode.md files (legacy)', () => { + const uri = URI.file('/workspace/test.chatmode.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('.md files in .github/agents/ folder should be recognized as agent files', () => { + const uri = URI.file('/workspace/.github/agents/demonstrate.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.agent); + }); + + test('README.md in .github/agents/ should NOT be recognized as agent file', () => { + const uri = URI.file('/workspace/.github/agents/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in .github/agents/ subfolder should NOT be recognized as agent files', () => { + const uri = URI.file('/workspace/.github/agents/subfolder/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files outside .github/agents/ should not be recognized as agent files', () => { + const uri = URI.file('/workspace/test/foo.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('.md files in other .github/ subfolders should not be recognized as agent files', () => { + const uri = URI.file('/workspace/.github/prompts/test.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('copilot-instructions.md should be recognized as instructions', () => { + const uri = URI.file('/workspace/.github/copilot-instructions.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.instructions); + }); + + test('regular .md files should return undefined', () => { + const uri = URI.file('/workspace/README.md'); + assert.strictEqual(getPromptFileType(uri), undefined); + }); + + test('SKILL.md (uppercase) should be recognized as skill', () => { + const uri = URI.file('/workspace/.github/skills/test/SKILL.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.skill); + }); + + test('skill.md (lowercase) should be recognized as skill', () => { + const uri = URI.file('/workspace/.github/skills/test/skill.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.skill); + }); + + test('Skill.md (mixed case) should be recognized as skill', () => { + const uri = URI.file('/workspace/.github/skills/test/Skill.md'); + assert.strictEqual(getPromptFileType(uri), PromptsType.skill); + }); + }); + + suite('getCleanPromptName', () => { + test('removes .prompt.md extension', () => { + const uri = URI.file('/workspace/test.prompt.md'); + assert.strictEqual(getCleanPromptName(uri), 'test'); + }); + + test('removes .instructions.md extension', () => { + const uri = URI.file('/workspace/test.instructions.md'); + assert.strictEqual(getCleanPromptName(uri), 'test'); + }); + + test('removes .agent.md extension', () => { + const uri = URI.file('/workspace/test.agent.md'); + assert.strictEqual(getCleanPromptName(uri), 'test'); + }); + + test('removes .chatmode.md extension (legacy)', () => { + const uri = URI.file('/workspace/test.chatmode.md'); + assert.strictEqual(getCleanPromptName(uri), 'test'); + }); + + test('removes .md extension for files in .github/agents/', () => { + const uri = URI.file('/workspace/.github/agents/demonstrate.md'); + assert.strictEqual(getCleanPromptName(uri), 'demonstrate'); + }); + + test('README.md in .github/agents/ should keep .md extension', () => { + const uri = URI.file('/workspace/.github/agents/README.md'); + assert.strictEqual(getCleanPromptName(uri), 'README.md'); + }); + + test('removes .md extension for copilot-instructions.md', () => { + const uri = URI.file('/workspace/.github/copilot-instructions.md'); + assert.strictEqual(getCleanPromptName(uri), 'copilot-instructions'); + }); + + test('keeps .md extension for regular files', () => { + const uri = URI.file('/workspace/README.md'); + assert.strictEqual(getCleanPromptName(uri), 'README.md'); + }); + + test('keeps full filename for files without known extensions', () => { + const uri = URI.file('/workspace/test.txt'); + assert.strictEqual(getCleanPromptName(uri), 'test.txt'); + }); + + test('removes .md extension for SKILL.md (uppercase)', () => { + const uri = URI.file('/workspace/.github/skills/test/SKILL.md'); + assert.strictEqual(getCleanPromptName(uri), 'SKILL'); + }); + + test('removes .md extension for skill.md (lowercase)', () => { + const uri = URI.file('/workspace/.github/skills/test/skill.md'); + assert.strictEqual(getCleanPromptName(uri), 'skill'); + }); + + test('removes .md extension for Skill.md (mixed case)', () => { + const uri = URI.file('/workspace/.github/skills/test/Skill.md'); + assert.strictEqual(getCleanPromptName(uri), 'Skill'); + }); + }); + + suite('isPromptOrInstructionsFile', () => { + test('SKILL.md files should return true', () => { + assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.github/skills/test/SKILL.md')), true); + }); + + test('skill.md (lowercase) should return true', () => { + assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/.claude/skills/myskill/skill.md')), true); + }); + + test('Skill.md (mixed case) should return true', () => { + assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/skills/Skill.md')), true); + }); + + test('regular .md files should return false', () => { + assert.strictEqual(isPromptOrInstructionsFile(URI.file('/workspace/SKILL2.md')), false); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileLocations.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileLocations.test.ts deleted file mode 100644 index f850fd6fb2f..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/promptFileLocations.test.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 'assert'; -import { URI } from '../../../../../../base/common/uri.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { getPromptFileType, getCleanPromptName } from '../../../common/promptSyntax/config/promptFileLocations.js'; -import { PromptsType } from '../../../common/promptSyntax/promptTypes.js'; - -suite('promptFileLocations', function () { - ensureNoDisposablesAreLeakedInTestSuite(); - - suite('getPromptFileType', () => { - test('.prompt.md files', () => { - const uri = URI.file('/workspace/test.prompt.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.prompt); - }); - - test('.instructions.md files', () => { - const uri = URI.file('/workspace/test.instructions.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.instructions); - }); - - test('.agent.md files', () => { - const uri = URI.file('/workspace/test.agent.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.agent); - }); - - test('.chatmode.md files (legacy)', () => { - const uri = URI.file('/workspace/test.chatmode.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.agent); - }); - - test('.md files in .github/agents/ folder should be recognized as agent files', () => { - const uri = URI.file('/workspace/.github/agents/demonstrate.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.agent); - }); - - test('.md files in .github/agents/ subfolder should NOT be recognized as agent files', () => { - const uri = URI.file('/workspace/.github/agents/subfolder/test.md'); - assert.strictEqual(getPromptFileType(uri), undefined); - }); - - test('.md files outside .github/agents/ should not be recognized as agent files', () => { - const uri = URI.file('/workspace/test/foo.md'); - assert.strictEqual(getPromptFileType(uri), undefined); - }); - - test('.md files in other .github/ subfolders should not be recognized as agent files', () => { - const uri = URI.file('/workspace/.github/prompts/test.md'); - assert.strictEqual(getPromptFileType(uri), undefined); - }); - - test('copilot-instructions.md should be recognized as instructions', () => { - const uri = URI.file('/workspace/.github/copilot-instructions.md'); - assert.strictEqual(getPromptFileType(uri), PromptsType.instructions); - }); - - test('regular .md files should return undefined', () => { - const uri = URI.file('/workspace/README.md'); - assert.strictEqual(getPromptFileType(uri), undefined); - }); - }); - - suite('getCleanPromptName', () => { - test('removes .prompt.md extension', () => { - const uri = URI.file('/workspace/test.prompt.md'); - assert.strictEqual(getCleanPromptName(uri), 'test'); - }); - - test('removes .instructions.md extension', () => { - const uri = URI.file('/workspace/test.instructions.md'); - assert.strictEqual(getCleanPromptName(uri), 'test'); - }); - - test('removes .agent.md extension', () => { - const uri = URI.file('/workspace/test.agent.md'); - assert.strictEqual(getCleanPromptName(uri), 'test'); - }); - - test('removes .chatmode.md extension (legacy)', () => { - const uri = URI.file('/workspace/test.chatmode.md'); - assert.strictEqual(getCleanPromptName(uri), 'test'); - }); - - test('removes .md extension for files in .github/agents/', () => { - const uri = URI.file('/workspace/.github/agents/demonstrate.md'); - assert.strictEqual(getCleanPromptName(uri), 'demonstrate'); - }); - - test('removes .md extension for copilot-instructions.md', () => { - const uri = URI.file('/workspace/.github/copilot-instructions.md'); - assert.strictEqual(getCleanPromptName(uri), 'copilot-instructions'); - }); - - test('keeps .md extension for regular files', () => { - const uri = URI.file('/workspace/README.md'); - assert.strictEqual(getCleanPromptName(uri), 'README.md'); - }); - - test('keeps full filename for files without known extensions', () => { - const uri = URI.file('/workspace/test.txt'); - assert.strictEqual(getCleanPromptName(uri), 'test.txt'); - }); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts new file mode 100644 index 00000000000..1fb0c86c423 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/mockPromptsService.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Emitter, Event } from '../../../../../../../base/common/event.js'; +import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ITextModel } from '../../../../../../../editor/common/model.js'; +import { IExtensionDescription } from '../../../../../../../platform/extensions/common/extensions.js'; +import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; +import { ParsedPromptFile } from '../../../../common/promptSyntax/promptFileParser.js'; +import { IAgentSkill, ICustomAgent, IPromptFileContext, IPromptFileResource, IPromptPath, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ResourceSet } from '../../../../../../../base/common/map.js'; + +export class MockPromptsService implements IPromptsService { + + _serviceBrand: undefined; + + private readonly _onDidChangeCustomChatModes = new Emitter(); + readonly onDidChangeCustomAgents = this._onDidChangeCustomChatModes.event; + + private _customModes: ICustomAgent[] = []; + + setCustomModes(modes: ICustomAgent[]): void { + this._customModes = modes; + this._onDidChangeCustomChatModes.fire(); + } + + async getCustomAgents(token: CancellationToken): Promise { + return this._customModes; + } + + // Stub implementations for required interface methods + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getSyntaxParserFor(_model: any): any { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listPromptFiles(_type: any): Promise { throw new Error('Not implemented'); } + listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getSourceFolders(_type: any): Promise { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getResolvedSourceFolders(_type: any): Promise { throw new Error('Not implemented'); } + isValidSlashCommandName(_command: string): boolean { return false; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolvePromptSlashCommand(command: string, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + get onDidChangeSlashCommands(): Event { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getPromptSlashCommands(_token: CancellationToken): Promise { throw new Error('Not implemented'); } + getPromptSlashCommandName(uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parse(_uri: URI, _type: any, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseNew(_uri: URI, _token: CancellationToken): Promise { throw new Error('Not implemented'); } + getParsedPromptFile(textModel: ITextModel): ParsedPromptFile { throw new Error('Not implemented'); } + registerContributedFile(type: PromptsType, uri: URI, extension: IExtensionDescription, name: string | undefined, description: string | undefined): IDisposable { throw new Error('Not implemented'); } + getPromptLocationLabel(promptPath: IPromptPath): string { throw new Error('Not implemented'); } + findAgentMDsInWorkspace(token: CancellationToken): Promise { throw new Error('Not implemented'); } + listAgentMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } + listCopilotInstructionsMDs(token: CancellationToken): Promise { throw new Error('Not implemented'); } + getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { throw new Error('Not implemented'); } + getDisabledPromptFiles(type: PromptsType): ResourceSet { throw new Error('Method not implemented.'); } + setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void { throw new Error('Method not implemented.'); } + registerPromptFileProvider(extension: IExtensionDescription, type: PromptsType, provider: { providePromptFiles: (context: IPromptFileContext, token: CancellationToken) => Promise }): IDisposable { throw new Error('Method not implemented.'); } + findAgentSkills(token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getPromptDiscoveryInfo(_type: any, _token: CancellationToken): Promise { throw new Error('Method not implemented.'); } + dispose(): void { } +} diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts index 7f761c90a59..0c70640aadb 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/newPromptsParser.test.ts @@ -22,7 +22,7 @@ suite('NewPromptsParser', () => { /* 04 */`tools: ['tool1', 'tool2']`, /* 05 */'---', /* 06 */'This is an agent test.', - /* 07 */'Here is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md).', + /* 07 */'Here is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md) and an image ![image](./image.png).', ].join('\n'); const result = new PromptFileParser().parse(uri, content); assert.deepEqual(result.uri, uri); @@ -42,7 +42,7 @@ suite('NewPromptsParser', () => { ]); assert.deepEqual(result.body.range, { startLineNumber: 6, startColumn: 1, endLineNumber: 8, endColumn: 1 }); assert.equal(result.body.offset, 75); - assert.equal(result.body.getContent(), 'This is an agent test.\nHere is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md).'); + assert.equal(result.body.getContent(), 'This is an agent test.\nHere is a #tool:tool1 variable (and one with closing parenthesis after: #tool:tool-2) and a #file:./reference1.md as well as a [reference](./reference2.md) and an image ![image](./image.png).'); assert.deepEqual(result.body.fileReferences, [ { range: new Range(7, 99, 7, 114), content: './reference1.md', isMarkdownLink: false }, @@ -53,7 +53,7 @@ suite('NewPromptsParser', () => { { range: new Range(7, 79, 7, 85), name: 'tool-2', offset: 170 } ]); assert.deepEqual(result.header.description, 'Agent test'); - assert.deepEqual(result.header.model, 'GPT 4.1'); + assert.deepEqual(result.header.model, ['GPT 4.1']); assert.ok(result.header.tools); assert.deepEqual(result.header.tools, ['tool1', 'tool2']); }); @@ -110,7 +110,7 @@ suite('NewPromptsParser', () => { }, ]); assert.deepEqual(result.header.description, 'Agent test'); - assert.deepEqual(result.header.model, 'GPT 4.1'); + assert.deepEqual(result.header.model, ['GPT 4.1']); assert.ok(result.header.handOffs); assert.deepEqual(result.header.handOffs, [ { label: 'Implement', agent: 'Default', prompt: 'Implement the plan', send: false }, @@ -118,6 +118,53 @@ suite('NewPromptsParser', () => { ]); }); + test('mode with handoff and showContinueOn per handoff', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + /* 01 */'---', + /* 02 */`description: "Agent test"`, + /* 03 */'model: GPT 4.1', + /* 04 */'handoffs:', + /* 05 */' - label: "Implement"', + /* 06 */' agent: Default', + /* 07 */' prompt: "Implement the plan"', + /* 08 */' send: false', + /* 09 */' showContinueOn: false', + /* 10 */' - label: "Save"', + /* 11 */' agent: Default', + /* 12 */' prompt: "Save the plan"', + /* 13 */' send: true', + /* 14 */' showContinueOn: true', + /* 15 */'---', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.ok(result.header.handOffs); + assert.deepEqual(result.header.handOffs, [ + { label: 'Implement', agent: 'Default', prompt: 'Implement the plan', send: false, showContinueOn: false }, + { label: 'Save', agent: 'Default', prompt: 'Save the plan', send: true, showContinueOn: true } + ]); + }); + + test('showContinueOn defaults to undefined when not specified per handoff', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + /* 01 */'---', + /* 02 */`description: "Agent test"`, + /* 03 */'handoffs:', + /* 04 */' - label: "Save"', + /* 05 */' agent: Default', + /* 06 */' prompt: "Save the plan"', + /* 07 */'---', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.ok(result.header.handOffs); + assert.deepEqual(result.header.handOffs[0].showContinueOn, undefined); + }); + test('instructions', async () => { const uri = URI.parse('file:///test/prompt1.md'); const content = [ @@ -187,7 +234,7 @@ suite('NewPromptsParser', () => { ]); assert.deepEqual(result.header.description, 'General purpose coding assistant'); assert.deepEqual(result.header.agent, 'agent'); - assert.deepEqual(result.header.model, 'GPT 4.1'); + assert.deepEqual(result.header.model, ['GPT 4.1']); assert.ok(result.header.tools); assert.deepEqual(result.header.tools, ['search', 'terminal']); }); @@ -261,4 +308,68 @@ suite('NewPromptsParser', () => { assert.ok(result.header.tools); assert.deepEqual(result.header.tools, ['built-in', 'browser-click', 'openPullRequest', 'copilotCodingAgent']); }); + + test('agent with agents', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with restrictions"`, + 'agents: ["subagent1", "subagent2"]', + '---', + 'This is an agent with restricted subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.ok(result.body); + assert.deepEqual(result.header.description, 'Agent with restrictions'); + assert.deepEqual(result.header.agents, ['subagent1', 'subagent2']); + }); + + test('agent with empty agents array', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with no access"`, + 'agents: []', + '---', + 'This agent has no access to subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent with no access'); + assert.deepEqual(result.header.agents, []); + }); + + test('agent with wildcard agents', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent with full access"`, + 'agents: ["*"]', + '---', + 'This agent has access to all subagents.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent with full access'); + assert.deepEqual(result.header.agents, ['*']); + }); + + test('agent without agents (undefined)', async () => { + const uri = URI.parse('file:///test/test.agent.md'); + const content = [ + '---', + `description: "Agent without restrictions"`, + '---', + 'This agent has default access to all.', + ].join('\n'); + const result = new PromptFileParser().parse(uri, content); + assert.deepEqual(result.uri, uri); + assert.ok(result.header); + assert.deepEqual(result.header.description, 'Agent without restrictions'); + assert.deepEqual(result.header.agents, undefined); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 169e0d8d8f7..58bce6f2a91 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -6,8 +6,11 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { Event } from '../../../../../../../base/common/event.js'; +import { match } from '../../../../../../../base/common/glob.js'; import { ResourceSet } from '../../../../../../../base/common/map.js'; import { Schemas } from '../../../../../../../base/common/network.js'; +import { relativePath } from '../../../../../../../base/common/resources.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; import { Range } from '../../../../../../../editor/common/core/range.js'; @@ -30,16 +33,21 @@ import { testWorkspace } from '../../../../../../../platform/workspace/test/comm import { IWorkbenchEnvironmentService } from '../../../../../../services/environment/common/environmentService.js'; import { IFilesConfigurationService } from '../../../../../../services/filesConfiguration/common/filesConfigurationService.js'; import { IUserDataProfileService } from '../../../../../../services/userDataProfile/common/userDataProfile.js'; +import { toUserDataProfile } from '../../../../../../../platform/userDataProfile/common/userDataProfile.js'; import { TestContextService, TestUserDataProfileService } from '../../../../../../test/common/workbenchTestServices.js'; -import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/chatVariableEntries.js'; +import { ChatRequestVariableSet, isPromptFileVariableEntry, toFileVariableEntry } from '../../../../common/attachments/chatVariableEntries.js'; import { ComputeAutomaticInstructions, newInstructionsCollectionEvent } from '../../../../common/promptSyntax/computeAutomaticInstructions.js'; import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { INSTRUCTION_FILE_EXTENSION, INSTRUCTIONS_DEFAULT_SOURCE_FOLDER, LEGACY_MODE_DEFAULT_SOURCE_FOLDER, PROMPT_DEFAULT_SOURCE_FOLDER, PROMPT_FILE_EXTENSION } from '../../../../common/promptSyntax/config/promptFileLocations.js'; import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID, PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { ICustomAgent, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; +import { ExtensionAgentSourceType, ICustomAgent, IPromptFileContext, IPromptsService, PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; import { PromptsService } from '../../../../common/promptSyntax/service/promptsServiceImpl.js'; -import { MockFilesystem } from '../testUtils/mockFilesystem.js'; +import { mockFiles } from '../testUtils/mockFilesystem.js'; import { InMemoryStorageService, IStorageService } from '../../../../../../../platform/storage/common/storage.js'; +import { IPathService } from '../../../../../../services/path/common/pathService.js'; +import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js'; +import { IExtensionService } from '../../../../../../services/extensions/common/extensions.js'; +import { ChatMode } from '../../../../common/chatModes.js'; suite('PromptsService', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); @@ -47,6 +55,8 @@ suite('PromptsService', () => { let service: IPromptsService; let instaService: TestInstantiationService; let workspaceContextService: TestContextService; + let testConfigService: TestConfigurationService; + let fileService: IFileService; setup(async () => { instaService = disposables.add(new TestInstantiationService()); @@ -55,10 +65,12 @@ suite('PromptsService', () => { workspaceContextService = new TestContextService(); instaService.stub(IWorkspaceContextService, workspaceContextService); - const testConfigService = new TestConfigurationService(); + testConfigService = new TestConfigurationService(); testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); testConfigService.setUserConfiguration(PromptsConfig.USE_NESTED_AGENT_MD, false); + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true }); @@ -68,8 +80,12 @@ suite('PromptsService', () => { instaService.stub(IUserDataProfileService, new TestUserDataProfileService()); instaService.stub(ITelemetryService, NullTelemetryService); instaService.stub(IStorageService, InMemoryStorageService); + instaService.stub(IExtensionService, { + whenInstalledExtensionsRegistered: () => Promise.resolve(true), + activateByEvent: () => Promise.resolve() + }); - const fileService = disposables.add(instaService.createInstance(FileService)); + fileService = disposables.add(instaService.createInstance(FileService)); instaService.stub(IFileService, fileService); const modelService = disposables.add(instaService.createInstance(ModelService)); @@ -94,6 +110,47 @@ suite('PromptsService', () => { instaService.stub(IFilesConfigurationService, { updateReadonly: () => Promise.resolve() }); + const pathService = { + userHome: (): URI | Promise => { + return Promise.resolve(URI.file('/home/user')); + }, + } as IPathService; + instaService.stub(IPathService, pathService); + + instaService.stub(ISearchService, { + schemeHasFileSearchProvider: () => true, + async fileSearch(query: IFileQuery) { + // mock the search service - recursively find files matching pattern + const findFilesInLocation = async (location: URI, results: URI[] = []): Promise => { + try { + const resolve = await fileService.resolve(location); + if (resolve.isFile) { + results.push(resolve.resource); + } else if (resolve.isDirectory && resolve.children) { + for (const child of resolve.children) { + await findFilesInLocation(child.resource, results); + } + } + } catch (error) { + // folder doesn't exist + } + return results; + }; + + const results: IFileMatch[] = []; + for (const folderQuery of query.folderQueries) { + const allFiles = await findFilesInLocation(folderQuery.folder); + for (const resource of allFiles) { + const pathInFolder = relativePath(folderQuery.folder, resource) ?? ''; + if (query.filePattern === undefined || match(query.filePattern, pathInFolder)) { + results.push({ resource }); + } + } + } + return { results, messages: [] }; + } + }); + service = disposables.add(instaService.createInstance(PromptsService)); instaService.stub(IPromptsService, service); }); @@ -111,107 +168,87 @@ suite('PromptsService', () => { const rootFileUri = URI.joinPath(rootFolderUri, rootFileName); - await (instaService.createInstance(MockFilesystem, - // the file structure to be created on the disk for the test - [{ - name: rootFolderName, - children: [ - { - name: 'file1.prompt.md', - contents: [ - '## Some Header', - 'some contents', - ' ', - ], - }, - { - name: rootFileName, - contents: [ - '---', - 'description: \'Root prompt description.\'', - 'tools: [\'my-tool1\', , true]', - 'agent: "agent" ', - '---', - '## Files', - '\t- this file #file:folder1/file3.prompt.md ', - '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', - '## Vars', - '\t- #tool:my-tool', - '\t- #tool:my-other-tool', - ' ', - ], - }, - { - name: 'folder1', - children: [ - { - name: 'file3.prompt.md', - contents: [ - '---', - 'tools: [ false, \'my-tool1\' , ]', - 'agent: \'edit\'', - '---', - '', - '[](./some-other-folder/non-existing-folder)', - `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.instructions.md contents`, - ' some more\t content', - ], - }, - { - name: 'some-other-folder', - children: [ - { - name: 'file4.prompt.md', - contents: [ - '---', - 'tools: [\'my-tool1\', "my-tool2", true, , ]', - 'something: true', - 'agent: \'ask\'\t', - 'description: "File 4 splendid description."', - '---', - 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', - '', - '', - 'and some', - ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', - ], - }, - { - name: 'file.txt', - contents: [ - '---', - 'description: "Non-prompt file description".', - 'tools: ["my-tool-24"]', - '---', - ], - }, - { - name: 'yetAnotherFolder🤭', - children: [ - { - name: 'another-file.instructions.md', - contents: [ - '---', - 'description: "Another file description."', - 'tools: [\'my-tool3\', false, "my-tool2" ]', - 'applyTo: "**/*.tsx"', - '---', - `[](${rootFolder}/folder1/some-other-folder)`, - 'another-file.instructions.md contents\t [#file:file.txt](../file.txt)', - ], - }, - { - name: 'one_more_file_just_in_case.prompt.md', - contents: 'one_more_file_just_in_case.prompt.md contents', - }, - ], - }, - ], - }, - ], - }, + await mockFiles(fileService, [ + { + path: `${rootFolder}/file1.prompt.md`, + contents: [ + '## Some Header', + 'some contents', + ' ', + ], + }, + { + path: `${rootFolder}/${rootFileName}`, + contents: [ + '---', + 'description: \'Root prompt description.\'', + 'tools: [\'my-tool1\', , true]', + 'agent: "agent" ', + '---', + '## Files', + '\t- this file #file:folder1/file3.prompt.md ', + '\t- also this [file4.prompt.md](./folder1/some-other-folder/file4.prompt.md) please!', + '## Vars', + '\t- #tool:my-tool', + '\t- #tool:my-other-tool', + ' ', + ], + }, + { + path: `${rootFolder}/folder1/file3.prompt.md`, + contents: [ + '---', + 'tools: [ false, \'my-tool1\' , ]', + 'agent: \'edit\'', + '---', + '', + '[](./some-other-folder/non-existing-folder)', + `\t- some seemingly random #file:${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.instructions.md contents`, + ' some more\t content', + ], + }, + { + path: `${rootFolder}/folder1/some-other-folder/file4.prompt.md`, + contents: [ + '---', + 'tools: [\'my-tool1\', "my-tool2", true, , ]', + 'something: true', + 'agent: \'ask\'\t', + 'description: "File 4 splendid description."', + '---', + 'this file has a non-existing #file:./some-non-existing/file.prompt.md\t\treference', + '', + '', + 'and some', + ' non-prompt #file:./some-non-prompt-file.md\t\t \t[](../../folder1/)\t', + ], + }, + { + path: `${rootFolder}/folder1/some-other-folder/file.txt`, + contents: [ + '---', + 'description: "Non-prompt file description".', + 'tools: ["my-tool-24"]', + '---', + ], + }, + { + path: `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/another-file.instructions.md`, + contents: [ + '---', + 'description: "Another file description."', + 'tools: [\'my-tool3\', false, "my-tool2" ]', + 'applyTo: "**/*.tsx"', + '---', + `[](${rootFolder}/folder1/some-other-folder)`, + 'another-file.instructions.md contents\t [#file:file.txt](../file.txt)', ], - }])).mock(); + }, + { + path: `${rootFolder}/folder1/some-other-folder/yetAnotherFolder🤭/one_more_file_just_in_case.prompt.md`, + contents: ['one_more_file_just_in_case.prompt.md contents'], + }, + ]); const file3 = URI.joinPath(rootFolderUri, 'folder1/file3.prompt.md'); const file4 = URI.joinPath(rootFolderUri, 'folder1/some-other-folder/file4.prompt.md'); @@ -327,124 +364,107 @@ suite('PromptsService', () => { ])); // mock current workspace file structure - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: 'file1.prompt.md', - contents: [ - '## Some Header', - 'some contents', - ' ', - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'file1.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 1.\'', - 'applyTo: "**/*.tsx"', - '---', - 'Some instructions 1 contents.', - ], - }, - { - name: 'file2.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 2.\'', - 'applyTo: "**/folder1/*.tsx"', - '---', - 'Some instructions 2 contents.', - ], - }, - { - name: 'file3.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 3.\'', - 'applyTo: "**/folder2/*.tsx"', - '---', - 'Some instructions 3 contents.', - ], - }, - { - name: 'file4.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 4.\'', - 'applyTo: "src/build/*.tsx"', - '---', - 'Some instructions 4 contents.', - ], - }, - { - name: 'file5.prompt.md', - contents: [ - '---', - 'description: \'Prompt file 5.\'', - '---', - 'Some prompt 5 contents.', - ], - }, - ], - }, - { - name: 'folder1', - children: [ - { - name: 'main.tsx', - contents: 'console.log("Haalou!")', - }, - ], - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/file1.prompt.md`, + contents: [ + '## Some Header', + 'some contents', + ' ', + ] + }, + { + path: `${rootFolder}/.github/prompts/file1.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 1.\'', + 'applyTo: "**/*.tsx"', + '---', + 'Some instructions 1 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file2.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 2.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 2 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file3.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 3.\'', + 'applyTo: "**/folder2/*.tsx"', + '---', + 'Some instructions 3 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file4.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 4.\'', + 'applyTo: "src/build/*.tsx"', + '---', + 'Some instructions 4 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file5.prompt.md`, + contents: [ + '---', + 'description: \'Prompt file 5.\'', + '---', + 'Some prompt 5 contents.', + ] + }, + { + path: `${rootFolder}/folder1/main.tsx`, + contents: [ + 'console.log("Haalou!")' + ] + } + ]); // mock user data instructions - await (instaService.createInstance(MockFilesystem, [ + await mockFiles(fileService, [ { - name: userPromptsFolderName, - children: [ - { - name: 'file10.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 10.\'', - 'applyTo: "**/folder1/*.tsx"', - '---', - 'Some instructions 10 contents.', - ], - }, - { - name: 'file11.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 11.\'', - 'applyTo: "**/folder1/*.py"', - '---', - 'Some instructions 11 contents.', - ], - }, - { - name: 'file12.prompt.md', - contents: [ - '---', - 'description: \'Prompt file 12.\'', - '---', - 'Some prompt 12 contents.', - ], - }, - ], + path: `${userPromptsFolderName}/file10.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 10.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 10 contents.', + ] + }, + { + path: `${userPromptsFolderName}/file11.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 11.\'', + 'applyTo: "**/folder1/*.py"', + '---', + 'Some instructions 11 contents.', + ] + }, + { + path: `${userPromptsFolderName}/file12.prompt.md`, + contents: [ + '---', + 'description: \'Prompt file 12.\'', + '---', + 'Some prompt 12 contents.', + ] } - ])).mock(); + ]); const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); const context = { files: new ResourceSet([ URI.joinPath(rootFolderUri, 'folder1/main.tsx'), @@ -515,124 +535,107 @@ suite('PromptsService', () => { ])); // mock current workspace file structure - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: 'file1.prompt.md', - contents: [ - '## Some Header', - 'some contents', - ' ', - ], - }, - { - name: '.github/prompts', - children: [ - { - name: 'file1.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 1.\'', - 'applyTo: "**/*.tsx"', - '---', - 'Some instructions 1 contents.', - ], - }, - { - name: 'file2.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 2.\'', - 'applyTo: "**/folder1/*.tsx"', - '---', - 'Some instructions 2 contents. [](./file1.instructions.md)', - ], - }, - { - name: 'file3.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 3.\'', - 'applyTo: "**/folder2/*.tsx"', - '---', - 'Some instructions 3 contents.', - ], - }, - { - name: 'file4.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 4.\'', - 'applyTo: "src/build/*.tsx"', - '---', - '[](./file3.instructions.md) Some instructions 4 contents.', - ], - }, - { - name: 'file5.prompt.md', - contents: [ - '---', - 'description: \'Prompt file 5.\'', - '---', - 'Some prompt 5 contents.', - ], - }, - ], - }, - { - name: 'folder1', - children: [ - { - name: 'main.tsx', - contents: 'console.log("Haalou!")', - }, - ], - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/file1.prompt.md`, + contents: [ + '## Some Header', + 'some contents', + ' ', + ] + }, + { + path: `${rootFolder}/.github/prompts/file1.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 1.\'', + 'applyTo: "**/*.tsx"', + '---', + 'Some instructions 1 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file2.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 2.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 2 contents. [](./file1.instructions.md)', + ] + }, + { + path: `${rootFolder}/.github/prompts/file3.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 3.\'', + 'applyTo: "**/folder2/*.tsx"', + '---', + 'Some instructions 3 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file4.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 4.\'', + 'applyTo: "src/build/*.tsx"', + '---', + '[](./file3.instructions.md) Some instructions 4 contents.', + ] + }, + { + path: `${rootFolder}/.github/prompts/file5.prompt.md`, + contents: [ + '---', + 'description: \'Prompt file 5.\'', + '---', + 'Some prompt 5 contents.', + ] + }, + { + path: `${rootFolder}/folder1/main.tsx`, + contents: [ + 'console.log("Haalou!")' + ] + } + ]); // mock user data instructions - await (instaService.createInstance(MockFilesystem, [ + await mockFiles(fileService, [ { - name: userPromptsFolderName, - children: [ - { - name: 'file10.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 10.\'', - 'applyTo: "**/folder1/*.tsx"', - '---', - 'Some instructions 10 contents.', - ], - }, - { - name: 'file11.instructions.md', - contents: [ - '---', - 'description: \'Instructions file 11.\'', - 'applyTo: "**/folder1/*.py"', - '---', - 'Some instructions 11 contents.', - ], - }, - { - name: 'file12.prompt.md', - contents: [ - '---', - 'description: \'Prompt file 12.\'', - '---', - 'Some prompt 12 contents.', - ], - }, - ], + path: `${userPromptsFolderName}/file10.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 10.\'', + 'applyTo: "**/folder1/*.tsx"', + '---', + 'Some instructions 10 contents.', + ] + }, + { + path: `${userPromptsFolderName}/file11.instructions.md`, + contents: [ + '---', + 'description: \'Instructions file 11.\'', + 'applyTo: "**/folder1/*.py"', + '---', + 'Some instructions 11 contents.', + ] + }, + { + path: `${userPromptsFolderName}/file12.prompt.md`, + contents: [ + '---', + 'description: \'Prompt file 12.\'', + '---', + 'Some prompt 12 contents.', + ] } - ])).mock(); + ]); const instructionFiles = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); const context = { files: new ResourceSet([ URI.joinPath(rootFolderUri, 'folder1/main.tsx'), @@ -666,62 +669,47 @@ suite('PromptsService', () => { workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); // mock current workspace file structure - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: 'codestyle.md', - contents: [ - 'Can you see this?', - ], - }, - { - name: 'AGENTS.md', - contents: [ - 'What about this?', - ], - }, - { - name: 'README.md', - contents: [ - 'Thats my project?', - ], - }, - { - name: '.github', - children: [ - { - name: 'copilot-instructions.md', - contents: [ - 'Be nice and friendly. Also look at instructions at #file:../codestyle.md and [more-codestyle.md](./more-codestyle.md).', - ], - }, - { - name: 'more-codestyle.md', - contents: [ - 'I like it clean.', - ], - }, - ], - }, - { - name: 'folder1', - children: [ - // This will not be returned because we have PromptsConfig.USE_NESTED_AGENT_MD set to false. - { - name: 'AGENTS.md', - contents: [ - 'An AGENTS.md file in another repo' - ] - } - ] - } - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/codestyle.md`, + contents: [ + 'Can you see this?', + ] + }, + { + path: `${rootFolder}/AGENTS.md`, + contents: [ + 'What about this?', + ] + }, + { + path: `${rootFolder}/README.md`, + contents: [ + 'Thats my project?', + ] + }, + { + path: `${rootFolder}/.github/copilot-instructions.md`, + contents: [ + 'Be nice and friendly. Also look at instructions at #file:../codestyle.md and [more-codestyle.md](./more-codestyle.md).', + ] + }, + { + path: `${rootFolder}/.github/more-codestyle.md`, + contents: [ + 'I like it clean.', + ] + }, + { + path: `${rootFolder}/folder1/AGENTS.md`, + contents: [ + 'An AGENTS.md file in another repo' + ] + } + ]); - const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, undefined); + const contextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatMode.Agent, undefined, undefined); const context = new ChatRequestVariableSet(); context.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'README.md'))); @@ -753,34 +741,24 @@ suite('PromptsService', () => { workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: '.github/agents', - children: [ - { - name: 'agent1.agent.md', - contents: [ - '---', - 'description: \'Agent file 1.\'', - 'handoffs: [ { agent: "Edit", label: "Do it", prompt: "Do it now" } ]', - '---', - ], - } - ], - - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/agent1.agent.md`, + contents: [ + '---', + 'description: \'Agent file 1.\'', + 'handoffs: [ { agent: "Edit", label: "Do it", prompt: "Do it now" } ]', + '---', + ] + } + ]); const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ { name: 'agent1', description: 'Agent file 1.', - handOffs: [{ agent: 'Edit', label: 'Do it', prompt: 'Do it now', send: undefined }], + handOffs: [{ agent: 'Edit', label: 'Do it', prompt: 'Do it now' }], agentInstructions: { content: '', toolReferences: [], @@ -790,6 +768,8 @@ suite('PromptsService', () => { argumentHint: undefined, tools: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -810,34 +790,24 @@ suite('PromptsService', () => { workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); // mock current workspace file structure - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: '.github/agents', - children: [ - { - name: 'agent1.agent.md', - contents: [ - '---', - 'description: \'Agent file 1.\'', - 'tools: [ tool1, tool2 ]', - '---', - 'Do it with #tool:tool1', - ], - }, - { - name: 'agent2.agent.md', - contents: [ - 'First use #tool:tool2\nThen use #tool:tool1', - ], - } - ], - - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/agent1.agent.md`, + contents: [ + '---', + 'description: \'Agent file 1.\'', + 'tools: [ tool1, tool2 ]', + '---', + 'Do it with #tool:tool1', + ] + }, + { + path: `${rootFolder}/.github/agents/agent2.agent.md`, + contents: [ + 'First use #tool:tool2\nThen use #tool:tool1', + ] + } + ]); const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ @@ -854,6 +824,8 @@ suite('PromptsService', () => { model: undefined, argumentHint: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local }, }, @@ -869,6 +841,7 @@ suite('PromptsService', () => { }, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local }, + visibility: { userInvokable: true, agentInvokable: true } } ]; @@ -886,39 +859,29 @@ suite('PromptsService', () => { workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: '.github/agents', - children: [ - { - name: 'agent1.agent.md', - contents: [ - '---', - 'description: \'Code review agent.\'', - 'argument-hint: \'Provide file path or code snippet to review\'', - 'tools: [ code-analyzer, linter ]', - '---', - 'I will help review your code for best practices.', - ], - }, - { - name: 'agent2.agent.md', - contents: [ - '---', - 'description: \'Documentation generator.\'', - 'argument-hint: \'Specify function or class name to document\'', - '---', - 'I generate comprehensive documentation.', - ], - } - ], - - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/agent1.agent.md`, + contents: [ + '---', + 'description: \'Code review agent.\'', + 'argument-hint: \'Provide file path or code snippet to review\'', + 'tools: [ code-analyzer, linter ]', + '---', + 'I will help review your code for best practices.', + ] + }, + { + path: `${rootFolder}/.github/agents/agent2.agent.md`, + contents: [ + '---', + 'description: \'Documentation generator.\'', + 'argument-hint: \'Specify function or class name to document\'', + '---', + 'I generate comprehensive documentation.', + ] + } + ]); const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ @@ -935,6 +898,8 @@ suite('PromptsService', () => { handOffs: undefined, model: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'), source: { storage: PromptsStorage.local } }, @@ -951,6 +916,8 @@ suite('PromptsService', () => { model: undefined, tools: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'), source: { storage: PromptsStorage.local } }, @@ -970,49 +937,39 @@ suite('PromptsService', () => { workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: '.github/agents', - children: [ - { - name: 'github-agent.agent.md', - contents: [ - '---', - 'description: \'GitHub Copilot specialized agent.\'', - 'target: \'github-copilot\'', - 'tools: [ github-api, code-search ]', - '---', - 'I am optimized for GitHub Copilot workflows.', - ], - }, - { - name: 'vscode-agent.agent.md', - contents: [ - '---', - 'description: \'VS Code specialized agent.\'', - 'target: \'vscode\'', - 'model: \'gpt-4\'', - '---', - 'I am specialized for VS Code editor tasks.', - ], - }, - { - name: 'generic-agent.agent.md', - contents: [ - '---', - 'description: \'Generic agent without target.\'', - '---', - 'I work everywhere.', - ], - } - ], - - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/github-agent.agent.md`, + contents: [ + '---', + 'description: \'GitHub Copilot specialized agent.\'', + 'target: \'github-copilot\'', + 'tools: [ github-api, code-search ]', + '---', + 'I am optimized for GitHub Copilot workflows.', + ] + }, + { + path: `${rootFolder}/.github/agents/vscode-agent.agent.md`, + contents: [ + '---', + 'description: \'VS Code specialized agent.\'', + 'target: \'vscode\'', + 'model: \'gpt-4\'', + '---', + 'I am specialized for VS Code editor tasks.', + ] + }, + { + path: `${rootFolder}/.github/agents/generic-agent.agent.md`, + contents: [ + '---', + 'description: \'Generic agent without target.\'', + '---', + 'I work everywhere.', + ] + } + ]); const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ @@ -1029,6 +986,8 @@ suite('PromptsService', () => { handOffs: undefined, model: undefined, argumentHint: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1036,7 +995,7 @@ suite('PromptsService', () => { name: 'vscode-agent', description: 'VS Code specialized agent.', target: 'vscode', - model: 'gpt-4', + model: ['gpt-4'], agentInstructions: { content: 'I am specialized for VS Code editor tasks.', toolReferences: [], @@ -1045,6 +1004,8 @@ suite('PromptsService', () => { handOffs: undefined, argumentHint: undefined, tools: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1061,6 +1022,8 @@ suite('PromptsService', () => { argumentHint: undefined, tools: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'), source: { storage: PromptsStorage.local } }, @@ -1073,41 +1036,31 @@ suite('PromptsService', () => { ); }); - test('agents with .md extension (no .agent.md)', async () => { + test('agents with .md extension should be recognized, except README.md', async () => { const rootFolderName = 'custom-agents-md-extension'; const rootFolder = `/${rootFolderName}`; const rootFolderUri = URI.file(rootFolder); workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - await (instaService.createInstance(MockFilesystem, - [{ - name: rootFolderName, - children: [ - { - name: '.github/agents', - children: [ - { - name: 'demonstrate.md', - contents: [ - '---', - 'description: \'Demonstrate agent.\'', - 'tools: [ demo-tool ]', - '---', - 'This is a demonstration agent using .md extension.', - ], - }, - { - name: 'test.md', - contents: [ - 'Test agent without header.', - ], - } - ], - - }, - ], - }])).mock(); + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/demonstrate.md`, + contents: [ + '---', + 'description: \'Demonstrate agent.\'', + 'tools: [ demo-tool ]', + '---', + 'This is a demonstration agent using .md extension.', + ] + }, + { + path: `${rootFolder}/.github/agents/README.md`, + contents: [ + 'This is a README file.', + ] + } + ]); const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); const expected: ICustomAgent[] = [ @@ -1124,48 +1077,1455 @@ suite('PromptsService', () => { model: undefined, argumentHint: undefined, target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + agents: undefined, uri: URI.joinPath(rootFolderUri, '.github/agents/demonstrate.md'), - source: { storage: PromptsStorage.local }, + source: { storage: PromptsStorage.local } + } + ]; + + assert.deepEqual( + result, + expected, + 'Must recognize .md files as agents, except README.md', + ); + }); + + test('header with agents', async () => { + const rootFolderName = 'custom-agents-with-restrictions'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/agents/restricted-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with restricted access.\'', + 'agents: [ subagent1, subagent2 ]', + 'tools: [ tool1 ]', + '---', + 'This agent has restricted access.', + ] + }, + { + path: `${rootFolder}/.github/agents/no-access-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with no access to subagents, skills, or instructions.\'', + 'agents: []', + '---', + 'This agent has no access.', + ] }, { - name: 'test', + path: `${rootFolder}/.github/agents/full-access-agent.agent.md`, + contents: [ + '---', + 'description: \'Agent with full access.\'', + 'agents: [ "*" ]', + '---', + 'This agent has full access.', + ] + } + ]); + + const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + const expected: ICustomAgent[] = [ + { + name: 'restricted-agent', + description: 'Agent with restricted access.', + agents: ['subagent1', 'subagent2'], + tools: ['tool1'], agentInstructions: { - content: 'Test agent without header.', + content: 'This agent has restricted access.', toolReferences: [], metadata: undefined }, - uri: URI.joinPath(rootFolderUri, '.github/agents/test.md'), - source: { storage: PromptsStorage.local }, - } + handOffs: undefined, + model: undefined, + argumentHint: undefined, + target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + uri: URI.joinPath(rootFolderUri, '.github/agents/restricted-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, + { + name: 'no-access-agent', + description: 'Agent with no access to subagents, skills, or instructions.', + agents: [], + agentInstructions: { + content: 'This agent has no access.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + tools: undefined, + target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + uri: URI.joinPath(rootFolderUri, '.github/agents/no-access-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, + { + name: 'full-access-agent', + description: 'Agent with full access.', + agents: ['*'], + agentInstructions: { + content: 'This agent has full access.', + toolReferences: [], + metadata: undefined + }, + handOffs: undefined, + model: undefined, + argumentHint: undefined, + tools: undefined, + target: undefined, + visibility: { userInvokable: true, agentInvokable: true }, + uri: URI.joinPath(rootFolderUri, '.github/agents/full-access-agent.agent.md'), + source: { storage: PromptsStorage.local } + }, ]; assert.deepEqual( result, expected, - 'Must get custom agents with .md extension from .github/agents/ folder.', + 'Must get custom agents with agents, skills, and instructions attributes.', ); }); + + test('agents from user data folder', async () => { + const rootFolderName = 'custom-agents-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service to use a file:// URI that the InMemoryFileSystemProvider supports + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create agent files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace agent + { + path: `${rootFolder}/.github/agents/workspace-agent.agent.md`, + contents: [ + '---', + 'description: \'Workspace agent.\'', + '---', + 'I am a workspace agent.', + ] + }, + // User data agent + { + path: `${userPromptsFolder}/user-agent.agent.md`, + contents: [ + '---', + 'description: \'User data agent.\'', + 'tools: [ user-tool ]', + '---', + 'I am a user data agent.', + ] + }, + // Another user data agent without header + { + path: `${userPromptsFolder}/simple-user-agent.agent.md`, + contents: [ + 'A simple user agent without header.', + ] + } + ]); + + const result = (await testService.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) })); + + // Should find agents from both workspace and user data + assert.strictEqual(result.length, 3, 'Should find 3 agents (1 workspace + 2 user data)'); + + const workspaceAgent = result.find(a => a.source.storage === PromptsStorage.local); + assert.ok(workspaceAgent, 'Should find workspace agent'); + assert.strictEqual(workspaceAgent.name, 'workspace-agent'); + assert.strictEqual(workspaceAgent.description, 'Workspace agent.'); + + const userAgents = result.filter(a => a.source.storage === PromptsStorage.user); + assert.strictEqual(userAgents.length, 2, 'Should find 2 user data agents'); + + const userAgentWithHeader = userAgents.find(a => a.name === 'user-agent'); + assert.ok(userAgentWithHeader, 'Should find user agent with header'); + assert.strictEqual(userAgentWithHeader.description, 'User data agent.'); + assert.deepStrictEqual(userAgentWithHeader.tools, ['user-tool']); + + const simpleUserAgent = userAgents.find(a => a.name === 'simple-user-agent'); + assert.ok(simpleUserAgent, 'Should find simple user agent'); + assert.strictEqual(simpleUserAgent.agentInstructions.content, 'A simple user agent without header.'); + }); }); - suite('listPromptFiles - extensions', () => { + suite('listPromptFiles - prompts', () => { + test('prompts from user data folder', async () => { + const rootFolderName = 'prompts-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); - test('Contributed prompt file', async () => { - const uri = URI.parse('file://extensions/my-extension/textMate.instructions.md'); - const extension = {} as IExtensionDescription; - const registered = service.registerContributedFile(PromptsType.instructions, - 'TextMate Instructions', - 'Instructions to follow when authoring TextMate grammars', - uri, - extension - ); + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); - const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); - assert.strictEqual(actual.length, 1); - assert.strictEqual(actual[0].uri.toString(), uri.toString()); - assert.strictEqual(actual[0].name, 'TextMate Instructions'); - assert.strictEqual(actual[0].storage, PromptsStorage.extension); - assert.strictEqual(actual[0].type, PromptsType.instructions); - registered.dispose(); + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create prompt files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace prompt + { + path: `${rootFolder}/.github/prompts/workspace-prompt.prompt.md`, + contents: [ + '---', + 'description: \'Workspace prompt.\'', + '---', + 'I am a workspace prompt.', + ] + }, + // User data prompt + { + path: `${userPromptsFolder}/user-prompt.prompt.md`, + contents: [ + '---', + 'description: \'User data prompt.\'', + '---', + 'I am a user data prompt.', + ] + } + ]); + + const result = await testService.listPromptFiles(PromptsType.prompt, CancellationToken.None); + + // Should find prompts from both workspace and user data + assert.strictEqual(result.length, 2, 'Should find 2 prompts (1 workspace + 1 user data)'); + + const workspacePrompt = result.find(p => p.storage === PromptsStorage.local); + assert.ok(workspacePrompt, 'Should find workspace prompt'); + assert.ok(workspacePrompt.uri.path.includes('workspace-prompt.prompt.md')); + + const userPrompt = result.find(p => p.storage === PromptsStorage.user); + assert.ok(userPrompt, 'Should find user data prompt'); + assert.ok(userPrompt.uri.path.includes('user-prompt.prompt.md')); + }); + }); + + suite('listPromptFiles - instructions', () => { + test('instructions from user data folder', async () => { + const rootFolderName = 'instructions-user-data'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const userPromptsFolder = '/user-data/prompts'; + const userPromptsFolderUri = URI.file(userPromptsFolder); + + // Override the user data profile service + const customUserDataProfileService = { + _serviceBrand: undefined, + onDidChangeCurrentProfile: Event.None, + currentProfile: { + ...toUserDataProfile('test', 'test', URI.file(userPromptsFolder).with({ path: '/user-data' }), URI.file('/cache')), + promptsHome: userPromptsFolderUri, + }, + updateCurrentProfile: async () => { } + }; + instaService.stub(IUserDataProfileService, customUserDataProfileService); + + // Recreate the service with the new stub + const testService = disposables.add(instaService.createInstance(PromptsService)); + + // Create instructions files in both workspace and user data folder + await mockFiles(fileService, [ + // Workspace instructions + { + path: `${rootFolder}/.github/instructions/workspace-instructions.instructions.md`, + contents: [ + '---', + 'description: \'Workspace instructions.\'', + 'applyTo: "**/*.ts"', + '---', + 'I am workspace instructions.', + ] + }, + // User data instructions + { + path: `${userPromptsFolder}/user-instructions.instructions.md`, + contents: [ + '---', + 'description: \'User data instructions.\'', + 'applyTo: "**/*.tsx"', + '---', + 'I am user data instructions.', + ] + } + ]); + + const result = await testService.listPromptFiles(PromptsType.instructions, CancellationToken.None); + + // Should find instructions from both workspace and user data + assert.strictEqual(result.length, 2, 'Should find 2 instructions (1 workspace + 1 user data)'); + + const workspaceInstructions = result.find(p => p.storage === PromptsStorage.local); + assert.ok(workspaceInstructions, 'Should find workspace instructions'); + assert.ok(workspaceInstructions.uri.path.includes('workspace-instructions.instructions.md')); + + const userInstructions = result.find(p => p.storage === PromptsStorage.user); + assert.ok(userInstructions, 'Should find user data instructions'); + assert.ok(userInstructions.uri.path.includes('user-instructions.instructions.md')); + }); + }); + + suite('listPromptFiles - skills ', () => { + teardown(() => { + sinon.restore(); + }); + + test('should list skill files from workspace', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'list-skills-workspace'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/skill1/SKILL.md`, + contents: [ + '---', + 'name: "Skill 1"', + 'description: "First skill"', + '---', + 'Skill 1 content', + ], + }, + { + path: `${rootFolder}/.claude/skills/skill2/SKILL.md`, + contents: [ + '---', + 'name: "Skill 2"', + 'description: "Second skill"', + '---', + 'Skill 2 content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 2, 'Should find 2 skills'); + + const skill1 = result.find(s => s.uri.path.includes('skill1')); + assert.ok(skill1, 'Should find skill1'); + assert.strictEqual(skill1.type, PromptsType.skill); + assert.strictEqual(skill1.storage, PromptsStorage.local); + + const skill2 = result.find(s => s.uri.path.includes('skill2')); + assert.ok(skill2, 'Should find skill2'); + assert.strictEqual(skill2.type, PromptsType.skill); + assert.strictEqual(skill2.storage, PromptsStorage.local); + }); + + test('should list skill files from user home', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'list-skills-user-home'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: '/home/user/.copilot/skills/personal-skill/SKILL.md', + contents: [ + '---', + 'name: "Personal Skill"', + 'description: "A personal skill"', + '---', + 'Personal skill content', + ], + }, + { + path: '/home/user/.claude/skills/claude-personal/SKILL.md', + contents: [ + '---', + 'name: "Claude Personal Skill"', + 'description: "A Claude personal skill"', + '---', + 'Claude personal skill content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + const personalSkills = result.filter(s => s.storage === PromptsStorage.user); + assert.strictEqual(personalSkills.length, 2, 'Should find 2 personal skills'); + + const copilotSkill = personalSkills.find(s => s.uri.path.includes('.copilot')); + assert.ok(copilotSkill, 'Should find copilot personal skill'); + + const claudeSkill = personalSkills.find(s => s.uri.path.includes('.claude')); + assert.ok(claudeSkill, 'Should find claude personal skill'); + }); + + test('should not list skills when not in skill folder structure', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + const rootFolderName = 'no-skills'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create files in non-skill locations + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/prompts/SKILL.md`, + contents: [ + '---', + 'name: "Not a skill"', + '---', + 'This is in prompts folder, not skills', + ], + }, + { + path: `${rootFolder}/SKILL.md`, + contents: [ + '---', + 'name: "Root skill"', + '---', + 'This is in root, not skills folder', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 0, 'Should not find any skills in non-skill locations'); + }); + + test('should handle mixed workspace and user home skills', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'mixed-skills'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + // Workspace skills + { + path: `${rootFolder}/.github/skills/workspace-skill/SKILL.md`, + contents: [ + '---', + 'name: "Workspace Skill"', + 'description: "A workspace skill"', + '---', + 'Workspace skill content', + ], + }, + // User home skills + { + path: '/home/user/.copilot/skills/personal-skill/SKILL.md', + contents: [ + '---', + 'name: "Personal Skill"', + 'description: "A personal skill"', + '---', + 'Personal skill content', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + const workspaceSkills = result.filter(s => s.storage === PromptsStorage.local); + const userSkills = result.filter(s => s.storage === PromptsStorage.user); + + assert.strictEqual(workspaceSkills.length, 1, 'Should find 1 workspace skill'); + assert.strictEqual(userSkills.length, 1, 'Should find 1 user skill'); + }); + + test('should respect disabled default paths via config', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + // Disable .github/skills, only .claude/skills should be searched + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { + '.github/skills': false, + '.claude/skills': true, + }); + + const rootFolderName = 'disabled-default-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/github-skill/SKILL.md`, + contents: [ + '---', + 'name: "GitHub Skill"', + 'description: "Should NOT be found"', + '---', + 'This skill is in a disabled folder', + ], + }, + { + path: `${rootFolder}/.claude/skills/claude-skill/SKILL.md`, + contents: [ + '---', + 'name: "Claude Skill"', + 'description: "Should be found"', + '---', + 'This skill is in an enabled folder', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 1, 'Should find only 1 skill (from enabled folder)'); + assert.ok(result[0].uri.path.includes('.claude/skills'), 'Should only find skill from .claude/skills'); + assert.ok(!result[0].uri.path.includes('.github/skills'), 'Should not find skill from disabled .github/skills'); + }); + + test('should expand tilde paths in custom locations', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + // Add a tilde path as custom location + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, { + '.github/skills': false, + '.claude/skills': false, + '~/my-custom-skills': true, + }); + + const rootFolderName = 'tilde-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // The mock user home is /home/user, so ~/my-custom-skills should resolve to /home/user/my-custom-skills + await mockFiles(fileService, [ + { + path: '/home/user/my-custom-skills/custom-skill/SKILL.md', + contents: [ + '---', + 'name: "Custom Skill"', + 'description: "A skill from tilde path"', + '---', + 'Skill content from ~/my-custom-skills', + ], + }, + ]); + + const result = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + + assert.strictEqual(result.length, 1, 'Should find 1 skill from tilde-expanded path'); + assert.ok(result[0].uri.path.includes('/home/user/my-custom-skills'), 'Path should be expanded from tilde'); + }); + }); + + suite('listPromptFiles - extensions', () => { + + test('Contributed prompt file', async () => { + const uri = URI.parse('file://extensions/my-extension/textMate.instructions.md'); + const extension = {} as IExtensionDescription; + const registered = service.registerContributedFile(PromptsType.instructions, + uri, + extension, + 'TextMate Instructions', + 'Instructions to follow when authoring TextMate grammars', + ); + + const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + assert.strictEqual(actual.length, 1); + assert.strictEqual(actual[0].uri.toString(), uri.toString()); + assert.strictEqual(actual[0].name, 'TextMate Instructions'); + assert.strictEqual(actual[0].storage, PromptsStorage.extension); + assert.strictEqual(actual[0].type, PromptsType.instructions); + registered.dispose(); + }); + + test('Custom agent provider', async () => { + const agentUri = URI.parse('file://extensions/my-extension/myAgent.agent.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the agent file content + await mockFiles(fileService, [ + { + path: agentUri.path, + contents: [ + '---', + 'description: \'My custom agent from provider\'', + 'tools: [ tool1, tool2 ]', + '---', + 'I am a custom agent from a provider.', + ] + } + ]); + + const provider = { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { + return [ + { + uri: agentUri + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.agent, provider); + + const actual = await service.getCustomAgents(CancellationToken.None); + assert.strictEqual(actual.length, 1); + assert.strictEqual(actual[0].name, 'myAgent'); + assert.strictEqual(actual[0].description, 'My custom agent from provider'); + assert.strictEqual(actual[0].uri.toString(), agentUri.toString()); + assert.strictEqual(actual[0].source.storage, PromptsStorage.extension); + + registered.dispose(); + + // After disposal, the agent should no longer be listed + const actualAfterDispose = await service.getCustomAgents(CancellationToken.None); + assert.strictEqual(actualAfterDispose.length, 0); + }); + + test('Contributed agent file that does not exist should not crash', async () => { + const nonExistentUri = URI.parse('file://extensions/my-extension/nonexistent.agent.md'); + const existingUri = URI.parse('file://extensions/my-extension/existing.agent.md'); + const extension = { + identifier: { value: 'test.my-extension' } + } as unknown as IExtensionDescription; + + // Only create the existing file + await mockFiles(fileService, [ + { + path: existingUri.path, + contents: [ + '---', + 'name: \'Existing Agent\'', + 'description: \'An agent that exists\'', + '---', + 'I am an existing agent.', + ] + } + ]); + + // Register both agents (one exists, one doesn't) + const registered1 = service.registerContributedFile( + PromptsType.agent, + nonExistentUri, + extension, + 'NonExistent Agent', + 'An agent that does not exist', + ); + + const registered2 = service.registerContributedFile( + PromptsType.agent, + existingUri, + extension, + 'Existing Agent', + 'An agent that exists', + ); + + // Verify that getCustomAgents doesn't crash and returns only the valid agent + const agents = await service.getCustomAgents(CancellationToken.None); + + // Should only get the existing agent, not the non-existent one + assert.strictEqual(agents.length, 1, 'Should only return the agent that exists'); + assert.strictEqual(agents[0].name, 'Existing Agent'); + assert.strictEqual(agents[0].description, 'An agent that exists'); + assert.strictEqual(agents[0].uri.toString(), existingUri.toString()); + + registered1.dispose(); + registered2.dispose(); + }); + }); + + test('Instructions provider', async () => { + const instructionUri = URI.parse('file://extensions/my-extension/myInstruction.instructions.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the instruction file content + await mockFiles(fileService, [ + { + path: instructionUri.path, + contents: [ + '# Test instruction content' + ] + } + ]); + + const provider = { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { + return [ + { + uri: instructionUri + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.instructions, provider); + + const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + const providerInstruction = actual.find(i => i.uri.toString() === instructionUri.toString()); + + assert.ok(providerInstruction, 'Provider instruction should be found'); + assert.strictEqual(providerInstruction!.uri.toString(), instructionUri.toString()); + assert.strictEqual(providerInstruction!.storage, PromptsStorage.extension); + assert.strictEqual(providerInstruction!.source, ExtensionAgentSourceType.provider); + + registered.dispose(); + + // After disposal, the instruction should no longer be listed + const actualAfterDispose = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None); + const foundAfterDispose = actualAfterDispose.find(i => i.uri.toString() === instructionUri.toString()); + assert.strictEqual(foundAfterDispose, undefined); + }); + + test('Prompt file provider', async () => { + const promptUri = URI.parse('file://extensions/my-extension/myPrompt.prompt.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the prompt file content + await mockFiles(fileService, [ + { + path: promptUri.path, + contents: [ + '# Test prompt content' + ] + } + ]); + + const provider = { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { + return [ + { + uri: promptUri + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.prompt, provider); + + const actual = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); + const providerPrompt = actual.find(i => i.uri.toString() === promptUri.toString()); + + assert.ok(providerPrompt, 'Provider prompt should be found'); + assert.strictEqual(providerPrompt!.uri.toString(), promptUri.toString()); + assert.strictEqual(providerPrompt!.storage, PromptsStorage.extension); + assert.strictEqual(providerPrompt!.source, ExtensionAgentSourceType.provider); + + registered.dispose(); + + // After disposal, the prompt should no longer be listed + const actualAfterDispose = await service.listPromptFiles(PromptsType.prompt, CancellationToken.None); + const foundAfterDispose = actualAfterDispose.find(i => i.uri.toString() === promptUri.toString()); + assert.strictEqual(foundAfterDispose, undefined); + }); + + test('Skill file provider', async () => { + const skillUri = URI.parse('file://extensions/my-extension/mySkill/SKILL.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Mock the skill file content + await mockFiles(fileService, [ + { + path: skillUri.path, + contents: [ + '---', + 'name: "My Custom Skill"', + 'description: "A custom skill from provider"', + '---', + 'Custom skill content.', + ] + } + ]); + + const provider = { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { + return [ + { + uri: skillUri + } + ]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.skill, provider); + + const actual = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + const providerSkill = actual.find(i => i.uri.toString() === skillUri.toString()); + + assert.ok(providerSkill, 'Provider skill should be found'); + assert.strictEqual(providerSkill!.uri.toString(), skillUri.toString()); + assert.strictEqual(providerSkill!.storage, PromptsStorage.extension); + assert.strictEqual(providerSkill!.source, ExtensionAgentSourceType.provider); + + registered.dispose(); + + // After disposal, the skill should no longer be listed + const actualAfterDispose = await service.listPromptFiles(PromptsType.skill, CancellationToken.None); + const foundAfterDispose = actualAfterDispose.find(i => i.uri.toString() === skillUri.toString()); + assert.strictEqual(foundAfterDispose, undefined); + }); + + suite('findAgentSkills', () => { + teardown(() => { + sinon.restore(); + }); + + test('should return undefined when USE_AGENT_SKILLS is disabled', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, false); + + const result = await service.findAgentSkills(CancellationToken.None); + assert.strictEqual(result, undefined); + }); + + test('should find skills in workspace and user home', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'agent-skills-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create mock filesystem with skills in both .github/skills and .claude/skills + // Folder names must match the skill names exactly (per agentskills.io specification) + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/GitHub Skill 1/SKILL.md`, + contents: [ + '---', + 'name: "GitHub Skill 1"', + 'description: "A GitHub skill for testing"', + '---', + 'This is GitHub skill 1 content', + ], + }, + { + path: `${rootFolder}/.claude/skills/Claude Skill 1/SKILL.md`, + contents: [ + '---', + 'name: "Claude Skill 1"', + 'description: "A Claude skill for testing"', + '---', + 'This is Claude skill 1 content', + ], + }, + { + path: `${rootFolder}/.claude/skills/invalid-skill/SKILL.md`, + contents: [ + '---', + 'description: "Invalid skill, no name"', + '---', + 'This is invalid skill content', + ], + }, + { + path: `${rootFolder}/.github/skills/not-a-skill-dir/README.md`, + contents: ['This is not a skill'], + }, + { + path: '/home/user/.claude/skills/Personal Skill 1/SKILL.md', + contents: [ + '---', + 'name: "Personal Skill 1"', + 'description: "A personal skill for testing"', + '---', + 'This is personal skill 1 content', + ], + }, + { + path: '/home/user/.claude/skills/not-a-skill/other-file.md', + contents: ['Not a skill file'], + }, + { + path: '/home/user/.copilot/skills/Copilot Skill 1/SKILL.md', + contents: [ + '---', + 'name: "Copilot Skill 1"', + 'description: "A Copilot skill for testing"', + '---', + 'This is Copilot skill 1 content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results when agent skills are enabled'); + assert.strictEqual(result.length, 4, 'Should find 4 skills total'); + + // Check project skills (both from .github/skills and .claude/skills) + const projectSkills = result.filter(skill => skill.storage === PromptsStorage.local); + assert.strictEqual(projectSkills.length, 2, 'Should find 2 project skills'); + + const githubSkill1 = projectSkills.find(skill => skill.name === 'GitHub Skill 1'); + assert.ok(githubSkill1, 'Should find GitHub skill 1'); + assert.strictEqual(githubSkill1.description, 'A GitHub skill for testing'); + assert.strictEqual(githubSkill1.uri.path, `${rootFolder}/.github/skills/GitHub Skill 1/SKILL.md`); + + const claudeSkill1 = projectSkills.find(skill => skill.name === 'Claude Skill 1'); + assert.ok(claudeSkill1, 'Should find Claude skill 1'); + assert.strictEqual(claudeSkill1.description, 'A Claude skill for testing'); + assert.strictEqual(claudeSkill1.uri.path, `${rootFolder}/.claude/skills/Claude Skill 1/SKILL.md`); + + // Check personal skills + const personalSkills = result.filter(skill => skill.storage === PromptsStorage.user); + assert.strictEqual(personalSkills.length, 2, 'Should find 2 personal skills'); + + const personalSkill1 = personalSkills.find(skill => skill.name === 'Personal Skill 1'); + assert.ok(personalSkill1, 'Should find Personal Skill 1'); + assert.strictEqual(personalSkill1.description, 'A personal skill for testing'); + assert.strictEqual(personalSkill1.uri.path, '/home/user/.claude/skills/Personal Skill 1/SKILL.md'); + + const copilotSkill1 = personalSkills.find(skill => skill.name === 'Copilot Skill 1'); + assert.ok(copilotSkill1, 'Should find Copilot Skill 1'); + assert.strictEqual(copilotSkill1.description, 'A Copilot skill for testing'); + assert.strictEqual(copilotSkill1.uri.path, '/home/user/.copilot/skills/Copilot Skill 1/SKILL.md'); + }); + + test('should handle parsing errors gracefully', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'skills-error-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create mock filesystem with malformed skill file in .github/skills + // Folder names must match the skill names exactly + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/Valid Skill/SKILL.md`, + contents: [ + '---', + 'name: "Valid Skill"', + 'description: "A valid skill"', + '---', + 'Valid skill content', + ], + }, + { + path: `${rootFolder}/.claude/skills/invalid-skill/SKILL.md`, + contents: [ + '---', + 'invalid yaml: [unclosed', + '---', + 'Invalid skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + // Should still return the valid skill, even if one has parsing errors + assert.ok(result, 'Should return results even with parsing errors'); + assert.strictEqual(result.length, 1, 'Should find 1 valid skill'); + assert.strictEqual(result[0].name, 'Valid Skill'); + assert.strictEqual(result[0].storage, PromptsStorage.local); + }); + + test('should return empty array when no skills found', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + + const rootFolderName = 'empty-workspace'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create empty mock filesystem + await mockFiles(fileService, []); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results array'); + assert.strictEqual(result.length, 0, 'Should find no skills'); + }); + + test('should truncate long names and descriptions', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'truncation-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const longName = 'A'.repeat(100); // Exceeds 64 characters + const truncatedName = 'A'.repeat(64); // Expected after truncation + const longDescription = 'B'.repeat(1500); // Exceeds 1024 characters + + await mockFiles(fileService, [ + { + // Folder name must match the truncated skill name + path: `${rootFolder}/.github/skills/${truncatedName}/SKILL.md`, + contents: [ + '---', + `name: "${longName}"`, + `description: "${longDescription}"`, + '---', + 'Skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find 1 skill'); + assert.strictEqual(result[0].name.length, 64, 'Name should be truncated to 64 characters'); + assert.strictEqual(result[0].description?.length, 1024, 'Description should be truncated to 1024 characters'); + }); + + test('should remove XML tags from name and description', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'xml-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Folder name must match the sanitized skill name (with XML tags removed) + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/Skill with XML tags/SKILL.md`, + contents: [ + '---', + 'name: "Skill with XML tags"', + 'description: "Description with HTML and other tags"', + '---', + 'Skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find 1 skill'); + assert.strictEqual(result[0].name, 'Skill with XML tags', 'XML tags should be removed from name'); + assert.strictEqual(result[0].description, 'Description with HTML and other tags', 'XML tags should be removed from description'); + }); + + test('should handle both truncation and XML removal', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'combined-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const longNameWithXml = '

' + 'A'.repeat(100) + '

'; // Exceeds 64 chars and has XML + const truncatedName = 'A'.repeat(64); // Expected after XML removal and truncation + const longDescWithXml = '
' + 'B'.repeat(1500) + '
'; // Exceeds 1024 chars and has XML + + // Folder name must match the fully sanitized skill name + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/${truncatedName}/SKILL.md`, + contents: [ + '---', + `name: "${longNameWithXml}"`, + `description: "${longDescWithXml}"`, + '---', + 'Skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find 1 skill'); + // XML tags are removed first, then truncation happens + assert.ok(!result[0].name.includes('<'), 'Name should not contain XML tags'); + assert.ok(!result[0].name.includes('>'), 'Name should not contain XML tags'); + assert.strictEqual(result[0].name.length, 64, 'Name should be truncated to 64 characters'); + assert.ok(!result[0].description?.includes('<'), 'Description should not contain XML tags'); + assert.ok(!result[0].description?.includes('>'), 'Description should not contain XML tags'); + assert.strictEqual(result[0].description?.length, 1024, 'Description should be truncated to 1024 characters'); + }); + + test('should skip duplicate skill names and keep first by priority', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'duplicate-skills-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create skills with duplicate names in different locations + // Workspace skill should be kept (higher priority), user skill should be skipped + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/Duplicate Skill/SKILL.md`, + contents: [ + '---', + 'name: "Duplicate Skill"', + 'description: "Workspace version"', + '---', + 'Workspace skill content', + ], + }, + { + path: '/home/user/.copilot/skills/Duplicate Skill/SKILL.md', + contents: [ + '---', + 'name: "Duplicate Skill"', + 'description: "User version - should be skipped"', + '---', + 'User skill content', + ], + }, + { + path: `${rootFolder}/.claude/skills/Unique Skill/SKILL.md`, + contents: [ + '---', + 'name: "Unique Skill"', + 'description: "A unique skill"', + '---', + 'Unique skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 2, 'Should find 2 skills (duplicate skipped)'); + + const duplicateSkill = result.find(s => s.name === 'Duplicate Skill'); + assert.ok(duplicateSkill, 'Should find the duplicate skill'); + assert.strictEqual(duplicateSkill.description, 'Workspace version', 'Should keep workspace version (higher priority)'); + assert.strictEqual(duplicateSkill.storage, PromptsStorage.local, 'Should be from workspace'); + + const uniqueSkill = result.find(s => s.name === 'Unique Skill'); + assert.ok(uniqueSkill, 'Should find the unique skill'); + }); + + test('should prioritize skills by source: workspace > user > extension', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'priority-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + // Create skills from different sources with same name + await mockFiles(fileService, [ + { + path: '/home/user/.copilot/skills/Priority Skill/SKILL.md', + contents: [ + '---', + 'name: "Priority Skill"', + 'description: "User version"', + '---', + 'User skill content', + ], + }, + { + path: `${rootFolder}/.github/skills/Priority Skill/SKILL.md`, + contents: [ + '---', + 'name: "Priority Skill"', + 'description: "Workspace version - highest priority"', + '---', + 'Workspace skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find 1 skill (duplicates resolved by priority)'); + assert.strictEqual(result[0].description, 'Workspace version - highest priority', 'Workspace should win over user'); + assert.strictEqual(result[0].storage, PromptsStorage.local); + }); + + test('should skip skills where name does not match folder name', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'name-mismatch-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + // Folder name "wrong-folder-name" doesn't match skill name "Correct Skill Name" + path: `${rootFolder}/.github/skills/wrong-folder-name/SKILL.md`, + contents: [ + '---', + 'name: "Correct Skill Name"', + 'description: "This skill should be skipped due to name mismatch"', + '---', + 'Skill content', + ], + }, + { + // Folder name matches skill name + path: `${rootFolder}/.github/skills/Valid Skill/SKILL.md`, + contents: [ + '---', + 'name: "Valid Skill"', + 'description: "This skill should be found"', + '---', + 'Valid skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find only 1 skill (mismatched one skipped)'); + assert.strictEqual(result[0].name, 'Valid Skill', 'Should only find the valid skill'); + }); + + test('should skip skills with missing name attribute', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'missing-name-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/no-name-skill/SKILL.md`, + contents: [ + '---', + 'description: "This skill has no name attribute"', + '---', + 'Skill content without name', + ], + }, + { + path: `${rootFolder}/.github/skills/Valid Named Skill/SKILL.md`, + contents: [ + '---', + 'name: "Valid Named Skill"', + 'description: "This skill has a name"', + '---', + 'Valid skill content', + ], + }, + ]); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 1, 'Should find only 1 skill (one without name skipped)'); + assert.strictEqual(result[0].name, 'Valid Named Skill', 'Should only find skill with name attribute'); + }); + + test('should include extension-provided skills in findAgentSkills', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'extension-skills-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const extensionSkillUri = URI.parse('file://extensions/my-extension/Extension Skill/SKILL.md'); + const extension = { + identifier: { value: 'test.my-extension' }, + enabledApiProposals: ['chatParticipantPrivate'] + } as unknown as IExtensionDescription; + + // Create workspace skill and extension skill + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/Workspace Skill/SKILL.md`, + contents: [ + '---', + 'name: "Workspace Skill"', + 'description: "A workspace skill"', + '---', + 'Workspace skill content', + ], + }, + { + path: extensionSkillUri.path, + contents: [ + '---', + 'name: "Extension Skill"', + 'description: "A skill from extension provider"', + '---', + 'Extension skill content', + ], + }, + ]); + + const provider = { + providePromptFiles: async (_context: IPromptFileContext, _token: CancellationToken) => { + return [{ uri: extensionSkillUri }]; + } + }; + + const registered = service.registerPromptFileProvider(extension, PromptsType.skill, provider); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 2, 'Should find 2 skills (workspace + extension)'); + + const workspaceSkill = result.find(s => s.name === 'Workspace Skill'); + assert.ok(workspaceSkill, 'Should find workspace skill'); + assert.strictEqual(workspaceSkill.storage, PromptsStorage.local); + + const extensionSkill = result.find(s => s.name === 'Extension Skill'); + assert.ok(extensionSkill, 'Should find extension skill'); + assert.strictEqual(extensionSkill.storage, PromptsStorage.extension); + + registered.dispose(); + }); + + test('should include contributed skill files in findAgentSkills', async () => { + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); + testConfigService.setUserConfiguration(PromptsConfig.SKILLS_LOCATION_KEY, {}); + + const rootFolderName = 'contributed-skills-test'; + const rootFolder = `/${rootFolderName}`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + const contributedSkillUri = URI.parse('file://extensions/my-extension/Contributed Skill/SKILL.md'); + const extension = { + identifier: { value: 'test.my-extension' } + } as unknown as IExtensionDescription; + + await mockFiles(fileService, [ + { + path: `${rootFolder}/.github/skills/Local Skill/SKILL.md`, + contents: [ + '---', + 'name: "Local Skill"', + 'description: "A local skill"', + '---', + 'Local skill content', + ], + }, + { + path: contributedSkillUri.path, + contents: [ + '---', + 'name: "Contributed Skill"', + 'description: "A contributed skill from extension"', + '---', + 'Contributed skill content', + ], + }, + ]); + + const registered = service.registerContributedFile( + PromptsType.skill, + contributedSkillUri, + extension, + 'Contributed Skill', + 'A contributed skill from extension' + ); + + const result = await service.findAgentSkills(CancellationToken.None); + + assert.ok(result, 'Should return results'); + assert.strictEqual(result.length, 2, 'Should find 2 skills (local + contributed)'); + + const localSkill = result.find(s => s.name === 'Local Skill'); + assert.ok(localSkill, 'Should find local skill'); + assert.strictEqual(localSkill.storage, PromptsStorage.local); + + const contributedSkill = result.find(s => s.name === 'Contributed Skill'); + assert.ok(contributedSkill, 'Should find contributed skill'); + assert.strictEqual(contributedSkill.storage, PromptsStorage.extension); + + registered.dispose(); + + // After disposal, only local skill should remain + const resultAfterDispose = await service.findAgentSkills(CancellationToken.None); + assert.strictEqual(resultAfterDispose?.length, 1, 'Should find 1 skill after disposal'); + assert.strictEqual(resultAfterDispose?.[0].name, 'Local Skill'); }); }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts index 097af623225..38836f677a5 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { MockFilesystem } from './mockFilesystem.js'; +import { mockFiles, MockFilesystem } from './mockFilesystem.js'; import { URI } from '../../../../../../../base/common/uri.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { assertDefined } from '../../../../../../../base/common/types.js'; @@ -195,33 +195,23 @@ suite('MockFilesystem', () => { instantiationService.stub(IFileService, fileService); }); - test('mocks file structure', async () => { + test('mocks file structure using new simplified format', async () => { const mockFilesystem = instantiationService.createInstance(MockFilesystem, [ { - name: '/root/folder', - children: [ - { - name: 'file.txt', - contents: 'contents', - }, - { - name: 'Subfolder', - children: [ - { - name: 'test.ts', - contents: 'other contents', - }, - { - name: 'file.test.ts', - contents: 'hello test', - }, - { - name: '.file-2.TEST.ts', - contents: 'test hello', - }, - ] - } - ] + path: '/root/folder/file.txt', + contents: ['contents'] + }, + { + path: '/root/folder/Subfolder/test.ts', + contents: ['other contents'] + }, + { + path: '/root/folder/Subfolder/file.test.ts', + contents: ['hello test'] + }, + { + path: '/root/folder/Subfolder/.file-2.TEST.ts', + contents: ['test hello'] } ]); @@ -286,4 +276,26 @@ suite('MockFilesystem', () => { fileService, ); }); + + test('can be created using static factory method', async () => { + await mockFiles(fileService, [ + { + path: '/simple/test.txt', + contents: ['line 1', 'line 2', 'line 3'] + } + ]); + + await validateFile( + '/simple/test.txt', + { + resource: URI.file('/simple/test.txt'), + name: 'test.txt', + isFile: true, + isDirectory: false, + isSymbolicLink: false, + contents: 'line 1\nline 2\nline 3', + }, + fileService, + ); + }); }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts index 73256648b06..cb408b18a05 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/testUtils/mockFilesystem.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from '../../../../../../../base/common/uri.js'; -import { assert } from '../../../../../../../base/common/assert.js'; import { VSBuffer } from '../../../../../../../base/common/buffer.js'; -import { timeout } from '../../../../../../../base/common/async.js'; import { IFileService } from '../../../../../../../platform/files/common/files.js'; +import { dirname } from '../../../../../../../base/common/resources.js'; /** * Represents a generic file system node. @@ -30,95 +29,128 @@ export interface IMockFolder extends IMockFilesystemNode { children: (IMockFolder | IMockFile)[]; } + +/** + * Represents a file entry for simplified initialization. + */ +export interface IMockFileEntry { + path: string; + contents: string[]; +} + /** - * Type for a mocked file or a folder that has absolute path URI. + * Creates mock filesystem from provided file entries. + * @param fileService File service instance + * @param files Array of file entries with path and contents */ -type TWithURI = T & { uri: URI }; +export function mockFiles(fileService: IFileService, files: IMockFileEntry[], parentFolder?: URI): Promise { + return new MockFilesystem(files, fileService).mock(parentFolder); +} /** * Utility to recursively creates provided filesystem structure. */ export class MockFilesystem { + private createdFiles: URI[] = []; + private createdFolders: URI[] = []; private createdRootFolders: URI[] = []; constructor( - private readonly folders: IMockFolder[], + private readonly input: IMockFolder[] | IMockFileEntry[], @IFileService private readonly fileService: IFileService, ) { } + + /** * Starts the mock process. */ - public async mock(parentFolder?: URI): Promise[]> { - const result = await Promise.all( - this.folders - .map((folder) => { - return this.mockFolder(folder, parentFolder); - }), - ); - - // wait for the filesystem event to settle before proceeding - // this is temporary workaround and should be fixed once we - // improve behavior of the `settled()` / `allSettled()` methods - await timeout(25); - - this.createdRootFolders.push(...result.map(r => r.uri)); - - return result; + public async mock(parentFolder?: URI): Promise { + // Check if input is the new simplified format + if (this.input.length > 0 && 'path' in this.input[0]) { + return this.mockFromFileEntries(this.input as IMockFileEntry[]); + } + + // Use the old format + return this.mockFromFolders(this.input as IMockFolder[], parentFolder); + } + + /** + * Mock using the new simplified file entry format. + */ + private async mockFromFileEntries(fileEntries: IMockFileEntry[]): Promise { + // Create all files and their parent directories + for (const fileEntry of fileEntries) { + const fileUri = URI.file(fileEntry.path); + + // Ensure parent directories exist + await this.ensureParentDirectories(dirname(fileUri)); + + // Create the file + const contents = fileEntry.contents.join('\n'); + await this.fileService.writeFile(fileUri, VSBuffer.fromString(contents)); + + this.createdFiles.push(fileUri); + } + } + + /** + * Mock using the old nested folder format. + */ + private async mockFromFolders(folders: IMockFolder[], parentFolder?: URI): Promise { + const result = await Promise.all(folders.map((folder) => this.mockFolder(folder, parentFolder))); + this.createdRootFolders.push(...result); } public async delete(): Promise { + // Delete files created by the new format + for (const fileUri of this.createdFiles) { + if (await this.fileService.exists(fileUri)) { + await this.fileService.del(fileUri, { useTrash: false }); + } + } + + for (const folderUri of this.createdFolders.reverse()) { // reverse to delete children first + if (await this.fileService.exists(folderUri)) { + await this.fileService.del(folderUri, { recursive: true, useTrash: false }); + } + } + + // Delete root folders created by the old format for (const folder of this.createdRootFolders) { await this.fileService.del(folder, { recursive: true, useTrash: false }); } } /** - * The internal implementation of the filesystem mocking process. - * - * @throws If a folder or file in the filesystem structure already exists. - * This is to prevent subtle errors caused by overwriting existing files. + * The internal implementation of the filesystem mocking process for the old format. */ - private async mockFolder( - folder: IMockFolder, - parentFolder?: URI, - ): Promise> { + private async mockFolder(folder: IMockFolder, parentFolder?: URI): Promise { const folderUri = parentFolder ? URI.joinPath(parentFolder, folder.name) : URI.file(folder.name); - assert( - !(await this.fileService.exists(folderUri)), - `Folder '${folderUri.path}' already exists.`, - ); - - try { - await this.fileService.createFolder(folderUri); - } catch (error) { - throw new Error(`Failed to create folder '${folderUri.fsPath}': ${error}.`); + if (!(await this.fileService.exists(folderUri))) { + try { + await this.fileService.createFolder(folderUri); + } catch (error) { + throw new Error(`Failed to create folder '${folderUri.fsPath}': ${error}.`); + } } - const resolvedChildren: (TWithURI | TWithURI)[] = []; + const resolvedChildren: URI[] = []; for (const child of folder.children) { const childUri = URI.joinPath(folderUri, child.name); // create child file if ('contents' in child) { - assert( - !(await this.fileService.exists(childUri)), - `File '${folderUri.path}' already exists.`, - ); - const contents: string = (typeof child.contents === 'string') ? child.contents : child.contents.join('\n'); await this.fileService.writeFile(childUri, VSBuffer.fromString(contents)); - resolvedChildren.push({ - ...child, - uri: childUri, - }); + resolvedChildren.push(childUri); continue; } @@ -127,9 +159,25 @@ export class MockFilesystem { resolvedChildren.push(await this.mockFolder(child, folderUri)); } - return { - ...folder, - uri: folderUri, - }; + return folderUri; + } + + /** + * Ensures that all parent directories of the given file URI exist. + */ + private async ensureParentDirectories(dirUri: URI): Promise { + if (!await this.fileService.exists(dirUri)) { + // First ensure the parent directory exists (recursive call) + if (dirUri.path !== '/') { + await this.ensureParentDirectories(dirname(dirUri)); + } + // Then create this directory + try { + await this.fileService.createFolder(dirUri); + this.createdFolders.push(dirUri); + } catch (error) { + throw new Error(`Failed to create directory '${dirUri.toString()}': ${error}.`); + } + } } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index fc0b79bf65c..6eb3fd520c9 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../../../../base/common/cancellation.js'; import { match } from '../../../../../../../base/common/glob.js'; import { Schemas } from '../../../../../../../base/common/network.js'; import { basename, relativePath } from '../../../../../../../base/common/resources.js'; @@ -21,10 +21,11 @@ import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from '../../.. import { IWorkbenchEnvironmentService } from '../../../../../../services/environment/common/environmentService.js'; import { IFileMatch, IFileQuery, ISearchService } from '../../../../../../services/search/common/search.js'; import { IUserDataProfileService } from '../../../../../../services/userDataProfile/common/userDataProfile.js'; +import { IPathService } from '../../../../../../services/path/common/pathService.js'; import { PromptsConfig } from '../../../../common/promptSyntax/config/config.js'; import { PromptsType } from '../../../../common/promptSyntax/promptTypes.js'; -import { isValidGlob, PromptFilesLocator } from '../../../../common/promptSyntax/utils/promptFilesLocator.js'; -import { IMockFolder, MockFilesystem } from '../testUtils/mockFilesystem.js'; +import { hasGlobPattern, isValidGlob, isValidPromptFolderPath, PromptFilesLocator } from '../../../../common/promptSyntax/utils/promptFilesLocator.js'; +import { IMockFileEntry, IMockFolder, MockFilesystem } from '../testUtils/mockFilesystem.js'; import { mockService } from './mock.js'; import { TestUserDataProfileService } from '../../../../../../test/common/workbenchTestServices.js'; import { PromptsStorage } from '../../../../common/promptSyntax/service/promptsService.js'; @@ -33,23 +34,20 @@ import { runWithFakedTimers } from '../../../../../../../base/test/common/timeTr /** * Mocked instance of {@link IConfigurationService}. */ -function mockConfigService(value: T): IConfigurationService { +function mockConfigService(configValues: Record): IConfigurationService { return mockService({ getValue(key?: string | IConfigurationOverrides) { - assert( - typeof key === 'string', - `Expected string configuration key, got '${typeof key}'.`, - ); - if ('explorer.excludeGitIgnore' === key) { - return false; + // Handle object configuration overrides (e.g., for file exclude patterns) + if (typeof key === 'object') { + return {}; } - - assert( - [PromptsConfig.PROMPT_LOCATIONS_KEY, PromptsConfig.INSTRUCTIONS_LOCATION_KEY, PromptsConfig.MODE_LOCATION_KEY].includes(key), - `Unsupported configuration key '${key}'.`, - ); - - return value; + if (typeof key !== 'string') { + assert.fail(`Unsupported configuration key '${key}'.`); + } + if (configValues.hasOwnProperty(key)) { + return configValues[key]; + } + assert.fail(`Unsupported configuration key '${key}'.`); }, }); } @@ -78,10 +76,6 @@ function testT(name: string, fn: () => Promise): Mocha.Test { suite('PromptFilesLocator', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - // if (isWindows) { - // return; - // } - let instantiationService: TestInstantiationService; setup(async () => { instantiationService = disposables.add(new TestInstantiationService()); @@ -103,7 +97,15 @@ suite('PromptFilesLocator', () => { const mockFs = instantiationService.createInstance(MockFilesystem, filesystem); await mockFs.mock(); - instantiationService.stub(IConfigurationService, mockConfigService(configValue)); + instantiationService.stub(IConfigurationService, mockConfigService({ + 'explorer.excludeGitIgnore': false, + 'files.exclude': {}, + 'search.exclude': {}, + [PromptsConfig.PROMPT_LOCATIONS_KEY]: configValue, + [PromptsConfig.INSTRUCTIONS_LOCATION_KEY]: configValue, + [PromptsConfig.MODE_LOCATION_KEY]: configValue, + [PromptsConfig.SKILLS_LOCATION_KEY]: configValue, + })); const workspaceFolders = workspaceFolderPaths.map((path, index) => { const uri = URI.file(path); @@ -118,6 +120,9 @@ suite('PromptFilesLocator', () => { instantiationService.stub(IWorkbenchEnvironmentService, {} as IWorkbenchEnvironmentService); instantiationService.stub(IUserDataProfileService, new TestUserDataProfileService()); instantiationService.stub(ISearchService, { + schemeHasFileSearchProvider(scheme: string): boolean { + return true; + }, async fileSearch(query: IFileQuery) { // mock the search service const fs = instantiationService.get(IFileService); @@ -149,6 +154,15 @@ suite('PromptFilesLocator', () => { return { results, messages: [] }; } }); + instantiationService.stub(IPathService, { + userHome(options?: { preferLocal: boolean }): URI | Promise { + const uri = URI.file('/Users/legomushroom'); + if (options?.preferLocal) { + return uri; + } + return Promise.resolve(uri); + } + } as IPathService); const locator = instantiationService.createInstance(PromptFilesLocator); @@ -156,11 +170,13 @@ suite('PromptFilesLocator', () => { async listFiles(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise { return locator.listFiles(type, storage, token); }, - getConfigBasedSourceFolders(type: PromptsType): readonly URI[] { - return locator.getConfigBasedSourceFolders(type); + async getConfigBasedSourceFolders(type: PromptsType): Promise { + return await locator.getConfigBasedSourceFolders(type); + }, + async findAgentSkills(token: CancellationToken) { + return await locator.findAgentSkills(token); }, async disposeAsync(): Promise { - locator.dispose(); await mockFs.delete(); } }; @@ -2350,6 +2366,473 @@ suite('PromptFilesLocator', () => { }); }); + suite('skills', () => { + suite('findAgentSkills', () => { + testT('finds skill files in configured locations', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [ + { + name: '/Users/legomushroom/repos/vscode/.claude/skills', + children: [ + { + name: 'pptx', + children: [ + { + name: 'SKILL.md', + contents: '# PPTX Skill', + }, + ], + }, + { + name: 'excel', + children: [ + { + name: 'SKILL.md', + contents: '# Excel Skill', + }, + ], + }, + ], + }, + ], + ); + + const skills = await locator.findAgentSkills(CancellationToken.None); + assertOutcome( + skills.map(s => s.fileUri), + [ + '/Users/legomushroom/repos/vscode/.claude/skills/pptx/SKILL.md', + '/Users/legomushroom/repos/vscode/.claude/skills/excel/SKILL.md', + ], + 'Must find skill files.', + ); + await locator.disposeAsync(); + }); + + testT('ignores folders without SKILL.md', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [ + { + name: '/Users/legomushroom/repos/vscode/.claude/skills', + children: [ + { + name: 'valid-skill', + children: [ + { + name: 'SKILL.md', + contents: '# Valid Skill', + }, + ], + }, + { + name: 'invalid-skill', + children: [ + { + name: 'readme.md', + contents: 'Not a skill file', + }, + ], + }, + { + name: 'another-invalid', + children: [ + { + name: 'index.js', + contents: 'console.log("not a skill")', + }, + ], + }, + ], + }, + ], + ); + + const skills = await locator.findAgentSkills(CancellationToken.None); + assertOutcome( + skills.map(s => s.fileUri), + [ + '/Users/legomushroom/repos/vscode/.claude/skills/valid-skill/SKILL.md', + ], + 'Must only find folders with SKILL.md.', + ); + await locator.disposeAsync(); + }); + + testT('returns empty array when no skills exist', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [ + { + name: '/Users/legomushroom/repos/vscode/.claude/skills', + children: [], + }, + ], + ); + + const skills = await locator.findAgentSkills(CancellationToken.None); + assertOutcome( + skills.map(s => s.fileUri), + [], + 'Must return empty array when no skills exist.', + ); + await locator.disposeAsync(); + }); + + testT('returns empty array when skill folder does not exist', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], // empty filesystem + ); + + const skills = await locator.findAgentSkills(CancellationToken.None); + assertOutcome( + skills.map(s => s.fileUri), + [], + 'Must return empty array when folder does not exist.', + ); + await locator.disposeAsync(); + }); + + testT('finds skills across multiple workspace folders', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + [ + '/Users/legomushroom/repos/vscode', + '/Users/legomushroom/repos/node', + ], + [ + { + name: '/Users/legomushroom/repos/vscode/.claude/skills', + children: [ + { + name: 'skill-a', + children: [ + { + name: 'SKILL.md', + contents: '# Skill A', + }, + ], + }, + ], + }, + { + name: '/Users/legomushroom/repos/node/.claude/skills', + children: [ + { + name: 'skill-b', + children: [ + { + name: 'SKILL.md', + contents: '# Skill B', + }, + ], + }, + ], + }, + ], + ); + + const skills = await locator.findAgentSkills(CancellationToken.None); + assertOutcome( + skills.map(s => s.fileUri), + [ + '/Users/legomushroom/repos/vscode/.claude/skills/skill-a/SKILL.md', + '/Users/legomushroom/repos/node/.claude/skills/skill-b/SKILL.md', + ], + 'Must find skills across all workspace folders.', + ); + await locator.disposeAsync(); + }); + }); + + suite('listFiles with PromptsType.skill', () => { + testT('does not list skills when location is disabled', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': false, + // disable other defaults + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [ + { + name: '/Users/legomushroom/repos/vscode/.claude/skills', + children: [ + { + name: 'pptx', + children: [ + { + name: 'SKILL.md', + contents: '# PPTX Skill', + }, + ], + }, + ], + }, + ], + ); + + const files = await locator.listFiles(PromptsType.skill, PromptsStorage.local, CancellationToken.None); + assertOutcome( + files, + [], + 'Must not list skills when location is disabled.', + ); + await locator.disposeAsync(); + }); + }); + + suite('toAbsoluteLocationsForSkills path validation', () => { + testT('rejects glob patterns in skill paths via getConfigBasedSourceFolders', async () => { + const locator = await createPromptsLocator( + { + 'skills/**': true, + 'skills/*': true, + '**/skills': true, + // disable defaults + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [], + 'Must reject glob patterns in skill paths.', + ); + await locator.disposeAsync(); + }); + + testT('rejects absolute paths in skill paths via getConfigBasedSourceFolders', async () => { + const locator = await createPromptsLocator( + { + '/absolute/path/skills': true, + // disable defaults + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [], + 'Must reject absolute paths in skill paths.', + ); + await locator.disposeAsync(); + }); + + testT('accepts relative paths in skill paths via getConfigBasedSourceFolders', async () => { + const locator = await createPromptsLocator( + { + './my-skills': true, + 'custom/skills': true, + // disable defaults + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + '/Users/legomushroom/repos/vscode/my-skills', + '/Users/legomushroom/repos/vscode/custom/skills', + ], + 'Must accept relative paths in skill paths.', + ); + await locator.disposeAsync(); + }); + + testT('accepts parent relative paths for monorepos via getConfigBasedSourceFolders', async () => { + const locator = await createPromptsLocator( + { + '../shared-skills': true, + // disable defaults + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + '/Users/legomushroom/repos/shared-skills', + ], + 'Must accept parent relative paths for monorepos.', + ); + await locator.disposeAsync(); + }); + + testT('accepts tilde paths for user home skills', async () => { + const locator = await createPromptsLocator( + { + '~/my-skills': true, + // disable defaults + '.github/skills': false, + '.claude/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + '/Users/legomushroom/my-skills', + ], + 'Must accept tilde paths for user home skills.', + ); + await locator.disposeAsync(); + }); + }); + + suite('getConfigBasedSourceFolders for skills', () => { + testT('returns source folders without glob processing', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + 'custom-skills': true, + // explicitly disable other defaults we don't want for this test + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + [ + '/Users/legomushroom/repos/vscode', + '/Users/legomushroom/repos/node', + ], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + '/Users/legomushroom/repos/vscode/.claude/skills', + '/Users/legomushroom/repos/node/.claude/skills', + '/Users/legomushroom/repos/vscode/custom-skills', + '/Users/legomushroom/repos/node/custom-skills', + ], + 'Must return skill source folders without glob processing.', + ); + await locator.disposeAsync(); + }); + + testT('filters out invalid skill paths from source folders', async () => { + const locator = await createPromptsLocator( + { + '.claude/skills': true, + 'skills/**': true, // glob - should be filtered out + '/absolute/skills': true, // absolute - should be filtered out + // explicitly disable other defaults we don't want for this test + '.github/skills': false, + '~/.copilot/skills': false, + '~/.claude/skills': false, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + '/Users/legomushroom/repos/vscode/.claude/skills', + ], + 'Must filter out invalid skill paths.', + ); + await locator.disposeAsync(); + }); + + testT('includes default skill source folders from defaults', async () => { + const locator = await createPromptsLocator( + { + 'custom-skills': true, + }, + ['/Users/legomushroom/repos/vscode'], + [], + ); + + const folders = await locator.getConfigBasedSourceFolders(PromptsType.skill); + assertOutcome( + folders, + [ + // defaults + '/Users/legomushroom/repos/vscode/.github/skills', + '/Users/legomushroom/repos/vscode/.claude/skills', + '/Users/legomushroom/.copilot/skills', + '/Users/legomushroom/.claude/skills', + // custom + '/Users/legomushroom/repos/vscode/custom-skills', + ], + 'Must include default skill source folders.', + ); + await locator.disposeAsync(); + }); + }); + }); + suite('isValidGlob', () => { testT('valid patterns', async () => { const globs = [ @@ -2425,6 +2908,229 @@ suite('PromptFilesLocator', () => { }); }); + suite('isValidSkillPath', () => { + testT('accepts relative paths', async () => { + const validPaths = [ + 'someFolder', + './someFolder', + 'my-skills', + './my-skills', + 'folder/subfolder', + './folder/subfolder', + ]; + + for (const path of validPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + true, + `'${path}' must be accepted as a valid skill path (relative path).`, + ); + } + }); + + testT('accepts user home paths', async () => { + const validPaths = [ + '~/folder', + '~/.copilot/skills', + '~/.claude/skills', + '~/my-skills', + ]; + + for (const path of validPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + true, + `'${path}' must be accepted as a valid skill path (user home path).`, + ); + } + }); + + testT('accepts parent relative paths for monorepos', async () => { + const validPaths = [ + '../folder', + '../shared-skills', + '../../common/skills', + '../parent/folder', + ]; + + for (const path of validPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + true, + `'${path}' must be accepted as a valid skill path (parent relative path).`, + ); + } + }); + + testT('rejects absolute paths', async () => { + const invalidPaths = [ + // Unix absolute paths + '/Users/username/skills', + '/absolute/path', + '/usr/local/skills', + // Windows absolute paths + 'C:\\Users\\skills', + 'D:/skills', + 'c:\\folder', + ]; + + for (const path of invalidPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + false, + `'${path}' must be rejected (absolute paths not supported for portability).`, + ); + } + }); + + testT('rejects tilde paths without path separator', async () => { + const invalidPaths = [ + '~abc', + '~skills', + '~.config', + // Windows-style backslash paths are not supported for cross-platform sharing + '~\\folder', + '~\\.copilot\\skills', + ]; + + for (const path of invalidPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + false, + `'${path}' must be rejected (tilde must be followed by / only, not \\).`, + ); + } + }); + + testT('rejects paths with backslashes', async () => { + const invalidPaths = [ + 'folder\\subfolder', + '.\\skills', + '..\\parent\\folder', + 'my\\skills\\folder', + ]; + + for (const path of invalidPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + false, + `'${path}' must be rejected (backslash paths not supported for cross-platform sharing).`, + ); + } + }); + + testT('rejects glob patterns', async () => { + const invalidPaths = [ + 'skills/*', + 'skills/**', + '**/skills', + 'skills/*.md', + 'skills/**/*.md', + '{skill1,skill2}', + 'skill[1,2,3]', + 'skills?', + './skills/*', + '~/skills/**', + ]; + + for (const path of invalidPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + false, + `'${path}' must be rejected (glob patterns not supported for performance).`, + ); + } + }); + + testT('rejects empty or whitespace paths', async () => { + const invalidPaths = [ + '', + ' ', + '\t', + '\n', + ]; + + for (const path of invalidPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + false, + `'${path}' must be rejected (empty or whitespace only).`, + ); + } + }); + + testT('handles paths with spaces', async () => { + const validPaths = [ + 'my skills', + './my skills/folder', + '~/my skills', + '../shared skills', + ]; + + for (const path of validPaths) { + assert.strictEqual( + isValidPromptFolderPath(path), + true, + `'${path}' must be accepted (paths with spaces are valid).`, + ); + } + }); + }); + + suite('hasGlobPattern', () => { + testT('detects single wildcard', async () => { + const pathsWithGlob = [ + 'skills/*', + 'my-skills/*', + '*.md', + '*/folder', + ]; + + for (const path of pathsWithGlob) { + assert.strictEqual( + hasGlobPattern(path), + true, + `'${path}' must be detected as having a glob pattern.`, + ); + } + }); + + testT('detects double wildcard', async () => { + const pathsWithGlob = [ + 'skills/**', + '**/skills', + '**/*.md', + 'a/**/b', + ]; + + for (const path of pathsWithGlob) { + assert.strictEqual( + hasGlobPattern(path), + true, + `'${path}' must be detected as having a glob pattern.`, + ); + } + }); + + testT('returns false for paths without wildcards', async () => { + const pathsWithoutGlob = [ + 'skills', + './skills/folder', + '~/skills', + '../parent/folder', + '.github/prompts', + ]; + + for (const path of pathsWithoutGlob) { + assert.strictEqual( + hasGlobPattern(path), + false, + `'${path}' must not be detected as having a glob pattern.`, + ); + } + }); + }); + suite('getConfigBasedSourceFolders', () => { testT('gets unambiguous list of folders', async () => { const locator = await createPromptsLocator( @@ -2446,7 +3152,7 @@ suite('PromptFilesLocator', () => { ); assertOutcome( - locator.getConfigBasedSourceFolders(PromptsType.prompt), + await locator.getConfigBasedSourceFolders(PromptsType.prompt), [ '/Users/legomushroom/repos/vscode/.github/prompts', '/Users/legomushroom/repos/prompts/.github/prompts', @@ -2463,6 +3169,183 @@ suite('PromptFilesLocator', () => { await locator.disposeAsync(); }); }); + + suite('findAgentMDsInWorkspace', () => { + testT('finds AGENTS.md files using FileSearchProvider', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/AGENTS.md', + contents: ['# Src agents'] + } + ], + true // has FileSearchProvider + ); + + const result = await locator.findAgentMDsInWorkspace(CancellationToken.None); + assertOutcome( + result, + [ + '/Users/legomushroom/repos/workspace/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/AGENTS.md' + ], + 'Must find all AGENTS.md files using search service.' + ); + await locator.disposeAsync(); + }); + + testT('finds AGENTS.md files using file service fallback', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/AGENTS.md', + contents: ['# Src agents'] + }, + { + path: '/Users/legomushroom/repos/workspace/src/nested/AGENTS.md', + contents: ['# Nested agents'] + } + ], + false // no FileSearchProvider - should use file service fallback + ); + + const result = await locator.findAgentMDsInWorkspace(CancellationToken.None); + assertOutcome( + result, + [ + '/Users/legomushroom/repos/workspace/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/AGENTS.md', + '/Users/legomushroom/repos/workspace/src/nested/AGENTS.md' + ], + 'Must find all AGENTS.md files using file service fallback.' + ); + await locator.disposeAsync(); + }); + + testT('handles cancellation token in file service fallback', async () => { + const locator = await createPromptsLocatorForAgentMD( + {}, + ['/Users/legomushroom/repos/workspace'], + [ + { + path: '/Users/legomushroom/repos/workspace/AGENTS.md', + contents: ['# Root agents'] + } + ], + false // no FileSearchProvider + ); + + const source = new CancellationTokenSource(); + // Cancel immediately + source.cancel(); + const result = await locator.findAgentMDsInWorkspace(source.token); + assertOutcome( + result, + [], + 'Must return empty array when cancelled.' + ); + await locator.disposeAsync(); + }); + + const createPromptsLocatorForAgentMD = async ( + configValue: unknown, + workspaceFolderPaths: string[], + filesystem: IMockFileEntry[], + hasFileSearchProvider: boolean + ) => { + const mockFs = instantiationService.createInstance(MockFilesystem, filesystem); + await mockFs.mock(); + + instantiationService.stub(IConfigurationService, mockConfigService({ + 'explorer.excludeGitIgnore': false, + 'files.exclude': {}, + 'search.exclude': {} + })); + + const workspaceFolders = workspaceFolderPaths.map((path, index) => { + const uri = URI.file(path); + + return new class extends mock() { + override uri = uri; + override name = basename(uri); + override index = index; + }; + }); + instantiationService.stub(IWorkspaceContextService, mockWorkspaceService(workspaceFolders)); + instantiationService.stub(IWorkbenchEnvironmentService, {} as IWorkbenchEnvironmentService); + instantiationService.stub(IUserDataProfileService, new TestUserDataProfileService()); + instantiationService.stub(ISearchService, { + schemeHasFileSearchProvider(scheme: string): boolean { + return hasFileSearchProvider; + }, + async fileSearch(query: IFileQuery) { + if (!hasFileSearchProvider) { + throw new Error('FileSearchProvider not available'); + } + // mock the search service + const fs = instantiationService.get(IFileService); + const findFilesInLocation = async (location: URI, results: URI[] = []) => { + try { + const resolve = await fs.resolve(location); + if (resolve.isFile) { + results.push(resolve.resource); + } else if (resolve.isDirectory && resolve.children) { + for (const child of resolve.children) { + await findFilesInLocation(child.resource, results); + } + } + } catch (error) { + } + return results; + }; + const results: IFileMatch[] = []; + for (const folderQuery of query.folderQueries) { + const allFiles = await findFilesInLocation(folderQuery.folder); + for (const resource of allFiles) { + const pathInFolder = relativePath(folderQuery.folder, resource) ?? ''; + if (query.filePattern === undefined || match(query.filePattern, pathInFolder)) { + results.push({ resource }); + } + } + + } + return { results, messages: [] }; + } + }); + instantiationService.stub(IPathService, { + userHome(options?: { preferLocal: boolean }): URI | Promise { + const uri = URI.file('/Users/legomushroom'); + if (options?.preferLocal) { + return uri; + } + return Promise.resolve(uri); + } + } as IPathService); + + const locator = instantiationService.createInstance(PromptFilesLocator); + + return { + async findAgentMDsInWorkspace(token: CancellationToken): Promise { + return locator.findAgentMDsInWorkspace(token); + }, + async disposeAsync(): Promise { + await mockFs.delete(); + } + }; + }; + }); }); function assertOutcome(actual: readonly URI[], expected: string[], message: string) { diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptsServiceUtils.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptsServiceUtils.test.ts new file mode 100644 index 00000000000..58e9f47da29 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptsServiceUtils.test.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js'; +import { IProductService } from '../../../../../../../platform/product/common/productService.js'; +import { isOrganizationPromptFile } from '../../../../common/promptSyntax/utils/promptsServiceUtils.js'; +import { mockService } from './mock.js'; + +suite('promptsServiceUtils', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('isOrganizationPromptFile', () => { + const CHAT_EXTENSION_ID = 'github.copilot-chat'; + + function createProductService(chatExtensionId: string | undefined): IProductService { + return mockService({ + defaultChatAgent: chatExtensionId ? { chatExtensionId } : undefined, + } as Partial); + } + + test('returns false when no chatExtensionId is configured', () => { + const uri = URI.file('/some/path/github/prompt.md'); + const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID); + const productService = createProductService(undefined); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + false, + 'Should return false when chatExtensionId is not configured', + ); + }); + + test('returns false when extension ID does not match', () => { + const uri = URI.file('/some/path/github/prompt.md'); + const extensionId = new ExtensionIdentifier('some.other-extension'); + const productService = createProductService(CHAT_EXTENSION_ID); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + false, + 'Should return false when extension ID does not match the built-in chat extension', + ); + }); + + test('returns false when path does not contain /github/', () => { + const uri = URI.file('/some/path/to/prompt.md'); + const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID); + const productService = createProductService(CHAT_EXTENSION_ID); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + false, + 'Should return false when path does not contain /github/', + ); + }); + + test('returns true when extension matches and path contains /github/', () => { + const uri = URI.file('/some/path/github/prompts/prompt.md'); + const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID); + const productService = createProductService(CHAT_EXTENSION_ID); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + true, + 'Should return true when extension matches and path contains /github/', + ); + }); + + test('extension ID comparison is case-insensitive', () => { + const uri = URI.file('/some/github/prompt.md'); + const extensionId = new ExtensionIdentifier('GITHUB.COPILOT-CHAT'); + const productService = createProductService('github.copilot-chat'); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + true, + 'Extension ID comparison should be case-insensitive', + ); + }); + + test('returns false when defaultChatAgent exists but chatExtensionId is empty', () => { + const uri = URI.file('/some/github/prompt.md'); + const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID); + const productService = mockService({ + defaultChatAgent: { chatExtensionId: '' }, + } as Partial); + + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + false, + 'Should return false when chatExtensionId is empty string', + ); + }); + + test('returns false for similar but incorrect paths', () => { + const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID); + const productService = createProductService(CHAT_EXTENSION_ID); + + const invalidPaths = [ + '/some/githubs/prompt.md', // extra 's' + '/some/github-org/prompt.md', // hyphenated + '/some/mygithub/prompt.md', // prefix + '/some/githubstuff/prompt.md', // suffix + '/some/GITHUB/prompt.md', // uppercase (path matching is case-sensitive) + '/some/Github/prompt.md', // mixed case + ]; + + for (const path of invalidPaths) { + const uri = URI.file(path); + assert.strictEqual( + isOrganizationPromptFile(uri, extensionId, productService), + false, + `Should return false for path: ${path}`, + ); + } + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_and_subcommand_after_newline.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_and_subcommand_with_leading_whitespace.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_but_edit_mode.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_not_first.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_not_first.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_not_first.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_with_question_mark.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agent_with_subcommand_after_text.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents__subCommand.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents__subCommand.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents__subCommand.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_agents_and_tools_and_multiline__part2.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_invalid_slash_command.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_multiple_slash_commands.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_plain_text.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_plain_text.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_plain_text_with_newlines.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap similarity index 91% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap index 70b24f7309e..9999e2caa0b 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command.0.snap @@ -25,7 +25,7 @@ endLineNumber: 1, endColumn: 12 }, - slashPromptCommand: { command: "prompt" }, + name: "prompt", kind: "prompt" } ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_after_slash.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_after_text.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap similarity index 91% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap index 9981978ac07..0b5ae38de10 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_prompt_slash_command_with_numbers.0.snap @@ -11,7 +11,7 @@ endLineNumber: 1, endColumn: 12 }, - slashPromptCommand: { command: "001-sample" }, + name: "001-sample", kind: "prompt" }, { diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command_after_whitespace.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_command_not_first.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap b/src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_in_text.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatRequestParser_slash_in_text.0.snap rename to src/vs/workbench/contrib/chat/test/common/requestParser/__snapshots__/ChatRequestParser_slash_in_text.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts b/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts similarity index 87% rename from src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts rename to src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts index 86c9dbccfab..1ec95eed15f 100644 --- a/src/vs/workbench/contrib/chat/test/common/chatRequestParser.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/requestParser/chatRequestParser.test.ts @@ -3,28 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mockObject } from '../../../../../base/test/common/mock.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IStorageService } from '../../../../../platform/storage/common/storage.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { TestExtensionService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; -import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatRequestParser } from '../../common/chatRequestParser.js'; -import { IChatService } from '../../common/chatService.js'; -import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { LocalChatSessionUri } from '../../common/chatUri.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; -import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; -import { IToolData, ToolDataSource, ToolSet } from '../../common/languageModelToolsService.js'; -import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js'; -import { MockChatService } from './mockChatService.js'; -import { MockChatVariablesService } from './mockChatVariables.js'; -import { MockPromptsService } from './mockPromptsService.js'; +import { mockObject } from '../../../../../../base/test/common/mock.js'; +import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; +import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { ILogService, NullLogService } from '../../../../../../platform/log/common/log.js'; +import { IStorageService } from '../../../../../../platform/storage/common/storage.js'; +import { IExtensionService, nullExtensionDescription } from '../../../../../services/extensions/common/extensions.js'; +import { TestExtensionService, TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { ChatAgentService, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../../common/participants/chatAgents.js'; +import { ChatRequestParser } from '../../../common/requestParser/chatRequestParser.js'; +import { IChatService } from '../../../common/chatService/chatService.js'; +import { IChatSlashCommandService } from '../../../common/participants/chatSlashCommands.js'; +import { LocalChatSessionUri } from '../../../common/model/chatUri.js'; +import { IChatVariablesService } from '../../../common/attachments/chatVariables.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { IToolData, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; +import { IPromptsService } from '../../../common/promptSyntax/service/promptsService.js'; +import { MockChatService } from '../chatService/mockChatService.js'; +import { MockChatVariablesService } from '../mockChatVariables.js'; +import { MockPromptsService } from '../promptSyntax/service/mockPromptsService.js'; const testSessionUri = LocalChatSessionUri.forSession('test-session'); @@ -136,11 +136,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatSlashCommandService, slashCommandService as any); const promptSlashCommandService = mockObject()({}); - promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { - if (command.match(/^[\w_\-\.]+$/)) { - return { command }; - } - return undefined; + promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { + return !!command.match(/^[\w_\-\.]+$/); }); // eslint-disable-next-line local/code-no-any-casts instantiationService.stub(IPromptsService, promptSlashCommandService as any); @@ -158,11 +155,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatSlashCommandService, slashCommandService as any); const promptSlashCommandService = mockObject()({}); - promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { - if (command.match(/^[\w_\-\.]+$/)) { - return { command }; - } - return undefined; + promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { + return !!command.match(/^[\w_\-\.]+$/); }); // eslint-disable-next-line local/code-no-any-casts instantiationService.stub(IPromptsService, promptSlashCommandService as any); @@ -180,11 +174,9 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatSlashCommandService, slashCommandService as any); const promptSlashCommandService = mockObject()({}); - promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { - if (command.match(/^[\w_\-\.]+$/)) { - return { command }; - } - return undefined; + promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { + return !!command.match(/^[\w_\-\.]+$/); + }); // eslint-disable-next-line local/code-no-any-casts instantiationService.stub(IPromptsService, promptSlashCommandService as any); @@ -202,11 +194,8 @@ suite('ChatRequestParser', () => { instantiationService.stub(IChatSlashCommandService, slashCommandService as any); const promptSlashCommandService = mockObject()({}); - promptSlashCommandService.asPromptSlashCommand.callsFake((command: string) => { - if (command.match(/^[\w_\-\.]+$/)) { - return { command }; - } - return undefined; + promptSlashCommandService.isValidSlashCommandName.callsFake((command: string) => { + return !!command.match(/^[\w_\-\.]+$/); }); // eslint-disable-next-line local/code-no-any-casts instantiationService.stub(IPromptsService, promptSlashCommandService as any); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatUrlFetchingPatterns.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatUrlFetchingPatterns.test.ts new file mode 100644 index 00000000000..4d07a805464 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/chatUrlFetchingPatterns.test.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { extractUrlPatterns, getPatternLabel, isUrlApproved, getMatchingPattern, IUrlApprovalSettings } from '../../../../common/tools/builtinTools/chatUrlFetchingPatterns.js'; + +suite('ChatUrlFetchingPatterns', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('extractUrlPatterns', () => { + test('simple domain', () => { + const url = URI.parse('https://example.com'); + const patterns = extractUrlPatterns(url); + assert.deepStrictEqual(patterns, [ + 'https://example.com', + ]); + }); + + test('subdomain', () => { + const url = URI.parse('https://api.example.com'); + const patterns = extractUrlPatterns(url); + assert.deepStrictEqual(patterns, [ + 'https://api.example.com', + 'https://*.example.com' + ]); + }); + + test('multiple subdomains', () => { + const url = URI.parse('https://foo.bar.example.com/path'); + const patterns = extractUrlPatterns(url); + assert.deepStrictEqual(patterns, [ + 'https://foo.bar.example.com/path', + 'https://foo.bar.example.com', + 'https://*.bar.example.com', + 'https://*.example.com', + ]); + }); + + test('with path', () => { + const url = URI.parse('https://example.com/api/v1/users'); + const patterns = extractUrlPatterns(url); + assert.deepStrictEqual(patterns, [ + 'https://example.com/api/v1/users', + 'https://example.com', + 'https://example.com/api/v1', + 'https://example.com/api', + ]); + }); + + test('IP address - no wildcard subdomain', () => { + const url = URI.parse('https://192.168.1.1'); + const patterns = extractUrlPatterns(url); + assert.strictEqual(patterns.filter(p => p.includes('*')).length, 0); + }); + + test('with query and fragment', () => { + const url = URI.parse('https://example.com/path?query=1#fragment'); + const patterns = extractUrlPatterns(url); + assert.deepStrictEqual(patterns, [ + 'https://example.com/path?query=1#fragment', + 'https://example.com', + ]); + }); + }); + + suite('getPatternLabel', () => { + test('removes https protocol', () => { + const url = URI.parse('https://example.com'); + const label = getPatternLabel(url, 'https://example.com'); + assert.strictEqual(label, 'example.com'); + }); + + test('removes http protocol', () => { + const url = URI.parse('http://example.com'); + const label = getPatternLabel(url, 'http://example.com'); + assert.strictEqual(label, 'example.com'); + }); + + test('removes trailing slashes', () => { + const url = URI.parse('https://example.com/'); + const label = getPatternLabel(url, 'https://example.com/'); + assert.strictEqual(label, 'example.com'); + }); + + test('preserves path', () => { + const url = URI.parse('https://example.com/api/v1'); + const label = getPatternLabel(url, 'https://example.com/api/v1'); + assert.strictEqual(label, 'example.com/api/v1'); + }); + }); + + suite('isUrlApproved', () => { + test('exact match with boolean', () => { + const url = URI.parse('https://example.com'); + const approved = { 'https://example.com': true }; + assert.strictEqual(isUrlApproved(url, approved, true), true); + assert.strictEqual(isUrlApproved(url, approved, false), true); + }); + + test('no match returns false', () => { + const url = URI.parse('https://example.com'); + const approved = { 'https://other.com': true }; + assert.strictEqual(isUrlApproved(url, approved, true), false); + }); + + test('wildcard subdomain match', () => { + const url = URI.parse('https://api.example.com'); + const approved = { 'https://*.example.com': true }; + assert.strictEqual(isUrlApproved(url, approved, true), true); + }); + + test('path wildcard match', () => { + const url = URI.parse('https://example.com/api/users'); + const approved = { 'https://example.com/api/*': true }; + assert.strictEqual(isUrlApproved(url, approved, true), true); + }); + + test('granular settings - request approved', () => { + const url = URI.parse('https://example.com'); + const approved: Record = { + 'https://example.com': { approveRequest: true, approveResponse: false } + }; + assert.strictEqual(isUrlApproved(url, approved, true), true); + assert.strictEqual(isUrlApproved(url, approved, false), false); + }); + + test('granular settings - response approved', () => { + const url = URI.parse('https://example.com'); + const approved: Record = { + 'https://example.com': { approveRequest: false, approveResponse: true } + }; + assert.strictEqual(isUrlApproved(url, approved, true), false); + assert.strictEqual(isUrlApproved(url, approved, false), true); + }); + + test('granular settings - both approved', () => { + const url = URI.parse('https://example.com'); + const approved: Record = { + 'https://example.com': { approveRequest: true, approveResponse: true } + }; + assert.strictEqual(isUrlApproved(url, approved, true), true); + assert.strictEqual(isUrlApproved(url, approved, false), true); + }); + + test('granular settings - missing property defaults to false', () => { + const url = URI.parse('https://example.com'); + const approved: Record = { + 'https://example.com': { approveRequest: true } + }; + assert.strictEqual(isUrlApproved(url, approved, false), false); + }); + }); + + suite('getMatchingPattern', () => { + test('exact match', () => { + const url = URI.parse('https://example.com/path'); + const approved = { 'https://example.com/path': true }; + const pattern = getMatchingPattern(url, approved); + assert.strictEqual(pattern, 'https://example.com/path'); + }); + + test('wildcard match', () => { + const url = URI.parse('https://api.example.com'); + const approved = { 'https://*.example.com': true }; + const pattern = getMatchingPattern(url, approved); + assert.strictEqual(pattern, 'https://*.example.com'); + }); + + test('no match returns undefined', () => { + const url = URI.parse('https://example.com'); + const approved = { 'https://other.com': true }; + const pattern = getMatchingPattern(url, approved); + assert.strictEqual(pattern, undefined); + }); + + test('most specific match', () => { + const url = URI.parse('https://api.example.com/v1/users'); + const approved = { + 'https://*.example.com': true, + 'https://api.example.com': true, + 'https://api.example.com/v1/*': true + }; + const pattern = getMatchingPattern(url, approved); + assert.ok(pattern !== undefined); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.ts new file mode 100644 index 00000000000..5b8ad6895d5 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/manageTodoListTool.test.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 assert from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { createManageTodoListToolData } from '../../../../common/tools/builtinTools/manageTodoListTool.js'; +import { IToolData } from '../../../../common/tools/languageModelToolsService.js'; +import { IJSONSchema } from '../../../../../../../base/common/jsonSchema.js'; + +suite('ManageTodoListTool Schema', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + function getSchemaProperties(toolData: IToolData): { properties: Record; required: string[] } { + assert.ok(toolData.inputSchema); + const schema = toolData.inputSchema; + const todolistItems = schema?.properties?.todoList?.items as IJSONSchema | undefined; + const properties = todolistItems?.properties as Record | undefined; + const required = todolistItems?.required; + + assert.ok(properties, 'Schema properties should be defined'); + assert.ok(required, 'Schema required fields should be defined'); + + return { properties, required }; + } + + test('createManageTodoListToolData returns valid tool data with proper schema', () => { + const toolData = createManageTodoListToolData(); + + assert.ok(toolData.id, 'Tool should have an id'); + assert.ok(toolData.inputSchema, 'Tool should have an input schema'); + assert.strictEqual(toolData.inputSchema?.type, 'object', 'Schema should be an object type'); + }); + + test('createManageTodoListToolData schema has required todoList field', () => { + const toolData = createManageTodoListToolData(); + + assert.ok(toolData.inputSchema?.required?.includes('todoList'), 'todoList should be required'); + assert.ok(toolData.inputSchema?.properties?.todoList, 'todoList property should exist'); + }); + + test('createManageTodoListToolData todoList items have correct required fields', () => { + const toolData = createManageTodoListToolData(); + const { properties, required } = getSchemaProperties(toolData); + + assert.ok('id' in properties, 'Schema should have id property'); + assert.ok('title' in properties, 'Schema should have title property'); + assert.ok('status' in properties, 'Schema should have status property'); + assert.deepStrictEqual(required, ['id', 'title', 'status'], 'Required fields should be id, title, status'); + }); + + test('createManageTodoListToolData status has correct enum values', () => { + const toolData = createManageTodoListToolData(); + const { properties } = getSchemaProperties(toolData); + + const statusProperty = properties['status']; + assert.ok(statusProperty, 'Status property should exist'); + assert.deepStrictEqual(statusProperty.enum, ['not-started', 'in-progress', 'completed'], 'Status should have correct enum values'); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts new file mode 100644 index 00000000000..9d7940db1de --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/builtinTools/runSubagentTool.test.ts @@ -0,0 +1,196 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { NullLogService } from '../../../../../../../platform/log/common/log.js'; +import { TestConfigurationService } from '../../../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { RunSubagentTool } from '../../../../common/tools/builtinTools/runSubagentTool.js'; +import { MockLanguageModelToolsService } from '../mockLanguageModelToolsService.js'; +import { IChatAgentService } from '../../../../common/participants/chatAgents.js'; +import { IChatModeService } from '../../../../common/chatModes.js'; +import { IChatService } from '../../../../common/chatService/chatService.js'; +import { ILanguageModelsService } from '../../../../common/languageModels.js'; +import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'; + +suite('RunSubagentTool', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('resultText trimming', () => { + test('trims leading empty codeblocks (```\\n```) from result', () => { + // This tests the regex: /^\n*```\n+```\n*/g + const testCases = [ + { input: '```\n```\nActual content', expected: 'Actual content' }, + { input: '\n```\n```\nActual content', expected: 'Actual content' }, + { input: '\n\n```\n\n```\n\nActual content', expected: 'Actual content' }, + { input: '```\n```\n```\n```\nActual content', expected: '```\n```\nActual content' }, // Only trims leading + { input: 'No codeblock here', expected: 'No codeblock here' }, + { input: '```\n```\n', expected: '' }, + { input: '', expected: '' }, + ]; + + for (const { input, expected } of testCases) { + const result = input.replace(/^\n*```\n+```\n*/g, '').trim(); + assert.strictEqual(result, expected, `Failed for input: ${JSON.stringify(input)}`); + } + }); + }); + + suite('prepareToolInvocation', () => { + test('returns correct toolSpecificData', async () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService(); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const result = await tool.prepareToolInvocation( + { + parameters: { + prompt: 'Test prompt', + description: 'Test task', + agentName: 'CustomAgent', + }, + chatSessionResource: URI.parse('test://session'), + }, + CancellationToken.None + ); + + assert.ok(result); + assert.strictEqual(result.invocationMessage, 'Test task'); + assert.deepStrictEqual(result.toolSpecificData, { + kind: 'subagent', + description: 'Test task', + agentName: 'CustomAgent', + prompt: 'Test prompt', + }); + }); + }); + + suite('getToolData', () => { + test('returns basic tool data', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService(); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const toolData = tool.getToolData(); + + assert.strictEqual(toolData.id, 'runSubagent'); + assert.ok(toolData.inputSchema); + assert.ok(toolData.inputSchema.properties?.prompt); + assert.ok(toolData.inputSchema.properties?.description); + assert.deepStrictEqual(toolData.inputSchema.required, ['prompt', 'description']); + }); + + test('includes agentName property when SubagentToolCustomAgents is enabled', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const configService = new TestConfigurationService({ + 'chat.customAgentInSubagent.enabled': true, + }); + + const tool = testDisposables.add(new RunSubagentTool( + {} as IChatAgentService, + {} as IChatService, + {} as IChatModeService, + mockToolsService, + {} as ILanguageModelsService, + new NullLogService(), + mockToolsService, + configService, + {} as IInstantiationService, + )); + + const toolData = tool.getToolData(); + + assert.ok(toolData.inputSchema?.properties?.agentName, 'agentName should be in schema when custom agents enabled'); + }); + }); + + suite('onDidInvokeTool event', () => { + test('mock service fires onDidInvokeTool events with correct data', () => { + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const sessionResource = URI.parse('test://session'); + const receivedEvents: { toolId: string; sessionResource: URI | undefined; requestId: string | undefined; subagentInvocationId: string | undefined }[] = []; + + testDisposables.add(mockToolsService.onDidInvokeTool(e => { + receivedEvents.push(e); + })); + + mockToolsService.fireOnDidInvokeTool({ + toolId: 'test-tool', + sessionResource, + requestId: 'request-123', + subagentInvocationId: 'subagent-456', + }); + + assert.strictEqual(receivedEvents.length, 1); + assert.deepStrictEqual(receivedEvents[0], { + toolId: 'test-tool', + sessionResource, + requestId: 'request-123', + subagentInvocationId: 'subagent-456', + }); + }); + + test('events with different subagentInvocationId are distinguishable', () => { + // This tests the filtering logic used in RunSubagentTool.invoke() + // The tool subscribes to onDidInvokeTool and checks if e.subagentInvocationId matches its own callId + const mockToolsService = testDisposables.add(new MockLanguageModelToolsService()); + const targetSubagentId = 'target-subagent'; + + const matchingEvents: string[] = []; + testDisposables.add(mockToolsService.onDidInvokeTool(e => { + if (e.subagentInvocationId === targetSubagentId) { + matchingEvents.push(e.toolId); + } + })); + + // Fire events with different subagentInvocationIds + mockToolsService.fireOnDidInvokeTool({ + toolId: 'unrelated-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: 'different-subagent', + }); + mockToolsService.fireOnDidInvokeTool({ + toolId: 'matching-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: targetSubagentId, + }); + mockToolsService.fireOnDidInvokeTool({ + toolId: 'another-unrelated-tool', + sessionResource: undefined, + requestId: undefined, + subagentInvocationId: undefined, + }); + + // Only the matching event should be captured + assert.deepStrictEqual(matchingEvents, ['matching-tool']); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts b/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts deleted file mode 100644 index 66d65a99690..00000000000 --- a/src/vs/workbench/contrib/chat/test/common/tools/manageTodoListTool.test.ts +++ /dev/null @@ -1,53 +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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; -import { createManageTodoListToolData } from '../../../common/tools/manageTodoListTool.js'; -import { IToolData } from '../../../common/languageModelToolsService.js'; - -suite('ManageTodoListTool Description Field Setting', () => { - ensureNoDisposablesAreLeakedInTestSuite(); - - function getSchemaProperties(toolData: IToolData): { properties: any; required: string[] } { - assert.ok(toolData.inputSchema); - // eslint-disable-next-line local/code-no-any-casts - const schema = toolData.inputSchema as any; - const properties = schema?.properties?.todoList?.items?.properties; - const required = schema?.properties?.todoList?.items?.required; - - assert.ok(properties, 'Schema properties should be defined'); - assert.ok(required, 'Schema required fields should be defined'); - - return { properties, required }; - } - - test('createManageTodoListToolData should include description field when enabled', () => { - const toolData = createManageTodoListToolData(false, true); - const { properties, required } = getSchemaProperties(toolData); - - assert.strictEqual('description' in properties, true); - assert.strictEqual(required.includes('description'), true); - assert.deepStrictEqual(required, ['id', 'title', 'description', 'status']); - }); - - test('createManageTodoListToolData should exclude description field when disabled', () => { - const toolData = createManageTodoListToolData(false, false); - const { properties, required } = getSchemaProperties(toolData); - - assert.strictEqual('description' in properties, false); - assert.strictEqual(required.includes('description'), false); - assert.deepStrictEqual(required, ['id', 'title', 'status']); - }); - - test('createManageTodoListToolData should use default value for includeDescription', () => { - const toolDataDefault = createManageTodoListToolData(false); - const { properties, required } = getSchemaProperties(toolDataDefault); - - // Default should be true (includes description) - assert.strictEqual('description' in properties, true); - assert.strictEqual(required.includes('description'), true); - }); -}); diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts new file mode 100644 index 00000000000..e8d3da161cb --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsConfirmationService.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { ConfirmedReason } from '../../../common/chatService/chatService.js'; +import { ILanguageModelToolConfirmationActions, ILanguageModelToolConfirmationContribution, ILanguageModelToolConfirmationRef, ILanguageModelToolsConfirmationService } from '../../../common/tools/languageModelToolsConfirmationService.js'; +import { IToolData } from '../../../common/tools/languageModelToolsService.js'; + +export class MockLanguageModelToolsConfirmationService implements ILanguageModelToolsConfirmationService { + manageConfirmationPreferences(tools: readonly IToolData[], options?: { defaultScope?: 'workspace' | 'profile' | 'session' }): void { + throw new Error('Method not implemented.'); + } + registerConfirmationContribution(toolName: string, contribution: ILanguageModelToolConfirmationContribution): IDisposable { + throw new Error('Method not implemented.'); + } + resetToolAutoConfirmation(): void { + + } + getPreConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { + return undefined; + } + getPostConfirmAction(ref: ILanguageModelToolConfirmationRef): ConfirmedReason | undefined { + return undefined; + } + getPreConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { + return []; + } + getPostConfirmActions(ref: ILanguageModelToolConfirmationRef): ILanguageModelToolConfirmationActions[] { + return []; + } + declare readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts new file mode 100644 index 00000000000..0541af5cfca --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/tools/mockLanguageModelToolsService.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Emitter, Event } from '../../../../../../base/common/event.js'; +import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js'; +import { constObservable, IObservable, IReader } from '../../../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { MockContextKeyService } from '../../../../../../platform/keybinding/test/common/mockKeybindingService.js'; +import { IProgressStep } from '../../../../../../platform/progress/common/progress.js'; +import { ChatRequestToolReferenceEntry } from '../../../common/attachments/chatVariableEntries.js'; +import { IVariableReference } from '../../../common/chatModes.js'; +import { IChatToolInvocation } from '../../../common/chatService/chatService.js'; +import { ILanguageModelChatMetadata } from '../../../common/languageModels.js'; +import { CountTokensCallback, IBeginToolCallOptions, ILanguageModelToolsService, IToolAndToolSetEnablementMap, IToolData, IToolImpl, IToolInvocation, IToolInvokedEvent, IToolResult, IToolSet, ToolDataSource, ToolSet } from '../../../common/tools/languageModelToolsService.js'; + +export class MockLanguageModelToolsService extends Disposable implements ILanguageModelToolsService { + _serviceBrand: undefined; + vscodeToolSet: ToolSet = new ToolSet('vscode', 'vscode', ThemeIcon.fromId(Codicon.code.id), ToolDataSource.Internal, undefined, undefined, new MockContextKeyService()); + executeToolSet: ToolSet = new ToolSet('execute', 'execute', ThemeIcon.fromId(Codicon.terminal.id), ToolDataSource.Internal, undefined, undefined, new MockContextKeyService()); + readToolSet: ToolSet = new ToolSet('read', 'read', ThemeIcon.fromId(Codicon.book.id), ToolDataSource.Internal, undefined, undefined, new MockContextKeyService()); + agentToolSet: ToolSet = new ToolSet('agent', 'agent', ThemeIcon.fromId(Codicon.agent.id), ToolDataSource.Internal, undefined, undefined, new MockContextKeyService()); + + private readonly _onDidInvokeTool = this._register(new Emitter()); + + constructor() { + super(); + } + + readonly onDidChangeTools: Event = Event.None; + readonly onDidPrepareToolCallBecomeUnresponsive: Event<{ sessionResource: URI; toolData: IToolData }> = Event.None; + readonly onDidInvokeTool = this._onDidInvokeTool.event; + + fireOnDidInvokeTool(event: IToolInvokedEvent): void { + this._onDidInvokeTool.fire(event); + } + + registerToolData(toolData: IToolData): IDisposable { + return Disposable.None; + } + + resetToolAutoConfirmation(): void { + + } + + getToolPostExecutionAutoConfirmation(toolId: string): 'workspace' | 'profile' | 'session' | 'never' { + return 'never'; + } + + resetToolPostExecutionAutoConfirmation(): void { + + } + + flushToolUpdates(): void { + + } + + cancelToolCallsForRequest(requestId: string): void { + + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + setToolAutoConfirmation(toolId: string, scope: any): void { + + } + + getToolAutoConfirmation(toolId: string): 'never' { + return 'never'; + } + + registerToolImplementation(name: string, tool: IToolImpl): IDisposable { + return Disposable.None; + } + + registerTool(toolData: IToolData, tool: IToolImpl): IDisposable { + return Disposable.None; + } + + getTools(): Iterable { + return []; + } + + getAllToolsIncludingDisabled(): Iterable { + return []; + } + + getTool(id: string): IToolData | undefined { + return undefined; + } + + observeTools(): IObservable { + return constObservable([]); + } + + getToolByName(name: string): IToolData | undefined { + return undefined; + } + + acceptProgress(sessionId: string | undefined, callId: string, progress: IProgressStep): void { + + } + + async invokeTool(dto: IToolInvocation, countTokens: CountTokensCallback, token: CancellationToken): Promise { + return { + content: [{ kind: 'text', value: 'result' }] + }; + } + + beginToolCall(_options: IBeginToolCallOptions): IChatToolInvocation | undefined { + // Mock implementation - return undefined + return undefined; + } + + async updateToolStream(_toolCallId: string, _partialInput: unknown, _token: CancellationToken): Promise { + // Mock implementation - do nothing + } + + toolSets: IObservable = constObservable([]); + + getToolSetsForModel(model: ILanguageModelChatMetadata | undefined, reader?: IReader): Iterable { + return []; + } + + getToolSetByName(name: string): IToolSet | undefined { + return undefined; + } + + getToolSet(id: string): IToolSet | undefined { + return undefined; + } + + createToolSet(): ToolSet & IDisposable { + throw new Error('Method not implemented.'); + } + + toToolAndToolSetEnablementMap(toolOrToolSetNames: readonly string[], target: string | undefined): IToolAndToolSetEnablementMap { + throw new Error('Method not implemented.'); + } + + toToolReferences(variableReferences: readonly IVariableReference[]): ChatRequestToolReferenceEntry[] { + throw new Error('Method not implemented.'); + } + + getFullReferenceNames(): Iterable { + throw new Error('Method not implemented.'); + } + + getToolByFullReferenceName(qualifiedName: string): IToolData | IToolSet | undefined { + throw new Error('Method not implemented.'); + } + + getFullReferenceName(tool: IToolData, set?: IToolSet): string { + throw new Error('Method not implemented.'); + } + + toFullReferenceNames(map: IToolAndToolSetEnablementMap): string[] { + throw new Error('Method not implemented.'); + } + + getDeprecatedFullReferenceNames(): Map> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts index f9182eaa062..8264b98c30f 100644 --- a/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/voiceChatService.test.ts @@ -12,9 +12,9 @@ import { ExtensionIdentifier } from '../../../../../platform/extensions/common/e import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; import { nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; import { ISpeechProvider, ISpeechService, ISpeechToTextEvent, ISpeechToTextSession, ITextToSpeechSession, KeywordRecognitionStatus, SpeechToTextStatus } from '../../../speech/common/speechService.js'; -import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider, UserSelectedTools } from '../../common/chatAgents.js'; -import { IChatModel } from '../../common/chatModel.js'; -import { IChatFollowup, IChatProgress } from '../../common/chatService.js'; +import { IChatAgent, IChatAgentCommand, IChatAgentCompletionItem, IChatAgentData, IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult, IChatAgentService, IChatParticipantDetectionProvider, UserSelectedTools } from '../../common/participants/chatAgents.js'; +import { IChatModel } from '../../common/model/chatModel.js'; +import { IChatFollowup, IChatProgress } from '../../common/chatService/chatService.js'; import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js'; import { IVoiceChatSessionOptions, IVoiceChatTextEvent, VoiceChatService } from '../../common/voiceChatService.js'; diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiline.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_multiple_vulns.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.0.snap diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap b/src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap similarity index 100% rename from src/vs/workbench/contrib/chat/test/common/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap rename to src/vs/workbench/contrib/chat/test/common/widget/__snapshots__/Annotations_extractVulnerabilitiesFromText_single_line.1.snap diff --git a/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts new file mode 100644 index 00000000000..65e1db898f4 --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/widget/annotations.test.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { MarkdownString } from '../../../../../../base/common/htmlContent.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { assertSnapshot } from '../../../../../../base/test/common/snapshot.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { IChatMarkdownContent, IChatResponseCodeblockUriPart } from '../../../common/chatService/chatService.js'; +import { annotateSpecialMarkdownContent, extractCodeblockUrisFromText, extractSubAgentInvocationIdFromText, extractVulnerabilitiesFromText } from '../../../common/widget/annotations.js'; + +function content(str: string): IChatMarkdownContent { + return { kind: 'markdownContent', content: new MarkdownString(str) }; +} + +suite('Annotations', function () { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('extractVulnerabilitiesFromText', () => { + test('single line', async () => { + const before = 'some code '; + const vulnContent = 'content with vuln'; + const after = ' after'; + const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, content(after)]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + + test('multiline', async () => { + const before = 'some code\nover\nmultiple lines '; + const vulnContent = 'content with vuln\nand\nnewlines'; + const after = 'more code\nwith newline'; + const annotatedResult = annotateSpecialMarkdownContent([content(before), { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, content(after)]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + + test('multiple vulns', async () => { + const before = 'some code\nover\nmultiple lines '; + const vulnContent = 'content with vuln\nand\nnewlines'; + const after = 'more code\nwith newline'; + const annotatedResult = annotateSpecialMarkdownContent([ + content(before), + { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, + content(after), + { kind: 'markdownVuln', content: new MarkdownString(vulnContent), vulnerabilities: [{ title: 'title', description: 'vuln' }] }, + ]); + await assertSnapshot(annotatedResult); + + const markdown = annotatedResult[0] as IChatMarkdownContent; + const result = extractVulnerabilitiesFromText(markdown.content.value); + await assertSnapshot(result); + }); + }); + + suite('extractSubAgentInvocationIdFromText', () => { + test('extracts subAgentInvocationId from codeblock uri tag', () => { + const subAgentId = 'test-agent-123'; + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, subAgentId); + }); + + test('returns undefined when no subAgentInvocationId', () => { + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, undefined); + }); + + test('returns undefined for text without codeblock uri tag', () => { + const result = extractSubAgentInvocationIdFromText('some random text'); + assert.strictEqual(result, undefined); + }); + + test('handles special characters in subAgentInvocationId via URL encoding', () => { + const subAgentId = 'agent-with-special&chars=value'; + const uri = URI.parse('file:///test.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractSubAgentInvocationIdFromText(markdown.content.value); + assert.strictEqual(result, subAgentId); + }); + + test('handles malformed URL encoding gracefully', () => { + // Manually construct a malformed tag with invalid URL encoding + const malformedTag = 'file:///test.ts'; + const result = extractSubAgentInvocationIdFromText(malformedTag); + // Should return the raw value when decoding fails + assert.strictEqual(result, '%ZZ'); + }); + }); + + suite('extractCodeblockUrisFromText with subAgentInvocationId', () => { + test('extracts subAgentInvocationId from codeblock uri', () => { + const subAgentId = 'test-subagent-456'; + const uri = URI.parse('file:///example.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const result = extractCodeblockUrisFromText(markdown.content.value); + assert.ok(result); + assert.strictEqual(result.subAgentInvocationId, subAgentId); + assert.strictEqual(result.uri.toString(), uri.toString()); + assert.strictEqual(result.isEdit, true); + }); + + test('round-trip encoding/decoding with special characters', () => { + const subAgentId = 'agent/with spaces&special=chars?more'; + const uri = URI.parse('file:///path/to/file.ts'); + const codeblockUriPart: IChatResponseCodeblockUriPart = { + kind: 'codeblockUri', + uri, + isEdit: true, + subAgentInvocationId: subAgentId + }; + const annotated = annotateSpecialMarkdownContent([content('code'), codeblockUriPart]); + const markdown = annotated[0] as IChatMarkdownContent; + + const extracted = extractCodeblockUrisFromText(markdown.content.value); + assert.ok(extracted); + assert.strictEqual(extracted.subAgentInvocationId, subAgentId); + }); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/common/widget/chatWidgetHistoryService.test.ts b/src/vs/workbench/contrib/chat/test/common/widget/chatWidgetHistoryService.test.ts new file mode 100644 index 00000000000..9b8c79ba4cb --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/common/widget/chatWidgetHistoryService.test.ts @@ -0,0 +1,413 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IStorageService, StorageScope } from '../../../../../../platform/storage/common/storage.js'; +import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js'; +import { IChatModelInputState } from '../../../common/model/chatModel.js'; +import { ChatAgentLocation, ChatModeKind } from '../../../common/constants.js'; +import { ChatHistoryNavigator, ChatInputHistoryMaxEntries, ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../common/widget/chatWidgetHistoryService.js'; +import { Memento } from '../../../../../common/memento.js'; + +suite('ChatWidgetHistoryService', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + // Clear memento cache before each test to prevent state leakage + Memento.clear(StorageScope.APPLICATION); + Memento.clear(StorageScope.PROFILE); + Memento.clear(StorageScope.WORKSPACE); + }); + + function createHistoryService(): ChatWidgetHistoryService { + // Create fresh instances for each test to avoid state leakage + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + return testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + } + + function createInputState(text: string, modeKind = ChatModeKind.Ask): IChatModelInputState { + return { + inputText: text, + attachments: [], + mode: { id: modeKind, kind: modeKind }, + selectedModel: undefined, + selections: [], + contrib: {} + }; + } + + test('should start with empty history', () => { + const historyService = createHistoryService(); + const history = historyService.getHistory(ChatAgentLocation.Chat); + assert.strictEqual(history.length, 0); + }); + + test('should append and retrieve history entries', () => { + const historyService = createHistoryService(); + const entry = createInputState('test query'); + historyService.append(ChatAgentLocation.Chat, entry); + + const history = historyService.getHistory(ChatAgentLocation.Chat); + assert.strictEqual(history.length, 1); + assert.strictEqual(history[0].inputText, 'test query'); + }); + + test('should maintain separate history per location', () => { + const historyService = createHistoryService(); + historyService.append(ChatAgentLocation.Chat, createInputState('chat query')); + historyService.append(ChatAgentLocation.Terminal, createInputState('terminal query')); + + const chatHistory = historyService.getHistory(ChatAgentLocation.Chat); + const terminalHistory = historyService.getHistory(ChatAgentLocation.Terminal); + + assert.strictEqual(chatHistory.length, 1); + assert.strictEqual(terminalHistory.length, 1); + assert.strictEqual(chatHistory[0].inputText, 'chat query'); + assert.strictEqual(terminalHistory[0].inputText, 'terminal query'); + }); + + test('should limit history to max entries', () => { + const historyService = createHistoryService(); + for (let i = 0; i < ChatInputHistoryMaxEntries + 10; i++) { + historyService.append(ChatAgentLocation.Chat, createInputState(`query ${i}`)); + } + + const history = historyService.getHistory(ChatAgentLocation.Chat); + assert.strictEqual(history.length, ChatInputHistoryMaxEntries); + assert.strictEqual(history[0].inputText, 'query 10'); // First 10 should be dropped + assert.strictEqual(history[history.length - 1].inputText, `query ${ChatInputHistoryMaxEntries + 9}`); + }); + + test('should fire append event when history is added', () => { + const historyService = createHistoryService(); + let eventFired = false; + let firedEntry: IChatModelInputState | undefined; + + testDisposables.add(historyService.onDidChangeHistory(e => { + if (e.kind === 'append') { + eventFired = true; + firedEntry = e.entry; + } + })); + + const entry = createInputState('test'); + historyService.append(ChatAgentLocation.Chat, entry); + + assert.ok(eventFired); + assert.strictEqual(firedEntry?.inputText, 'test'); + }); + + test('should clear all history', () => { + const historyService = createHistoryService(); + historyService.append(ChatAgentLocation.Chat, createInputState('query 1')); + historyService.append(ChatAgentLocation.Terminal, createInputState('query 2')); + + historyService.clearHistory(); + + assert.strictEqual(historyService.getHistory(ChatAgentLocation.Chat).length, 0); + assert.strictEqual(historyService.getHistory(ChatAgentLocation.Terminal).length, 0); + }); + + test('should fire clear event when history is cleared', () => { + const historyService = createHistoryService(); + let clearEventFired = false; + + testDisposables.add(historyService.onDidChangeHistory(e => { + if (e.kind === 'clear') { + clearEventFired = true; + } + })); + + historyService.clearHistory(); + assert.ok(clearEventFired); + }); +}); + +suite('ChatHistoryNavigator', () => { + const testDisposables = ensureNoDisposablesAreLeakedInTestSuite(); + + setup(() => { + // Clear memento cache before each test to prevent state leakage + Memento.clear(StorageScope.APPLICATION); + Memento.clear(StorageScope.PROFILE); + Memento.clear(StorageScope.WORKSPACE); + }); + + function createNavigator(): ChatHistoryNavigator { + // Create fresh instances for each test to avoid state leakage + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + + const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + instantiationService.stub(IChatWidgetHistoryService, historyService); + + return testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + } + + function createInputState(text: string): IChatModelInputState { + return { + inputText: text, + attachments: [], + mode: { id: ChatModeKind.Ask, kind: ChatModeKind.Ask }, + selectedModel: undefined, + selections: [], + contrib: {} + }; + } + + test('should start at end of empty history', () => { + const nav = createNavigator(); + assert.ok(nav.isAtEnd()); + assert.ok(nav.isAtStart()); + }); + + test('should navigate backwards through history', () => { + const nav = createNavigator(); + nav.append(createInputState('first')); + nav.append(createInputState('second')); + nav.append(createInputState('third')); + + assert.ok(nav.isAtEnd()); + + const prev1 = nav.previous(); + assert.strictEqual(prev1?.inputText, 'third'); + + const prev2 = nav.previous(); + assert.strictEqual(prev2?.inputText, 'second'); + + const prev3 = nav.previous(); + assert.strictEqual(prev3?.inputText, 'first'); + assert.ok(nav.isAtStart()); + }); + + test('should navigate forwards through history', () => { + const nav = createNavigator(); + nav.append(createInputState('first')); + nav.append(createInputState('second')); + + nav.previous(); + nav.previous(); + assert.ok(nav.isAtStart()); + + const next1 = nav.next(); + assert.strictEqual(next1?.inputText, 'second'); + + const next2 = nav.next(); + assert.strictEqual(next2, undefined); + assert.ok(nav.isAtEnd()); + }); + + test('should reset cursor to end', () => { + const nav = createNavigator(); + nav.append(createInputState('first')); + nav.append(createInputState('second')); + + nav.previous(); + assert.ok(!nav.isAtEnd()); + + nav.resetCursor(); + assert.ok(nav.isAtEnd()); + }); + + test('should overlay edited entries', () => { + const nav = createNavigator(); + nav.append(createInputState('first')); + nav.append(createInputState('second')); + + nav.previous(); + const edited = createInputState('second edited'); + nav.overlay(edited); + + const current = nav.current(); + assert.strictEqual(current?.inputText, 'second edited'); + + // Original history should be unchanged + assert.strictEqual(nav.values[1].inputText, 'second'); + }); + + test('should clear overlay on append', () => { + const nav = createNavigator(); + nav.append(createInputState('first')); + + nav.previous(); + nav.overlay(createInputState('first edited')); + + const currentBefore = nav.current(); + assert.strictEqual(currentBefore?.inputText, 'first edited'); + + nav.append(createInputState('second')); + + // After append, cursor should be at end and overlay cleared + assert.ok(nav.isAtEnd()); + nav.previous(); + assert.strictEqual(nav.current()?.inputText, 'second'); + }); + + test('should stop at start when navigating backwards', () => { + const nav = createNavigator(); + nav.append(createInputState('only')); + + nav.previous(); + assert.ok(nav.isAtStart()); + + const prev = nav.previous(); + assert.strictEqual(prev?.inputText, 'only'); // Should stay at first + assert.ok(nav.isAtStart()); + }); + + test('should stop at end when navigating forwards', () => { + const nav = createNavigator(); + nav.append(createInputState('only')); + + const next1 = nav.next(); + assert.strictEqual(next1, undefined); + assert.ok(nav.isAtEnd()); + + const next2 = nav.next(); + assert.strictEqual(next2, undefined); + assert.ok(nav.isAtEnd()); + }); + + test('should update when history service appends entries', () => { + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + + const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + instantiationService.stub(IChatWidgetHistoryService, historyService); + + const nav = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + + historyService.append(ChatAgentLocation.Chat, createInputState('from service')); + + const history = nav.values; + assert.strictEqual(history.length, 1); + assert.strictEqual(history[0].inputText, 'from service'); + }); + + test('should adjust cursor when history is cleared', () => { + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + + const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + instantiationService.stub(IChatWidgetHistoryService, historyService); + + const nav = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + + nav.append(createInputState('first')); + nav.append(createInputState('second')); + + nav.previous(); + assert.ok(!nav.isAtEnd()); + + historyService.clearHistory(); + + assert.ok(nav.isAtEnd()); + assert.ok(nav.isAtStart()); + assert.strictEqual(nav.values.length, 0); + }); + + test('should handle cursor adjustment when max entries reached', () => { + const nav = createNavigator(); + // Add entries up to the max + for (let i = 0; i < ChatInputHistoryMaxEntries; i++) { + nav.append(createInputState(`entry ${i}`)); + } + + // Navigate to middle of history + for (let i = 0; i < 20; i++) { + nav.previous(); + } + + // Add one more entry (should drop oldest) + nav.append(createInputState('new entry')); + + // Cursor should be at end after append + assert.ok(nav.isAtEnd()); + }); + + test('should support concurrent navigators', () => { + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + + const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + instantiationService.stub(IChatWidgetHistoryService, historyService); + + const nav1 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + const nav2 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + + nav1.append(createInputState('query 1')); + + assert.strictEqual(nav1.values.length, 1); + assert.strictEqual(nav2.values.length, 1); + assert.strictEqual(nav1.values[0].inputText, 'query 1'); + assert.strictEqual(nav2.values[0].inputText, 'query 1'); + + nav1.previous(); + assert.ok(!nav1.isAtEnd()); + assert.ok(nav2.isAtEnd()); + + nav2.append(createInputState('query 2')); + + assert.strictEqual(nav1.values.length, 2); + assert.strictEqual(nav2.values.length, 2); + + // nav1 should stay at same position (pointing to query 1) + assert.strictEqual(nav1.current()?.inputText, 'query 1'); + + // nav2 should be at end + assert.ok(nav2.isAtEnd()); + }); + + test('should support concurrent navigators with mixed positions', () => { + const instantiationService = testDisposables.add(new TestInstantiationService()); + const storageService = testDisposables.add(new TestStorageService()); + instantiationService.stub(IStorageService, storageService); + + const historyService = testDisposables.add(instantiationService.createInstance(ChatWidgetHistoryService)); + instantiationService.stub(IChatWidgetHistoryService, historyService); + + const nav1 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + const nav2 = testDisposables.add(instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat)); + + nav1.append(createInputState('query 1')); + nav1.append(createInputState('query 2')); + nav1.append(createInputState('query 3')); + + // Both at end + assert.ok(nav1.isAtEnd()); + assert.ok(nav2.isAtEnd()); + + // Move nav1 back to 'query 2' + nav1.previous(); + assert.strictEqual(nav1.current()?.inputText, 'query 3'); + nav1.previous(); + assert.strictEqual(nav1.current()?.inputText, 'query 2'); + + // Move nav2 back to 'query 1' + nav2.previous(); + nav2.previous(); + nav2.previous(); + assert.strictEqual(nav2.current()?.inputText, 'query 1'); + + // Append new query + nav1.append(createInputState('query 4')); + + // nav1 should be at end (because it appended) + assert.ok(nav1.isAtEnd()); + assert.strictEqual(nav1.values.length, 4); + + // nav2 should stay at 'query 1' + assert.strictEqual(nav2.current()?.inputText, 'query 1'); + assert.strictEqual(nav2.values.length, 4); + }); +}); diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/voiceChatActions.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/actions/voiceChatActions.test.ts similarity index 93% rename from src/vs/workbench/contrib/chat/test/electron-browser/voiceChatActions.test.ts rename to src/vs/workbench/contrib/chat/test/electron-browser/actions/voiceChatActions.test.ts index bcf00d4b79f..dc58646011c 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/voiceChatActions.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/actions/voiceChatActions.test.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { parseNextChatResponseChunk } from '../../electron-browser/actions/voiceChatActions.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { parseNextChatResponseChunk } from '../../../electron-browser/actions/voiceChatActions.js'; suite('VoiceChatActions', function () { diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/fetchPageTool.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts similarity index 91% rename from src/vs/workbench/contrib/chat/test/electron-browser/fetchPageTool.test.ts rename to src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts index 43cb02cf26a..acee94cc06c 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/fetchPageTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts @@ -4,17 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IFileContent, IReadFileOptions } from '../../../../../platform/files/common/files.js'; -import { IWebContentExtractorService, WebContentExtractResult } from '../../../../../platform/webContentExtractor/common/webContentExtractor.js'; -import { FetchWebPageTool } from '../../electron-browser/tools/fetchPageTool.js'; -import { TestFileService } from '../../../../test/common/workbenchTestServices.js'; -import { MockTrustedDomainService } from '../../../url/test/browser/mockTrustedDomainService.js'; -import { InternalFetchWebPageToolId } from '../../common/tools/tools.js'; +import { CancellationToken } from '../../../../../../../base/common/cancellation.js'; +import { VSBuffer } from '../../../../../../../base/common/buffer.js'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ResourceMap } from '../../../../../../../base/common/map.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js'; +import { IFileContent, IReadFileOptions } from '../../../../../../../platform/files/common/files.js'; +import { IWebContentExtractorService, WebContentExtractResult } from '../../../../../../../platform/webContentExtractor/common/webContentExtractor.js'; +import { FetchWebPageTool } from '../../../../electron-browser/builtInTools/fetchPageTool.js'; +import { TestFileService } from '../../../../../../test/common/workbenchTestServices.js'; +import { MockTrustedDomainService } from '../../../../../url/test/browser/mockTrustedDomainService.js'; +import { InternalFetchWebPageToolId } from '../../../../common/tools/builtinTools/tools.js'; +import { MockChatService } from '../../../common/chatService/mockChatService.js'; +import { upcastDeepPartial } from '../../../../../../../base/test/common/mock.js'; +import { IChatService } from '../../../../common/chatService/chatService.js'; +import { LocalChatSessionUri } from '../../../../common/model/chatUri.js'; class TestWebContentExtractorService implements IWebContentExtractorService { _serviceBrand: undefined; @@ -53,7 +57,8 @@ class ExtendedTestFileService extends TestFileService { mtime: 0, ctime: 0, readonly: false, - locked: false + locked: false, + executable: false }; } @@ -85,6 +90,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(webContentMap), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const testUrls = [ @@ -133,6 +139,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService([]), + new MockChatService(), ); // Test empty array @@ -181,10 +188,11 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(webContentMap), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const preparation = await tool.prepareToolInvocation( - { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] } }, + { parameters: { urls: ['https://valid.com', 'test://valid/resource', 'invalid://invalid'] }, chatSessionResource: undefined }, CancellationToken.None ); @@ -195,6 +203,49 @@ suite('FetchWebPageTool', () => { assert.ok(messageText.includes('invalid://invalid'), 'Should mention invalid URL'); }); + test('should approve when all URLs were mentioned in chat', async () => { + const webContentMap = new ResourceMap([ + [URI.parse('https://valid.com'), 'Valid content'] + ]); + + const fileContentMap = new ResourceMap([ + [URI.parse('test://valid/resource'), 'Valid MCP content'] + ]); + + const tool = new FetchWebPageTool( + new TestWebContentExtractorService(webContentMap), + new ExtendedTestFileService(fileContentMap), + new MockTrustedDomainService(), + upcastDeepPartial({ + getSession: () => { + return { + getRequests: () => [{ + message: { + text: 'fetch https://example.com' + } + }], + }; + }, + }), + ); + + const preparation1 = await tool.prepareToolInvocation( + { parameters: { urls: ['https://example.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, + CancellationToken.None + ); + + assert.ok(preparation1, 'Should return prepared invocation'); + assert.strictEqual(preparation1.confirmationMessages?.title, undefined); + + const preparation2 = await tool.prepareToolInvocation( + { parameters: { urls: ['https://other.com'] }, chatSessionResource: LocalChatSessionUri.forSession('a') }, + CancellationToken.None + ); + + assert.ok(preparation2, 'Should return prepared invocation'); + assert.ok(preparation2.confirmationMessages?.title); + }); + test('should return message for binary files indicating they are not supported', async () => { // Create binary content (a simple PNG-like header with null bytes) const binaryContent = new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D]); @@ -209,6 +260,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -256,6 +308,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -296,6 +349,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -342,6 +396,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -407,6 +462,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -446,6 +502,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -489,6 +546,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(webContentMap), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const testUrls = [ @@ -547,6 +605,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(webContentMap), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService([]), + new MockChatService(), ); const testUrls = [ @@ -582,6 +641,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const testUrls = [ @@ -623,6 +683,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(webContentMap), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const testUrls = [ @@ -670,6 +731,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), // Empty - all web requests fail new ExtendedTestFileService(new ResourceMap()), // Empty - all file , new MockTrustedDomainService([]), + new MockChatService(), ); const testUrls = [ @@ -703,6 +765,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService([]), + new MockChatService(), ); const result = await tool.invoke( @@ -728,6 +791,7 @@ suite('FetchWebPageTool', () => { new TestWebContentExtractorService(new ResourceMap()), new ExtendedTestFileService(fileContentMap), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -764,6 +828,7 @@ suite('FetchWebPageTool', () => { }(), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -790,6 +855,7 @@ suite('FetchWebPageTool', () => { }(), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -821,6 +887,7 @@ suite('FetchWebPageTool', () => { }(), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( @@ -846,6 +913,7 @@ suite('FetchWebPageTool', () => { }(), new ExtendedTestFileService(new ResourceMap()), new MockTrustedDomainService(), + new MockChatService(), ); const result = await tool.invoke( diff --git a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts index 19baf02bc3c..702c514e38c 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/accessibility/accessibility.ts @@ -7,13 +7,17 @@ import './accessibility.css'; import * as nls from '../../../../../nls.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; +import { CONTEXT_ACCESSIBILITY_MODE_ENABLED, IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { accessibilityHelpIsShown } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { AccessibilityHelpNLS } from '../../../../../editor/common/standaloneStrings.js'; +import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; +import { alert } from '../../../../../base/browser/ui/aria/aria.js'; +import { CursorColumns } from '../../../../../editor/common/core/cursorColumns.js'; +import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; +import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; class ToggleScreenReaderMode extends Action2 { @@ -48,3 +52,41 @@ class ToggleScreenReaderMode extends Action2 { } registerAction2(ToggleScreenReaderMode); + +class AnnounceCursorPosition extends Action2 { + constructor() { + super({ + id: 'editor.action.announceCursorPosition', + title: nls.localize2('announceCursorPosition', "Announce Cursor Position"), + f1: true, + metadata: { + description: nls.localize2('announceCursorPosition.description', "Announce the current cursor position (line and column) via screen reader.") + }, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyG, + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CONTEXT_ACCESSIBILITY_MODE_ENABLED) + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const codeEditorService = accessor.get(ICodeEditorService); + const editor = codeEditorService.getFocusedCodeEditor(); + if (!editor) { + return; + } + const position = editor.getPosition(); + const model = editor.getModel(); + if (!position || !model) { + return; + } + // Use visible column to match status bar display (accounts for tabs) + const tabSize = model.getOptions().tabSize; + const lineContent = model.getLineContent(position.lineNumber); + const visibleColumn = CursorColumns.visibleColumnFromColumn(lineContent, position.column, tabSize) + 1; + alert(nls.localize('screenReader.lineColPosition', "Line {0}, Column {1}", position.lineNumber, visibleColumn)); + } +} + +registerAction2(AnnounceCursorPosition); diff --git a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts index acd2306dcf1..1e42e3c27a0 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.ts @@ -28,7 +28,7 @@ import { ChangeLanguageAction } from '../../../../browser/parts/editor/editorSta import { LOG_MODE_ID, OUTPUT_MODE_ID } from '../../../../services/output/common/output.js'; import { SEARCH_RESULT_LANGUAGE_ID } from '../../../../services/search/common/search.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { IInlineChatSessionService } from '../../../inlineChat/browser/inlineChatSessionService.js'; import './emptyTextEditorHint.css'; @@ -69,10 +69,8 @@ export class EmptyTextEditorHintContribution extends Disposable implements IEdit this.textHintContentWidget?.dispose(); } })); - this._register(inlineChatSessionService.onDidEndSession(e => { - if (this.editor === e.editor) { - this.update(); - } + this._register(inlineChatSessionService.onDidChangeSessions(() => { + this.update(); })); } @@ -92,7 +90,7 @@ export class EmptyTextEditorHintContribution extends Disposable implements IEdit return false; } - if (this.inlineChatSessionService.getSession(this.editor, model.uri)) { + if (this.inlineChatSessionService.getSessionByTextModel(model.uri)) { return false; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index eff5ea8254c..a3c7b7d6ce5 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -275,11 +275,7 @@ export abstract class SimpleFindWidget extends Widget implements IVerticalSashLa } private _getKeybinding(actionId: string): string { - const kb = this._keybindingService?.lookupKeybinding(actionId); - if (!kb) { - return ''; - } - return ` (${kb.getLabel()})`; + return this._keybindingService.appendKeybinding('', actionId); } override dispose() { diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index e785920549d..cc1baaf197a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -287,13 +287,16 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const semTokenText = semanticTokenInfo && renderTokenText(this._model.getValueInRange(semanticTokenInfo.range)); const tmTokenText = textMateTokenInfo && renderTokenText(this._model.getLineContent(position.lineNumber).substring(textMateTokenInfo.token.startIndex, textMateTokenInfo.token.endIndex)); + const semTokenLength = semanticTokenInfo && this._model.getValueLengthInRange(semanticTokenInfo.range); + const tmTokenLength = textMateTokenInfo && (textMateTokenInfo.token.endIndex - textMateTokenInfo.token.startIndex); const tokenText = semTokenText || tmTokenText || ''; + const tokenLength = semTokenLength || tmTokenLength || 0; dom.reset(this._domNode, $('h2.tiw-token', undefined, tokenText, - $('span.tiw-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); + $('span.tiw-token-length', undefined, `${tokenLength} ${tokenLength === 1 ? 'char' : 'chars'}`))); dom.append(this._domNode, $('hr.tiw-metadata-separator', { 'style': 'clear:both' })); dom.append(this._domNode, $('table.tiw-metadata-table', undefined, $('tbody', undefined, diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts index a0f1261b5d2..e83d4d5d4cc 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsOutline.ts @@ -5,7 +5,7 @@ import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { OutlineConfigCollapseItemsValues, IBreadcrumbsDataSource, IOutline, IOutlineCreator, IOutlineListConfig, IOutlineService, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget, } from '../../../../services/outline/browser/outline.js'; +import { OutlineConfigCollapseItemsValues, IBreadcrumbsDataSource, IBreadcrumbsOutlineElement, IOutline, IOutlineCreator, IOutlineListConfig, IOutlineService, OutlineChangeEvent, OutlineConfigKeys, OutlineTarget, } from '../../../../services/outline/browser/outline.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; @@ -38,14 +38,14 @@ type DocumentSymbolItem = OutlineGroup | OutlineElement; class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource { - private _breadcrumbs: (OutlineGroup | OutlineElement)[] = []; + private _breadcrumbs: IBreadcrumbsOutlineElement[] = []; constructor( private readonly _editor: ICodeEditor, @ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService, ) { } - getBreadcrumbElements(): readonly DocumentSymbolItem[] { + getBreadcrumbElements(): readonly IBreadcrumbsOutlineElement[] { return this._breadcrumbs; } @@ -55,7 +55,10 @@ class DocumentSymbolBreadcrumbsSource implements IBreadcrumbsDataSource ({ + element, + label: element instanceof OutlineElement ? element.symbol.name : '' + })); } private _computeBreadcrumbs(model: OutlineModel, position: IPosition): Array { @@ -180,7 +183,7 @@ class DocumentSymbolsOutline implements IOutline { treeDataSource, comparator, options, - quickPickDataSource: { getQuickPickElements: () => { throw new Error('not implemented'); } } + quickPickDataSource: { getQuickPickElements: () => { throw new Error('not implemented'); } }, }; diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css index 107c29992aa..168e14e4308 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.css @@ -45,5 +45,5 @@ } .monaco-list .outline-element .outline-element-icon { - margin-right: 4px; + padding-right: 6px; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts index b515c002e92..c6298b30f43 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/outline/documentSymbolsTree.ts @@ -221,7 +221,7 @@ export class DocumentSymbolRenderer implements ITreeRenderer= 0) { extraClasses.push(`deprecated`); diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts index 9830e93dc8b..769547a6891 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -10,7 +10,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { IRange } from '../../../../../editor/common/core/range.js'; import { AbstractGotoLineQuickAccessProvider } from '../../../../../editor/contrib/quickAccess/browser/gotoLineQuickAccess.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IQuickAccessRegistry, Extensions as QuickaccesExtensions } from '../../../../../platform/quickinput/common/quickAccess.js'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../../platform/quickinput/common/quickAccess.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IWorkbenchEditorConfiguration } from '../../../../common/editor.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; @@ -89,15 +89,41 @@ class GotoLineAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - accessor.get(IQuickInputService).quickAccess.show(GotoLineQuickAccessProvider.PREFIX); + accessor.get(IQuickInputService).quickAccess.show(GotoLineQuickAccessProvider.GO_TO_LINE_PREFIX); } } registerAction2(GotoLineAction); -Registry.as(QuickaccesExtensions.Quickaccess).registerQuickAccessProvider({ +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ ctor: GotoLineQuickAccessProvider, - prefix: AbstractGotoLineQuickAccessProvider.PREFIX, + prefix: AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX, placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. :42:5 for line 42, column 5). Type :: to go to a character offset (e.g. ::1024 for character 1024 from the start of the file). Use negative values to navigate backwards."), helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line/Column"), commandId: GotoLineAction.ID }] }); + +class GotoOffsetAction extends Action2 { + + static readonly ID = 'workbench.action.gotoOffset'; + + constructor() { + super({ + id: GotoOffsetAction.ID, + title: localize2('gotoOffset', 'Go to Offset...'), + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + accessor.get(IQuickInputService).quickAccess.show(GotoLineQuickAccessProvider.GO_TO_OFFSET_PREFIX); + } +} + +registerAction2(GotoOffsetAction); + +Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ + ctor: GotoLineQuickAccessProvider, + prefix: GotoLineQuickAccessProvider.GO_TO_OFFSET_PREFIX, + placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. :42:5 for line 42, column 5). Type :: to go to a character offset (e.g. ::1024 for character 1024 from the start of the file). Use negative values to navigate backwards."), + helpEntries: [{ description: localize('gotoOffsetQuickAccess', "Go to Offset"), commandId: GotoOffsetAction.ID }] +}); diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css index 0c378f88922..7b5530e7fa7 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.css @@ -5,7 +5,7 @@ .suggest-input-container { padding: 2px 6px; - border-radius: 2px; + border-radius: 4px; } .suggest-input-container .monaco-editor-background, diff --git a/src/vs/workbench/contrib/codeEditor/browser/workbenchEditorWorkerService.ts b/src/vs/workbench/contrib/codeEditor/browser/workbenchEditorWorkerService.ts deleted file mode 100644 index b74d233147b..00000000000 --- a/src/vs/workbench/contrib/codeEditor/browser/workbenchEditorWorkerService.ts +++ /dev/null @@ -1,26 +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 { WebWorkerDescriptor } from '../../../../base/browser/webWorkerFactory.js'; -import { FileAccess } from '../../../../base/common/network.js'; -import { EditorWorkerService } from '../../../../editor/browser/services/editorWorkerService.js'; -import { ILanguageConfigurationService } from '../../../../editor/common/languages/languageConfigurationRegistry.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; - -export class WorkbenchEditorWorkerService extends EditorWorkerService { - constructor( - @IModelService modelService: IModelService, - @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, - @ILogService logService: ILogService, - @ILanguageConfigurationService languageConfigurationService: ILanguageConfigurationService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, - ) { - const workerDescriptor = new WebWorkerDescriptor(FileAccess.asBrowserUri('vs/editor/common/services/editorWebWorkerMain.js'), 'TextEditorWorker'); - super(workerDescriptor, modelService, configurationService, logService, languageConfigurationService, languageFeaturesService); - } -} diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/displayChangeRemeasureFonts.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/displayChangeRemeasureFonts.ts index d76daa8532d..69795402434 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/displayChangeRemeasureFonts.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/displayChangeRemeasureFonts.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ThrottledDelayer } from '../../../../base/common/async.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { FontMeasurements } from '../../../../editor/browser/config/fontMeasurements.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -12,13 +13,18 @@ import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js' class DisplayChangeRemeasureFonts extends Disposable implements IWorkbenchContribution { + private readonly _delayer = this._register(new ThrottledDelayer(2000)); + constructor( @INativeHostService nativeHostService: INativeHostService ) { super(); this._register(nativeHostService.onDidChangeDisplay(() => { - FontMeasurements.clearAllFontInfos(); + this._delayer.trigger(() => { + FontMeasurements.clearAllFontInfos(); + return Promise.resolve(); + }); })); } } diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts index 0516a684364..ba585c28245 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts @@ -125,7 +125,7 @@ class PasteSelectionClipboardAction extends EditorAction { }); } - public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + public async run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): Promise { const clipboardService = accessor.get(IClipboardService); // read selection clipboard diff --git a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts index cc144be8284..7bfd3ded3e3 100644 --- a/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts +++ b/src/vs/workbench/contrib/codeEditor/test/node/autoindent.test.ts @@ -89,7 +89,7 @@ function registerTokenizationSupport(instantiationService: TestInstantiationServ ((encodedLanguageId << MetadataConsts.LANGUAGEID_OFFSET) | (tokensOnLine[i].standardTokenType << MetadataConsts.TOKEN_TYPE_OFFSET)); } - return new EncodedTokenizationResult(result, state); + return new EncodedTokenizationResult(result, [], state); } }; return TokenizationRegistry.register(languageId, tokenizationSupport); diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index f83a1b60a53..7acbf42409f 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -60,8 +60,9 @@ export class CommentFormActions implements IDisposable { secondary: !isPrimary, title, addPrimaryActionToDropdown: false, + small: true, ...defaultButtonStyles - }) : new Button(this.container, { secondary: !isPrimary, title, ...defaultButtonStyles }); + }) : new Button(this.container, { secondary: !isPrimary, title, small: true, ...defaultButtonStyles }); isPrimary = false; this._buttonElements.push(button.element); diff --git a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts index e5cecf7c684..2abd1f6b853 100644 --- a/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts @@ -18,15 +18,18 @@ import { Emitter } from '../../../../base/common/event.js'; export const overviewRulerCommentingRangeForeground = registerColor('editorGutter.commentRangeForeground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterCommentRangeForeground', 'Editor gutter decoration color for commenting ranges. This color should be opaque.')); const overviewRulerCommentForeground = registerColor('editorOverviewRuler.commentForeground', overviewRulerCommentingRangeForeground, nls.localize('editorOverviewRuler.commentForeground', 'Editor overview ruler decoration color for resolved comments. This color should be opaque.')); const overviewRulerCommentUnresolvedForeground = registerColor('editorOverviewRuler.commentUnresolvedForeground', overviewRulerCommentForeground, nls.localize('editorOverviewRuler.commentUnresolvedForeground', 'Editor overview ruler decoration color for unresolved comments. This color should be opaque.')); +const overviewRulerCommentDraftForeground = registerColor('editorOverviewRuler.commentDraftForeground', overviewRulerCommentUnresolvedForeground, nls.localize('editorOverviewRuler.commentDraftForeground', 'Editor overview ruler decoration color for comment threads with draft comments. This color should be opaque.')); const editorGutterCommentGlyphForeground = registerColor('editorGutter.commentGlyphForeground', { dark: editorForeground, light: editorForeground, hcDark: Color.black, hcLight: Color.white }, nls.localize('editorGutterCommentGlyphForeground', 'Editor gutter decoration color for commenting glyphs.')); registerColor('editorGutter.commentUnresolvedGlyphForeground', editorGutterCommentGlyphForeground, nls.localize('editorGutterCommentUnresolvedGlyphForeground', 'Editor gutter decoration color for commenting glyphs for unresolved comment threads.')); +registerColor('editorGutter.commentDraftGlyphForeground', editorGutterCommentGlyphForeground, nls.localize('editorGutterCommentDraftGlyphForeground', 'Editor gutter decoration color for commenting glyphs for comment threads with draft comments.')); export class CommentGlyphWidget extends Disposable { public static description = 'comment-glyph-widget'; private _lineNumber!: number; private _editor: ICodeEditor; private _threadState: CommentThreadState | undefined; + private _threadHasDraft: boolean = false; private readonly _commentsDecorations: IEditorDecorationsCollection; private _commentsOptions: ModelDecorationOptions; @@ -50,24 +53,34 @@ export class CommentGlyphWidget extends Disposable { } private createDecorationOptions(): ModelDecorationOptions { - const unresolved = this._threadState === CommentThreadState.Unresolved; + // Priority: draft > unresolved > resolved + let className: string; + if (this._threadHasDraft) { + className = 'comment-range-glyph comment-thread-draft'; + } else { + const unresolved = this._threadState === CommentThreadState.Unresolved; + className = `comment-range-glyph comment-thread${unresolved ? '-unresolved' : ''}`; + } + const decorationOptions: IModelDecorationOptions = { description: CommentGlyphWidget.description, isWholeLine: true, overviewRuler: { - color: themeColorFromId(unresolved ? overviewRulerCommentUnresolvedForeground : overviewRulerCommentForeground), + color: themeColorFromId(this._threadHasDraft ? overviewRulerCommentDraftForeground : + (this._threadState === CommentThreadState.Unresolved ? overviewRulerCommentUnresolvedForeground : overviewRulerCommentForeground)), position: OverviewRulerLane.Center }, collapseOnReplaceEdit: true, - linesDecorationsClassName: `comment-range-glyph comment-thread${unresolved ? '-unresolved' : ''}` + linesDecorationsClassName: className }; return ModelDecorationOptions.createDynamic(decorationOptions); } - setThreadState(state: CommentThreadState | undefined): void { - if (this._threadState !== state) { + setThreadState(state: CommentThreadState | undefined, hasDraft: boolean = false): void { + if (this._threadState !== state || this._threadHasDraft !== hasDraft) { this._threadState = state; + this._threadHasDraft = hasDraft; this._commentsOptions = this.createDecorationOptions(); this._updateDecorations(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index e0c772782f6..b4ded353a75 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -52,7 +52,7 @@ import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/ import { Position } from '../../../../editor/common/core/position.js'; class CommentsActionRunner extends ActionRunner { - protected override async runAction(action: IAction, context: any[]): Promise { + protected override async runAction(action: IAction, context: unknown[]): Promise { await action.run(...context); } } @@ -279,7 +279,7 @@ export class CommentNode extends Disposable { return result; } - private get commentNodeContext(): [any, MarshalledCommentThread] { + private get commentNodeContext(): [{ thread: languages.CommentThread; commentUniqueId: number; $mid: MarshalledId.CommentNode }, MarshalledCommentThread] { return [{ thread: this.commentThread, commentUniqueId: this.comment.uniqueIdInThread, @@ -413,6 +413,14 @@ export class CommentNode extends Disposable { this._reactionsActionBar.clear(); this._reactionActions.clear(); + const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); + const reactions = this.comment.commentReactions?.filter(reaction => !!reaction.count) || []; + + // Only create the container if there are reactions to show or if there's a reaction handler + if (reactions.length === 0 && !hasReactionHandler) { + return; + } + this._reactionActionsContainer = dom.append(commentDetailsContainer, dom.$('div.comment-reactions')); this._reactionsActionBar.value = new ActionBar(this._reactionActionsContainer, { actionViewItemProvider: (action, options) => { @@ -432,8 +440,7 @@ export class CommentNode extends Disposable { } }); - const hasReactionHandler = this.commentService.hasReactionHandler(this.owner); - this.comment.commentReactions?.filter(reaction => !!reaction.count).map(reaction => { + reactions.map(reaction => { const action = this._reactionActions.add(new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && (reaction.canEdit || hasReactionHandler) ? 'active' : '', (reaction.canEdit || hasReactionHandler), async () => { try { await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread, this.comment, reaction); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index adae03846f6..7fb4e06542e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -5,7 +5,6 @@ import './media/review.css'; import * as dom from '../../../../base/browser/dom.js'; -import * as domStylesheets from '../../../../base/browser/domStylesheets.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; @@ -21,11 +20,7 @@ import { CommentThreadHeader } from './commentThreadHeader.js'; import { CommentThreadAdditionalActions } from './commentThreadAdditionalActions.js'; import { CommentContextKeys } from '../common/commentContextKeys.js'; import { ICommentThreadWidget } from '../common/commentThreadWidget.js'; -import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; -import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textLinkActiveForeground, textLinkForeground } from '../../../../platform/theme/common/colorRegistry.js'; -import { PANEL_BORDER } from '../../../common/theme.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from './commentColors.js'; import { ICellRange } from '../../notebook/common/notebookRange.js'; import { FontInfo } from '../../../../editor/common/config/fontInfo.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; @@ -48,7 +43,6 @@ export class CommentThreadWidget extends private _commentMenus: CommentMenus; private _commentThreadDisposables: IDisposable[] = []; private _threadIsEmpty: IContextKey; - private _styleElement: HTMLStyleElement; private _commentThreadContextValue: IContextKey; private _focusedContextKey: IContextKey; private _onDidResize = new Emitter(); @@ -139,8 +133,6 @@ export class CommentThreadWidget extends ) as unknown as CommentThreadBody; this._register(this._body); this._setAriaLabel(); - this._styleElement = domStylesheets.createStyleSheet(this.container); - this._commentThreadContextValue = CommentContextKeys.commentThreadContext.bindTo(this._contextKeyService); this._commentThreadContextValue.set(_commentThread.contextValue); @@ -382,72 +374,12 @@ export class CommentThreadWidget extends } - applyTheme(theme: IColorTheme, fontInfo: FontInfo) { - const content: string[] = []; - - content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`); - content.push(`.monaco-editor .review-widget > .head { background-color: var(${commentThreadStateBackgroundColorVar}) }`); - - const linkColor = theme.getColor(textLinkForeground); - if (linkColor) { - content.push(`.review-widget .body .comment-body a { color: ${linkColor} }`); - } - - const linkActiveColor = theme.getColor(textLinkActiveForeground); - if (linkActiveColor) { - content.push(`.review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`); - } - - const focusColor = theme.getColor(focusBorder); - if (focusColor) { - content.push(`.review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`); - content.push(`.review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`); - } - - const blockQuoteBackground = theme.getColor(textBlockQuoteBackground); - if (blockQuoteBackground) { - content.push(`.review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`); - } - - const border = theme.getColor(PANEL_BORDER); - if (border) { - content.push(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`); - } - - const hcBorder = theme.getColor(contrastBorder); - if (hcBorder) { - content.push(`.review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`); - content.push(`.review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`); - } - - const errorBorder = theme.getColor(inputValidationErrorBorder); - if (errorBorder) { - content.push(`.review-widget .validation-error { border: 1px solid ${errorBorder}; }`); - } - - const errorBackground = theme.getColor(inputValidationErrorBackground); - if (errorBackground) { - content.push(`.review-widget .validation-error { background: ${errorBackground}; }`); - } - - const errorForeground = theme.getColor(inputValidationErrorForeground); - if (errorForeground) { - content.push(`.review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`); - } - + applyTheme(fontInfo: FontInfo) { const fontFamilyVar = '--comment-thread-editor-font-family'; - const fontSizeVar = '--comment-thread-editor-font-size'; const fontWeightVar = '--comment-thread-editor-font-weight'; this.container?.style.setProperty(fontFamilyVar, fontInfo.fontFamily); - this.container?.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`); this.container?.style.setProperty(fontWeightVar, fontInfo.fontWeight); - content.push(`.review-widget .body code { - font-family: var(${fontFamilyVar}); - font-weight: var(${fontWeightVar}); - }`); - - this._styleElement.textContent = content.join('\n'); this._commentReply?.setCommentEditorDecorations(); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 21051fe928c..85e5fba39ef 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -33,6 +33,17 @@ function getCommentThreadWidgetStateColor(thread: languages.CommentThreadState | return getCommentThreadStateBorderColor(thread, theme) ?? theme.getColor(peekViewBorder); } +/** + * Check if a comment thread has any draft comments + */ +function commentThreadHasDraft(commentThread: languages.CommentThread): boolean { + const comments = commentThread.comments; + if (!comments) { + return false; + } + return comments.some(comment => comment.state === languages.CommentState.Draft); +} + export enum CommentWidgetFocus { None = 0, Widget = 1, @@ -105,6 +116,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget private _commentThreadWidget!: CommentThreadWidget; private readonly _onDidClose = new Emitter(); private readonly _onDidCreateThread = new Emitter(); + private readonly _onDidChangeExpandedState = new Emitter(); private _isExpanded?: boolean; private _initialCollapsibleState?: languages.CommentThreadCollapsibleState; private _commentGlyph?: CommentGlyphWidget; @@ -159,10 +171,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget this._globalToDispose.add(this.themeService.onDidColorThemeChange(this._applyTheme, this)); this._globalToDispose.add(this.editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.fontInfo)) { - this._applyTheme(this.themeService.getColorTheme()); + this._applyTheme(); } })); - this._applyTheme(this.themeService.getColorTheme()); + this._applyTheme(); } @@ -174,6 +186,10 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget return this._onDidCreateThread.event; } + public get onDidChangeExpandedState(): Event { + return this._onDidChangeExpandedState.event; + } + public getPosition(): IPosition | undefined { if (this.position) { return this.position; @@ -378,7 +394,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget const lineNumber = this._commentThread.range?.endLineNumber ?? 1; let shouldMoveWidget = false; if (this._commentGlyph) { - this._commentGlyph.setThreadState(commentThread.state); + const hasDraft = commentThreadHasDraft(commentThread); + this._commentGlyph.setThreadState(commentThread.state, hasDraft); if (this._commentGlyph.getPosition().position!.lineNumber !== lineNumber) { shouldMoveWidget = true; this._commentGlyph.setLineNumber(lineNumber); @@ -403,7 +420,8 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget async display(range: IRange | undefined, shouldReveal: boolean) { if (range) { this._commentGlyph = new CommentGlyphWidget(this.editor, range?.endLineNumber ?? -1); - this._commentGlyph.setThreadState(this._commentThread.state); + const hasDraft = commentThreadHasDraft(this._commentThread); + this._commentGlyph.setThreadState(this._commentThread.state, hasDraft); this._globalToDispose.add(this._commentGlyph.onDidChangeLineNumber(async e => { if (!this._commentThread.range) { return; @@ -506,7 +524,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } } - private _applyTheme(theme: IColorTheme) { + private _applyTheme() { const borderColor = getCommentThreadWidgetStateColor(this._commentThread.state, this.themeService.getColorTheme()) || Color.transparent; this.style({ arrowColor: borderColor, @@ -514,8 +532,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget }); const fontInfo = this.editor.getOption(EditorOption.fontInfo); - // Editor decorations should also be responsive to theme changes - this._commentThreadWidget.applyTheme(theme, fontInfo); + this._commentThreadWidget.applyTheme(fontInfo); } override show(rangeOrPos: IRange | IPosition | undefined, heightInLines: number): void { @@ -528,10 +545,14 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget range = new Range(range.startLineNumber + distance, range.startColumn, range.endLineNumber + distance, range.endColumn); } + const wasExpanded = this._isExpanded; this._isExpanded = true; super.show(range ?? new Range(0, 0, 0, 0), heightInLines); this._commentThread.collapsibleState = languages.CommentThreadCollapsibleState.Expanded; this._refresh(this._commentThreadWidget.getDimensions()); + if (!wasExpanded) { + this._onDidChangeExpandedState.fire(true); + } } async collapseAndFocusRange() { @@ -551,6 +572,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget if (!this._commentThread.comments || !this._commentThread.comments.length) { this.deleteCommentThread(); } + this._onDidChangeExpandedState.fire(false); } super.hide(); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 89a885968f0..3eeff582704 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -20,7 +20,7 @@ export namespace CommentAccessibilityHelpNLS { export const intro = nls.localize('intro', "The editor contains commentable range(s). Some useful commands include:"); export const tabFocus = nls.localize('introWidget', "This widget contains a text area, for composition of new comments, and actions, that can be tabbed to once tab moves focus mode has been enabled with the command Toggle Tab Key Moves Focus{0}.", ``); export const commentCommands = nls.localize('commentCommands', "Some useful comment commands include:"); - export const escape = nls.localize('escape', "- Dismiss Comment (Escape)"); + export const escape = nls.localize('escape', "- Dismiss Comment{0}.", ``); export const nextRange = nls.localize('next', "- Go to Next Commenting Range{0}.", ``); export const previousRange = nls.localize('previous', "- Go to Previous Commenting Range{0}.", ``); export const nextCommentThread = nls.localize('nextCommentThreadKb', "- Go to Next Comment Thread{0}.", ``); diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts index 3efc58ca953..11900e05519 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibleView.ts @@ -15,6 +15,7 @@ import { COMMENTS_VIEW_ID, CommentsMenus } from './commentsTreeViewer.js'; import { CommentsPanel, CONTEXT_KEY_COMMENT_FOCUSED } from './commentsView.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { ICommentService } from './commentService.js'; +import { CommentNode } from '../common/commentModel.js'; import { CommentContextKeys } from '../common/commentContextKeys.js'; import { moveToNextCommentInThread as findNextCommentInThread, revealCommentThread } from './commentsController.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -76,7 +77,7 @@ class CommentsAccessibleContentProvider extends Disposable implements IAccessibl public readonly actions: IAction[]; constructor( private readonly _commentsView: CommentsPanel, - private readonly _focusedCommentNode: any, + private readonly _focusedCommentNode: CommentNode, private readonly _menus: CommentsMenus, ) { super(); diff --git a/src/vs/workbench/contrib/comments/browser/commentsController.ts b/src/vs/workbench/contrib/comments/browser/commentsController.ts index 2feb4477f32..7c2d1b45d28 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsController.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsController.ts @@ -463,7 +463,6 @@ export class CommentController implements IEditorContribution { private _computeAndSetPromise: Promise | undefined; private _addInProgress!: boolean; private _emptyThreadsToAddQueue: [Range | undefined, IEditorMouseEvent | undefined][] = []; - private _computeCommentingRangePromise!: CancelablePromise | null; private _computeCommentingRangeScheduler!: Delayer> | null; private _pendingNewCommentCache: { [key: string]: { [key: string]: languages.PendingComment } }; private _pendingEditsCache: { [key: string]: { [key: string]: { [key: number]: languages.PendingComment } } }; // uniqueOwner -> threadId -> uniqueIdInThread -> pending comment @@ -472,6 +471,7 @@ export class CommentController implements IEditorContribution { private _activeCursorHasCommentingRange: IContextKey; private _activeCursorHasComment: IContextKey; private _activeEditorHasCommentingRange: IContextKey; + private _commentWidgetVisible: IContextKey; private _hasRespondedToEditorChange: boolean = false; constructor( @@ -497,6 +497,7 @@ export class CommentController implements IEditorContribution { this._activeCursorHasCommentingRange = CommentContextKeys.activeCursorHasCommentingRange.bindTo(contextKeyService); this._activeCursorHasComment = CommentContextKeys.activeCursorHasComment.bindTo(contextKeyService); this._activeEditorHasCommentingRange = CommentContextKeys.activeEditorHasCommentingRange.bindTo(contextKeyService); + this._commentWidgetVisible = CommentContextKeys.commentWidgetVisible.bindTo(contextKeyService); if (editor instanceof EmbeddedCodeEditorWidget) { return; @@ -558,7 +559,7 @@ export class CommentController implements IEditorContribution { })); this.onModelChanged(); - this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); + this.globalToDispose.add(this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {})); this.globalToDispose.add( this.commentService.registerContinueOnCommentProvider({ provideContinueOnComments: () => { @@ -694,11 +695,6 @@ export class CommentController implements IEditorContribution { private beginComputeCommentingRanges() { if (this._computeCommentingRangeScheduler) { - if (this._computeCommentingRangePromise) { - this._computeCommentingRangePromise.cancel(); - this._computeCommentingRangePromise = null; - } - this._computeCommentingRangeScheduler.trigger(() => { const editorURI = this.editor && this.editor.hasModel() && this.editor.getModel().uri; @@ -746,6 +742,28 @@ export class CommentController implements IEditorContribution { } } + public async collapseVisibleComments(): Promise { + if (!this.editor) { + return; + } + const visibleRanges = this.editor.getVisibleRanges(); + for (const widget of this._commentWidgets) { + if (widget.expanded && widget.commentThread.range) { + const isVisible = visibleRanges.some(visibleRange => + Range.areIntersectingOrTouching(visibleRange, widget.commentThread.range!) + ); + if (isVisible) { + await widget.collapse(true); + } + } + } + } + + private _updateCommentWidgetVisibleContext(): void { + const hasExpanded = this._commentWidgets.some(widget => widget.expanded); + this._commentWidgetVisible.set(hasExpanded); + } + public expandAll(): void { for (const widget of this._commentWidgets) { widget.expand(); @@ -1080,6 +1098,8 @@ export class CommentController implements IEditorContribution { const zoneWidget = this.instantiationService.createInstance(ReviewZoneWidget, this.editor, uniqueOwner, thread, pendingComment ?? continueOnCommentReply?.comment, pendingEdits); await zoneWidget.display(thread.range, shouldReveal); this._commentWidgets.push(zoneWidget); + zoneWidget.onDidChangeExpandedState(() => this._updateCommentWidgetVisibleContext()); + zoneWidget.onDidClose(() => this._updateCommentWidgetVisibleContext()); this.openCommentsView(thread); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index 810eec3e263..aeb70b11d9a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -29,6 +29,7 @@ import { CommentsInputContentProvider } from './commentsInputContentProvider.js' import { AccessibleViewProviderId } from '../../../../platform/accessibility/browser/accessibleView.js'; import { CommentWidgetFocus } from './commentThreadZoneWidget.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { CommentThread, CommentThreadCollapsibleState, CommentThreadState } from '../../../../editor/common/languages.js'; registerEditorContribution(ID, CommentController, EditorContributionInstantiation.AfterFirstRender); registerWorkbenchContribution2(CommentsInputContentProvider.ID, CommentsInputContentProvider, WorkbenchPhase.BlockRestore); @@ -330,6 +331,14 @@ registerAction2(class extends Action2 { } }); +function changeAllCollapseState(commentService: ICommentService, newState: (commentThread: CommentThread) => CommentThreadCollapsibleState) { + for (const resource of commentService.commentsModel.resourceCommentThreads) { + for (const thread of resource.commentThreads) { + thread.thread.collapsibleState = newState(thread.thread); + } + } +} + registerAction2(class extends Action2 { constructor() { super({ @@ -349,7 +358,8 @@ registerAction2(class extends Action2 { }); } override run(accessor: ServicesAccessor, ...args: unknown[]): void { - getActiveController(accessor)?.collapseAll(); + const commentService = accessor.get(ICommentService); + changeAllCollapseState(commentService, () => CommentThreadCollapsibleState.Collapsed); } }); @@ -372,7 +382,8 @@ registerAction2(class extends Action2 { }); } override run(accessor: ServicesAccessor, ...args: unknown[]): void { - getActiveController(accessor)?.expandAll(); + const commentService = accessor.get(ICommentService); + changeAllCollapseState(commentService, () => CommentThreadCollapsibleState.Expanded); } }); @@ -395,7 +406,10 @@ registerAction2(class extends Action2 { }); } override run(accessor: ServicesAccessor, ...args: unknown[]): void { - getActiveController(accessor)?.expandUnresolved(); + const commentService = accessor.get(ICommentService); + changeAllCollapseState(commentService, (commentThread) => { + return commentThread.state === CommentThreadState.Unresolved ? CommentThreadCollapsibleState.Expanded : CommentThreadCollapsibleState.Collapsed; + }); } }); @@ -421,6 +435,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: async (accessor, args) => { const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); const keybindingService = accessor.get(IKeybindingService); + const notificationService = accessor.get(INotificationService); + const commentService = accessor.get(ICommentService); // Unfortunate, but collapsing the comment thread might cause a dialog to show // If we don't wait for the key up here, then the dialog will consume it and immediately close await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide); @@ -431,8 +447,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ if (!controller) { return; } - const notificationService = accessor.get(INotificationService); - const commentService = accessor.get(ICommentService); + let error = false; try { const activeComment = commentService.lastActiveCommentcontroller?.activeComment; @@ -451,6 +466,27 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: CommentCommandId.Hide, + weight: KeybindingWeight.EditorContrib, + primary: KeyMod.CtrlCmd | KeyCode.Escape, + win: { primary: KeyMod.Alt | KeyCode.Backspace }, + when: ContextKeyExpr.and(EditorContextKeys.focus, CommentContextKeys.commentWidgetVisible), + handler: async (accessor, args) => { + const activeCodeEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); + const keybindingService = accessor.get(IKeybindingService); + // Unfortunate, but collapsing the comment thread might cause a dialog to show + // If we don't wait for the key up here, then the dialog will consume it and immediately close + await keybindingService.enableKeybindingHoldMode(CommentCommandId.Hide); + if (activeCodeEditor) { + const controller = CommentController.get(activeCodeEditor); + if (controller) { + await controller.collapseVisibleComments(); + } + } + } +}); + export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | null { let activeTextEditorControl = accessor.get(IEditorService).activeTextEditorControl; @@ -469,16 +505,3 @@ export function getActiveEditor(accessor: ServicesAccessor): IActiveCodeEditor | return activeTextEditorControl; } -function getActiveController(accessor: ServicesAccessor): CommentController | undefined { - const activeEditor = getActiveEditor(accessor); - if (!activeEditor) { - return undefined; - } - - const controller = CommentController.get(activeEditor); - if (!controller) { - return undefined; - } - return controller; -} - diff --git a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts index 7bcb69a2c61..b5234b61404 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsTreeViewer.ts @@ -21,7 +21,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { commentViewThreadStateColorVar, getCommentThreadStateIconColor } from './commentColors.js'; -import { CommentThreadApplicability, CommentThreadState } from '../../../../editor/common/languages.js'; +import { CommentThreadApplicability, CommentThreadState, CommentState } from '../../../../editor/common/languages.js'; import { Color } from '../../../../base/common/color.js'; import { IMatch } from '../../../../base/common/filters.js'; import { FilterOptions } from './commentsFilterOptions.js'; @@ -265,8 +265,11 @@ export class CommentNodeRenderer implements IListRenderer return renderedComment; } - private getIcon(threadState?: CommentThreadState): ThemeIcon { - if (threadState === CommentThreadState.Unresolved) { + private getIcon(threadState?: CommentThreadState, hasDraft?: boolean): ThemeIcon { + // Priority: draft > unresolved > resolved + if (hasDraft) { + return Codicon.commentDraft; + } else if (threadState === CommentThreadState.Unresolved) { return Codicon.commentUnresolved; } else { return Codicon.comment; @@ -289,7 +292,9 @@ export class CommentNodeRenderer implements IListRenderer templateData.threadMetadata.icon.classList.remove(...Array.from(templateData.threadMetadata.icon.classList.values()) .filter(value => value.startsWith('codicon'))); - templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState))); + // Check if any comment in the thread has draft state + const hasDraft = node.element.thread.comments?.some(comment => comment.state === CommentState.Draft); + templateData.threadMetadata.icon.classList.add(...ThemeIcon.asClassNameArray(this.getIcon(node.element.threadState, hasDraft))); if (node.element.threadState !== undefined) { const color = this.getCommentThreadWidgetStateColor(node.element.threadState, this.themeService.getColorTheme()); templateData.threadMetadata.icon.style.setProperty(commentViewThreadStateColorVar, `${color}`); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index d09628d47d4..1d42ac39101 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -75,6 +75,10 @@ border-left-color: var(--vscode-textBlockQuote-border); } +.review-widget .body .review-comment blockquote { + background: var(--vscode-textBlockQuote-background); +} + .review-widget .body .review-comment .avatar-container { margin-top: 4px !important; } @@ -194,6 +198,7 @@ .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border: 1px solid; + border-color: var(--vscode-panel-border); } .review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label.disabled { @@ -210,6 +215,16 @@ } .review-widget .body .review-comment .review-comment-contents .comment-body a { cursor: pointer; + color: var(--vscode-textLink-foreground); +} + +.review-widget .body .comment-body a:hover, +.review-widget .body .comment-body a:active { + color: var(--vscode-textLink-activeForeground); +} + +.review-widget .body .comment-body a:focus { + outline: 1px solid var(--vscode-focusBorder); } .review-widget .body .comment-body p, @@ -269,6 +284,12 @@ margin-top: -1px; margin-left: -1px; word-wrap: break-word; + border: 1px solid var(--vscode-inputValidation-errorBorder); + background: var(--vscode-inputValidation-errorBackground); +} + +.review-widget .body .comment-form .validation-error { + color: var(--vscode-inputValidation-errorForeground); } @@ -298,10 +319,6 @@ margin: 0 10px 0 0; } -.review-widget .body .comment-additional-actions .button-bar .monaco-text-button { - padding: 4px 10px; -} - .review-widget .body .comment-additional-actions .codicon-drop-down-button { align-items: center; } @@ -310,6 +327,11 @@ color: var(--vscode-editor-foreground); } +.review-widget .body code { + font-family: var(--comment-thread-editor-font-family); + font-weight: var(--comment-thread-editor-font-weight); +} + .review-widget .body .comment-form-container .comment-form { display: flex; flex-direction: row; @@ -342,6 +364,7 @@ white-space: nowrap; border: 0px; outline: 1px solid transparent; + outline-color: var(--vscode-contrastBorder); background-color: var(--vscode-editorCommentsWidget-replyInputBackground); color: var(--vscode-editor-foreground); font-size: inherit; @@ -368,6 +391,11 @@ border: 0px; box-sizing: content-box; padding: 6px 0 6px 12px; + outline: 1px solid var(--vscode-contrastBorder); +} + +.review-widget .body .monaco-editor.focused { + outline: 1px solid var(--vscode-focusBorder); } .review-widget .body .comment-form-container .monaco-editor, @@ -393,7 +421,6 @@ .review-widget .body .comment-form-container .form-actions .monaco-text-button, .review-widget .body .edit-container .monaco-text-button { width: auto; - padding: 4px 10px; margin-left: 5px; } @@ -454,6 +481,14 @@ margin: 0; } +.monaco-editor .review-widget > .body { + border-top: 1px solid var(--comment-thread-state-color); +} + +.monaco-editor .review-widget > .head { + background-color: var(--comment-thread-state-background-color); +} + .review-widget > .body { border-top: 1px solid; position: relative; @@ -486,7 +521,8 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .comment-thread:before, -.monaco-editor .comment-thread-unresolved:before { +.monaco-editor .comment-thread-unresolved:before, +.monaco-editor .comment-thread-draft:before { background: var(--vscode-editorGutter-commentRangeForeground); } @@ -500,14 +536,16 @@ div.preview.inline .monaco-editor .comment-range-glyph { .monaco-editor .margin-view-overlays .comment-range-glyph.line-hover, .monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread, -.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread-unresolved { +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread-unresolved, +.monaco-editor .margin-view-overlays .comment-range-glyph.comment-thread-draft { margin-left: 13px; } .monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before, .monaco-editor .margin-view-overlays .comment-range-glyph.line-hover:before, .monaco-editor .comment-range-glyph.comment-thread:before, -.monaco-editor .comment-range-glyph.comment-thread-unresolved:before { +.monaco-editor .comment-range-glyph.comment-thread-unresolved:before, +.monaco-editor .comment-range-glyph.comment-thread-draft:before { position: absolute; height: 100%; width: 9px; @@ -525,6 +563,10 @@ div.preview.inline .monaco-editor .comment-range-glyph { color: var(--vscode-editorGutter-commentUnresolvedGlyphForeground); } +.monaco-editor .comment-range-glyph.comment-thread-draft:before { + color: var(--vscode-editorGutter-commentDraftGlyphForeground); +} + .monaco-editor .margin-view-overlays .comment-range-glyph.multiline-add { border-left-width: 3px; border-left-style: dotted; @@ -544,12 +586,14 @@ div.preview.inline .monaco-editor .comment-range-glyph { } .monaco-editor .comment-range-glyph.comment-thread, -.monaco-editor .comment-range-glyph.comment-thread-unresolved { +.monaco-editor .comment-range-glyph.comment-thread-unresolved, +.monaco-editor .comment-range-glyph.comment-thread-draft { z-index: 20; } .monaco-editor .comment-range-glyph.comment-thread:before, -.monaco-editor .comment-range-glyph.comment-thread-unresolved:before { +.monaco-editor .comment-range-glyph.comment-thread-unresolved:before, +.monaco-editor .comment-range-glyph.comment-thread-draft:before { font-family: "codicon"; font-size: 13px; width: 18px !important; @@ -570,6 +614,10 @@ div.preview.inline .monaco-editor .comment-range-glyph { content: var(--vscode-icon-comment-unresolved-content); font-family: var(--vscode-icon-comment-unresolved-font-family); } +.monaco-editor .comment-range-glyph.comment-thread-draft:before { + content: var(--vscode-icon-comment-draft-content); + font-family: var(--vscode-icon-comment-draft-font-family); +} .monaco-editor.inline-comment .margin-view-overlays .codicon-folding-expanded, .monaco-editor.inline-comment .margin-view-overlays .codicon-folding-collapsed { diff --git a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts index 2a5d0776c45..a26eee8c8b5 100644 --- a/src/vs/workbench/contrib/comments/common/commentContextKeys.ts +++ b/src/vs/workbench/contrib/comments/common/commentContextKeys.ts @@ -67,6 +67,11 @@ export namespace CommentContextKeys { */ export const commentFocused = new RawContextKey('commentFocused', false, { type: 'boolean', description: nls.localize('commentFocused', "Set when the comment is focused") }); + /** + * A context key that is set when a comment widget is visible in the editor. + */ + export const commentWidgetVisible = new RawContextKey('commentWidgetVisible', false, { type: 'boolean', description: nls.localize('commentWidgetVisible', "Set when a comment widget is visible in the editor") }); + /** * A context key that is set when commenting is enabled. */ diff --git a/src/vs/workbench/contrib/cortexide/browser/terminalToolService.ts b/src/vs/workbench/contrib/cortexide/browser/terminalToolService.ts index 1bf0206b587..41fc13f9eb6 100644 --- a/src/vs/workbench/contrib/cortexide/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/cortexide/browser/terminalToolService.ts @@ -125,7 +125,18 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) { const { cwd: override_cwd, config, hidden } = props; - const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri; + let cwd: URI | string | undefined; + if (override_cwd !== null) { + cwd = override_cwd; + } else { + const workspace = this.workspaceContextService.getWorkspace(); + if (workspace.folders.length > 0) { + const firstFolder = workspace.folders[0]; + if (firstFolder) { + cwd = firstFolder.uri; + } + } + } const options: ICreateTerminalOptions = { cwd, @@ -216,7 +227,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`); } - // Ensure the xterm.js instance has been created – otherwise we cannot access the buffer. + // Ensure the xterm.js instance has been created - otherwise we cannot access the buffer. if (!terminal.xterm) { throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.'); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts index 17c575eadf5..ab9cf7ccf96 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts @@ -9,7 +9,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js'; import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js'; -import { ComplexCustomWorkingCopyEditorHandler as ComplexCustomWorkingCopyEditorHandler, CustomEditorInputSerializer } from './customEditorInputFactory.js'; +import { ComplexCustomWorkingCopyEditorHandler, CustomEditorInputSerializer } from './customEditorInputFactory.js'; import { ICustomEditorService } from '../common/customEditor.js'; import { WebviewEditor } from '../../webviewPanel/browser/webviewEditor.js'; import { CustomEditorInput } from './customEditorInput.js'; diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 6ced9bac527..cef8490460b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -33,13 +33,13 @@ import { IFilesConfigurationService } from '../../../services/filesConfiguration import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { IUntitledTextEditorService } from '../../../services/untitled/common/untitledTextEditorService.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { WebviewIcons } from '../../webviewPanel/browser/webviewEditorInput.js'; +import { WebviewIconPath } from '../../webviewPanel/browser/webviewEditorInput.js'; interface CustomEditorInputInitInfo { readonly resource: URI; readonly viewType: string; readonly webviewTitle: string | undefined; - readonly iconPath: WebviewIcons | undefined; + readonly iconPath: WebviewIconPath | undefined; } export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { @@ -52,8 +52,15 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { ): EditorInput { return instantiationService.invokeFunction(accessor => { // If it's an untitled file we must populate the untitledDocumentData - const untitledString = accessor.get(IUntitledTextEditorService).getValue(init.resource); + const untitledTextEditorService = accessor.get(IUntitledTextEditorService); + const untitledTextModel = untitledTextEditorService.get(init.resource); + const untitledString = untitledTextModel?.textEditorModel?.getValue(); const untitledDocumentData = untitledString ? VSBuffer.fromString(untitledString) : undefined; + + // If we're taking over an untitled text editor, revert it so it's no longer + // tracked as a dirty working copy (fixes #125293). + untitledTextModel?.revert(); + const webview = accessor.get(IWebviewService).createWebviewOverlay({ providedViewType: init.viewType, title: init.webviewTitle, diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 02c270e4661..cb9da93c471 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -14,18 +14,19 @@ import { CustomEditorInput } from './customEditorInput.js'; import { ICustomEditorService } from '../common/customEditor.js'; import { NotebookEditorInput } from '../../notebook/common/notebookEditorInput.js'; import { IWebviewService, WebviewContentOptions, WebviewContentPurpose, WebviewExtensionDescription, WebviewOptions } from '../../webview/browser/webview.js'; -import { DeserializedWebview, restoreWebviewContentOptions, restoreWebviewOptions, reviveWebviewExtensionDescription, SerializedWebview, SerializedWebviewOptions, WebviewEditorInputSerializer } from '../../webviewPanel/browser/webviewEditorInputSerializer.js'; +import { DeserializedWebview, restoreWebviewContentOptions, restoreWebviewOptions, reviveWebviewExtensionDescription, reviveWebviewIconPath, SerializedWebview, SerializedWebviewOptions, WebviewEditorInputSerializer } from '../../webviewPanel/browser/webviewEditorInputSerializer.js'; import { IWebviewWorkbenchService } from '../../webviewPanel/browser/webviewWorkbenchService.js'; import { IWorkingCopyBackupMeta, IWorkingCopyIdentifier } from '../../../services/workingCopy/common/workingCopy.js'; import { IWorkingCopyBackupService } from '../../../services/workingCopy/common/workingCopyBackup.js'; import { IWorkingCopyEditorHandler, IWorkingCopyEditorService } from '../../../services/workingCopy/common/workingCopyEditorService.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; export interface CustomDocumentBackupData extends IWorkingCopyBackupMeta { readonly viewType: string; readonly editorResource: UriComponents; readonly customTitle: string | undefined; - readonly iconPath: { dark: UriComponents; light: UriComponents } | undefined; + readonly iconPath: { dark: UriComponents; light: UriComponents } | ThemeIcon | undefined; backupId: string; @@ -201,12 +202,9 @@ export class ComplexCustomWorkingCopyEditorHandler extends Disposable implements resource: URI.revive(backupData.editorResource), viewType: backupData.viewType, webviewTitle: backupData.customTitle, - iconPath: backupData.iconPath - ? { dark: URI.revive(backupData.iconPath.dark), light: URI.revive(backupData.iconPath.light) } - : undefined + iconPath: reviveWebviewIconPath(backupData.iconPath) }, webview, { backupId: backupData.backupId }); editor.updateGroup(0); return editor; } } - diff --git a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts index 67bb900540c..8adf5da4e25 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointWidget.ts @@ -97,6 +97,7 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi private context: Context; private heightInPx: number | undefined; private triggeredByBreakpointInput: IBreakpoint | undefined; + private availableBreakpoints: IBreakpoint[] = []; constructor(editor: ICodeEditor, private lineNumber: number, private column: number | undefined, context: Context | undefined, @IContextViewService private readonly contextViewService: IContextViewService, @@ -140,8 +141,12 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi if (this.breakpoint && e && e.removed && e.removed.indexOf(this.breakpoint) >= 0) { this.dispose(); } + // Update the breakpoint list when in trigger point context + if (this.context === Context.TRIGGER_POINT && this.selectBreakpointBox) { + this.updateTriggerBreakpointList(); + } })); - this.codeEditorService.registerDecorationType('breakpoint-widget', DECORATION_KEY, {}); + this.store.add(this.codeEditorService.registerDecorationType('breakpoint-widget', DECORATION_KEY, {})); this.create(); } @@ -268,34 +273,17 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi } private createTriggerBreakpointInput(container: HTMLElement) { - const breakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); - const breakpointOptions: ISelectOptionItem[] = [ - { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, - ...breakpoints.map(bp => ({ - text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}`, - description: nls.localize('triggerByLoading', 'Loading...') - })), - ]; + this.availableBreakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); + const breakpointOptions = this.buildBreakpointOptions(); - const index = breakpoints.findIndex((bp) => this.breakpoint?.triggeredBy === bp.getId()); - for (const [i, bp] of breakpoints.entries()) { - this.textModelService.createModelReference(bp.uri).then(ref => { - try { - breakpointOptions[i + 1].description = ref.object.textEditorModel.getLineContent(bp.lineNumber).trim(); - } finally { - ref.dispose(); - } - }).catch(() => { - breakpointOptions[i + 1].description = nls.localize('noBpSource', 'Could not load source.'); - }); - } + const index = this.availableBreakpoints.findIndex((bp) => this.breakpoint?.triggeredBy === bp.getId()); const selectBreakpointBox = this.selectBreakpointBox = this.store.add(new SelectBox(breakpointOptions, index + 1, this.contextViewService, defaultSelectBoxStyles, { ariaLabel: nls.localize('selectBreakpoint', 'Select breakpoint'), useCustomDrawn: !hasNativeContextMenu(this._configurationService) })); this.store.add(selectBreakpointBox.onDidSelect(e => { if (e.index === 0) { this.triggeredByBreakpointInput = undefined; } else { - this.triggeredByBreakpointInput = breakpoints[e.index - 1]; + this.triggeredByBreakpointInput = this.availableBreakpoints[e.index - 1]; } })); this.selectBreakpointContainer = $('.select-breakpoint-container'); @@ -318,10 +306,58 @@ export class BreakpointWidget extends ZoneWidget implements IPrivateBreakpointWi this.store.add(closeButton); } + private buildBreakpointOptions(): ISelectOptionItem[] { + const breakpointOptions: ISelectOptionItem[] = [ + { text: nls.localize('noTriggerByBreakpoint', 'None'), isDisabled: true }, + ...this.availableBreakpoints.map(bp => ({ + text: `${this.labelService.getUriLabel(bp.uri, { relative: true })}: ${bp.lineNumber}`, + description: nls.localize('triggerByLoading', 'Loading...') + })), + ]; + + // Load the source code for each breakpoint asynchronously + for (const [i, bp] of this.availableBreakpoints.entries()) { + this.textModelService.createModelReference(bp.uri).then(ref => { + try { + breakpointOptions[i + 1].description = ref.object.textEditorModel.getLineContent(bp.lineNumber).trim(); + } finally { + ref.dispose(); + } + }).catch(() => { + breakpointOptions[i + 1].description = nls.localize('noBpSource', 'Could not load source.'); + }); + } + + return breakpointOptions; + } + + private updateTriggerBreakpointList(): void { + this.availableBreakpoints = this.debugService.getModel().getBreakpoints().filter(bp => bp !== this.breakpoint && !bp.logMessage); + + // Try to preserve the current selection if the breakpoint still exists + let selectedIndex = 0; // Default to "None" + if (this.triggeredByBreakpointInput) { + const newIndex = this.availableBreakpoints.findIndex(bp => bp.getId() === this.triggeredByBreakpointInput?.getId()); + if (newIndex !== -1) { + selectedIndex = newIndex + 1; + } else { + // The selected breakpoint was removed, clear the selection + this.triggeredByBreakpointInput = undefined; + } + } + + const breakpointOptions = this.buildBreakpointOptions(); + this.selectBreakpointBox.setOptions(breakpointOptions, selectedIndex); + } + private updateContextInput() { if (this.context === Context.TRIGGER_POINT) { this.inputContainer.hidden = true; this.selectBreakpointContainer.hidden = false; + // Update the breakpoint list when switching to trigger point context + if (this.selectBreakpointBox) { + this.updateTriggerBreakpointList(); + } } else { this.inputContainer.hidden = false; this.selectBreakpointContainer.hidden = true; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 781b40d4874..6f82df1624a 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -5,28 +5,32 @@ import * as dom from '../../../../base/browser/dom.js'; import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { Gesture } from '../../../../base/browser/touch.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { AriaRole } from '../../../../base/browser/ui/aria/aria.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; -import { IListContextMenuEvent, IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; +import { Checkbox, TriStateCheckbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { Orientation } from '../../../../base/browser/ui/splitview/splitview.js'; +import { ICompressedTreeElement, ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; +import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js'; +import { ITreeContextMenuEvent, ITreeNode } from '../../../../base/browser/ui/tree/tree.js'; import { Action } from '../../../../base/common/actions.js'; -import { equals } from '../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { DisposableStore, dispose } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; import * as resources from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; import { Constants } from '../../../../base/common/uint.js'; import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../nls.js'; import { getActionBarActions, getContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -38,11 +42,11 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; +import { WorkbenchCompressibleObjectTree } from '../../../../platform/list/browser/listService.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; -import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; @@ -55,14 +59,15 @@ import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, In import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; import * as icons from './debugIcons.js'; import { DisassemblyView } from './disassemblyView.js'; +import { equals } from '../../../../base/common/arrays.js'; +import { hasKey } from '../../../../base/common/types.js'; const $ = dom.$; -function createCheckbox(disposables: DisposableStore): HTMLInputElement { - const checkbox = $('input'); - checkbox.type = 'checkbox'; - checkbox.tabIndex = -1; - disposables.add(Gesture.ignoreTarget(checkbox)); +function createCheckbox(disposables: DisposableStore): Checkbox { + const checkbox = new Checkbox('', false, defaultCheckboxStyles); + checkbox.domNode.tabIndex = -1; + disposables.add(checkbox); return checkbox; } @@ -74,6 +79,31 @@ export function getExpandedBodySize(model: IDebugModel, sessionId: string | unde } type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint | IInstructionBreakpoint; +/** + * Represents a file node in the breakpoints tree that groups breakpoints by file. + */ +export class BreakpointsFolderItem { + constructor( + readonly uri: URI, + readonly breakpoints: IBreakpoint[] + ) { } + + getId(): string { + return this.uri.toString(); + } + + get enabled(): boolean { + return this.breakpoints.every(bp => bp.enabled); + } + + get indeterminate(): boolean { + const enabledCount = this.breakpoints.filter(bp => bp.enabled).length; + return enabledCount > 0 && enabledCount < this.breakpoints.length; + } +} + +type BreakpointTreeElement = BreakpointsFolderItem | BreakpointItem; + interface InputBoxData { breakpoint: IFunctionBreakpoint | IExceptionBreakpoint | IDataBreakpoint; type: 'condition' | 'hitCount' | 'name'; @@ -86,7 +116,7 @@ function getModeKindForBreakpoint(breakpoint: IBreakpoint) { export class BreakpointsView extends ViewPane { - private list!: WorkbenchList; + private tree!: WorkbenchCompressibleObjectTree; private needsRefresh = false; private needsStateChange = false; private ignoreLayout = false; @@ -97,11 +127,16 @@ export class BreakpointsView extends ViewPane { private breakpointSupportsCondition: IContextKey; private _inputBoxData: InputBoxData | undefined; breakpointInputFocused: IContextKey; - private autoFocusedIndex = -1; + private autoFocusedElement: BreakpointItem | undefined; + private collapsedState = new Set(); private hintContainer: IconLabel | undefined; private hintDelayer: RunOnceScheduler; + private getPresentation(): 'tree' | 'list' { + return this.configurationService.getValue<'tree' | 'list'>('debug.breakpointsView.presentation'); + } + constructor( options: IViewletViewOptions, @IContextMenuService contextMenuService: IContextMenuService, @@ -140,30 +175,72 @@ export class BreakpointsView extends ViewPane { this.element.classList.add('debug-pane'); container.classList.add('debug-breakpoints'); - const delegate = new BreakpointsDelegate(this); - - this.list = this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ - this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), - new ExceptionBreakpointsRenderer(this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService, this.hoverService), - new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), - this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), - new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), - this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), - new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), - this.instantiationService.createInstance(InstructionBreakpointsRenderer), - ], { - identityProvider: { getId: (element: IEnablement) => element.getId() }, - multipleSelectionSupport: false, - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IEnablement) => e }, - accessibilityProvider: new BreakpointsAccessibilityProvider(this.debugService, this.labelService), - overrideStyles: this.getLocationBasedColors().listOverrideStyles - }) as WorkbenchList; - - CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.list.contextKeyService); - - this._register(this.list.onContextMenu(this.onListContextMenu, this)); - - this._register(this.list.onMouseMiddleClick(async ({ element }) => { + + this.tree = this.instantiationService.createInstance( + WorkbenchCompressibleObjectTree, + 'BreakpointsView', + container, + new BreakpointsDelegate(this), + [ + this.instantiationService.createInstance(BreakpointsFolderRenderer), + this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType), + new ExceptionBreakpointsRenderer(this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService, this.hoverService), + new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), + this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), + this.instantiationService.createInstance(DataBreakpointsRenderer, this.menu, this.breakpointHasMultipleModes, this.breakpointSupportsCondition, this.breakpointItemType, this.breakpointIsDataBytes), + new DataBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.hoverService, this.labelService), + this.instantiationService.createInstance(InstructionBreakpointsRenderer), + ], + { + compressionEnabled: this.getPresentation() === 'tree', + hideTwistiesOfChildlessElements: true, + identityProvider: { + getId: (element: BreakpointTreeElement) => element.getId() + }, + keyboardNavigationLabelProvider: { + getKeyboardNavigationLabel: (element: BreakpointTreeElement) => { + if (element instanceof BreakpointsFolderItem) { + return resources.basenameOrAuthority(element.uri); + } + if (element instanceof Breakpoint) { + return `${resources.basenameOrAuthority(element.uri)}:${element.lineNumber}`; + } + if (element instanceof FunctionBreakpoint) { + return element.name; + } + if (element instanceof DataBreakpoint) { + return element.description; + } + if (element instanceof ExceptionBreakpoint) { + return element.label || element.filter; + } + if (element instanceof InstructionBreakpoint) { + return `0x${element.address.toString(16)}`; + } + return ''; + }, + getCompressedNodeKeyboardNavigationLabel: (elements: BreakpointTreeElement[]) => { + return elements.map(e => { + if (e instanceof BreakpointsFolderItem) { + return resources.basenameOrAuthority(e.uri); + } + return ''; + }).join('/'); + } + }, + accessibilityProvider: new BreakpointsAccessibilityProvider(this.debugService, this.labelService), + multipleSelectionSupport: false, + overrideStyles: this.getLocationBasedColors().listOverrideStyles + } + ); + this._register(this.tree); + + CONTEXT_BREAKPOINTS_FOCUSED.bindTo(this.tree.contextKeyService); + + this._register(this.tree.onContextMenu(this.onTreeContextMenu, this)); + + this._register(this.tree.onMouseMiddleClick(async ({ element }) => { if (element instanceof Breakpoint) { await this.debugService.removeBreakpoints(element.getId()); } else if (element instanceof FunctionBreakpoint) { @@ -172,11 +249,14 @@ export class BreakpointsView extends ViewPane { await this.debugService.removeDataBreakpoints(element.getId()); } else if (element instanceof InstructionBreakpoint) { await this.debugService.removeInstructionBreakpoints(element.instructionReference, element.offset); + } else if (element instanceof BreakpointsFolderItem) { + await this.debugService.removeBreakpoints(element.breakpoints.map(bp => bp.getId())); } })); - this._register(this.list.onDidOpen(async e => { - if (!e.element) { + this._register(this.tree.onDidOpen(async e => { + const element = e.element; + if (!element) { return; } @@ -184,21 +264,43 @@ export class BreakpointsView extends ViewPane { return; } - if (e.element instanceof Breakpoint) { - openBreakpointSource(e.element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); + if (element instanceof Breakpoint) { + openBreakpointSource(element, e.sideBySide, e.editorOptions.preserveFocus || false, e.editorOptions.pinned || !e.editorOptions.preserveFocus, this.debugService, this.editorService); } - if (e.element instanceof InstructionBreakpoint) { + if (element instanceof InstructionBreakpoint) { const disassemblyView = await this.editorService.openEditor(DisassemblyViewInput.instance); // Focus on double click - (disassemblyView as DisassemblyView).goToInstructionAndOffset(e.element.instructionReference, e.element.offset, dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2); + (disassemblyView as DisassemblyView).goToInstructionAndOffset(element.instructionReference, element.offset, dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2); } - if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2 && e.element instanceof FunctionBreakpoint && e.element !== this.inputBoxData?.breakpoint) { + if (dom.isMouseEvent(e.browserEvent) && e.browserEvent.detail === 2 && element instanceof FunctionBreakpoint && element !== this.inputBoxData?.breakpoint) { // double click - this.renderInputBox({ breakpoint: e.element, type: 'name' }); + this.renderInputBox({ breakpoint: element, type: 'name' }); + } + })); + + // Track collapsed state and update size (items are expanded by default) + this._register(this.tree.onDidChangeCollapseState(e => { + const element = e.node.element; + if (element instanceof BreakpointsFolderItem) { + if (e.node.collapsed) { + this.collapsedState.add(element.getId()); + } else { + this.collapsedState.delete(element.getId()); + } + this.updateSize(); + } + })); + + // React to configuration changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('debug.breakpointsView.presentation')) { + const presentation = this.getPresentation(); + this.tree.updateOptions({ compressionEnabled: presentation === 'tree' }); + this.onBreakpointsChange(); } })); - this.list.splice(0, this.list.length, this.elements); + this.setTreeInput(); this._register(this.onDidChangeBodyVisibility(visible => { if (visible) { @@ -233,7 +335,7 @@ export class BreakpointsView extends ViewPane { override focus(): void { super.focus(); - this.list?.domFocus(); + this.tree?.domFocus(); } renderInputBox(data: InputBoxData | undefined): void { @@ -252,7 +354,7 @@ export class BreakpointsView extends ViewPane { } super.layoutBody(height, width); - this.list?.layout(height, width); + this.tree?.layout(height, width); try { this.ignoreLayout = true; this.updateSize(); @@ -261,8 +363,20 @@ export class BreakpointsView extends ViewPane { } } - private onListContextMenu(e: IListContextMenuEvent): void { + private onTreeContextMenu(e: ITreeContextMenuEvent): void { const element = e.element; + if (element instanceof BreakpointsFolderItem) { + // For folder items, show file-level context menu + this.breakpointItemType.set('breakpointFolder'); + const { secondary } = getContextMenuActions(this.menu.getActions({ arg: element, shouldForwardArgs: false }), 'inline'); + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => secondary, + getActionsContext: () => element + }); + return; + } + const type = element instanceof Breakpoint ? 'breakpoint' : element instanceof ExceptionBreakpoint ? 'exceptionBreakpoint' : element instanceof FunctionBreakpoint ? 'functionBreakpoint' : element instanceof DataBreakpoint ? 'dataBreakpoint' : element instanceof InstructionBreakpoint ? 'instructionBreakpoint' : undefined; @@ -285,10 +399,12 @@ export class BreakpointsView extends ViewPane { private updateSize(): void { const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!); - // Adjust expanded body size - const sessionId = this.debugService.getViewModel().focusedSession?.getId(); - this.minimumBodySize = this.orientation === Orientation.VERTICAL ? getExpandedBodySize(this.debugService.getModel(), sessionId, MAX_VISIBLE_BREAKPOINTS) : 170; - this.maximumBodySize = this.orientation === Orientation.VERTICAL && containerModel.visibleViewDescriptors.length > 1 ? getExpandedBodySize(this.debugService.getModel(), sessionId, Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY; + // Calculate visible row count from tree's content height + // Each row is 22px high + const rowHeight = 22; + + this.minimumBodySize = this.orientation === Orientation.VERTICAL ? Math.min(MAX_VISIBLE_BREAKPOINTS * rowHeight, this.tree.contentHeight) : 170; + this.maximumBodySize = this.orientation === Orientation.VERTICAL && containerModel.visibleViewDescriptors.length > 1 ? this.tree.contentHeight : Number.POSITIVE_INFINITY; } private updateBreakpointsHint(delayed = false): void { @@ -323,18 +439,12 @@ export class BreakpointsView extends ViewPane { private onBreakpointsChange(): void { if (this.isBodyVisible()) { - this.updateSize(); - if (this.list) { - const lastFocusIndex = this.list.getFocus()[0]; - // Check whether focused element was removed - const needsRefocus = lastFocusIndex && !this.elements.includes(this.list.element(lastFocusIndex)); - this.list.splice(0, this.list.length, this.elements); + if (this.tree) { + this.setTreeInput(); this.needsRefresh = false; - if (needsRefocus) { - this.list.focusNth(Math.min(lastFocusIndex, this.list.length - 1)); - } } this.updateBreakpointsHint(); + this.updateSize(); } else { this.needsRefresh = true; } @@ -347,27 +457,27 @@ export class BreakpointsView extends ViewPane { let found = false; if (thread && thread.stoppedDetails && thread.stoppedDetails.hitBreakpointIds && thread.stoppedDetails.hitBreakpointIds.length > 0) { const hitBreakpointIds = thread.stoppedDetails.hitBreakpointIds; - const elements = this.elements; - const index = elements.findIndex(e => { + const elements = this.flatElements; + const hitElement = elements.find(e => { const id = e.getIdFromAdapter(thread.session.getId()); return typeof id === 'number' && hitBreakpointIds.indexOf(id) !== -1; }); - if (index >= 0) { - this.list.setFocus([index]); - this.list.setSelection([index]); + if (hitElement) { + this.tree.setFocus([hitElement]); + this.tree.setSelection([hitElement]); found = true; - this.autoFocusedIndex = index; + this.autoFocusedElement = hitElement; } } if (!found) { // Deselect breakpoint in breakpoint view when no longer stopped on it #125528 - const focus = this.list.getFocus(); - const selection = this.list.getSelection(); - if (this.autoFocusedIndex >= 0 && equals(focus, selection) && focus.indexOf(this.autoFocusedIndex) >= 0) { - this.list.setFocus([]); - this.list.setSelection([]); + const focus = this.tree.getFocus(); + const selection = this.tree.getSelection(); + if (this.autoFocusedElement && equals(focus, selection) && selection.includes(this.autoFocusedElement)) { + this.tree.setFocus([]); + this.tree.setSelection([]); } - this.autoFocusedIndex = -1; + this.autoFocusedElement = undefined; } this.updateBreakpointsHint(); } else { @@ -375,7 +485,82 @@ export class BreakpointsView extends ViewPane { } } - private get elements(): BreakpointItem[] { + private setTreeInput(): void { + const treeInput = this.getTreeElements(); + this.tree.setChildren(null, treeInput); + } + + private getTreeElements(): ICompressedTreeElement[] { + const model = this.debugService.getModel(); + const sessionId = this.debugService.getViewModel().focusedSession?.getId(); + const showAsTree = this.getPresentation() === 'tree'; + + const result: ICompressedTreeElement[] = []; + + // Exception breakpoints at the top (root level) + for (const exBp of model.getExceptionBreakpointsForSession(sessionId)) { + result.push({ element: exBp, incompressible: true }); + } + + // Function breakpoints (root level) + for (const funcBp of model.getFunctionBreakpoints()) { + result.push({ element: funcBp, incompressible: true }); + } + + // Data breakpoints (root level) + for (const dataBp of model.getDataBreakpoints()) { + result.push({ element: dataBp, incompressible: true }); + } + + // Source breakpoints - group by file if showAsTree is enabled + const sourceBreakpoints = model.getBreakpoints(); + if (showAsTree && sourceBreakpoints.length > 0) { + // Group breakpoints by URI + const breakpointsByUri = new Map(); + for (const bp of sourceBreakpoints) { + const key = bp.uri.toString(); + if (!breakpointsByUri.has(key)) { + breakpointsByUri.set(key, []); + } + breakpointsByUri.get(key)!.push(bp); + } + + // Create folder items for each file + for (const [uriStr, breakpoints] of breakpointsByUri) { + const uri = URI.parse(uriStr); + const folderItem = new BreakpointsFolderItem(uri, breakpoints); + + // Sort breakpoints by line number + breakpoints.sort((a, b) => a.lineNumber - b.lineNumber); + + const children: ICompressedTreeElement[] = breakpoints.map(bp => ({ + element: bp, + incompressible: false + })); + + result.push({ + element: folderItem, + incompressible: false, + collapsed: this.collapsedState.has(folderItem.getId()), + children + }); + } + } else { + // Flat mode - just add all source breakpoints + for (const bp of sourceBreakpoints) { + result.push({ element: bp, incompressible: true }); + } + } + + // Instruction breakpoints (root level) + for (const instrBp of model.getInstructionBreakpoints()) { + result.push({ element: instrBp, incompressible: true }); + } + + return result; + } + + private get flatElements(): BreakpointItem[] { const model = this.debugService.getModel(); const sessionId = this.debugService.getViewModel().focusedSession?.getId(); const elements = (>model.getExceptionBreakpointsForSession(sessionId)).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()).concat(model.getInstructionBreakpoints()); @@ -384,17 +569,20 @@ export class BreakpointsView extends ViewPane { } } -class BreakpointsDelegate implements IListVirtualDelegate { +class BreakpointsDelegate implements IListVirtualDelegate { constructor(private view: BreakpointsView) { // noop } - getHeight(_element: BreakpointItem): number { + getHeight(_element: BreakpointTreeElement): number { return 22; } - getTemplateId(element: BreakpointItem): string { + getTemplateId(element: BreakpointTreeElement): string { + if (element instanceof BreakpointsFolderItem) { + return BreakpointsFolderRenderer.ID; + } if (element instanceof Breakpoint) { return BreakpointsRenderer.ID; } @@ -432,7 +620,7 @@ class BreakpointsDelegate implements IListVirtualDelegate { interface IBaseBreakpointTemplateData { breakpoint: HTMLElement; name: HTMLElement; - checkbox: HTMLInputElement; + checkbox: Checkbox; context: BreakpointItem; actionBar: ActionBar; templateDisposables: DisposableStore; @@ -467,7 +655,7 @@ interface IInstructionBreakpointTemplateData extends IBaseBreakpointWithIconTemp interface IFunctionBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; icon: HTMLElement; breakpoint: IFunctionBreakpoint; templateDisposables: DisposableStore; @@ -478,7 +666,7 @@ interface IFunctionBreakpointInputTemplateData { interface IDataBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; icon: HTMLElement; breakpoint: IDataBreakpoint; elementDisposables: DisposableStore; @@ -489,14 +677,154 @@ interface IDataBreakpointInputTemplateData { interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; - checkbox: HTMLInputElement; + checkbox: Checkbox; currentBreakpoint?: IExceptionBreakpoint; templateDisposables: DisposableStore; elementDisposables: DisposableStore; } +interface IBreakpointsFolderTemplateData { + container: HTMLElement; + checkbox: TriStateCheckbox; + name: HTMLElement; + actionBar: ActionBar; + context: BreakpointsFolderItem; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; +} + const breakpointIdToActionBarDomeNode = new Map(); -class BreakpointsRenderer implements IListRenderer { + +class BreakpointsFolderRenderer implements ICompressibleTreeRenderer { + + static readonly ID = 'breakpointFolder'; + + constructor( + @IDebugService private readonly debugService: IDebugService, + @ILabelService private readonly labelService: ILabelService, + @IHoverService private readonly hoverService: IHoverService, + ) { } + + get templateId() { + return BreakpointsFolderRenderer.ID; + } + + renderTemplate(container: HTMLElement): IBreakpointsFolderTemplateData { + const data: IBreakpointsFolderTemplateData = Object.create(null); + data.elementDisposables = new DisposableStore(); + data.templateDisposables = new DisposableStore(); + data.templateDisposables.add(data.elementDisposables); + + data.container = container; + container.classList.add('breakpoint', 'breakpoint-folder'); + + data.templateDisposables.add(toDisposable(() => { + container.classList.remove('breakpoint', 'breakpoint-folder'); + })); + + data.checkbox = new TriStateCheckbox('', false, defaultCheckboxStyles); + data.checkbox.domNode.tabIndex = -1; + data.templateDisposables.add(data.checkbox); + data.templateDisposables.add(data.checkbox.onChange(() => { + const checked = data.checkbox.checked; + const enabled = checked === 'mixed' ? true : checked; + for (const bp of data.context.breakpoints) { + this.debugService.enableOrDisableBreakpoints(enabled, bp); + } + })); + + dom.append(data.container, data.checkbox.domNode); + data.name = dom.append(data.container, $('span.name')); + dom.append(data.container, $('span.file-path')); + + data.actionBar = new ActionBar(data.container); + data.templateDisposables.add(data.actionBar); + + return data; + } + + renderElement(node: ITreeNode, _index: number, data: IBreakpointsFolderTemplateData): void { + const folderItem = node.element; + data.context = folderItem; + + data.name.textContent = this.labelService.getUriBasenameLabel(folderItem.uri); + data.container.classList.toggle('disabled', !this.debugService.getModel().areBreakpointsActivated()); + + const fullPath = this.labelService.getUriLabel(folderItem.uri, { relative: true }); + data.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.container, fullPath)); + + // Set checkbox state + if (folderItem.indeterminate) { + data.checkbox.checked = 'mixed'; + } else { + data.checkbox.checked = folderItem.enabled; + } + + // Add remove action + data.actionBar.clear(); + const removeAction = data.elementDisposables.add(new Action( + 'debug.removeBreakpointsInFile', + localize('removeBreakpointsInFile', "Remove Breakpoints in File"), + ThemeIcon.asClassName(Codicon.close), + true, + async () => { + for (const bp of folderItem.breakpoints) { + await this.debugService.removeBreakpoints(bp.getId()); + } + } + )); + data.actionBar.push(removeAction, { icon: true, label: false }); + } + + renderCompressedElements(node: ITreeNode, void>, _index: number, data: IBreakpointsFolderTemplateData): void { + const elements = node.element.elements; + const folderItem = elements[elements.length - 1]; + data.context = folderItem; + + // For compressed nodes, show the combined path + const names = elements.map(e => resources.basenameOrAuthority(e.uri)); + data.name.textContent = names.join('/'); + + const fullPath = this.labelService.getUriLabel(folderItem.uri, { relative: true }); + data.elementDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), data.container, fullPath)); + + // Set checkbox state + if (folderItem.indeterminate) { + data.checkbox.checked = 'mixed'; + } else { + data.checkbox.checked = folderItem.enabled; + } + + // Add remove action + data.actionBar.clear(); + const removeAction = data.elementDisposables.add(new Action( + 'debug.removeBreakpointsInFile', + localize('removeBreakpointsInFile', "Remove Breakpoints in File"), + ThemeIcon.asClassName(Codicon.close), + true, + async () => { + for (const bp of folderItem.breakpoints) { + await this.debugService.removeBreakpoints(bp.getId()); + } + } + )); + data.actionBar.push(removeAction, { icon: true, label: false }); + } + + disposeElement(element: ITreeNode, index: number, templateData: IBreakpointsFolderTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IBreakpointsFolderTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeTemplate(templateData: IBreakpointsFolderTemplateData): void { + templateData.templateDisposables.dispose(); + } +} + +class BreakpointsRenderer implements ICompressibleTreeRenderer { constructor( private menu: IMenu, @@ -505,7 +833,8 @@ class BreakpointsRenderer implements IListRenderer, @IDebugService private readonly debugService: IDebugService, @IHoverService private readonly hoverService: IHoverService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @ITextModelService private readonly textModelService: ITextModelService ) { // noop } @@ -522,17 +851,22 @@ class BreakpointsRenderer implements IListRenderer { + container.classList.remove('breakpoint'); + })); data.icon = $('.icon'); data.checkbox = createCheckbox(data.templateDisposables); - data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); @@ -545,11 +879,28 @@ class BreakpointsRenderer implements IListRenderer, index: number, data: IBreakpointTemplateData): void { + const breakpoint = node.element; data.context = breakpoint; - data.breakpoint.classList.toggle('disabled', !this.debugService.getModel().areBreakpointsActivated()); - data.name.textContent = resources.basenameOrAuthority(breakpoint.uri); + if (node.depth > 1) { + this.renderBreakpointLineLabel(breakpoint, data); + } else { + this.renderBreakpointFileLabel(breakpoint, data); + } + + this.renderBreakpointCommon(breakpoint, data); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, data: IBreakpointTemplateData): void { + const breakpoint = node.element.elements[node.element.elements.length - 1]; + data.context = breakpoint; + this.renderBreakpointFileLabel(breakpoint, data); + this.renderBreakpointCommon(breakpoint, data); + } + + private renderBreakpointCommon(breakpoint: IBreakpoint, data: IBreakpointTemplateData): void { + data.breakpoint.classList.toggle('disabled', !this.debugService.getModel().areBreakpointsActivated()); let badgeContent = breakpoint.lineNumber.toString(); if (breakpoint.column) { badgeContent += `:${breakpoint.column}`; @@ -558,7 +909,6 @@ class BreakpointsRenderer implements IListRenderer { + if (data.context !== breakpoint) { + reference.dispose(); + return; + } + data.elementDisposables.add(reference); + const model = reference.object.textEditorModel; + if (model && breakpoint.lineNumber <= model.getLineCount()) { + const lineContent = model.getLineContent(breakpoint.lineNumber).trim(); + data.name.textContent = lineContent || localize('emptyLine', "(empty line)"); + } else { + data.name.textContent = localize('lineNotFound', "(line not found)"); + } + }).catch(() => { + if (data.context === breakpoint) { + data.name.textContent = localize('cannotLoadLine', "(cannot load line)"); + } + }); + } + + disposeElement(node: ITreeNode, index: number, template: IBreakpointTemplateData): void { + template.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, template: IBreakpointTemplateData): void { template.elementDisposables.clear(); } @@ -589,7 +972,7 @@ class BreakpointsRenderer implements IListRenderer { +class ExceptionBreakpointsRenderer implements ICompressibleTreeRenderer { constructor( private menu: IMenu, @@ -616,11 +999,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.condition = dom.append(data.breakpoint, $('span.condition')); @@ -634,7 +1017,17 @@ class ExceptionBreakpointsRenderer implements IListRenderer, index: number, data: IExceptionBreakpointTemplateData): void { + const exceptionBreakpoint = node.element; + this.renderExceptionBreakpoint(exceptionBreakpoint, data); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, data: IExceptionBreakpointTemplateData): void { + const exceptionBreakpoint = node.element.elements[node.element.elements.length - 1]; + this.renderExceptionBreakpoint(exceptionBreakpoint, data); + } + + private renderExceptionBreakpoint(exceptionBreakpoint: IExceptionBreakpoint, data: IExceptionBreakpointTemplateData): void { data.context = exceptionBreakpoint; data.name.textContent = exceptionBreakpoint.label || `${exceptionBreakpoint.filter} exceptions`; const exceptionBreakpointtitle = exceptionBreakpoint.verified ? (exceptionBreakpoint.description || data.name.textContent) : exceptionBreakpoint.message || localize('unverifiedExceptionBreakpoint', "Unverified Exception Breakpoint"); @@ -660,7 +1053,11 @@ class ExceptionBreakpointsRenderer implements IListRenderer, index: number, templateData: IExceptionBreakpointTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IExceptionBreakpointTemplateData): void { templateData.elementDisposables.clear(); } @@ -669,7 +1066,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { +class FunctionBreakpointsRenderer implements ICompressibleTreeRenderer { constructor( private menu: IMenu, @@ -697,12 +1094,12 @@ class FunctionBreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.condition = dom.append(data.breakpoint, $('span.condition')); @@ -715,7 +1112,15 @@ class FunctionBreakpointsRenderer implements IListRenderer, _index: number, data: IFunctionBreakpointTemplateData): void { + this.renderFunctionBreakpoint(node.element, data); + } + + renderCompressedElements(node: ITreeNode, void>, _index: number, data: IFunctionBreakpointTemplateData): void { + this.renderFunctionBreakpoint(node.element.elements[node.element.elements.length - 1], data); + } + + private renderFunctionBreakpoint(functionBreakpoint: FunctionBreakpoint, data: IFunctionBreakpointTemplateData): void { data.context = functionBreakpoint; data.name.textContent = functionBreakpoint.name; const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), functionBreakpoint, this.labelService, this.debugService.getModel()); @@ -751,7 +1156,11 @@ class FunctionBreakpointsRenderer implements IListRenderer, index: number, templateData: IFunctionBreakpointTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IFunctionBreakpointTemplateData): void { templateData.elementDisposables.clear(); } @@ -760,7 +1169,7 @@ class FunctionBreakpointsRenderer implements IListRenderer { +class DataBreakpointsRenderer implements ICompressibleTreeRenderer { constructor( private menu: IMenu, @@ -790,12 +1199,12 @@ class DataBreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); data.accessType = dom.append(data.breakpoint, $('span.access-type')); @@ -809,7 +1218,15 @@ class DataBreakpointsRenderer implements IListRenderer, _index: number, data: IDataBreakpointTemplateData): void { + this.renderDataBreakpoint(node.element, data); + } + + renderCompressedElements(node: ITreeNode, void>, _index: number, data: IDataBreakpointTemplateData): void { + this.renderDataBreakpoint(node.element.elements[node.element.elements.length - 1], data); + } + + private renderDataBreakpoint(dataBreakpoint: DataBreakpoint, data: IDataBreakpointTemplateData): void { data.context = dataBreakpoint; data.name.textContent = dataBreakpoint.description; const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), dataBreakpoint, this.labelService, this.debugService.getModel()); @@ -854,7 +1271,11 @@ class DataBreakpointsRenderer implements IListRenderer, index: number, templateData: IDataBreakpointTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IDataBreakpointTemplateData): void { templateData.elementDisposables.clear(); } @@ -863,7 +1284,7 @@ class DataBreakpointsRenderer implements IListRenderer { +class InstructionBreakpointsRenderer implements ICompressibleTreeRenderer { constructor( @IDebugService private readonly debugService: IDebugService, @@ -888,12 +1309,12 @@ class InstructionBreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(data.checkbox.onChange(() => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); dom.append(data.breakpoint, data.icon); - dom.append(data.breakpoint, data.checkbox); + dom.append(data.breakpoint, data.checkbox.domNode); data.name = dom.append(data.breakpoint, $('span.name')); @@ -906,7 +1327,15 @@ class InstructionBreakpointsRenderer implements IListRenderer, index: number, data: IInstructionBreakpointTemplateData): void { + this.renderInstructionBreakpoint(node.element, data); + } + + renderCompressedElements(node: ITreeNode, void>, index: number, data: IInstructionBreakpointTemplateData): void { + this.renderInstructionBreakpoint(node.element.elements[node.element.elements.length - 1], data); + } + + private renderInstructionBreakpoint(breakpoint: IInstructionBreakpoint, data: IInstructionBreakpointTemplateData): void { data.context = breakpoint; data.breakpoint.classList.toggle('disabled', !this.debugService.getModel().areBreakpointsActivated()); @@ -931,8 +1360,11 @@ class InstructionBreakpointsRenderer implements IListRenderer, index: number, templateData: IInstructionBreakpointTemplateData): void { + templateData.elementDisposables.clear(); + } - disposeElement(element: IInstructionBreakpoint, index: number, templateData: IInstructionBreakpointTemplateData): void { + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IInstructionBreakpointTemplateData): void { templateData.elementDisposables.clear(); } @@ -941,7 +1373,7 @@ class InstructionBreakpointsRenderer implements IListRenderer { +class FunctionBreakpointInputRenderer implements ICompressibleTreeRenderer { constructor( private view: BreakpointsView, @@ -966,7 +1398,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer, _index: number, data: IFunctionBreakpointInputTemplateData): void { + const functionBreakpoint = node.element; data.breakpoint = functionBreakpoint; data.type = this.view.inputBoxData?.type || 'name'; // If there is no type set take the 'name' as the default const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), functionBreakpoint, this.labelService, this.debugService.getModel()); @@ -1033,7 +1466,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer, void>, _index: number, data: IFunctionBreakpointInputTemplateData): void { + // Function breakpoints are not compressible + } + + disposeElement(node: ITreeNode, index: number, templateData: IFunctionBreakpointInputTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IFunctionBreakpointInputTemplateData): void { templateData.elementDisposables.clear(); } @@ -1065,7 +1506,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { +class DataBreakpointInputRenderer implements ICompressibleTreeRenderer { constructor( private view: BreakpointsView, @@ -1090,7 +1531,7 @@ class DataBreakpointInputRenderer implements IListRenderer, _index: number, data: IDataBreakpointInputTemplateData): void { + const dataBreakpoint = node.element; data.breakpoint = dataBreakpoint; data.type = this.view.inputBoxData?.type || 'condition'; // If there is no type set take the 'condition' as the default const { icon, message } = getBreakpointMessageAndIcon(this.debugService.state, this.debugService.getModel().areBreakpointsActivated(), dataBreakpoint, this.labelService, this.debugService.getModel()); @@ -1149,7 +1591,7 @@ class DataBreakpointInputRenderer implements IListRenderer, void>, _index: number, data: IDataBreakpointInputTemplateData): void { + // Data breakpoints are not compressible + } + + disposeElement(node: ITreeNode, index: number, templateData: IDataBreakpointInputTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IDataBreakpointInputTemplateData): void { templateData.elementDisposables.clear(); } @@ -1180,7 +1630,7 @@ class DataBreakpointInputRenderer implements IListRenderer { +class ExceptionBreakpointInputRenderer implements ICompressibleTreeRenderer { constructor( private view: BreakpointsView, @@ -1203,7 +1653,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer, _index: number, data: IExceptionBreakpointInputTemplateData): void { + const exceptionBreakpoint = node.element; const placeHolder = exceptionBreakpoint.conditionDescription || localize('exceptionBreakpointPlaceholder', "Break when expression evaluates to true"); data.inputBox.setPlaceHolder(placeHolder); data.currentBreakpoint = exceptionBreakpoint; data.checkbox.checked = exceptionBreakpoint.enabled; - data.checkbox.disabled = true; + data.checkbox.disable(); data.inputBox.value = exceptionBreakpoint.condition || ''; setTimeout(() => { data.inputBox.focus(); @@ -1268,7 +1719,15 @@ class ExceptionBreakpointInputRenderer implements IListRenderer, void>, _index: number, data: IExceptionBreakpointInputTemplateData): void { + // Exception breakpoints are not compressible + } + + disposeElement(node: ITreeNode, index: number, templateData: IExceptionBreakpointInputTemplateData): void { + templateData.elementDisposables.clear(); + } + + disposeCompressedElements(node: ITreeNode, void>, index: number, templateData: IExceptionBreakpointInputTemplateData): void { templateData.elementDisposables.clear(); } @@ -1277,7 +1736,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { +class BreakpointsAccessibilityProvider implements IListAccessibilityProvider { constructor( private readonly debugService: IDebugService, @@ -1292,11 +1751,18 @@ class BreakpointsAccessibilityProvider implements IListAccessibilityProvider { - return ('message' in breakpoint && breakpoint.message) ? text.concat(', ' + breakpoint.message) : text; + return breakpoint.message ? text.concat(', ' + breakpoint.message) : text; }; if (debugActive && breakpoint instanceof Breakpoint && breakpoint.pending) { @@ -1362,7 +1828,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: if (debugActive && !breakpoint.verified) { return { icon: breakpointIcon.unverified, - message: ('message' in breakpoint && breakpoint.message) ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")), + message: breakpoint.message ? breakpoint.message : (breakpoint.logMessage ? localize('unverifiedLogpoint', "Unverified Logpoint") : localize('unverifiedBreakpoint', "Unverified Breakpoint")), showAdapterUnverifiedMessage: true }; } @@ -1462,7 +1928,7 @@ export function getBreakpointMessageAndIcon(state: State, breakpointsActivated: }; } - const message = ('message' in breakpoint && breakpoint.message) ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint"); + const message = breakpoint.message ? breakpoint.message : breakpoint instanceof Breakpoint && labelService ? labelService.getUriLabel(breakpoint.uri) : localize('breakpoint', "Breakpoint"); return { icon: breakpointIcon.regular, message @@ -1574,7 +2040,7 @@ abstract class MemoryBreakpointAction extends Action2 { })); disposables.add(input.onDidAccept(() => { const r = this.parseAddress(input.value, true); - if ('error' in r) { + if (hasKey(r, { error: true })) { input.validationMessage = r.error; } else { resolve(r); @@ -1615,7 +2081,10 @@ abstract class MemoryBreakpointAction extends Action2 { const end = BigInt(endStr); const address = `0x${start.toString(16)}`; if (sign === '-') { - return { address, bytes: Number(start - end) }; + if (start > end) { + return { error: localize('dataBreakpointAddrOrder', 'End ({1}) should be greater than Start ({0})', startStr, endStr) }; + } + return { address, bytes: Number(end - start) }; } return { address, bytes: Number(end) }; @@ -1838,6 +2307,30 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.viewlet.action.toggleBreakpointsPresentation', + title: localize2('toggleBreakpointsPresentation', "Toggle Breakpoints View Presentation"), + f1: true, + icon: icons.breakpointsViewIcon, + menu: { + id: MenuId.ViewTitle, + group: 'navigation', + order: 10, + when: ContextKeyExpr.equals('view', BREAKPOINTS_VIEW_ID) + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const currentPresentation = configurationService.getValue<'list' | 'tree'>('debug.breakpointsView.presentation'); + const newPresentation = currentPresentation === 'tree' ? 'list' : 'tree'; + await configurationService.updateValue('debug.breakpointsView.presentation', newPresentation); + } +}); + registerAction2(class extends ViewAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts index ff61c059012..42e4cbebafe 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackWidget.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackWidget.ts @@ -167,8 +167,9 @@ export class CallStackWidget extends Disposable { public setFrames(frames: AnyStackFrame[]): void { // cancel any existing load this.currentFramesDs.clear(); - this.cts = new CancellationTokenSource(); - this._register(toDisposable(() => this.cts!.dispose(true))); + const cts = new CancellationTokenSource(); + this.currentFramesDs.add(toDisposable(() => cts.dispose(true))); + this.cts = cts; this.list.splice(0, this.list.length, this.mapFrames(frames)); } diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 5967e7595e2..7b2cbce78b2 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -65,6 +65,7 @@ import { StatusBarColorProvider } from './statusbarColorProvider.js'; import { SET_VARIABLE_ID, VIEW_MEMORY_ID, VariablesView } from './variablesView.js'; import { ADD_WATCH_ID, ADD_WATCH_LABEL, REMOVE_WATCH_EXPRESSIONS_COMMAND_ID, REMOVE_WATCH_EXPRESSIONS_LABEL, WatchExpressionsView } from './watchExpressionsView.js'; import { WelcomeView } from './welcomeView.js'; +import { DebugChatContextContribution } from './debugChatIntegration.js'; const debugCategory = nls.localize('debugCategory', "Debug"); registerColors(); @@ -82,6 +83,7 @@ Registry.as(WorkbenchExtensions.Workbench).regi Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(StatusBarColorProvider, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DisassemblyViewContribution, LifecyclePhase.Eventually); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugLifecycle, LifecyclePhase.Eventually); +registerWorkbenchContribution2(DebugChatContextContribution.ID, DebugChatContextContribution, WorkbenchPhase.AfterRestored); // Register Quick Access Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ @@ -543,8 +545,8 @@ configurationRegistry.registerConfiguration({ }, 'debug.showInStatusBar': { enum: ['never', 'always', 'onFirstSessionStart'], - enumDescriptions: [nls.localize('never', "Never show debug in Status bar"), nls.localize('always', "Always show debug in Status bar"), nls.localize('onFirstSessionStart', "Show debug in Status bar only after debug was started for the first time")], - description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug Status bar should be visible."), + enumDescriptions: [nls.localize('never', "Never show debug item in status bar"), nls.localize('always', "Always show debug item in status bar"), nls.localize('onFirstSessionStart', "Show debug item in status bar only after debug was started for the first time")], + description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInStatusBar' }, "Controls when the debug status bar item should be visible."), default: 'onFirstSessionStart' }, 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, @@ -636,6 +638,12 @@ configurationRegistry.registerConfiguration({ description: nls.localize({ comment: ['This is the description for a setting'], key: 'showBreakpointsInOverviewRuler' }, "Controls whether breakpoints should be shown in the overview ruler."), default: false }, + 'debug.breakpointsView.presentation': { + type: 'string', + description: nls.localize('debug.breakpointsView.presentation', "Controls whether breakpoints are displayed in a tree view grouped by file, or as a flat list."), + enum: ['tree', 'list'], + default: 'list' + }, 'debug.showInlineBreakpointCandidates': { type: 'boolean', description: nls.localize({ comment: ['This is the description for a setting'], key: 'showInlineBreakpointCandidates' }, "Controls whether inline breakpoints candidate decorations should be shown in the editor while debugging."), @@ -680,7 +688,7 @@ configurationRegistry.registerConfiguration({ }, 'debug.enableStatusBarColor': { type: 'boolean', - description: nls.localize('debug.enableStatusBarColor', "Color of the Status bar when debugger is active."), + description: nls.localize('debug.enableStatusBarColor', "Color of the status bar when the debugger is active."), default: true }, 'debug.hideLauncherWhileDebugging': { diff --git a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts index 4db2d7eb8c7..85c117bc202 100644 --- a/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts +++ b/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts @@ -11,13 +11,13 @@ import { registerThemingParticipant } from '../../../../platform/theme/common/th import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from '../../../common/theme.js'; import { ansiColorIdentifiers } from '../../terminal/common/terminalColorRegistry.js'; -import { ILinkDetector } from './linkDetector.js'; +import { DebugLinkHoverBehaviorTypeData, ILinkDetector } from './linkDetector.js'; /** * @param text The content to stylize. * @returns An {@link HTMLSpanElement} that contains the potentially stylized text. */ -export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, highlights: IHighlight[] | undefined): HTMLSpanElement { +export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, highlights: IHighlight[] | undefined, hoverBehavior: DebugLinkHoverBehaviorTypeData): HTMLSpanElement { const root: HTMLSpanElement = document.createElement('span'); const textLength: number = text.length; @@ -63,8 +63,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work unprintedChars += 2 + ansiSequence.length; // Flush buffer with previous styles. - appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length - unprintedChars); - + appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length - unprintedChars, hoverBehavior); buffer = ''; /* @@ -109,7 +108,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work // Flush remaining text buffer if not empty. if (buffer) { - appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length); + appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length, hoverBehavior); } return root; @@ -401,6 +400,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work * @param customUnderlineColor If provided, will apply custom textDecorationColor with inline style. * @param highlights The ranges to highlight. * @param offset The starting index of the stringContent in the original text. + * @param hoverBehavior hover behavior with disposable store for managing event listeners. */ export function appendStylizedStringToContainer( root: HTMLElement, @@ -413,6 +413,7 @@ export function appendStylizedStringToContainer( customUnderlineColor: RGBA | string | undefined, highlights: IHighlight[] | undefined, offset: number, + hoverBehavior: DebugLinkHoverBehaviorTypeData, ): void { if (!root || !stringContent) { return; @@ -420,10 +421,10 @@ export function appendStylizedStringToContainer( const container = linkDetector.linkify( stringContent, + hoverBehavior, true, workspaceFolder, undefined, - undefined, highlights?.map(h => ({ start: h.start - offset, end: h.end - offset, extraClasses: h.extraClasses })), ); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index 504b14eabd5..522fbd58a71 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -8,7 +8,7 @@ import { IAction } from '../../../../base/common/actions.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import * as dom from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { SelectBox, ISelectOptionItem } from '../../../../base/browser/ui/selectBox/selectBox.js'; +import { SelectBox, ISelectOptionItem, SeparatorSelectOption } from '../../../../base/browser/ui/selectBox/selectBox.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IDebugService, IDebugSession, IDebugConfiguration, IConfig, ILaunch, State } from '../common/debug.js'; @@ -34,8 +34,6 @@ const $ = dom.$; export class StartDebugActionViewItem extends BaseActionViewItem { - private static readonly SEPARATOR = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; - private container!: HTMLElement; private start!: HTMLElement; private selectBox: SelectBox; @@ -81,9 +79,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { this.container = container; container.classList.add('start-debug-action-item'); this.start = dom.append(container, $(ThemeIcon.asCSSSelector(debugStart))); - const keybinding = this.keybindingService.lookupKeybinding(this.action.id)?.getLabel(); - const keybindingLabel = keybinding ? ` (${keybinding})` : ''; - const title = this.action.label + keybindingLabel; + const title = this.keybindingService.appendKeybinding(this.action.label, this.action.id); this.toDispose.push(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), this.start, title)); this.start.setAttribute('role', 'button'); this._setAriaLabel(title); @@ -206,7 +202,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { if (lastGroup !== presentation?.group) { lastGroup = presentation?.group; if (this.debugOptions.length) { - this.debugOptions.push({ label: StartDebugActionViewItem.SEPARATOR, handler: () => Promise.resolve(false) }); + this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) }); disabledIdxs.push(this.debugOptions.length - 1); } } @@ -241,7 +237,7 @@ export class StartDebugActionViewItem extends BaseActionViewItem { this.debugOptions.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: async () => false }); } - this.debugOptions.push({ label: StartDebugActionViewItem.SEPARATOR, handler: () => Promise.resolve(false) }); + this.debugOptions.push({ label: SeparatorSelectOption.text, handler: () => Promise.resolve(false) }); disabledIdxs.push(this.debugOptions.length - 1); this.providers.forEach(p => { diff --git a/src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts b/src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts new file mode 100644 index 00000000000..7594b98ba75 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugChatIntegration.ts @@ -0,0 +1,504 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, debouncedObservable, derived, IObservable, ISettableObservable, ObservablePromise, observableValue } from '../../../../base/common/observable.js'; +import { basename } from '../../../../base/common/resources.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { localize } from '../../../../nls.js'; +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IChatWidget, IChatWidgetService } from '../../chat/browser/chat.js'; +import { ChatContextPick, IChatContextPicker, IChatContextPickerItem, IChatContextPickService } from '../../chat/browser/attachments/chatContextPickService.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IChatRequestFileEntry, IChatRequestVariableEntry, IDebugVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; +import { IDebugService, IExpression, IScope, IStackFrame, State } from '../common/debug.js'; +import { Variable } from '../common/debugModel.js'; + +const enum PickerMode { + Main = 'main', + Expression = 'expression', +} + +class DebugSessionContextPick implements IChatContextPickerItem { + readonly type = 'pickerPick'; + readonly label = localize('chatContext.debugSession', 'Debug Session...'); + readonly icon = Codicon.debug; + readonly ordinal = -200; + + constructor( + @IDebugService private readonly debugService: IDebugService, + ) { } + + isEnabled(): boolean { + // Only enabled when there's a focused session that is stopped (paused) + const viewModel = this.debugService.getViewModel(); + const focusedSession = viewModel.focusedSession; + return !!focusedSession && focusedSession.state === State.Stopped; + } + + asPicker(_widget: IChatWidget): IChatContextPicker { + const store = new DisposableStore(); + const mode: ISettableObservable = observableValue('debugPicker.mode', PickerMode.Main); + const query: ISettableObservable = observableValue('debugPicker.query', ''); + + const picksObservable = this.createPicksObservable(mode, query, store); + + return { + placeholder: localize('selectDebugData', 'Select debug data to attach'), + picks: (_queryObs: IObservable, token: CancellationToken) => { + // Connect the external query observable to our internal one + store.add(autorun(reader => { + query.set(_queryObs.read(reader), undefined); + })); + + const cts = new CancellationTokenSource(token); + store.add(toDisposable(() => cts.dispose(true))); + + return picksObservable; + }, + goBack: () => { + if (mode.get() === PickerMode.Expression) { + mode.set(PickerMode.Main, undefined); + return true; // Stay in picker + } + return false; // Go back to main context menu + }, + dispose: () => store.dispose(), + }; + } + + private createPicksObservable( + mode: ISettableObservable, + query: IObservable, + store: DisposableStore + ): IObservable<{ busy: boolean; picks: ChatContextPick[] }> { + const debouncedQuery = debouncedObservable(query, 300); + + return derived(reader => { + const currentMode = mode.read(reader); + + if (currentMode === PickerMode.Expression) { + return this.getExpressionPicks(debouncedQuery, store); + } else { + return this.getMainPicks(mode); + } + }).flatten(); + } + + private getMainPicks(mode: ISettableObservable): IObservable<{ busy: boolean; picks: ChatContextPick[] }> { + // Return an observable that resolves to the main picks + const promise = derived(_reader => { + return new ObservablePromise(this.buildMainPicks(mode)); + }); + + return promise.map((value, reader) => { + const result = value.promiseResult.read(reader); + return { picks: result?.data || [], busy: result === undefined }; + }); + } + + private async buildMainPicks(mode: ISettableObservable): Promise { + const picks: ChatContextPick[] = []; + const viewModel = this.debugService.getViewModel(); + const stackFrame = viewModel.focusedStackFrame; + const session = viewModel.focusedSession; + + if (!session || !stackFrame) { + return picks; + } + + // Add "Expression Value..." option at the top + picks.push({ + label: localize('expressionValue', 'Expression Value...'), + iconClass: ThemeIcon.asClassName(Codicon.symbolVariable), + asAttachment: () => { + // Switch to expression mode + mode.set(PickerMode.Expression, undefined); + return 'noop'; + }, + }); + + // Add watch expressions section + const watches = this.debugService.getModel().getWatchExpressions(); + if (watches.length > 0) { + picks.push({ type: 'separator', label: localize('watchExpressions', 'Watch Expressions') }); + for (const watch of watches) { + picks.push({ + label: watch.name, + description: watch.value, + iconClass: ThemeIcon.asClassName(Codicon.eye), + asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(watch)), + }); + } + } + + // Add scopes and their variables + let scopes: IScope[] = []; + try { + scopes = await stackFrame.getScopes(); + } catch { + // Ignore errors when fetching scopes + } + + for (const scope of scopes) { + // Include variables from non-expensive scopes + if (scope.expensive && !scope.childrenHaveBeenLoaded) { + continue; + } + + picks.push({ type: 'separator', label: scope.name }); + try { + const variables = await scope.getChildren(); + if (variables.length > 1) { + picks.push({ + label: localize('allVariablesInScope', 'All variables in {0}', scope.name), + iconClass: ThemeIcon.asClassName(Codicon.symbolNamespace), + asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createScopeEntry(scope, variables)), + }); + } + for (const variable of variables) { + picks.push({ + label: variable.name, + description: formatVariableDescription(variable), + iconClass: ThemeIcon.asClassName(Codicon.symbolVariable), + asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, createDebugVariableEntry(variable)), + }); + } + } catch { + // Ignore errors when fetching variables + } + } + + return picks; + } + + private getExpressionPicks( + query: IObservable, + _store: DisposableStore + ): IObservable<{ busy: boolean; picks: ChatContextPick[] }> { + const promise = derived((reader) => { + const queryValue = query.read(reader); + const cts = new CancellationTokenSource(); + reader.store.add(toDisposable(() => cts.dispose(true))); + return new ObservablePromise(this.evaluateExpression(queryValue, cts.token)); + }); + + return promise.map((value, r) => { + const result = value.promiseResult.read(r); + return { picks: result?.data || [], busy: result === undefined }; + }); + } + + private async evaluateExpression(expression: string, token: CancellationToken): Promise { + if (!expression.trim()) { + return [{ + label: localize('typeExpression', 'Type an expression to evaluate...'), + disabled: true, + asAttachment: () => 'noop', + }]; + } + + const viewModel = this.debugService.getViewModel(); + const session = viewModel.focusedSession; + const stackFrame = viewModel.focusedStackFrame; + + if (!session || !stackFrame) { + return [{ + label: localize('noDebugSession', 'No active debug session'), + disabled: true, + asAttachment: () => 'noop', + }]; + } + + try { + const response = await session.evaluate(expression, stackFrame.frameId, 'watch'); + + if (token.isCancellationRequested) { + return []; + } + + if (response?.body) { + const resultValue = response.body.result; + const resultType = response.body.type; + return [{ + label: expression, + description: formatExpressionResult(resultValue, resultType), + iconClass: ThemeIcon.asClassName(Codicon.symbolVariable), + asAttachment: (): IChatRequestVariableEntry[] => createDebugAttachments(stackFrame, { + kind: 'debugVariable', + id: `debug-expression:${expression}`, + name: expression, + fullName: expression, + icon: Codicon.debug, + value: resultValue, + expression: expression, + type: resultType, + modelDescription: formatModelDescription(expression, resultValue, resultType), + }), + }]; + } else { + return [{ + label: expression, + description: localize('noResult', 'No result'), + disabled: true, + asAttachment: () => 'noop', + }]; + } + } catch (err) { + return [{ + label: expression, + description: err instanceof Error ? err.message : localize('evaluationError', 'Evaluation error'), + disabled: true, + asAttachment: () => 'noop', + }]; + } + } +} + +function createDebugVariableEntry(expression: IExpression): IDebugVariableEntry { + return { + kind: 'debugVariable', + id: `debug-variable:${expression.getId()}`, + name: expression.name, + fullName: expression.name, + icon: Codicon.debug, + value: expression.value, + expression: expression.name, + type: expression.type, + modelDescription: formatModelDescription(expression.name, expression.value, expression.type), + }; +} + +function createPausedLocationEntry(stackFrame: IStackFrame): IChatRequestFileEntry { + const uri = stackFrame.source.uri; + let range = Range.lift(stackFrame.range); + if (range.isEmpty()) { + range = range.setEndPosition(range.startLineNumber + 1, 1); + } + + return { + kind: 'file', + value: { uri, range }, + id: `debug-paused-location:${uri.toString()}:${range.startLineNumber}`, + name: basename(uri), + modelDescription: 'The debugger is currently paused at this location', + }; +} + +function createDebugAttachments(stackFrame: IStackFrame, variableEntry: IDebugVariableEntry): IChatRequestVariableEntry[] { + return [ + createPausedLocationEntry(stackFrame), + variableEntry, + ]; +} + +function createScopeEntry(scope: IScope, variables: IExpression[]): IDebugVariableEntry { + const variablesSummary = variables.map(v => `${v.name}: ${v.value}`).join('\n'); + return { + kind: 'debugVariable', + id: `debug-scope:${scope.name}`, + name: `Scope: ${scope.name}`, + fullName: `Scope: ${scope.name}`, + icon: Codicon.debug, + value: variablesSummary, + expression: scope.name, + type: 'scope', + modelDescription: `Debug scope "${scope.name}" with ${variables.length} variables:\n${variablesSummary}`, + }; +} + +function formatVariableDescription(expression: IExpression): string { + const value = expression.value; + const type = expression.type; + if (type && value) { + return `${type}: ${value}`; + } + return value || type || ''; +} + +function formatExpressionResult(value: string, type?: string): string { + if (type && value) { + return `${type}: ${value}`; + } + return value || type || ''; +} + +function formatModelDescription(name: string, value: string, type?: string): string { + let description = `Debug variable "${name}"`; + if (type) { + description += ` of type ${type}`; + } + description += ` with value: ${value}`; + return description; +} + +export class DebugChatContextContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'workbench.contrib.chat.debugChatContextContribution'; + + constructor( + @IChatContextPickService contextPickService: IChatContextPickService, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._register(contextPickService.registerChatContextItem(instantiationService.createInstance(DebugSessionContextPick))); + } +} + +// Context menu action: Add variable to chat +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.action.addVariableToChat', + title: localize('addToChat', 'Add to Chat'), + f1: false, + menu: { + id: MenuId.DebugVariablesContext, + group: 'z_commands', + order: 110, + when: ChatContextKeys.enabled + } + }); + } + + override async run(accessor: ServicesAccessor, context: unknown): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const debugService = accessor.get(IDebugService); + const widget = await chatWidgetService.revealWidget(); + if (!widget) { + return; + } + + // Context is the variable from the variables view + const entry = createDebugVariableEntryFromContext(context); + if (entry) { + const stackFrame = debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame)); + } + widget.attachmentModel.addContext(entry); + } + } +}); + +// Context menu action: Add watch expression to chat +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.action.addWatchExpressionToChat', + title: localize('addToChat', 'Add to Chat'), + f1: false, + menu: { + id: MenuId.DebugWatchContext, + group: 'z_commands', + order: 110, + when: ChatContextKeys.enabled + } + }); + } + + override async run(accessor: ServicesAccessor, context: IExpression): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const debugService = accessor.get(IDebugService); + const widget = await chatWidgetService.revealWidget(); + if (!context || !widget) { + return; + } + + // Context is the expression (watch expression or variable under it) + const stackFrame = debugService.getViewModel().focusedStackFrame; + if (stackFrame) { + widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame)); + } + widget.attachmentModel.addContext(createDebugVariableEntry(context)); + } +}); + +// Context menu action: Add scope to chat +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.debug.action.addScopeToChat', + title: localize('addToChat', 'Add to Chat'), + f1: false, + menu: { + id: MenuId.DebugScopesContext, + group: 'z_commands', + order: 1, + when: ChatContextKeys.enabled + } + }); + } + + override async run(accessor: ServicesAccessor, context: IScopesContext): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const debugService = accessor.get(IDebugService); + const widget = await chatWidgetService.revealWidget(); + if (!context || !widget) { + return; + } + + // Get the actual scope and its variables + const viewModel = debugService.getViewModel(); + const stackFrame = viewModel.focusedStackFrame; + if (!stackFrame) { + return; + } + + try { + const scopes = await stackFrame.getScopes(); + const scope = scopes.find(s => s.name === context.scope.name); + if (scope) { + const variables = await scope.getChildren(); + widget.attachmentModel.addContext(createPausedLocationEntry(stackFrame)); + widget.attachmentModel.addContext(createScopeEntry(scope, variables)); + } + } catch { + // Ignore errors + } + } +}); + +interface IScopesContext { + scope: { name: string }; +} + +interface IVariablesContext { + sessionId: string | undefined; + variable: { name: string; value: string; type?: string; evaluateName?: string }; +} + +function isVariablesContext(context: unknown): context is IVariablesContext { + return typeof context === 'object' && context !== null && 'variable' in context && 'sessionId' in context; +} + +function createDebugVariableEntryFromContext(context: unknown): IDebugVariableEntry | undefined { + // The context can be either a Variable directly, or an IVariablesContext object + if (context instanceof Variable) { + return createDebugVariableEntry(context); + } + + // Handle IVariablesContext format from the variables view + if (isVariablesContext(context)) { + const variable = context.variable; + return { + kind: 'debugVariable', + id: `debug-variable:${variable.name}`, + name: variable.name, + fullName: variable.evaluateName ?? variable.name, + icon: Codicon.debug, + value: variable.value, + expression: variable.evaluateName ?? variable.name, + type: variable.type, + modelDescription: formatModelDescription(variable.evaluateName || variable.name, variable.value, variable.type), + }; + } + + return undefined; +} diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index a03c0e5dc52..18210209161 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -33,7 +33,7 @@ import { ViewContainerLocation } from '../../../common/views.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; import { CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_DEBUG_STATE, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_EXPRESSION_SELECTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_IN_DEBUG_MODE, CONTEXT_IN_DEBUG_REPL, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_VARIABLES_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, DataBreakpointSetType, EDITOR_CONTRIBUTION_ID, getStateLabel, IConfig, IDataBreakpointInfoResponse, IDebugConfiguration, IDebugEditorContribution, IDebugService, IDebugSession, IEnablement, IExceptionBreakpoint, isFrameDeemphasized, IStackFrame, IThread, REPL_VIEW_ID, State, VIEWLET_ID } from '../common/debug.js'; diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index e196fe59530..3e67ae929f4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -23,7 +23,7 @@ import { ServicesAccessor } from '../../../../platform/instantiation/common/inst import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { PanelFocusContext } from '../../../common/contextkeys.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { openBreakpointSource } from './breakpointsView.js'; import { DisassemblyView, IDisassembledInstructionEntry } from './disassemblyView.js'; import { Repl } from './repl.js'; @@ -39,9 +39,10 @@ class ToggleBreakpointAction extends Action2 { super({ id: TOGGLE_BREAKPOINT_ID, title: { - ...nls.localize2('toggleBreakpointAction', "Debug: Toggle Breakpoint"), + ...nls.localize2('toggleBreakpointAction', "Toggle Breakpoint"), mnemonicTitle: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint"), }, + category: nls.localize2('debugCategory', "Debug"), f1: true, precondition: CONTEXT_DEBUGGERS_AVAILABLE, keybinding: { @@ -49,12 +50,12 @@ class ToggleBreakpointAction extends Action2 { primary: KeyCode.F9, weight: KeybindingWeight.EditorContrib }, - menu: { + menu: [{ id: MenuId.MenubarDebugMenu, when: CONTEXT_DEBUGGERS_AVAILABLE, group: '4_new_breakpoint', order: 1 - } + }] }); } diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts index 9ce76504992..008d4b876b4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts @@ -429,18 +429,18 @@ export class DebugEditorContribution implements IDebugEditorContribution { } private preventDefaultEditorHover() { - if (this.defaultHoverLockout.value || this.editorHoverOptions?.enabled === false) { + if (this.defaultHoverLockout.value || this.editorHoverOptions?.enabled === 'off') { return; } const hoverController = this.editor.getContribution(ContentHoverController.ID); hoverController?.hideContentHover(); - this.editor.updateOptions({ hover: { enabled: false } }); + this.editor.updateOptions({ hover: { enabled: 'off' } }); this.defaultHoverLockout.value = { dispose: () => { this.editor.updateOptions({ - hover: { enabled: this.editorHoverOptions?.enabled ?? true } + hover: { enabled: this.editorHoverOptions?.enabled ?? 'on' } }); } }; diff --git a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts index 0835365d408..3009f58a2cb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts +++ b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts @@ -177,7 +177,7 @@ export class DebugExpressionRenderer { const session = options.session ?? ((expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined); // Only use hovers for links if thre's not going to be a hover for the value. - const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None }; + const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None, store }; dom.clearNode(container); const locationReference = options.locationReference ?? (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference); @@ -187,9 +187,9 @@ export class DebugExpressionRenderer { } if (supportsANSI) { - container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined, options.highlights)); + container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined, options.highlights, hoverBehavior)); } else { - container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior, options.highlights)); + container.appendChild(linkDetector.linkify(value, hoverBehavior, false, session?.root, true, options.highlights)); } if (options.hover !== false) { @@ -202,7 +202,7 @@ export class DebugExpressionRenderer { if (supportsANSI) { // note: intentionally using `this.linkDetector` so we don't blindly linkify the // entire contents and instead only link file paths that it contains. - hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined, options.highlights)); + hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined, options.highlights, hoverBehavior)); } else { hoverContentsPre.textContent = value; } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index e76162ba639..5705179af39 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -1127,9 +1127,13 @@ export class DebugService implements IDebugService { } } - async removeBreakpoints(id?: string): Promise { + async removeBreakpoints(id?: string | string[]): Promise { const breakpoints = this.model.getBreakpoints(); - const toRemove = breakpoints.filter(bp => !id || bp.getId() === id); + const toRemove = id === undefined + ? breakpoints + : id instanceof Array + ? breakpoints.filter(bp => id.includes(bp.getId())) + : breakpoints.filter(bp => bp.getId() === id); // note: using the debugger-resolved uri for aria to reflect UI state toRemove.forEach(bp => aria.status(nls.localize('breakpointRemoved', "Removed breakpoint, line {0}, file {1}", bp.lineNumber, bp.uri.fsPath))); const urisToClear = new Set(toRemove.map(bp => bp.originalUri.toString())); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 34cf1145a7a..107ea3389ce 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -12,6 +12,7 @@ import { CancellationToken, CancellationTokenSource } from '../../../../base/com import { canceled } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { normalizeDriveLetter } from '../../../../base/common/labels.js'; +import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableMap, DisposableStore, MutableDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { mixin } from '../../../../base/common/objects.js'; import * as platform from '../../../../base/common/platform.js'; @@ -65,7 +66,13 @@ export class DebugSession implements IDebugSession { private cancellationMap = new Map(); private readonly rawListeners = new DisposableStore(); private readonly globalDisposables = new DisposableStore(); - private fetchThreadsScheduler: RunOnceScheduler | undefined; + private fetchThreadsScheduler = new Lazy(() => { + const inst = new RunOnceScheduler(() => { + this.fetchThreads(); + }, 100); + this.rawListeners.add(inst); + return inst; + }); private passFocusScheduler: RunOnceScheduler; private lastContinuedThreadId: number | undefined; private repl: ReplModel; @@ -1098,15 +1105,8 @@ export class DebugSession implements IDebugSession { this.rawListeners.add(this.raw.onDidThread(event => { statusQueue.cancel([event.body.threadId]); if (event.body.reason === 'started') { - // debounce to reduce threadsRequest frequency and improve performance - if (!this.fetchThreadsScheduler) { - this.fetchThreadsScheduler = new RunOnceScheduler(() => { - this.fetchThreads(); - }, 100); - this.rawListeners.add(this.fetchThreadsScheduler); - } - if (!this.fetchThreadsScheduler.isScheduled()) { - this.fetchThreadsScheduler.schedule(); + if (!this.fetchThreadsScheduler.value.isScheduled()) { + this.fetchThreadsScheduler.value.schedule(); } } else if (event.body.reason === 'exited') { this.model.clearThreads(this.getId(), true, event.body.threadId); @@ -1138,11 +1138,11 @@ export class DebugSession implements IDebugSession { if (this.threadIds.includes(event.body.threadId)) { affectedThreads = [event.body.threadId]; } else { - this.fetchThreadsScheduler?.cancel(); + this.fetchThreadsScheduler.rawValue?.cancel(); affectedThreads = this.fetchThreads().then(() => [event.body.threadId]); } - } else if (this.fetchThreadsScheduler?.isScheduled()) { - this.fetchThreadsScheduler.cancel(); + } else if (this.fetchThreadsScheduler.value.isScheduled()) { + this.fetchThreadsScheduler.value.cancel(); affectedThreads = this.fetchThreads().then(() => this.threadIds); } else { affectedThreads = this.threadIds; @@ -1330,9 +1330,15 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); this.model.clearThreads(this.getId(), true); - const details = this.stoppedDetails; - this.stoppedDetails.length = 1; - await Promise.all(details.map(d => this.handleStop(d))); + const details = this.stoppedDetails.slice(); + this.stoppedDetails.length = 0; + if (details.length) { + await Promise.all(details.map(d => this.handleStop(d))); + } else if (!this.fetchThreadsScheduler.value.isScheduled()) { + // threads are fetched as a side-effect of processing the stopped + // event(s), but if there are none, schedule a thread update manually (#282777) + this.fetchThreadsScheduler.value.schedule(); + } } const viewModel = this.debugService.getViewModel(); @@ -1489,11 +1495,13 @@ export class DebugSession implements IDebugSession { this.raw.dispose(); this.raw = undefined; } - this.fetchThreadsScheduler?.dispose(); - this.fetchThreadsScheduler = undefined; this.passFocusScheduler.cancel(); this.passFocusScheduler.dispose(); this.model.clearThreads(this.getId(), true); + this.sources.clear(); + this.threads.clear(); + this.threadIds = []; + this.stoppedDetails = []; this._onDidChangeState.fire(); } @@ -1501,6 +1509,7 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); this.rawListeners.dispose(); this.globalDisposables.dispose(); + this._waitToResume = undefined; } //---- sources diff --git a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts index 3fabe31ebfa..d24fa2363df 100644 --- a/src/vs/workbench/contrib/debug/browser/disassemblyView.ts +++ b/src/vs/workbench/contrib/debug/browser/disassemblyView.ts @@ -41,7 +41,7 @@ import * as icons from './debugIcons.js'; import { CONTEXT_LANGUAGE_SUPPORTS_DISASSEMBLE_REQUEST, DISASSEMBLY_VIEW_ID, IDebugConfiguration, IDebugService, IDebugSession, IInstructionBreakpoint, State } from '../common/debug.js'; import { InstructionBreakpoint } from '../common/debugModel.js'; import { getUriFromSource } from '../common/debugSource.js'; -import { isUri, sourcesEqual } from '../common/debugUtils.js'; +import { isUriString, sourcesEqual } from '../common/debugUtils.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -641,9 +641,23 @@ export class DisassemblyView extends EditorPane { this.clear(); this._instructionBpList = this._debugService.getModel().getInstructionBreakpoints(); this.loadDisassembledInstructions(instructionReference, offset, -DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 4, DisassemblyView.NUM_INSTRUCTIONS_TO_LOAD * 8).then(() => { - // on load, set the target instruction in the middle of the page. + // on load, set the target instruction as the current instructionReference. if (this._disassembledInstructions!.length > 0) { - const targetIndex = Math.floor(this._disassembledInstructions!.length / 2); + let targetIndex: number | undefined = undefined; + const refBaseAddress = this._referenceToMemoryAddress.get(instructionReference); + if (refBaseAddress !== undefined) { + const da = this._disassembledInstructions!; + targetIndex = binarySearch2(da.length, i => Number(da.row(i).address - refBaseAddress)); + if (targetIndex < 0) { + targetIndex = ~targetIndex; // shouldn't happen, but fail gracefully if it does + } + } + + // If didn't find the instructonReference, set the target instruction in the middle of the page. + if (targetIndex === undefined) { + targetIndex = Math.floor(this._disassembledInstructions!.length / 2); + } + this._disassembledInstructions!.reveal(targetIndex, 0.5); // Always focus the target address on reload, or arrow key navigation would look terrible @@ -956,7 +970,7 @@ class InstructionRenderer extends Disposable implements ITableRenderer { const contribution = this.editor.getContribution(EDITOR_CONTRIBUTION_ID); contribution?.closeExceptionWidget(); @@ -100,7 +101,11 @@ export class ExceptionWidget extends ZoneWidget { if (this.exceptionInfo.details && this.exceptionInfo.details.stackTrace) { const stackTrace = $('.stack-trace'); const linkDetector = this.instantiationService.createInstance(LinkDetector); - const linkedStackTrace = linkDetector.linkify(this.exceptionInfo.details.stackTrace, true, this.debugSession ? this.debugSession.root : undefined, undefined, { type: DebugLinkHoverBehavior.Rich, store: this._disposables }); + const hoverBehaviour: DebugLinkHoverBehaviorTypeData = { + store: this._disposables, + type: DebugLinkHoverBehavior.Rich, + }; + const linkedStackTrace = linkDetector.linkify(this.exceptionInfo.details.stackTrace, hoverBehaviour, true, this.debugSession ? this.debugSession.root : undefined, undefined); stackTrace.appendChild(linkedStackTrace); dom.append(container, stackTrace); ariaLabel += ', ' + this.exceptionInfo.details.stackTrace; diff --git a/src/vs/workbench/contrib/debug/browser/linkDetector.ts b/src/vs/workbench/contrib/debug/browser/linkDetector.ts index 6e365eabb52..70dcdb87bb4 100644 --- a/src/vs/workbench/contrib/debug/browser/linkDetector.ts +++ b/src/vs/workbench/contrib/debug/browser/linkDetector.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getWindow, isHTMLElement, reset } from '../../../../base/browser/dom.js'; +import { addDisposableListener, getWindow, isHTMLElement, reset } from '../../../../base/browser/dom.js'; import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; @@ -29,13 +29,14 @@ import { Iterable } from '../../../../base/common/iterator.js'; const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); -const WIN_ABSOLUTE_PATH = /(?:[a-zA-Z]:(?:(?:\\|\/)[\w\.-]*)+)/; -const WIN_RELATIVE_PATH = /(?:(?:\~|\.+)(?:(?:\\|\/)[\w\.-]*)+)/; +const WIN_ABSOLUTE_PATH = /(?:[a-zA-Z]:(?:(?:\\|\/)[\w\s\.@\-\(\)\[\]{}!#$%^&'`~+=]+)+)/; +const WIN_RELATIVE_PATH = /(?:(?:\~|\.+)(?:(?:\\|\/)[\w\s\.@\-\(\)\[\]{}!#$%^&'`~+=]+)+)/; const WIN_PATH = new RegExp(`(${WIN_ABSOLUTE_PATH.source}|${WIN_RELATIVE_PATH.source})`); -const POSIX_PATH = /((?:\~|\.+)?(?:\/[\w\.-]*)+)/; -const LINE_COLUMN = /(?:\:([\d]+))?(?:\:([\d]+))?/; +const POSIX_PATH = /((?:\~|\.+)?(?:\/[\w\s\.@\-\(\)\[\]{}!#$%^&'`~+=]+)+)/; +// Support both ":line 123" and ":123:45" formats for line/column numbers +const LINE_COLUMN = /(?::(?:line\s+)?([\d]+))?(?::([\d]+))?/; const PATH_LINK_REGEX = new RegExp(`${platform.isWindows ? WIN_PATH.source : POSIX_PATH.source}${LINE_COLUMN.source}`, 'g'); -const LINE_COLUMN_REGEX = /:([\d]+)(?::([\d]+))?$/; +const LINE_COLUMN_REGEX = /:(?:line\s+)?([\d]+)(?::([\d]+))?$/; const MAX_LENGTH = 2000; @@ -60,12 +61,16 @@ export const enum DebugLinkHoverBehavior { } /** Store implies HoverBehavior=rich */ -export type DebugLinkHoverBehaviorTypeData = { type: DebugLinkHoverBehavior.None | DebugLinkHoverBehavior.Basic } +export type DebugLinkHoverBehaviorTypeData = + | { type: DebugLinkHoverBehavior.None; store: DisposableStore } + | { type: DebugLinkHoverBehavior.Basic; store: DisposableStore } | { type: DebugLinkHoverBehavior.Rich; store: DisposableStore }; + + export interface ILinkDetector { - linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[]): HTMLElement; - linkifyLocation(text: string, locationReference: number, session: IDebugSession, hoverBehavior?: DebugLinkHoverBehaviorTypeData): HTMLElement; + linkify(text: string, hoverBehavior: DebugLinkHoverBehaviorTypeData, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, highlights?: IHighlight[]): HTMLElement; + linkifyLocation(text: string, locationReference: number, session: IDebugSession, hoverBehavior: DebugLinkHoverBehaviorTypeData): HTMLElement; } export class LinkDetector implements ILinkDetector { @@ -88,14 +93,13 @@ export class LinkDetector implements ILinkDetector { * 'onclick' event is attached to all anchored links that opens them in the editor. * When splitLines is true, each line of the text, even if it contains no links, is wrapped in a * and added as a child of the returned . - * If a `hoverBehavior` is passed, hovers may be added using the workbench hover service. - * This should be preferred for new code where hovers are desirable. + * The `hoverBehavior` is required and manages the lifecycle of event listeners. */ - linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[]): HTMLElement { - return this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights); + linkify(text: string, hoverBehavior: DebugLinkHoverBehaviorTypeData, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, highlights?: IHighlight[]): HTMLElement { + return this._linkify(text, hoverBehavior, splitLines, workspaceFolder, includeFulltext, highlights); } - private _linkify(text: string, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, hoverBehavior?: DebugLinkHoverBehaviorTypeData, highlights?: IHighlight[], defaultRef?: { locationReference: number; session: IDebugSession }): HTMLElement { + private _linkify(text: string, hoverBehavior: DebugLinkHoverBehaviorTypeData, splitLines?: boolean, workspaceFolder?: IWorkspaceFolder, includeFulltext?: boolean, highlights?: IHighlight[], defaultRef?: { locationReference: number; session: IDebugSession }): HTMLElement { if (splitLines) { const lines = text.split('\n'); for (let i = 0; i < lines.length - 1; i++) { @@ -105,7 +109,7 @@ export class LinkDetector implements ILinkDetector { // Remove the last element ('') that split added. lines.pop(); } - const elements = lines.map(line => this._linkify(line, false, workspaceFolder, includeFulltext, hoverBehavior, highlights, defaultRef)); + const elements = lines.map(line => this._linkify(line, hoverBehavior, false, workspaceFolder, includeFulltext, highlights, defaultRef)); if (elements.length === 1) { // Do not wrap single line with extra span. return elements[0]; @@ -192,7 +196,7 @@ export class LinkDetector implements ILinkDetector { /** * Linkifies a location reference. */ - linkifyLocation(text: string, locationReference: number, session: IDebugSession, hoverBehavior?: DebugLinkHoverBehaviorTypeData) { + linkifyLocation(text: string, locationReference: number, session: IDebugSession, hoverBehavior: DebugLinkHoverBehaviorTypeData) { const link = this.createLink(text); this.decorateLink(link, undefined, text, hoverBehavior, async (preserveFocus: boolean) => { const location = await session.resolveLocationReference(locationReference); @@ -213,13 +217,13 @@ export class LinkDetector implements ILinkDetector { */ makeReferencedLinkDetector(locationReference: number, session: IDebugSession): ILinkDetector { return { - linkify: (text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights) => - this._linkify(text, splitLines, workspaceFolder, includeFulltext, hoverBehavior, highlights, { locationReference, session }), + linkify: (text, hoverBehavior, splitLines, workspaceFolder, includeFulltext, highlights) => + this._linkify(text, hoverBehavior, splitLines, workspaceFolder, includeFulltext, highlights, { locationReference, session }), linkifyLocation: this.linkifyLocation.bind(this), }; } - private createWebLink(fulltext: string | undefined, url: string, hoverBehavior?: DebugLinkHoverBehaviorTypeData): Node { + private createWebLink(fulltext: string | undefined, url: string, hoverBehavior: DebugLinkHoverBehaviorTypeData): Node { const link = this.createLink(url); let uri = URI.parse(url); @@ -251,7 +255,7 @@ export class LinkDetector implements ILinkDetector { resource: fileUri, options: { pinned: true, - selection: lineCol ? { startLineNumber: +lineCol[1], startColumn: +lineCol[2] } : undefined, + selection: lineCol ? { startLineNumber: +lineCol[1], startColumn: lineCol[2] ? +lineCol[2] : 1 } : undefined, }, }); return; @@ -263,13 +267,17 @@ export class LinkDetector implements ILinkDetector { return link; } - private createPathLink(fulltext: string | undefined, text: string, path: string, lineNumber: number, columnNumber: number, workspaceFolder: IWorkspaceFolder | undefined, hoverBehavior?: DebugLinkHoverBehaviorTypeData): Node { + private createPathLink(fulltext: string | undefined, text: string, path: string, lineNumber: number, columnNumber: number, workspaceFolder: IWorkspaceFolder | undefined, hoverBehavior: DebugLinkHoverBehaviorTypeData): Node { if (path[0] === '/' && path[1] === '/') { // Most likely a url part which did not match, for example ftp://path. return document.createTextNode(text); } - const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; + // Only set selection if we have a valid line number (greater than 0) + const options = lineNumber > 0 + ? { selection: { startLineNumber: lineNumber, startColumn: columnNumber > 0 ? columnNumber : 1 } } + : {}; + if (path[0] === '.') { if (!workspaceFolder) { return document.createTextNode(text); @@ -307,22 +315,31 @@ export class LinkDetector implements ILinkDetector { return link; } - private decorateLink(link: HTMLElement, uri: URI | undefined, fulltext: string | undefined, hoverBehavior: DebugLinkHoverBehaviorTypeData | undefined, onClick: (preserveFocus: boolean) => void) { + private decorateLink(link: HTMLElement, uri: URI | undefined, fulltext: string | undefined, hoverBehavior: DebugLinkHoverBehaviorTypeData, onClick: (preserveFocus: boolean) => void) { + if (hoverBehavior.store.isDisposed) { + return; + } link.classList.add('link'); const followLink = uri && this.tunnelService.canTunnel(uri) ? localize('followForwardedLink', "follow link using forwarded port") : localize('followLink', "follow link"); const title = link.ariaLabel = fulltext ? (platform.isMacintosh ? localize('fileLinkWithPathMac', "Cmd + click to {0}\n{1}", followLink, fulltext) : localize('fileLinkWithPath', "Ctrl + click to {0}\n{1}", followLink, fulltext)) : (platform.isMacintosh ? localize('fileLinkMac', "Cmd + click to {0}", followLink) : localize('fileLink', "Ctrl + click to {0}", followLink)); - if (hoverBehavior?.type === DebugLinkHoverBehavior.Rich) { + if (hoverBehavior.type === DebugLinkHoverBehavior.Rich) { hoverBehavior.store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), link, title)); - } else if (hoverBehavior?.type !== DebugLinkHoverBehavior.None) { + } else if (hoverBehavior.type !== DebugLinkHoverBehavior.None) { link.title = title; } - link.onmousemove = (event) => { link.classList.toggle('pointer', platform.isMacintosh ? event.metaKey : event.ctrlKey); }; - link.onmouseleave = () => link.classList.remove('pointer'); - link.onclick = (event) => { + hoverBehavior.store.add(addDisposableListener(link, 'mousemove', (event: MouseEvent) => { + link.classList.toggle('pointer', platform.isMacintosh ? event.metaKey : event.ctrlKey); + })); + + hoverBehavior.store.add(addDisposableListener(link, 'mouseleave', () => { + link.classList.remove('pointer'); + })); + + hoverBehavior.store.add(addDisposableListener(link, 'click', (event: MouseEvent) => { const selection = getWindow(link).getSelection(); if (!selection || selection.type === 'Range') { return; // do not navigate when user is selecting @@ -334,15 +351,16 @@ export class LinkDetector implements ILinkDetector { event.preventDefault(); event.stopImmediatePropagation(); onClick(false); - }; - link.onkeydown = e => { + })); + + hoverBehavior.store.add(addDisposableListener(link, 'keydown', (e: KeyboardEvent) => { const event = new StandardKeyboardEvent(e); if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { event.preventDefault(); event.stopPropagation(); onClick(event.keyCode === KeyCode.Space); } - }; + })); } private detectLinks(text: string): LinkPart[] { diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index fbf236948bf..f6ca5b3900c 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -44,8 +44,10 @@ .monaco-workbench .part > .title > .title-actions .start-debug-action-item .codicon-debug-start { width: 18px; height: 22px; - padding-left: 3px; - padding-right: 1px + padding-left: 2px; + padding-right: 1px; + margin-left: 1px; + border-radius: var(--vscode-cornerRadius-small) 0 0 var(--vscode-cornerRadius-small); } .monaco-workbench .monaco-action-bar .start-debug-action-item .configuration .monaco-select-box { @@ -110,8 +112,14 @@ } .debug-pane .debug-call-stack .thread, -.debug-pane .debug-call-stack .session { +.debug-pane .debug-call-stack .session, +.debug-pane .debug-call-stack .stack-frame { display: flex; + padding-right: 12px; +} + +.debug-pane .debug-call-stack .thread, +.debug-pane .debug-call-stack .session { align-items: center; } @@ -143,7 +151,6 @@ .debug-pane .monaco-list-row .monaco-action-bar { display: none; flex-shrink: 0; - margin-right: 6px; } .debug-pane .monaco-list-row:hover .monaco-action-bar, @@ -163,8 +170,6 @@ .debug-pane .debug-call-stack .stack-frame { overflow: hidden; text-overflow: ellipsis; - padding-right: 0.8em; - display: flex; } .debug-pane .debug-call-stack .stack-frame.label { @@ -185,7 +190,6 @@ .debug-pane .debug-call-stack .stack-frame > .file { display: flex; overflow: hidden; - flex-wrap: wrap; justify-content: flex-end; } @@ -291,23 +295,40 @@ line-height: 22px; } -.debug-pane .debug-breakpoints .monaco-list-row .breakpoint { - padding-left: 2px; -} - -.debug-pane .debug-breakpoints .breakpoint.exception { - padding-left: 21px; -} - .debug-pane .debug-breakpoints .breakpoint { display: flex; padding-right: 0.8em; flex: 1; align-items: center; + margin-left: -19px; +} + +.debug-pane .debug-breakpoints .breakpoint-folder, +.debug-pane .debug-breakpoints .exception { + margin-left: 0; } -.debug-pane .debug-breakpoints .breakpoint input { +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle { flex-shrink: 0; + margin-left: 0; + margin-right: 4px; +} + +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle.monaco-checkbox { + width: 18px; + min-width: 18px; + max-width: 18px; + height: 18px; + padding: 0; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.debug-pane .debug-breakpoints .breakpoint .monaco-custom-toggle.monaco-checkbox::before { + margin: 0; } .debug-pane .debug-breakpoints .breakpoint > .codicon { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 3ef3a621372..8971b30a2b0 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -9,7 +9,7 @@ import * as objects from '../../../../base/common/objects.js'; import { toAction } from '../../../../base/common/actions.js'; import * as errors from '../../../../base/common/errors.js'; import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; -import { formatPII, isUri } from '../common/debugUtils.js'; +import { formatPII, isUriString } from '../common/debugUtils.js'; import { IDebugAdapter, IConfig, AdapterEndEvent, IDebugger } from '../common/debug.js'; import { IExtensionHostDebugService, IOpenExtensionWindowResult } from '../../../../platform/debug/common/extensionHostDebug.js'; import { URI } from '../../../../base/common/uri.js'; @@ -738,8 +738,8 @@ export class RawDebugSession implements IDisposable { const key = match[1]; let value = match[2]; - if ((key === 'file-uri' || key === 'folder-uri') && !isUri(arg.path)) { - value = isUri(value) ? value : URI.file(value).toString(); + if ((key === 'file-uri' || key === 'folder-uri') && !isUriString(arg.path)) { + value = isUriString(value) ? value : URI.file(value).toString(); } args.push(`--${key}=${value}`); } else { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 59b4f8c5db9..e610aca89b3 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -164,7 +164,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.replOptions = this._register(this.instantiationService.createInstance(ReplOptions, this.id, () => this.getLocationBasedColors().background)); this._register(this.replOptions.onDidChange(() => this.onDidStyleChange())); - codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {}); + this._register(codeEditorService.registerDecorationType('repl-decoration', DECORATION_KEY, {})); this.multiSessionRepl.set(this.isMultiSessionView); this.registerListeners(); } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 2d5079adedb..9a087c15dd0 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -238,13 +238,31 @@ export class VariablesView extends ViewPane implements IDebugViewWithVariables { } private async onContextMenu(e: ITreeContextMenuEvent): Promise { - const variable = e.element; - if (!(variable instanceof Variable) || !variable.value) { + const element = e.element; + + // Handle scope context menu + if (element instanceof Scope) { + return this.openContextMenuForScope(e, element); + } + + // Handle variable context menu + if (!(element instanceof Variable) || !element.value) { return; } return openContextMenuForVariableTreeElement(this.contextKeyService, this.menuService, this.contextMenuService, MenuId.DebugVariablesContext, e); } + + private openContextMenuForScope(e: ITreeContextMenuEvent, scope: Scope): void { + const context = { scope: { name: scope.name } }; + const menu = this.menuService.getMenuActions(MenuId.DebugScopesContext, this.contextKeyService, { arg: context, shouldForwardArgs: false }); + const { secondary } = getContextMenuActions(menu, 'inline'); + + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => secondary + }); + } } export async function openContextMenuForVariableTreeElement(parentContextKeyService: IContextKeyService, menuService: IMenuService, contextMenuService: IContextMenuService, menuId: MenuId, e: ITreeContextMenuEvent) { diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index 481f775794f..32a641f082c 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -105,8 +105,7 @@ export class WelcomeView extends ViewPane { })); setContextKey(); - const debugKeybinding = this.keybindingService.lookupKeybinding(DEBUG_START_COMMAND_ID); - debugKeybindingLabel = debugKeybinding ? ` (${debugKeybinding.getLabel()})` : ''; + debugKeybindingLabel = this.keybindingService.appendKeybinding('', DEBUG_START_COMMAND_ID); } override shouldShowWelcome(): boolean { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index be135eff8f4..0b6c80a8fb0 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -553,6 +553,7 @@ export interface IScope extends IExpressionContainer { readonly expensive: boolean; readonly range?: IRange; readonly hasChildren: boolean; + readonly childrenHaveBeenLoaded: boolean; } export interface IStackFrame extends ITreeElement { @@ -1207,7 +1208,7 @@ export interface IDebugService { * Removes all breakpoints. If id is passed only removes the breakpoint associated with that id. * Notifies debug adapter of breakpoint changes. */ - removeBreakpoints(id?: string): Promise; + removeBreakpoints(id?: string | string[]): Promise; /** * Adds a new function breakpoint for the given name. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 4a4a0fdf9f4..8a468452695 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -466,6 +466,10 @@ export class Scope extends ExpressionContainer implements IScope { super(stackFrame.thread.session, stackFrame.thread.threadId, reference, `scope:${name}:${id}`, namedVariables, indexedVariables); } + get childrenHaveBeenLoaded(): boolean { + return !!this.children; + } + override toString(): string { return this.name; } @@ -1454,6 +1458,9 @@ export class DebugModel extends Disposable implements IDebugModel { private breakpointsActivated = true; private readonly _onDidChangeBreakpoints = this._register(new Emitter()); private readonly _onDidChangeCallStack = this._register(new Emitter()); + private _onDidChangeCallStackFire = this._register(new RunOnceScheduler(() => { + this._onDidChangeCallStack.fire(undefined); + }, 100)); private readonly _onDidChangeWatchExpressions = this._register(new Emitter()); private readonly _onDidChangeWatchExpressionValue = this._register(new Emitter()); private readonly _breakpointModes = new Map(); @@ -1571,15 +1578,28 @@ export class DebugModel extends Disposable implements IDebugModel { clearThreads(id: string, removeThreads: boolean, reference: number | undefined = undefined): void { const session = this.sessions.find(p => p.getId() === id); - this.schedulers.forEach(entry => { - entry.scheduler.dispose(); - entry.completeDeferred.complete(); - }); - this.schedulers.clear(); - if (session) { + let threads: IThread[]; + if (reference === undefined) { + threads = session.getAllThreads(); + } else { + const thread = session.getThread(reference); + threads = thread !== undefined ? [thread] : []; + } + for (const thread of threads) { + const threadId = thread.getId(); + const entry = this.schedulers.get(threadId); + if (entry !== undefined) { + entry.scheduler.dispose(); + entry.completeDeferred.complete(); + this.schedulers.delete(threadId); + } + } + session.clearThreads(removeThreads, reference); - this._onDidChangeCallStack.fire(undefined); + if (!this._onDidChangeCallStackFire.isScheduled()) { + this._onDidChangeCallStackFire.schedule(); + } } } diff --git a/src/vs/workbench/contrib/debug/common/debugSource.ts b/src/vs/workbench/contrib/debug/common/debugSource.ts index 7791782bb5e..a7845c15290 100644 --- a/src/vs/workbench/contrib/debug/common/debugSource.ts +++ b/src/vs/workbench/contrib/debug/common/debugSource.ts @@ -11,7 +11,7 @@ import { DEBUG_SCHEME } from './debug.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; import { Schemas } from '../../../../base/common/network.js'; -import { isUri } from './debugUtils.js'; +import { isUriString } from './debugUtils.js'; import { IEditorPane } from '../../../common/editor.js'; import { TextEditorSelectionRevealType } from '../../../../platform/editor/common/editor.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -139,7 +139,7 @@ export function getUriFromSource(raw: DebugProtocol.Source, path: string | undef }); } - if (path && isUri(path)) { // path looks like a uri + if (path && isUriString(path)) { // path looks like a uri return uriIdentityService.asCanonicalUri(URI.parse(path)); } // assume a filesystem path diff --git a/src/vs/workbench/contrib/debug/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 91382f6330c..cf8490229f9 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -174,7 +174,7 @@ export async function getEvaluatableExpressionAtPosition(languageFeaturesService // RFC 2396, Appendix A: https://www.ietf.org/rfc/rfc2396.txt const _schemePattern = /^[a-zA-Z][a-zA-Z0-9\+\-\.]+:/; -export function isUri(s: string | undefined): boolean { +export function isUriString(s: string | undefined): boolean { // heuristics: a valid uri starts with a scheme and // the scheme has at least 2 characters so that it doesn't look like a drive letter. return !!(s && s.match(_schemePattern)); @@ -185,7 +185,7 @@ function stringToUri(source: PathContainer): string | undefined { if (typeof source.sourceReference === 'number' && source.sourceReference > 0) { // if there is a source reference, don't touch path } else { - if (isUri(source.path)) { + if (isUriString(source.path)) { return uri.parse(source.path); } else { // assume path diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 7461a886bf0..c734119ad85 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -21,6 +21,7 @@ import { cleanRemoteAuthority } from '../../../../platform/telemetry/common/tele import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { filter } from '../../../../base/common/objects.js'; +import { IProductService } from '../../../../platform/product/common/productService.js'; export class Debugger implements IDebugger, IDebuggerMetadata { @@ -41,6 +42,7 @@ export class Debugger implements IDebugger, IDebuggerMetadata { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IDebugService private readonly debugService: IDebugService, @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IProductService private readonly productService: IProductService, ) { this.debuggerContribution = { type: dbgContribution.type }; this.merge(dbgContribution, extensionDescription); @@ -226,7 +228,7 @@ export class Debugger implements IDebugger, IDebuggerMetadata { return undefined; } - const sendErrorTelemtry = cleanRemoteAuthority(this.environmentService.remoteAuthority) !== 'other'; + const sendErrorTelemtry = cleanRemoteAuthority(this.environmentService.remoteAuthority, this.productService) !== 'other'; return { id: `${this.getMainExtensionDescriptor().publisher}.${this.type}`, aiKey, diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index cfaa1ab3f83..d5eab305fdc 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable no-restricted-syntax */ - import assert from 'assert'; import * as dom from '../../../../../base/browser/dom.js'; import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index 17d23dc6303..bbc50708d9a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -14,7 +14,7 @@ import { workbenchInstantiationService } from '../../../../test/browser/workbenc import { registerColors } from '../../../terminal/common/terminalColorRegistry.js'; import { appendStylizedStringToContainer, calcANSI8bitColor, handleANSIOutput } from '../../browser/debugANSIHandling.js'; import { DebugSession } from '../../browser/debugSession.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; +import { DebugLinkHoverBehavior, LinkDetector } from '../../browser/linkDetector.js'; import { DebugModel } from '../../common/debugModel.js'; import { createTestSession } from './callStack.test.js'; import { createMockDebugModel } from './mockDebugModel.js'; @@ -51,8 +51,9 @@ suite('Debug - ANSI Handling', () => { assert.strictEqual(0, root.children.length); - appendStylizedStringToContainer(root, 'content1', ['class1', 'class2'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0); - appendStylizedStringToContainer(root, 'content2', ['class2', 'class3'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0); + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + appendStylizedStringToContainer(root, 'content1', ['class1', 'class2'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0, hoverBehavior); + appendStylizedStringToContainer(root, 'content2', ['class2', 'class3'], linkDetector, session.root, undefined, undefined, undefined, undefined, 0, hoverBehavior); assert.strictEqual(2, root.children.length); @@ -73,6 +74,7 @@ suite('Debug - ANSI Handling', () => { } else { assert.fail('Unexpected assertion error'); } + hoverBehavior.store.dispose(); }); /** @@ -82,9 +84,11 @@ suite('Debug - ANSI Handling', () => { * @returns An {@link HTMLSpanElement} that contains the stylized text. */ function getSequenceOutput(sequence: string): HTMLSpanElement { - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, []); + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, [], hoverBehavior); assert.strictEqual(1, root.children.length); const child: Node = root.lastChild!; + hoverBehavior.store.dispose(); if (isHTMLSpanElement(child)) { return child; } else { @@ -395,7 +399,8 @@ suite('Debug - ANSI Handling', () => { if (elementsExpected === undefined) { elementsExpected = assertions.length; } - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, []); + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root, [], hoverBehavior); assert.strictEqual(elementsExpected, root.children.length); for (let i = 0; i < elementsExpected; i++) { const child: Node = root.children[i]; @@ -405,6 +410,7 @@ suite('Debug - ANSI Handling', () => { assert.fail('Unexpected assertion error'); } } + hoverBehavior.store.dispose(); } test('Expected multiple sequence operation', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts index 9e2d0c8c767..8b2efd87b62 100644 --- a/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/linkDetector.test.ts @@ -5,13 +5,14 @@ import assert from 'assert'; import { isHTMLAnchorElement } from '../../../../../base/browser/dom.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { isWindows } from '../../../../../base/common/platform.js'; import { URI } from '../../../../../base/common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { ITunnelService } from '../../../../../platform/tunnel/common/tunnel.js'; import { WorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; +import { DebugLinkHoverBehavior, LinkDetector } from '../../browser/linkDetector.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; import { IHighlight } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; @@ -39,39 +40,46 @@ suite('Debug - Link Detector', () => { } test('noLinks', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'I am a string'; const expectedOutput = 'I am a string'; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(0, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('trailingNewline', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'I am a string\n'; const expectedOutput = 'I am a string\n'; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(0, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('trailingNewlineSplit', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'I am a string\n'; const expectedOutput = 'I am a string\n'; - const output = linkDetector.linkify(input, true); + const output = linkDetector.linkify(input, hoverBehavior, true); assert.strictEqual(0, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('singleLineLink', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34<\/a><\/span>' : '/Users/foo/bar.js:12:34<\/a><\/span>'; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); @@ -79,29 +87,48 @@ suite('Debug - Link Detector', () => { assert.strictEqual(expectedOutput, output.outerHTML); assertElementIsLink(output.firstElementChild!); assert.strictEqual(isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34', output.firstElementChild!.textContent); + hoverBehavior.store.dispose(); + }); + + test('allows links with @ (#282635)', () => { + if (!isWindows) { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const input = '(/home/alexey_korepov/projects/dt2/playwright/node_modules/.pnpm/playwright-core@1.57.0/node_modules/playwright-core/lib/client/errors.js:56:16)'; + const expectedOutput = '(/home/alexey_korepov/projects/dt2/playwright/node_modules/.pnpm/playwright-core@1.57.0/node_modules/playwright-core/lib/client/errors.js:56:16)'; + const output = linkDetector.linkify(input, hoverBehavior); + + assert.strictEqual(expectedOutput, output.outerHTML); + assert.strictEqual(1, output.children.length); + hoverBehavior.store.dispose(); + } }); test('relativeLink', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = '\./foo/bar.js'; const expectedOutput = '\./foo/bar.js'; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(0, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('relativeLinkWithWorkspace', async () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = '\./foo/bar.js'; - const output = linkDetector.linkify(input, false, new WorkspaceFolder({ uri: URI.file('/path/to/workspace'), name: 'ws', index: 0 })); + const output = linkDetector.linkify(input, hoverBehavior, false, new WorkspaceFolder({ uri: URI.file('/path/to/workspace'), name: 'ws', index: 0 })); assert.strictEqual('SPAN', output.tagName); assert.ok(output.outerHTML.indexOf('link') >= 0); + hoverBehavior.store.dispose(); }); test('singleLineLinkAndText', function () { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'The link: C:/foo/bar.js:12:34' : 'The link: /Users/foo/bar.js:12:34'; const expectedOutput = /^The link: .*\/foo\/bar.js:12:34<\/a><\/span>$/; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); @@ -109,13 +136,15 @@ suite('Debug - Link Detector', () => { assert(expectedOutput.test(output.outerHTML)); assertElementIsLink(output.children[0]); assert.strictEqual(isWindows ? 'C:/foo/bar.js:12:34' : '/Users/foo/bar.js:12:34', output.children[0].textContent); + hoverBehavior.store.dispose(); }); test('singleLineMultipleLinks', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'Here is a link C:/foo/bar.js:12:34 and here is another D:/boo/far.js:56:78' : 'Here is a link /Users/foo/bar.js:12:34 and here is another /Users/boo/far.js:56:78'; const expectedOutput = /^Here is a link .*\/foo\/bar.js:12:34<\/a> and here is another .*\/boo\/far.js:56:78<\/a><\/span>$/; - const output = linkDetector.linkify(input); + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(2, output.children.length); assert.strictEqual('SPAN', output.tagName); @@ -126,12 +155,14 @@ suite('Debug - Link Detector', () => { assertElementIsLink(output.children[1]); assert.strictEqual(isWindows ? 'C:/foo/bar.js:12:34' : '/Users/foo/bar.js:12:34', output.children[0].textContent); assert.strictEqual(isWindows ? 'D:/boo/far.js:56:78' : '/Users/boo/far.js:56:78', output.children[1].textContent); + hoverBehavior.store.dispose(); }); test('multilineNoLinks', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'Line one\nLine two\nLine three'; const expectedOutput = /^Line one\n<\/span>Line two\n<\/span>Line three<\/span><\/span>$/; - const output = linkDetector.linkify(input, true); + const output = linkDetector.linkify(input, hoverBehavior, true); assert.strictEqual(3, output.children.length); assert.strictEqual('SPAN', output.tagName); @@ -139,25 +170,29 @@ suite('Debug - Link Detector', () => { assert.strictEqual('SPAN', output.children[1].tagName); assert.strictEqual('SPAN', output.children[2].tagName); assert(expectedOutput.test(output.outerHTML)); + hoverBehavior.store.dispose(); }); test('multilineTrailingNewline', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'I am a string\nAnd I am another\n'; const expectedOutput = 'I am a string\n<\/span>And I am another\n<\/span><\/span>'; - const output = linkDetector.linkify(input, true); + const output = linkDetector.linkify(input, hoverBehavior, true); assert.strictEqual(2, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual('SPAN', output.children[0].tagName); assert.strictEqual('SPAN', output.children[1].tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('multilineWithLinks', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'I have a link for you\nHere it is: C:/foo/bar.js:12:34\nCool, huh?' : 'I have a link for you\nHere it is: /Users/foo/bar.js:12:34\nCool, huh?'; const expectedOutput = /^I have a link for you\n<\/span>Here it is: .*\/foo\/bar.js:12:34<\/a>\n<\/span>Cool, huh\?<\/span><\/span>$/; - const output = linkDetector.linkify(input, true); + const output = linkDetector.linkify(input, hoverBehavior, true); assert.strictEqual(3, output.children.length); assert.strictEqual('SPAN', output.tagName); @@ -168,68 +203,131 @@ suite('Debug - Link Detector', () => { assert(expectedOutput.test(output.outerHTML)); assertElementIsLink(output.children[1].children[0]); assert.strictEqual(isWindows ? 'C:/foo/bar.js:12:34' : '/Users/foo/bar.js:12:34', output.children[1].children[0].textContent); + hoverBehavior.store.dispose(); }); test('highlightNoLinks', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = 'I am a string'; const highlights: IHighlight[] = [{ start: 2, end: 5 }]; const expectedOutput = 'I am a string'; - const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + const output = linkDetector.linkify(input, hoverBehavior, false, undefined, false, highlights); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual(expectedOutput, output.outerHTML); + hoverBehavior.store.dispose(); }); test('highlightWithLink', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; const highlights: IHighlight[] = [{ start: 0, end: 5 }]; const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + const output = linkDetector.linkify(input, hoverBehavior, false, undefined, false, highlights); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual('A', output.firstElementChild!.tagName); assert.strictEqual(expectedOutput, output.outerHTML); assertElementIsLink(output.firstElementChild!); + hoverBehavior.store.dispose(); }); test('highlightOverlappingLinkStart', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; const highlights: IHighlight[] = [{ start: 0, end: 10 }]; const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + const output = linkDetector.linkify(input, hoverBehavior, false, undefined, false, highlights); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual('A', output.firstElementChild!.tagName); assert.strictEqual(expectedOutput, output.outerHTML); assertElementIsLink(output.firstElementChild!); + hoverBehavior.store.dispose(); }); test('highlightOverlappingLinkEnd', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; const highlights: IHighlight[] = [{ start: 10, end: 20 }]; const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + const output = linkDetector.linkify(input, hoverBehavior, false, undefined, false, highlights); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual('A', output.firstElementChild!.tagName); assert.strictEqual(expectedOutput, output.outerHTML); assertElementIsLink(output.firstElementChild!); + hoverBehavior.store.dispose(); }); test('highlightOverlappingLinkStartAndEnd', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; const input = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; const highlights: IHighlight[] = [{ start: 5, end: 15 }]; const expectedOutput = isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34'; - const output = linkDetector.linkify(input, false, undefined, false, undefined, highlights); + const output = linkDetector.linkify(input, hoverBehavior, false, undefined, false, highlights); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + hoverBehavior.store.dispose(); + }); + + test('csharpStackTraceFormatWithLine', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const input = isWindows ? 'C:\\foo\\bar.cs:line 6' : '/Users/foo/bar.cs:line 6'; + const expectedOutput = isWindows ? 'C:\\foo\\bar.cs:line 6<\/a><\/span>' : '/Users/foo/bar.cs:line 6<\/a><\/span>'; + const output = linkDetector.linkify(input, hoverBehavior); + + assert.strictEqual(1, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.firstElementChild!.tagName); + assert.strictEqual(expectedOutput, output.outerHTML); + assertElementIsLink(output.firstElementChild!); + assert.strictEqual(isWindows ? 'C:\\foo\\bar.cs:line 6' : '/Users/foo/bar.cs:line 6', output.firstElementChild!.textContent); + hoverBehavior.store.dispose(); + }); + + test('csharpStackTraceFormatWithLineAndColumn', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const input = isWindows ? 'C:\\foo\\bar.cs:line 6:10' : '/Users/foo/bar.cs:line 6:10'; + const expectedOutput = isWindows ? 'C:\\foo\\bar.cs:line 6:10<\/a><\/span>' : '/Users/foo/bar.cs:line 6:10<\/a><\/span>'; + const output = linkDetector.linkify(input, hoverBehavior); assert.strictEqual(1, output.children.length); assert.strictEqual('SPAN', output.tagName); assert.strictEqual('A', output.firstElementChild!.tagName); assert.strictEqual(expectedOutput, output.outerHTML); assertElementIsLink(output.firstElementChild!); + assert.strictEqual(isWindows ? 'C:\\foo\\bar.cs:line 6:10' : '/Users/foo/bar.cs:line 6:10', output.firstElementChild!.textContent); + hoverBehavior.store.dispose(); + }); + + test('mixedStackTraceFormats', () => { + const hoverBehavior = { type: DebugLinkHoverBehavior.None, store: new DisposableStore() }; + const input = isWindows ? 'C:\\foo\\bar.js:12:34 and C:\\baz\\qux.cs:line 6' : + '/Users/foo/bar.js:12:34 and /Users/baz/qux.cs:line 6'; + // Use flexible path separator matching for cross-platform compatibility + const expectedOutput = isWindows ? + /^.*\\foo\\bar\.js:12:34<\/a> and .*\\baz\\qux\.cs:line 6<\/a><\/span>$/ : + /^.*\/foo\/bar\.js:12:34<\/a> and .*\/baz\/qux\.cs:line 6<\/a><\/span>$/; + const output = linkDetector.linkify(input, hoverBehavior); + + assert.strictEqual(2, output.children.length); + assert.strictEqual('SPAN', output.tagName); + assert.strictEqual('A', output.children[0].tagName); + assert.strictEqual('A', output.children[1].tagName); + assert(expectedOutput.test(output.outerHTML)); + assertElementIsLink(output.children[0]); + assertElementIsLink(output.children[1]); + assert.strictEqual(isWindows ? 'C:\\foo\\bar.js:12:34' : '/Users/foo/bar.js:12:34', output.children[0].textContent); + assert.strictEqual(isWindows ? 'C:\\baz\\qux.cs:line 6' : '/Users/baz/qux.cs:line 6', output.children[1].textContent); + hoverBehavior.store.dispose(); }); }); diff --git a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts index 379a813f5e7..3aa43a96262 100644 --- a/src/vs/workbench/contrib/debug/test/node/debugger.test.ts +++ b/src/vs/workbench/contrib/debug/test/node/debugger.test.ts @@ -15,7 +15,6 @@ import { TestTextResourcePropertiesService } from '../../../../../editor/test/co import { ExtensionIdentifier, IExtensionDescription, TargetPlatform } from '../../../../../platform/extensions/common/extensions.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; - suite('Debug - Debugger', () => { let _debugger: Debugger; @@ -144,7 +143,7 @@ suite('Debug - Debugger', () => { const testResourcePropertiesService = new TestTextResourcePropertiesService(configurationService); setup(() => { - _debugger = new Debugger(adapterManager, debuggerContribution, extensionDescriptor0, configurationService, testResourcePropertiesService, undefined!, undefined!, undefined!, undefined!); + _debugger = new Debugger(adapterManager, debuggerContribution, extensionDescriptor0, configurationService, testResourcePropertiesService, undefined!, undefined!, undefined!, undefined!, undefined!); }); teardown(() => { diff --git a/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts b/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts index 35c1b74c29d..55748620d94 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessionsStorageClient.ts @@ -6,5 +6,5 @@ import { UserDataSyncStoreClient } from '../../../../platform/userDataSync/common/userDataSyncStoreService.js'; export class EditSessionsStoreClient extends UserDataSyncStoreClient { - _serviceBrand: any; + _serviceBrand: undefined; } diff --git a/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts index 46acc992b99..70caed204d6 100644 --- a/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts +++ b/src/vs/workbench/contrib/editSessions/common/workspaceStateSync.ts @@ -36,7 +36,7 @@ class NullBackupStoreService implements IUserDataSyncLocalStoreService { } class NullEnablementService implements IUserDataSyncEnablementService { - _serviceBrand: any; + _serviceBrand: undefined; private _onDidChangeEnablement = new Emitter(); readonly onDidChangeEnablement: Event = this._onDidChangeEnablement.event; diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts new file mode 100644 index 00000000000..b02f21ebd85 --- /dev/null +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsChart.ts @@ -0,0 +1,285 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { $ } from '../../../../../base/browser/dom.js'; +import { localize } from '../../../../../nls.js'; +import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js'; +import { chartsBlue, chartsForeground, chartsLines } from '../../../../../platform/theme/common/colorRegistry.js'; + +export interface ISessionData { + startTime: number; + typedCharacters: number; + aiCharacters: number; + acceptedInlineSuggestions: number | undefined; + chatEditCount: number | undefined; +} + +export interface IDailyAggregate { + date: string; // ISO date string (YYYY-MM-DD) + displayDate: string; // Formatted for display + aiRate: number; + totalAiChars: number; + totalTypedChars: number; + inlineSuggestions: number; + chatEdits: number; + sessionCount: number; +} + +export type ChartViewMode = 'days' | 'sessions'; + +export function aggregateSessionsByDay(sessions: readonly ISessionData[]): IDailyAggregate[] { + const dayMap = new Map(); + + for (const session of sessions) { + const date = new Date(session.startTime); + const isoDate = date.toISOString().split('T')[0]; + const displayDate = date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + + let aggregate = dayMap.get(isoDate); + if (!aggregate) { + aggregate = { + date: isoDate, + displayDate, + aiRate: 0, + totalAiChars: 0, + totalTypedChars: 0, + inlineSuggestions: 0, + chatEdits: 0, + sessionCount: 0, + }; + dayMap.set(isoDate, aggregate); + } + + aggregate.totalAiChars += session.aiCharacters; + aggregate.totalTypedChars += session.typedCharacters; + aggregate.inlineSuggestions += session.acceptedInlineSuggestions ?? 0; + aggregate.chatEdits += session.chatEditCount ?? 0; + aggregate.sessionCount += 1; + } + + // Calculate AI rate for each day + for (const aggregate of dayMap.values()) { + const total = aggregate.totalAiChars + aggregate.totalTypedChars; + aggregate.aiRate = total > 0 ? aggregate.totalAiChars / total : 0; + } + + // Sort by date + return Array.from(dayMap.values()).sort((a, b) => a.date.localeCompare(b.date)); +} + +export interface IAiStatsChartOptions { + sessions: readonly ISessionData[]; + viewMode: ChartViewMode; +} + +export function createAiStatsChart( + options: IAiStatsChartOptions +): HTMLElement { + const { sessions: sessionsData, viewMode: mode } = options; + + const width = 280; + const height = 100; + const margin = { top: 10, right: 10, bottom: 25, left: 30 }; + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const container = $('.ai-stats-chart-container'); + container.style.position = 'relative'; + container.style.marginTop = '8px'; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('width', `${width}px`); + svg.setAttribute('height', `${height}px`); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + svg.style.display = 'block'; + container.appendChild(svg); + + const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); + g.setAttribute('transform', `translate(${margin.left},${margin.top})`); + svg.appendChild(g); + + if (sessionsData.length === 0) { + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', `${innerWidth / 2}`); + text.setAttribute('y', `${innerHeight / 2}`); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('fill', asCssVariable(chartsForeground)); + text.setAttribute('font-size', '11px'); + text.textContent = localize('noData', "No data yet"); + g.appendChild(text); + return container; + } + + // Draw axes + const xAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + xAxisLine.setAttribute('x1', '0'); + xAxisLine.setAttribute('y1', `${innerHeight}`); + xAxisLine.setAttribute('x2', `${innerWidth}`); + xAxisLine.setAttribute('y2', `${innerHeight}`); + xAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + xAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(xAxisLine); + + const yAxisLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + yAxisLine.setAttribute('x1', '0'); + yAxisLine.setAttribute('y1', '0'); + yAxisLine.setAttribute('x2', '0'); + yAxisLine.setAttribute('y2', `${innerHeight}`); + yAxisLine.setAttribute('stroke', asCssVariable(chartsLines)); + yAxisLine.setAttribute('stroke-width', '1px'); + g.appendChild(yAxisLine); + + // Y-axis labels (0%, 50%, 100%) + for (const pct of [0, 50, 100]) { + const y = innerHeight - (pct / 100) * innerHeight; + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', '-4'); + label.setAttribute('y', `${y + 3}`); + label.setAttribute('text-anchor', 'end'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '9px'); + label.textContent = `${pct}%`; + g.appendChild(label); + + if (pct > 0) { + const gridLine = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + gridLine.setAttribute('x1', '0'); + gridLine.setAttribute('y1', `${y}`); + gridLine.setAttribute('x2', `${innerWidth}`); + gridLine.setAttribute('y2', `${y}`); + gridLine.setAttribute('stroke', asCssVariable(chartsLines)); + gridLine.setAttribute('stroke-width', '0.5px'); + gridLine.setAttribute('stroke-dasharray', '2,2'); + g.appendChild(gridLine); + } + } + + if (mode === 'days') { + renderDaysView(); + } else { + renderSessionsView(); + } + + function renderDaysView() { + const dailyData = aggregateSessionsByDay(sessionsData); + const barCount = dailyData.length; + const barWidth = Math.min(20, (innerWidth - (barCount - 1) * 2) / barCount); + const gap = 2; + const totalBarSpace = barCount * barWidth + (barCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + // Calculate which labels to show based on available space + // Each label needs roughly 40px of space to not overlap + const minLabelSpacing = 40; + const totalWidth = totalBarSpace; + const maxLabels = Math.max(2, Math.floor(totalWidth / minLabelSpacing)); + const labelStep = Math.max(1, Math.ceil(barCount / maxLabels)); + + dailyData.forEach((day, i) => { + const x = startX + i * (barWidth + gap); + const barHeight = day.aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '2'); + g.appendChild(rect); + + // X-axis label - only show at calculated intervals to avoid overlap + const isFirst = i === 0; + const isLast = i === barCount - 1; + const isAtInterval = i % labelStep === 0; + + if (isFirst || isLast || (isAtInterval && barCount > 2)) { + // Skip middle labels if they would be too close to first/last + if (!isFirst && !isLast) { + const distFromFirst = i * (barWidth + gap); + const distFromLast = (barCount - 1 - i) * (barWidth + gap); + if (distFromFirst < minLabelSpacing || distFromLast < minLabelSpacing) { + return; // Skip this label + } + } + + const label = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + label.setAttribute('x', `${x + barWidth / 2}`); + label.setAttribute('y', `${innerHeight + 12}`); + label.setAttribute('text-anchor', 'middle'); + label.setAttribute('fill', asCssVariable(chartsForeground)); + label.setAttribute('font-size', '8px'); + label.textContent = day.displayDate; + g.appendChild(label); + } + }); + } + + function renderSessionsView() { + const sessionCount = sessionsData.length; + const barWidth = Math.min(8, (innerWidth - (sessionCount - 1) * 1) / sessionCount); + const gap = 1; + const totalBarSpace = sessionCount * barWidth + (sessionCount - 1) * gap; + const startX = (innerWidth - totalBarSpace) / 2; + + sessionsData.forEach((session, i) => { + const total = session.aiCharacters + session.typedCharacters; + const aiRate = total > 0 ? session.aiCharacters / total : 0; + const x = startX + i * (barWidth + gap); + const barHeight = aiRate * innerHeight; + const y = innerHeight - barHeight; + + // Bar for AI rate + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', `${x}`); + rect.setAttribute('y', `${y}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${Math.max(1, barHeight)}`); + rect.setAttribute('fill', asCssVariable(chartsBlue)); + rect.setAttribute('rx', '1'); + g.appendChild(rect); + }); + + // X-axis labels: only show first and last to avoid overlap + // Each label is roughly 40px wide (e.g., "Jan 15") + const minLabelSpacing = 40; + + if (sessionCount === 0) { + return; + } + + // Always show first label + const firstSession = sessionsData[0]; + const firstX = startX; + const firstDate = new Date(firstSession.startTime); + const firstLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + firstLabel.setAttribute('x', `${firstX + barWidth / 2}`); + firstLabel.setAttribute('y', `${innerHeight + 12}`); + firstLabel.setAttribute('text-anchor', 'start'); + firstLabel.setAttribute('fill', asCssVariable(chartsForeground)); + firstLabel.setAttribute('font-size', '8px'); + firstLabel.textContent = firstDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(firstLabel); + + // Show last label if there's enough space and more than 1 session + if (sessionCount > 1 && totalBarSpace >= minLabelSpacing) { + const lastSession = sessionsData[sessionCount - 1]; + const lastX = startX + (sessionCount - 1) * (barWidth + gap); + const lastDate = new Date(lastSession.startTime); + const lastLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + lastLabel.setAttribute('x', `${lastX + barWidth / 2}`); + lastLabel.setAttribute('y', `${innerHeight + 12}`); + lastLabel.setAttribute('text-anchor', 'end'); + lastLabel.setAttribute('fill', asCssVariable(chartsForeground)); + lastLabel.setAttribute('font-size', '8px'); + lastLabel.textContent = lastDate.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + g.appendChild(lastLabel); + } + } + + return container; +} diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts index da6c2ea7955..922e0ac5acd 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts @@ -113,6 +113,15 @@ export class AiStatsFeature extends Disposable { return val.sessions.length; }); + public readonly sessions = derived(this, r => { + this._dataVersion.read(r); + const val = this._data.getValue(); + if (!val) { + return []; + } + return val.sessions; + }); + public readonly acceptedInlineSuggestionsToday = derived(this, r => { this._dataVersion.read(r); const val = this._data.getValue(); diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts index 16248f06f26..9838eb00d44 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsStatusBar.ts @@ -9,7 +9,7 @@ import { IAction } from '../../../../../base/common/actions.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../../base/common/observable.js'; +import { autorun, derived, observableValue } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -18,11 +18,14 @@ import { ITelemetryService } from '../../../../../platform/telemetry/common/tele import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js'; import { AI_STATS_SETTING_ID } from '../settingIds.js'; import type { AiStatsFeature } from './aiStatsFeature.js'; +import { ChartViewMode, createAiStatsChart } from './aiStatsChart.js'; import './media.css'; export class AiStatsStatusBar extends Disposable { public static readonly hot = createHotClass(this); + private readonly _chartViewMode = observableValue(this, 'days'); + constructor( private readonly _aiStatsFeature: AiStatsFeature, @IStatusbarService private readonly _statusbarService: IStatusbarService, @@ -129,7 +132,7 @@ export class AiStatsStatusBar extends Disposable { n.div({ class: 'header', style: { - minWidth: '200px', + minWidth: '280px', } }, [ @@ -154,28 +157,89 @@ export class AiStatsStatusBar extends Disposable { n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text1', "AI vs Typing Average: {0}", aiRatePercent.get()), ]), - /* - TODO: Write article that explains the ratio and link to it. - - n.div({ style: { marginLeft: 'auto' } }, actionBar([ - { - action: { - id: 'aiStatsStatusBar.openSettings', - label: '', - enabled: true, - run: () => { }, - class: ThemeIcon.asClassName(Codicon.info), - tooltip: '' - }, - options: { icon: true, label: true, } - } - ]))*/ ]), n.div({ style: { flex: 1, paddingRight: '4px' } }, [ localize('text2', "Accepted inline suggestions today: {0}", this._aiStatsFeature.acceptedInlineSuggestionsToday.get()), ]), + + // Chart section + n.div({ + style: { + marginTop: '8px', + borderTop: '1px solid var(--vscode-widget-border)', + paddingTop: '8px', + } + }, [ + // Chart header with toggle + n.div({ + class: 'header', + style: { + display: 'flex', + alignItems: 'center', + marginBottom: '4px', + } + }, [ + n.div({ style: { flex: 1 } }, [ + this._chartViewMode.map(mode => + mode === 'days' + ? localize('chartHeaderDays', "AI Rate by Day") + : localize('chartHeaderSessions', "AI Rate by Session") + ) + ]), + n.div({ + class: 'chart-view-toggle', + style: { marginLeft: 'auto', display: 'flex', gap: '2px' } + }, [ + this._createToggleButton('days', localize('viewByDays', "Days"), Codicon.calendar), + this._createToggleButton('sessions', localize('viewBySessions', "Sessions"), Codicon.listFlat), + ]) + ]), + + // Chart container + derived(reader => { + const sessions = this._aiStatsFeature.sessions.read(reader); + const viewMode = this._chartViewMode.read(reader); + return n.div({ + ref: (container) => { + const chart = createAiStatsChart({ + sessions, + viewMode, + }); + container.appendChild(chart); + } + }); + }), + ]), ]); } + + private _createToggleButton(mode: ChartViewMode, tooltip: string, icon: ThemeIcon) { + return derived(reader => { + const currentMode = this._chartViewMode.read(reader); + const isActive = currentMode === mode; + + return n.div({ + class: ['chart-toggle-button', isActive ? 'active' : ''], + style: { + padding: '2px 4px', + borderRadius: '3px', + cursor: 'pointer', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + onclick: () => { + this._chartViewMode.set(mode, undefined); + }, + title: tooltip, + }, [ + n.div({ + class: ThemeIcon.asClassName(icon), + style: { fontSize: '14px' } + }) + ]); + }); + } } function actionBar(actions: { action: IAction; options: IActionOptions }[], options?: IActionBarOptions) { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css index e0eaa8eff4a..3668b5565fd 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css +++ b/src/vs/workbench/contrib/editTelemetry/browser/editStats/media.css @@ -33,6 +33,39 @@ margin-bottom: 5px; } + /* Chart toggle buttons */ + .chart-view-toggle { + display: flex; + gap: 2px; + } + + .chart-toggle-button { + padding: 2px 6px; + border-radius: 3px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s, background-color 0.15s; + } + + .chart-toggle-button:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); + } + + .chart-toggle-button.active { + opacity: 1; + background-color: var(--vscode-toolbar-activeBackground); + } + + /* Chart container */ + .ai-stats-chart-container { + margin-top: 4px; + } + + .ai-stats-chart-container svg { + overflow: visible; + } + /* Setup for New User */ .setup .chat-feature-container { diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts index 824d13f763c..170cced5980 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts @@ -31,7 +31,7 @@ configurationRegistry.registerConfiguration({ tags: ['experimental'], }, [AI_STATS_SETTING_ID]: { - markdownDescription: localize('editor.aiStats.enabled', "Controls whether to enable AI statistics in the editor. The gauge represents the average amount of code inserted by AI vs manual typing over a 24 hour period."), + markdownDescription: localize('editor.aiStats.enabled', "Controls whether to enable AI statistics in the editor. The gauge shows the average AI rate across 5-minute sessions, where each session's rate is calculated as AI-inserted characters divided by total inserted characters."), type: 'boolean', default: false, tags: ['experimental'], diff --git a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts index 0616e6fb0f0..da642b8a11d 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts @@ -13,36 +13,39 @@ import { AnnotatedDocuments } from './helpers/annotatedDocuments.js'; import { EditTrackingFeature } from './telemetry/editSourceTrackingFeature.js'; import { VSCodeWorkspace } from './helpers/vscodeObservableWorkspace.js'; import { AiStatsFeature } from './editStats/aiStatsFeature.js'; -import { EDIT_TELEMETRY_SETTING_ID, AI_STATS_SETTING_ID } from './settingIds.js'; +import { AI_STATS_SETTING_ID, EDIT_TELEMETRY_SETTING_ID } from './settingIds.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; export class EditTelemetryContribution extends Disposable { constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService telemetryService: ITelemetryService, + @IChatEntitlementService chatEntitlementService: IChatEntitlementService ) { super(); - const workspace = derived(reader => reader.store.add(this._instantiationService.createInstance(VSCodeWorkspace))); - const annotatedDocuments = derived(reader => reader.store.add(this._instantiationService.createInstance(AnnotatedDocuments, workspace.read(reader)))); + const workspace = derived(reader => reader.store.add(instantiationService.createInstance(VSCodeWorkspace))); + const annotatedDocuments = derived(reader => reader.store.add(instantiationService.createInstance(AnnotatedDocuments, workspace.read(reader)))); - const editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService); + const editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, configurationService); this._register(autorun(r => { const enabled = editSourceTrackingEnabled.read(r); - if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) { + if (!enabled || !telemetryLevelEnabled(telemetryService, TelemetryLevel.USAGE)) { return; } - r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace.read(r), annotatedDocuments.read(r))); + r.store.add(instantiationService.createInstance(EditTrackingFeature, workspace.read(r), annotatedDocuments.read(r))); })); - const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, this._configurationService); + const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, configurationService); this._register(autorun(r => { const enabled = aiStatsEnabled.read(r); - if (!enabled) { + const aiDisabled = chatEntitlementService.sentimentObs.read(r).hidden; + if (!enabled || aiDisabled) { return; } - r.store.add(this._instantiationService.createInstance(AiStatsFeature, annotatedDocuments.read(r))); + r.store.add(instantiationService.createInstance(AiStatsFeature, annotatedDocuments.read(r))); })); } } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts index 63bd4058042..387563fbd4a 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetryReporter.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { TimeoutTimer } from '../../../../../base/common/async.js'; -import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservableWithChange, IObservable, runOnChange } from '../../../../../base/common/observable.js'; import { BaseStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js'; import { StringText } from '../../../../../editor/common/core/text/abstractText.js'; @@ -25,17 +25,13 @@ export class ArcTelemetryReporter extends Disposable { private readonly _gitRepo: IObservable, private readonly _trackedEdit: BaseStringEdit, private readonly _sendTelemetryEvent: (res: ArcTelemetryReporterData) => void, - private readonly _onBeforeDispose: () => void, + private readonly _dispose: () => void, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { super(); this._arcTracker = new ArcTracker(this._documentValueBeforeTrackedEdit, this._trackedEdit); - this._store.add(toDisposable(() => { - this._onBeforeDispose(); - })); - this._store.add(runOnChange(this._document.value, (_val, _prevVal, changes) => { const edit = BaseStringEdit.composeOrUndefined(changes.map(c => c.edit)); if (edit) { @@ -54,7 +50,7 @@ export class ArcTelemetryReporter extends Disposable { this._report(timeMs); } else { this._reportAfter(timeMs, i === this._timesMs.length - 1 ? () => { - this.dispose(); + this._dispose(); } : undefined); } } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts index 280bcd94920..faa9c30d06c 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/arcTelemetrySender.ts @@ -47,6 +47,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { extensionVersion: string; opportunityId: string; languageId: string; + correlationId: string | undefined; didBranchChange: number; timeDelayMs: number; @@ -64,6 +65,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { extensionVersion: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The version of the extension.' }; opportunityId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Unique identifier for an opportunity to show an inline suggestion.' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; + correlationId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The correlation id of the inline suggestion.' }; didBranchChange: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Indicates if the branch changed in the meantime. If the branch changed (value is 1); this event should probably be ignored.' }; timeDelayMs: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'The time delay between the user accepting the edit and measuring the survival rate.' }; @@ -79,6 +81,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { extensionVersion: data.$extensionVersion ?? '', opportunityId: data.$$requestUuid ?? 'unknown', languageId: data.$$languageId, + correlationId: data.$$correlationId, didBranchChange: res.didBranchChange ? 1 : 0, timeDelayMs: res.timeDelayMs, @@ -92,7 +95,7 @@ export class EditTelemetryReportInlineEditArcSender extends Disposable { ...forwardToChannelIf(isCopilotLikeExtension(data.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } @@ -252,7 +255,7 @@ export class EditTelemetryReportEditArcForChatOrInlineChatSender extends Disposa ...forwardToChannelIf(isCopilotLikeExtension(data.props.$extensionId)), }); }, () => { - this._store.deleteAndLeak(reporter); + this._store.delete(reporter); })); })); } diff --git a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts index 520141cd79b..b5c9a99c873 100644 --- a/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts +++ b/src/vs/workbench/contrib/editTelemetry/browser/telemetry/editSourceTrackingImpl.ts @@ -9,6 +9,7 @@ import { toDisposable, Disposable } from '../../../../../base/common/lifecycle.j import { mapObservableArrayCached, derived, IObservable, observableSignal, runOnChange, autorun } from '../../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; +import { IUserAttentionService } from '../../../../services/userAttention/common/userAttentionService.js'; import { AnnotatedDocument, IAnnotatedDocuments } from '../helpers/annotatedDocuments.js'; import { CreateSuggestionIdForChatOrInlineChatCaller, EditTelemetryReportEditArcForChatOrInlineChatSender, EditTelemetryReportInlineEditArcSender } from './arcTelemetrySender.js'; import { createDocWithJustReason, EditSource } from '../helpers/documentWithAnnotatedEdits.js'; @@ -41,6 +42,7 @@ export class EditSourceTrackingImpl extends Disposable { class TrackedDocumentInfo extends Disposable { public readonly longtermTracker: IObservable | undefined>; public readonly windowedTracker: IObservable | undefined>; + public readonly windowedFocusTracker: IObservable | undefined>; private readonly _repo: IObservable; @@ -51,6 +53,7 @@ class TrackedDocumentInfo extends Disposable { @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IRandomService private readonly _randomService: IRandomService, + @IUserAttentionService private readonly _userAttentionService: IUserAttentionService, ) { super(); @@ -66,10 +69,12 @@ class TrackedDocumentInfo extends Disposable { longtermResetSignal.read(reader); const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + const startFocusTime = this._userAttentionService.totalFocusTimeMs; + const startTime = Date.now(); reader.store.add(toDisposable(() => { // send long term document telemetry if (!t.isEmpty()) { - this.sendTelemetry('longterm', longtermReason, t); + this.sendTelemetry('longterm', longtermReason, t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); } t.dispose(); })); @@ -104,6 +109,7 @@ class TrackedDocumentInfo extends Disposable { this._store.add(this._instantiationService.createInstance(EditTelemetryReportEditArcForChatOrInlineChatSender, _doc.documentWithAnnotations, this._repo)); this._store.add(this._instantiationService.createInstance(CreateSuggestionIdForChatOrInlineChatCaller, _doc.documentWithAnnotations)); + // Wall-clock time based 5-minute window tracker const resetSignal = observableSignal('resetSignal'); this.windowedTracker = derived((reader) => { @@ -114,15 +120,45 @@ class TrackedDocumentInfo extends Disposable { } resetSignal.read(reader); + // Reset after 5 minutes of wall-clock time reader.store.add(new TimeoutTimer(() => { - // Reset after 5 minutes resetSignal.trigger(undefined); }, 5 * 60 * 1000)); const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + const startFocusTime = this._userAttentionService.totalFocusTimeMs; + const startTime = Date.now(); reader.store.add(toDisposable(async () => { - // send long term document telemetry - this.sendTelemetry('5minWindow', 'time', t); + // send windowed document telemetry + this.sendTelemetry('5minWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); + t.dispose(); + })); + + return t; + }).recomputeInitiallyAndOnChange(this._store); + + // Focus time based 20-minute window tracker + const focusResetSignal = observableSignal('focusResetSignal'); + + this.windowedFocusTracker = derived((reader) => { + if (!this._statsEnabled.read(reader)) { return undefined; } + + if (!this._doc.isVisible.read(reader)) { + return undefined; + } + focusResetSignal.read(reader); + + // Reset after 20 minutes of accumulated focus time + reader.store.add(this._userAttentionService.fireAfterGivenFocusTimePassed(20 * 60 * 1000, () => { + focusResetSignal.trigger(undefined); + })); + + const t = reader.store.add(new DocumentEditSourceTracker(docWithJustReason, undefined)); + const startFocusTime = this._userAttentionService.totalFocusTimeMs; + const startTime = Date.now(); + reader.store.add(toDisposable(async () => { + // send focus-windowed document telemetry + this.sendTelemetry('20minFocusWindow', 'time', t, this._userAttentionService.totalFocusTimeMs - startFocusTime, Date.now() - startTime); t.dispose(); })); @@ -131,7 +167,7 @@ class TrackedDocumentInfo extends Disposable { } - async sendTelemetry(mode: 'longterm' | '5minWindow', trigger: string, t: DocumentEditSourceTracker) { + async sendTelemetry(mode: 'longterm' | '5minWindow' | '20minFocusWindow', trigger: string, t: DocumentEditSourceTracker, focusTime: number, actualTime: number) { const ranges = t.getTrackedRanges(); const keys = t.getAllKeys(); if (keys.length === 0) { @@ -178,9 +214,9 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCount: number; }, { owner: 'hediet'; - comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 5-minute windows for visible documents or longer periods ending on branch changes, commits, or 10-hour intervals. This event complements editSources.stats by providing source-specific details. @sentToGitHub'; + comment: 'Provides detailed character count breakdown for individual edit sources (typing, paste, inline completions, NES, etc.) within a session. Reports the top 10-30 sources per session with granular metadata including extension IDs and model IDs for AI edits. Sessions are scoped to either 5-minute wall-clock time windows, 20-minute focus time windows for visible documents, or longer periods ending on branch changes, commits, or 10-hour intervals. Focus time is computed as the accumulated time where VS Code has focus and there was recent user activity (within the last minute). This event complements editSources.stats by providing source-specific details. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\' or \'5minWindow\'.' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Describes the session mode. Is either \'longterm\', \'5minWindow\', or \'20minFocusWindow\'.' }; sourceKey: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A description of the source of the edit.' }; sourceKeyCleaned: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source of the edit with some properties (such as extensionId, extensionVersion and modelId) removed.' }; @@ -231,11 +267,14 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCharacters: number; externalModifiedCount: number; isTrackedByGit: number; + focusTime: number; + actualTime: number; + trigger: string; }, { owner: 'hediet'; - comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (5 minutes for visible documents, 10 hours otherwise). Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; + comment: 'Aggregates character counts by edit source category (user typing, AI completions, NES, IDE actions, external changes) for each editing session. Sessions represent units of work and end when documents close, branches change, commits occur, or time limits are reached (5 minutes of wall-clock time, 20 minutes of focus time for visible documents, or 10 hours otherwise). Focus time is computed as accumulated 1-minute blocks where VS Code has focus and there was recent user activity. Tracks both total characters inserted and characters remaining at session end to measure retention. This high-level summary complements editSources.details which provides granular per-source breakdowns. @sentToGitHub'; - mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm or 5minWindow' }; + mode: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'longterm, 5minWindow, or 20minFocusWindow' }; languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language id of the document.' }; statsUuid: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The unique identifier for the telemetry event.' }; @@ -249,6 +288,9 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCharacters: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Total modified characters'; isMeasurement: true }; externalModifiedCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Fraction of external modified characters'; isMeasurement: true }; isTrackedByGit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates if the document is tracked by git.' }; + focusTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The focus time in ms during the session.'; isMeasurement: true }; + actualTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The actual time in ms during the session.'; isMeasurement: true }; + trigger: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates why the session ended.' }; }>('editTelemetry.editSources.stats', { mode, languageId: this._doc.document.languageId.get(), @@ -263,6 +305,9 @@ class TrackedDocumentInfo extends Disposable { totalModifiedCharacters: data.totalModifiedCharactersInFinalState, externalModifiedCount: data.externalModifiedCount, isTrackedByGit: isTrackedByGit ? 1 : 0, + focusTime, + actualTime, + trigger, }); } diff --git a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts index 5bb6715d119..8c6cf105264 100644 --- a/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts +++ b/src/vs/workbench/contrib/editTelemetry/test/browser/editTelemetry.test.ts @@ -28,6 +28,9 @@ import { AiEditTelemetryServiceImpl } from '../../browser/telemetry/aiEditTeleme import { IRandomService, RandomService } from '../../browser/randomService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; +import { UserAttentionService, UserAttentionServiceEnv } from '../../../../services/userAttention/browser/userAttentionBrowser.js'; +import { IUserAttentionService } from '../../../../services/userAttention/common/userAttentionService.js'; +import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; suite('Edit Telemetry', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -36,9 +39,16 @@ suite('Edit Telemetry', () => { const disposables = new DisposableStore(); const instantiationService = disposables.add(new TestInstantiationService(new ServiceCollection( [IAiEditTelemetryService, new SyncDescriptor(AiEditTelemetryServiceImpl)], - ))); + [IUserAttentionService, new SyncDescriptor(UserAttentionService)] + ), false, undefined, true)); const sentTelemetry: unknown[] = []; + const userActive = observableValue('userActive', true); + instantiationService.stubInstance(UserAttentionServiceEnv, { + isUserActive: userActive, + isVsCodeFocused: constObservable(true), + dispose: () => { } + }); instantiationService.stub(ITelemetryService, { publicLog2(eventName, data) { sentTelemetry.push(`${formatTime(Date.now())} ${eventName}: ${JSON.stringify(data)}`); @@ -48,6 +58,7 @@ suite('Edit Telemetry', () => { instantiationService.stubInstance(ScmAdapter, { getRepo: (uri, reader) => undefined, }); instantiationService.stubInstance(UriVisibilityProvider, { isVisible: (uri, reader) => true, }); instantiationService.stub(IRandomService, new DeterministicRandomService()); + instantiationService.stub(ILogService, new NullLogService()); const w = new MutableObservableWorkspace(); const docs = disposables.add(new AnnotatedDocuments(w, instantiationService)); @@ -87,21 +98,28 @@ function fib(n) { d1.applyEdit(StringEditWithReason.replace(d1.findRange('Computes the nth fibonacci number'), 'Berechnet die nte Fibonacci Zahl', chatEdit)); - await timeout(6 * 60 * 1000); - - assert.deepStrictEqual(sentTelemetry, [ - '00:01:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e","didBranchChange":0,"timeDelayMs":0,"originalCharCount":37,"originalLineCount":1,"originalDeletedLineCount":0,"arc":37,"currentLineCount":1,"currentDeletedLineCount":0}', - '00:01:010 editTelemetry.codeSuggested: {"eventId":"evt-055ed5f5-c723-4ede-ba79-cccd7685c7ad","suggestionId":"sgt-f645627a-cacf-477a-9164-ecd6125616a5","presentation":"highlightedEdit","feature":"sideBarChat","languageId":"plaintext","editCharsInserted":37,"editCharsDeleted":0,"editLinesInserted":1,"editLinesDeleted":0,"modelId":{"isTrustedTelemetryValue":true}}', - '00:11:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"1eb8a394-2489-41c2-851b-6a79432fc6bc","didBranchChange":0,"timeDelayMs":0,"originalCharCount":19,"originalLineCount":1,"originalDeletedLineCount":1,"arc":19,"currentLineCount":1,"currentDeletedLineCount":1}', - '00:11:010 editTelemetry.codeSuggested: {"eventId":"evt-5c9c6fe7-b219-4ff8-aaa7-ab2b355b21c0","suggestionId":"sgt-74379122-0452-4e26-9c38-9d62f1e7ae73","presentation":"highlightedEdit","feature":"sideBarChat","languageId":"plaintext","editCharsInserted":19,"editCharsDeleted":20,"editLinesInserted":1,"editLinesDeleted":1,"modelId":{"isTrustedTelemetryValue":true}}', - '01:01:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e","didBranchChange":0,"timeDelayMs":60000,"originalCharCount":37,"originalLineCount":1,"originalDeletedLineCount":0,"arc":16,"currentLineCount":1,"currentDeletedLineCount":0}', - '01:11:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"1eb8a394-2489-41c2-851b-6a79432fc6bc","didBranchChange":0,"timeDelayMs":60000,"originalCharCount":19,"originalLineCount":1,"originalDeletedLineCount":1,"arc":19,"currentLineCount":1,"currentDeletedLineCount":1}', - '05:00:000 editTelemetry.editSources.details: {"mode":"5minWindow","sourceKey":"source:Chat.applyEdits","sourceKeyCleaned":"source:Chat.applyEdits","trigger":"time","languageId":"plaintext","statsUuid":"509b5d53-9109-40a2-bdf5-1aa735a229fe","modifiedCount":35,"deltaModifiedCount":56,"totalModifiedCount":39}', - '05:00:000 editTelemetry.editSources.details: {"mode":"5minWindow","sourceKey":"source:cursor-kind:type","sourceKeyCleaned":"source:cursor-kind:type","trigger":"time","languageId":"plaintext","statsUuid":"509b5d53-9109-40a2-bdf5-1aa735a229fe","modifiedCount":4,"deltaModifiedCount":4,"totalModifiedCount":39}', - '05:00:000 editTelemetry.editSources.stats: {"mode":"5minWindow","languageId":"plaintext","statsUuid":"509b5d53-9109-40a2-bdf5-1aa735a229fe","nesModifiedCount":0,"inlineCompletionsCopilotModifiedCount":0,"inlineCompletionsNESModifiedCount":0,"otherAIModifiedCount":35,"unknownModifiedCount":0,"userModifiedCount":4,"ideModifiedCount":0,"totalModifiedCharacters":39,"externalModifiedCount":0,"isTrackedByGit":0}', - '05:01:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e","didBranchChange":0,"timeDelayMs":300000,"originalCharCount":37,"originalLineCount":1,"originalDeletedLineCount":0,"arc":16,"currentLineCount":1,"currentDeletedLineCount":0}', - '05:11:010 editTelemetry.reportEditArc: {"sourceKeyCleaned":"source:Chat.applyEdits","languageId":"plaintext","uniqueEditId":"1eb8a394-2489-41c2-851b-6a79432fc6bc","didBranchChange":0,"timeDelayMs":300000,"originalCharCount":19,"originalLineCount":1,"originalDeletedLineCount":1,"arc":19,"currentLineCount":1,"currentDeletedLineCount":1}', - ]); + await timeout(3 * 60 * 1000); + userActive.set(false, undefined); + await timeout(3 * 60 * 1000); + userActive.set(true, undefined); + await timeout(18 * 60 * 1000); + + assert.deepStrictEqual(sentTelemetry, ([ + '00:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":0,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":37,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', + '00:01:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-055ed5f5-c723-4ede-ba79-cccd7685c7ad\",\"suggestionId\":\"sgt-f645627a-cacf-477a-9164-ecd6125616a5\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":37,\"editCharsDeleted\":0,\"editLinesInserted\":1,\"editLinesDeleted\":0,\"modelId\":{\"isTrustedTelemetryValue\":true}}', + '00:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":0,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', + '00:11:010 editTelemetry.codeSuggested: {\"eventId\":\"evt-5c9c6fe7-b219-4ff8-aaa7-ab2b355b21c0\",\"suggestionId\":\"sgt-74379122-0452-4e26-9c38-9d62f1e7ae73\",\"presentation\":\"highlightedEdit\",\"feature\":\"sideBarChat\",\"languageId\":\"plaintext\",\"editCharsInserted\":19,\"editCharsDeleted\":20,\"editLinesInserted\":1,\"editLinesDeleted\":1,\"modelId\":{\"isTrustedTelemetryValue\":true}}', + '01:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', + '01:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":60000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', + '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', + '05:00:000 editTelemetry.editSources.details: {\"mode\":\"5minWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', + '05:00:000 editTelemetry.editSources.stats: {\"mode\":\"5minWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"509b5d53-9109-40a2-bdf5-1aa735a229fe\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":250010,\"actualTime\":300000,\"trigger\":\"time\"}', + '05:01:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"8c97b7d8-9adb-4bd8-ac9f-a562704ce40e\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":37,\"originalLineCount\":1,\"originalDeletedLineCount\":0,\"arc\":16,\"currentLineCount\":1,\"currentDeletedLineCount\":0}', + '05:11:010 editTelemetry.reportEditArc: {\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"languageId\":\"plaintext\",\"uniqueEditId\":\"1eb8a394-2489-41c2-851b-6a79432fc6bc\",\"didBranchChange\":0,\"timeDelayMs\":300000,\"originalCharCount\":19,\"originalLineCount\":1,\"originalDeletedLineCount\":1,\"arc\":19,\"currentLineCount\":1,\"currentDeletedLineCount\":1}', + '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:Chat.applyEdits\",\"sourceKeyCleaned\":\"source:Chat.applyEdits\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":35,\"deltaModifiedCount\":56,\"totalModifiedCount\":39}', + '22:00:000 editTelemetry.editSources.details: {\"mode\":\"20minFocusWindow\",\"sourceKey\":\"source:cursor-kind:type\",\"sourceKeyCleaned\":\"source:cursor-kind:type\",\"trigger\":\"time\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"modifiedCount\":4,\"deltaModifiedCount\":4,\"totalModifiedCount\":39}', + '22:00:000 editTelemetry.editSources.stats: {\"mode\":\"20minFocusWindow\",\"languageId\":\"plaintext\",\"statsUuid\":\"a794406a-7779-4e9f-a856-1caca85123c7\",\"nesModifiedCount\":0,\"inlineCompletionsCopilotModifiedCount\":0,\"inlineCompletionsNESModifiedCount\":0,\"otherAIModifiedCount\":35,\"unknownModifiedCount\":0,\"userModifiedCount\":4,\"ideModifiedCount\":0,\"totalModifiedCharacters\":39,\"externalModifiedCount\":0,\"isTrackedByGit\":0,\"focusTime\":1200000,\"actualTime\":1320000,\"trigger\":\"time\"}' + ])); disposables.dispose(); })); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 0346db34828..4af65c2deba 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -664,12 +664,24 @@ export class ExtensionEditor extends EditorPane { } private open(id: string, extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + // Setup common container structure for all tabs + const details = append(template.content, $('.details')); + const contentContainer = append(details, $('.content-container')); + const additionalDetailsContainer = append(details, $('.additional-details-container')); + + const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500); + layout(); + this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout }))); + + // Render additional details synchronously to avoid flicker + this.renderAdditionalDetails(additionalDetailsContainer, extension); + switch (id) { - case ExtensionEditorTab.Readme: return this.openDetails(extension, template, token); - case ExtensionEditorTab.Features: return this.openFeatures(template, token); - case ExtensionEditorTab.Changelog: return this.openChangelog(extension, template, token); - case ExtensionEditorTab.Dependencies: return this.openExtensionDependencies(extension, template, token); - case ExtensionEditorTab.ExtensionPack: return this.openExtensionPack(extension, template, token); + case ExtensionEditorTab.Readme: return this.openDetails(extension, contentContainer, token); + case ExtensionEditorTab.Features: return this.openFeatures(extension, contentContainer, token); + case ExtensionEditorTab.Changelog: return this.openChangelog(extension, contentContainer, token); + case ExtensionEditorTab.Dependencies: return this.openExtensionDependencies(extension, contentContainer, token); + case ExtensionEditorTab.ExtensionPack: return this.openExtensionPack(extension, contentContainer, token); } return Promise.resolve(null); } @@ -835,24 +847,15 @@ export class ExtensionEditor extends EditorPane { `; } - private async openDetails(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise { - const details = append(template.content, $('.details')); - const readmeContainer = append(details, $('.readme-container')); - const additionalDetailsContainer = append(details, $('.additional-details-container')); - - const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500); - layout(); - this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout }))); - + private async openDetails(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise { let activeElement: IActiveElement | null = null; const manifest = await this.extensionManifest!.get().promise; if (manifest && manifest.extensionPack?.length && this.shallRenderAsExtensionPack(manifest)) { - activeElement = await this.openExtensionPackReadme(extension, manifest, readmeContainer, token); + activeElement = await this.openExtensionPackReadme(extension, manifest, contentContainer, token); } else { - activeElement = await this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), readmeContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token); + activeElement = await this.openMarkdown(extension, this.extensionReadme!.get(), localize('noReadme', "No README available."), contentContainer, WebviewIndex.Readme, localize('Readme title', "Readme"), token); } - this.renderAdditionalDetails(additionalDetailsContainer, extension); return activeElement; } @@ -870,21 +873,35 @@ export class ExtensionEditor extends EditorPane { extensionPackReadme.style.maxWidth = '882px'; const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' })); - if (manifest.extensionPack!.length <= 3) { - extensionPackReadme.classList.add('one-row'); - } else if (manifest.extensionPack!.length <= 6) { - extensionPackReadme.classList.add('two-rows'); - } else if (manifest.extensionPack!.length <= 9) { - extensionPackReadme.classList.add('three-rows'); - } else { - extensionPackReadme.classList.add('more-rows'); - } + + const packCount = manifest.extensionPack!.length; + const headerHeight = 37; // navbar height + const contentMinHeight = 200; // minimum height for readme content + + const layout = () => { + extensionPackReadme.classList.remove('one-row', 'two-rows', 'three-rows', 'more-rows'); + const availableHeight = container.clientHeight; + const availableForPack = Math.max(availableHeight - headerHeight - contentMinHeight, 0); + let rowClass = 'one-row'; + if (availableForPack >= 302 && packCount > 6) { + rowClass = 'more-rows'; + } else if (availableForPack >= 282 && packCount > 4) { + rowClass = 'three-rows'; + } else if (availableForPack >= 200 && packCount > 2) { + rowClass = 'two-rows'; + } else { + rowClass = 'one-row'; + } + extensionPackReadme.classList.add(rowClass); + }; + + layout(); + this.contentDisposables.add(toDisposable(arrays.insert(this.layoutParticipants, { layout }))); const extensionPackHeader = append(extensionPack, $('div.header')); extensionPackHeader.textContent = localize('extension pack', "Extension Pack ({0})", manifest.extensionPack!.length); const extensionPackContent = append(extensionPack, $('div', { class: 'extension-pack-content' })); extensionPackContent.setAttribute('tabindex', '0'); - append(extensionPack, $('div.footer')); const readmeContent = append(extensionPackReadme, $('div.readme-content')); await Promise.all([ @@ -909,12 +926,14 @@ export class ExtensionEditor extends EditorPane { scrollableContent.scanDomNode(); } - private openChangelog(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise { - return this.openMarkdown(extension, this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), template.content, WebviewIndex.Changelog, localize('Changelog title', "Changelog"), token); + private async openChangelog(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise { + const activeElement = await this.openMarkdown(extension, this.extensionChangelog!.get(), localize('noChangelog', "No Changelog available."), contentContainer, WebviewIndex.Changelog, localize('Changelog title', "Changelog"), token); + + return activeElement; } - private async openFeatures(template: IExtensionEditorTemplate, token: CancellationToken): Promise { - const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content); + private async openFeatures(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise { + const manifest = await this.loadContents(() => this.extensionManifest!.get(), contentContainer); if (token.isCancellationRequested) { return null; } @@ -923,27 +942,28 @@ export class ExtensionEditor extends EditorPane { } const extensionFeaturesTab = this.contentDisposables.add(this.instantiationService.createInstance(ExtensionFeaturesTab, manifest, (this.options)?.feature)); - const layout = () => extensionFeaturesTab.layout(template.content.clientHeight, template.content.clientWidth); - const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + const featureLayout = () => extensionFeaturesTab.layout(contentContainer.clientHeight, contentContainer.clientWidth); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: featureLayout }); this.contentDisposables.add(toDisposable(removeLayoutParticipant)); - append(template.content, extensionFeaturesTab.domNode); - layout(); + append(contentContainer, extensionFeaturesTab.domNode); + featureLayout(); + return extensionFeaturesTab.domNode; } - private openExtensionDependencies(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + private openExtensionDependencies(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise { if (token.isCancellationRequested) { return Promise.resolve(null); } if (arrays.isFalsyOrEmpty(extension.dependencies)) { - append(template.content, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); - return Promise.resolve(template.content); + append(contentContainer, $('p.nocontent')).textContent = localize('noDependencies', "No Dependencies"); + return Promise.resolve(contentContainer); } const content = $('div', { class: 'subcontent' }); const scrollableContent = new DomScrollableElement(content, {}); - append(template.content, scrollableContent.getDomNode()); + append(contentContainer, scrollableContent.getDomNode()); this.contentDisposables.add(scrollableContent); const dependenciesTree = this.instantiationService.createInstance(ExtensionsTree, @@ -951,31 +971,34 @@ export class ExtensionEditor extends EditorPane { { listBackground: editorBackground }); - const layout = () => { + const depLayout = () => { scrollableContent.scanDomNode(); const scrollDimensions = scrollableContent.getScrollDimensions(); dependenciesTree.layout(scrollDimensions.height); }; - const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout }); + const removeLayoutParticipant = arrays.insert(this.layoutParticipants, { layout: depLayout }); this.contentDisposables.add(toDisposable(removeLayoutParticipant)); this.contentDisposables.add(dependenciesTree); scrollableContent.scanDomNode(); + return Promise.resolve({ focus() { dependenciesTree.domFocus(); } }); } - private async openExtensionPack(extension: IExtension, template: IExtensionEditorTemplate, token: CancellationToken): Promise { + private async openExtensionPack(extension: IExtension, contentContainer: HTMLElement, token: CancellationToken): Promise { if (token.isCancellationRequested) { return Promise.resolve(null); } - const manifest = await this.loadContents(() => this.extensionManifest!.get(), template.content); + + const manifest = await this.loadContents(() => this.extensionManifest!.get(), contentContainer); if (token.isCancellationRequested) { return null; } if (!manifest) { return null; } - return this.renderExtensionPack(manifest, template.content, token); + + return this.renderExtensionPack(manifest, contentContainer, token); } private async renderExtensionPack(manifest: IExtensionManifest, parent: HTMLElement, token: CancellationToken): Promise { @@ -1155,6 +1178,8 @@ class AdditionalDetailsWidget extends Disposable { ); if (isNative && extension.source === 'resource' && extension.location.scheme === Schemas.file) { element.classList.add('link'); + element.tabIndex = 0; + element.setAttribute('role', 'link'); element.title = extension.location.fsPath; this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true }))); } @@ -1169,6 +1194,8 @@ class AdditionalDetailsWidget extends Disposable { ); if (isNative && extension.location.scheme === Schemas.file) { element.classList.add('link'); + element.tabIndex = 0; + element.setAttribute('role', 'link'); element.title = extension.location.fsPath; this.disposables.add(onClick(element, () => this.openerService.open(extension.location, { openExternal: true }))); } @@ -1189,6 +1216,8 @@ class AdditionalDetailsWidget extends Disposable { ); if (isNative && extension.location.scheme === Schemas.file) { element.classList.add('link'); + element.tabIndex = 0; + element.setAttribute('role', 'link'); element.title = cacheLocation.fsPath; this.disposables.add(onClick(element, () => this.openerService.open(cacheLocation.with({ scheme: Schemas.file }), { openExternal: true }))); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 921a24197d5..6956d7918b3 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -51,7 +51,7 @@ import { ResourceContextKey, WorkbenchStateContext } from '../../../common/conte import { IWorkbenchContribution, IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; import { EditorExtensions } from '../../../common/editor.js'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation } from '../../../common/views.js'; -import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/common/defaultAccount.js'; +import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; @@ -62,7 +62,7 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer import { CONTEXT_SYNC_ENABLEMENT } from '../../../services/userDataSync/common/userDataSync.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from '../../../services/workspaces/common/workspaceTrust.js'; -import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js'; import { CONTEXT_KEYBINDINGS_EDITOR } from '../../preferences/common/preferences.js'; import { IWebview } from '../../webview/browser/webview.js'; import { Query } from '../common/extensionQuery.js'; @@ -419,7 +419,7 @@ CommandsRegistry.registerCommand({ context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, }); } else { - await extensionsWorkbenchService.install(arg, { + await extensionsWorkbenchService.install(id, { version, installPreReleaseVersion: options?.installPreReleaseVersion, context: { ...options?.context, [EXTENSION_INSTALL_SOURCE_CONTEXT]: ExtensionInstallSource.COMMAND }, @@ -1207,7 +1207,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi this.registerExtensionAction({ id: `extensions.sort.${id}`, title, - precondition: ContextKeyExpr.and(precondition, ContextKeyExpr.regex(ExtensionsSearchValueContext.key, /^@feature:/).negate(), sortCapabilityContext), + precondition: ContextKeyExpr.and(precondition, ContextKeyExpr.regex(ExtensionsSearchValueContext.key, /^@contribute:/).negate(), sortCapabilityContext), menu: [{ id: extensionsSortSubMenu, when: ContextKeyExpr.and(ContextKeyExpr.or(CONTEXT_HAS_GALLERY, DefaultViewsContext), sortCapabilityContext), @@ -2024,6 +2024,7 @@ class ExtensionToolsContribution extends Disposable implements IWorkbenchContrib super(); const searchExtensionsTool = instantiationService.createInstance(SearchExtensionsTool); this._register(toolsService.registerTool(SearchExtensionsToolData, searchExtensionsTool)); + this._register(toolsService.vscodeToolSet.addTool(SearchExtensionsToolData)); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 8daf558a519..f2a1f70f54f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -29,7 +29,7 @@ import { CommandsRegistry, ICommandService } from '../../../../platform/commands import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { buttonBackground, buttonForeground, buttonHoverBackground, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator } from '../../../../platform/theme/common/colorRegistry.js'; +import { buttonBackground, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, buttonSecondaryBorder, registerColor, editorWarningForeground, editorInfoForeground, editorErrorForeground, buttonSeparator } from '../../../../platform/theme/common/colorRegistry.js'; import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; import { ITextEditorSelection } from '../../../../platform/editor/common/editor.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; @@ -290,7 +290,6 @@ export abstract class ExtensionAction extends Action implements IExtensionContai static readonly EXTENSION_ACTION_CLASS = 'extension-action'; static readonly TEXT_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} text`; static readonly LABEL_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} label`; - static readonly PROMINENT_LABEL_ACTION_CLASS = `${ExtensionAction.LABEL_ACTION_CLASS} prominent`; static readonly ICON_ACTION_CLASS = `${ExtensionAction.EXTENSION_ACTION_CLASS} icon`; private _extension: IExtension | null = null; @@ -952,7 +951,7 @@ export class UninstallAction extends ExtensionAction { export class UpdateAction extends ExtensionAction { - private static readonly EnabledClass = `${this.LABEL_ACTION_CLASS} prominent update`; + private static readonly EnabledClass = `${this.LABEL_ACTION_CLASS} update`; private static readonly DisabledClass = `${this.EnabledClass} disabled`; private readonly updateThrottler = new Throttler(); @@ -1495,7 +1494,7 @@ export class TogglePreReleaseExtensionAction extends ExtensionAction { static readonly ID = 'workbench.extensions.action.togglePreRlease'; static readonly LABEL = localize('togglePreRleaseLabel', "Pre-Release"); - private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} pre-release`; + private static readonly EnabledClass = `${ExtensionAction.LABEL_ACTION_CLASS} prominent pre-release`; private static readonly DisabledClass = `${this.EnabledClass} hide`; constructor( @@ -3177,26 +3176,33 @@ CommandsRegistry.registerCommand(showExtensionsWithIdsCommandId, function (acces }); registerColor('extensionButton.background', { - dark: buttonBackground, - light: buttonBackground, + dark: buttonSecondaryBackground, + light: buttonSecondaryBackground, hcDark: null, hcLight: null }, localize('extensionButtonBackground', "Button background color for extension actions.")); registerColor('extensionButton.foreground', { - dark: buttonForeground, - light: buttonForeground, + dark: buttonSecondaryForeground, + light: buttonSecondaryForeground, hcDark: null, hcLight: null }, localize('extensionButtonForeground', "Button foreground color for extension actions.")); registerColor('extensionButton.hoverBackground', { - dark: buttonHoverBackground, - light: buttonHoverBackground, + dark: buttonSecondaryHoverBackground, + light: buttonSecondaryHoverBackground, hcDark: null, hcLight: null }, localize('extensionButtonHoverBackground', "Button background hover color for extension actions.")); +registerColor('extensionButton.border', { + dark: buttonSecondaryBorder, + light: buttonSecondaryBorder, + hcDark: null, + hcLight: null +}, localize('extensionButtonBorder', "Button border color for extension actions.")); + registerColor('extensionButton.separator', buttonSeparator, localize('extensionButtonSeparator', "Button separator color for extension actions")); export const extensionButtonProminentBackground = registerColor('extensionButton.prominentBackground', { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 62485d9eade..942a2f4b91b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -68,7 +68,8 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IExtensionGalleryManifest, IExtensionGalleryManifestService, ExtensionGalleryManifestStatus } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { URI } from '../../../../base/common/uri.js'; -import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/common/defaultAccount.js'; +import { DEFAULT_ACCOUNT_SIGN_IN_COMMAND } from '../../../services/accounts/browser/defaultAccount.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); export const SearchMarketplaceExtensionsContext = new RawContextKey('searchMarketplaceExtensions', false); @@ -562,6 +563,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer status.dismiss())); this.notificationDisposables.value.add(addDisposableListener(dismissAction, EventType.KEY_DOWN, (e: KeyboardEvent) => { const standardKeyboardEvent = new StandardKeyboardEvent(e); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 3e1d8735e2e..7e3638e77ab 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -400,12 +400,8 @@ export class ExtensionsListView extends AbstractExtensionsListView { extensions = this.filterRecentlyUpdatedExtensions(local, query, options); } - else if (/@feature:/i.test(query.value)) { - const result = this.filterExtensionsByFeature(local, query); - if (result) { - extensions = result.extensions; - description = result.description; - } + else if (/@contribute:/i.test(query.value)) { + extensions = this.filterExtensionsByFeature(local, query); } else if (includeBuiltin) { @@ -665,12 +661,12 @@ export class ExtensionsListView extends AbstractExtensionsListView { return this.sortExtensions(result, options); } - private filterExtensionsByFeature(local: IExtension[], query: Query): { extensions: IExtension[]; description: string } | undefined { - const value = query.value.replace(/@feature:/g, '').trim(); + private filterExtensionsByFeature(local: IExtension[], query: Query): IExtension[] { + const value = query.value.replace(/@contribute:/g, '').trim(); const featureId = value.split(' ')[0]; const feature = Registry.as(Extensions.ExtensionFeaturesRegistry).getExtensionFeature(featureId); if (!feature) { - return undefined; + return []; } if (this.extensionsViewState) { this.extensionsViewState.filters.featureId = featureId; @@ -688,10 +684,7 @@ export class ExtensionsListView extends AbstractExtensionsListView { result.push([e, accessData?.accessTimes.length ?? 0]); } } - return { - extensions: result.sort(([, a], [, b]) => b - a).map(([e]) => e), - description: localize('showingExtensionsForFeature', "Extensions using {0} in the last 30 days", feature.label) - }; + return result.sort(([, a], [, b]) => b - a).map(([e]) => e); } finally { renderer?.dispose(); } @@ -1262,7 +1255,7 @@ export class ExtensionsListView extends AbstractExtensionsListView { } static isFeatureExtensionsQuery(query: string): boolean { - return /@feature:/i.test(query); + return /@contribute:/i.test(query); } override focus(): void { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 2f1c096f1aa..7131a2bc887 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -76,6 +76,7 @@ export function onClick(element: HTMLElement, callback: () => void): IDisposable export class ExtensionIconWidget extends ExtensionWidget { private readonly iconLoadingDisposable = this._register(new MutableDisposable()); + private readonly iconErrorDisposable = this._register(new MutableDisposable()); private readonly element: HTMLElement; private readonly iconElement: HTMLImageElement; private readonly defaultIconElement: HTMLElement; @@ -103,6 +104,7 @@ export class ExtensionIconWidget extends ExtensionWidget { this.iconElement.src = ''; this.iconElement.style.display = 'none'; this.defaultIconElement.style.display = 'none'; + this.iconErrorDisposable.clear(); this.iconLoadingDisposable.clear(); } @@ -117,7 +119,7 @@ export class ExtensionIconWidget extends ExtensionWidget { this.iconElement.style.display = 'inherit'; this.defaultIconElement.style.display = 'none'; this.iconUrl = this.extension.iconUrl; - this.iconLoadingDisposable.value = addDisposableListener(this.iconElement, 'error', () => { + this.iconErrorDisposable.value = addDisposableListener(this.iconElement, 'error', () => { if (this.extension?.iconUrlFallback) { this.iconElement.src = this.extension.iconUrlFallback; } else { @@ -128,7 +130,9 @@ export class ExtensionIconWidget extends ExtensionWidget { this.iconElement.src = this.iconUrl; if (!this.iconElement.complete) { this.iconElement.style.visibility = 'hidden'; - this.iconElement.onload = () => this.iconElement.style.visibility = 'inherit'; + this.iconLoadingDisposable.value = addDisposableListener(this.iconElement, 'load', () => { + this.iconElement.style.visibility = 'inherit'; + }); } else { this.iconElement.style.visibility = 'inherit'; } @@ -138,6 +142,7 @@ export class ExtensionIconWidget extends ExtensionWidget { this.iconElement.style.display = 'none'; this.iconElement.src = ''; this.defaultIconElement.style.display = 'inherit'; + this.iconErrorDisposable.clear(); this.iconLoadingDisposable.clear(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extension.css b/src/vs/workbench/contrib/extensions/browser/media/extension.css index 9bda6c319fc..0ba9d0381a8 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extension.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extension.css @@ -269,13 +269,13 @@ /* single install */ .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item.empty > .extension-action { - border-radius: 2px; + border-radius: var(--vscode-cornerRadius-small); } /* split install */ .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .extension-action.label:not(.dropdown) { - border-radius: 2px 0 0 2px; + border-radius: var(--vscode-cornerRadius-small) 0 0 var(--vscode-cornerRadius-small); } .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item > .monaco-dropdown .extension-action { - border-radius: 0 2px 2px 0; + border-radius: 0 var(--vscode-cornerRadius-small) var(--vscode-cornerRadius-small) 0; } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index ed8c3395ccc..6326d45f650 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -37,19 +37,47 @@ .monaco-action-bar .action-item .action-label.extension-action.label, .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator { - background-color: var(--vscode-extensionButton-background) !important; + background-color: var(--vscode-extensionButton-background); + border: 1px solid var(--vscode-extensionButton-border, transparent); } +.monaco-action-bar .action-item.action-dropdown-item > .action-label.extension-action.label { + border-right-width: 0; +} + +.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown .action-label.extension-action.label { + border-left-width: 0; +} + +.monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator { + border-left-width: 0; + border-right-width: 0; +} + +.monaco-action-bar .action-item.action-dropdown-item.empty > .action-label.extension-action.label { + border-right-width: 1px; +} + +.monaco-action-bar .action-item.action-dropdown-item.empty > .action-dropdown-item-separator { + display: none; +} + +.monaco-list-row.focused .extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label, +.monaco-list-row.selected .extension-list-item .monaco-action-bar .action-item .action-label.extension-action.label, .monaco-action-bar .action-item .action-label.extension-action.label { - color: var(--vscode-extensionButton-foreground) !important; + color: var(--vscode-extensionButton-foreground); } .monaco-action-bar .action-item:not(.disabled) .action-label.extension-action.label:hover { - background-color: var(--vscode-extensionButton-hoverBackground) !important; + background-color: var(--vscode-extensionButton-hoverBackground); } .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div { - background-color: var(--vscode-extensionButton-separator); + background-color: var(--vscode-extensionButton-border, var(--vscode-extensionButton-separator)); +} + +.vscode-high-contrast .monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div { + background-color: var(--vscode-button-border); } .monaco-action-bar .action-item .action-label.extension-action.label.prominent, @@ -65,15 +93,6 @@ background-color: var(--vscode-extensionButton-prominentHoverBackground); } -.monaco-action-bar .action-item .action-label.extension-action:not(.disabled) { - border: 1px solid var(--vscode-contrastBorder); -} - -.monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator { - border-top: 1px solid var(--vscode-contrastBorder); - border-bottom: 1px solid var(--vscode-contrastBorder); -} - .monaco-action-bar .action-item .action-label.extension-action.extension-status-error::before { color: var(--vscode-editorError-foreground); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index c7f8ebbccb8..2e1cece4685 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -131,9 +131,8 @@ .extension-editor > .header > .details > .subtitle { padding-top: 6px; - white-space: nowrap; - height: 20px; line-height: 20px; + flex-wrap: wrap; } .extension-editor > .header > .details > .subtitle .hide { @@ -179,9 +178,6 @@ .extension-editor > .header > .details > .description { margin-top: 10px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; } .extension-editor > .header > .details > .actions-status-container { @@ -197,6 +193,10 @@ text-align: initial; } +.extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container { + flex-wrap: wrap; +} + .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item { margin-right: 0; overflow: hidden; @@ -246,7 +246,6 @@ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item .extension-action.label { - font-weight: 600; max-width: 300px; } @@ -269,17 +268,17 @@ /* single install */ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item.empty > .extension-action.label { - border-radius: 2px; + border-radius: 4px; } /* split install */ .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .extension-action.label { - border-radius: 2px 0 0 2px; + border-radius: 4px 0 0 4px; } .extension-editor > .header > .details > .actions-status-container > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item:not(.empty) > .monaco-dropdown .extension-action.label { border-left-width: 0; - border-radius: 0 2px 2px 0; + border-radius: 0 4px 4px 0; padding: 0 2px; } @@ -431,16 +430,17 @@ display: flex; } -.extension-editor > .body > .content > .details > .readme-container { +.extension-editor > .body > .content > .details > .content-container { margin: 0px auto; max-width: 75%; height: 100%; flex: 1; } -.extension-editor > .body > .content > .details.narrow > .readme-container { +.extension-editor > .body > .content > .details.narrow > .content-container { margin: inherit; max-width: inherit; + min-width: 0; } .extension-editor > .body > .content > .details > .additional-details-container { @@ -526,67 +526,67 @@ padding: 0px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme { +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme { height: 100%; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack { - height: 224px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme > .extension-pack { + height: 200px; padding-left: 20px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.one-row > .extension-pack { - height: 142px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.one-row > .extension-pack { + height: 118px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.two-rows > .extension-pack { - height: 224px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.two-rows > .extension-pack { + height: 200px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.three-rows > .extension-pack { - height: 306px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.three-rows > .extension-pack { + height: 282px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.more-rows > .extension-pack { - height: 326px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.more-rows > .extension-pack { + height: 302px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.one-row > .readme-content { - height: calc(100% - 142px); +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.one-row > .readme-content { + height: calc(100% - 118px); } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.two-rows > .readme-content { - height: calc(100% - 224px); +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.two-rows > .readme-content { + height: calc(100% - 200px); } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.three-rows > .readme-content { - height: calc(100% - 306px); +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.three-rows > .readme-content { + height: calc(100% - 282px); } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.more-rows > .readme-content { - height: calc(100% - 326px); +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme.more-rows > .readme-content { + height: calc(100% - 302px); } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack > .header, -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack > .footer { - margin-bottom: 10px; +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme > .extension-pack > .header { margin-right: 30px; font-weight: bold; font-size: 120%; - border-bottom: 1px solid rgba(128, 128, 128, 0.22); - padding: 4px 6px; + padding: 6px; line-height: 22px; + min-width: 150px; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack > .extension-pack-content { - height: calc(100% - 60px); +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme > .extension-pack > .extension-pack-content { + height: calc(100% - 34px); + border-top: 1px solid rgba(128, 128, 128, 0.22); + border-bottom: 1px solid rgba(128, 128, 128, 0.22); } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element { +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element { height: 100%; } -.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element > .subcontent { +.extension-editor > .body > .content > .details > .content-container > .extension-pack-readme > .extension-pack > .extension-pack-content > .monaco-scrollable-element > .subcontent { height: 100%; overflow-y: scroll; box-sizing: border-box; @@ -757,7 +757,7 @@ .extension-editor .extensions-grid-view > .extension-container { width: 350px; - margin: 0 10px 20px 0; + margin: 5px 10px; } .extension-editor .extensions-grid-view .extension-list-item { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css index 98f1dd0395e..0f66f5fcc4d 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsWidgets.css @@ -87,3 +87,18 @@ border-top-color: var(--vscode-extensionButton-prominentBackground); color: var(--vscode-extensionButton-prominentForeground); } + +.hc-black .extension-bookmark .recommendation, +.hc-light .extension-bookmark .recommendation, +.hc-black .extension-bookmark .pre-release, +.hc-light .extension-bookmark .pre-release { + border-top-color: var(--vscode-contrastBorder); + color: var(--vscode-editor-background); +} + +.hc-black .extension-bookmark .recommendation .codicon, +.hc-light .extension-bookmark .recommendation .codicon, +.hc-black .extension-bookmark .pre-release .codicon, +.hc-light .extension-bookmark .pre-release .codicon { + color: var(--vscode-editor-background); +} diff --git a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts index fab81196b99..87a67e96fb6 100644 --- a/src/vs/workbench/contrib/extensions/common/extensionQuery.ts +++ b/src/vs/workbench/contrib/extensions/common/extensionQuery.ts @@ -6,6 +6,8 @@ import { IExtensionGalleryManifest } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js'; import { FilterType, SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { Extensions, IExtensionFeaturesRegistry } from '../../../services/extensionManagement/common/extensionFeatures.js'; export class Query { @@ -15,7 +17,7 @@ export class Query { static suggestions(query: string, galleryManifest: IExtensionGalleryManifest | null): string[] { - const commands = ['installed', 'updates', 'enabled', 'disabled', 'builtin']; + const commands = ['installed', 'updates', 'enabled', 'disabled', 'builtin', 'contribute']; if (galleryManifest?.capabilities.extensionQuery?.filtering?.some(c => c.name === FilterType.Featured)) { commands.push('featured'); } @@ -36,12 +38,18 @@ export class Query { } sortCommands.push('name', 'publishedDate', 'updateDate'); + const contributeCommands = []; + for (const feature of Registry.as(Extensions.ExtensionFeaturesRegistry).getExtensionFeatures()) { + contributeCommands.push(feature.id); + } + const subcommands = { 'sort': sortCommands, 'category': isCategoriesEnabled ? EXTENSION_CATEGORIES.map(c => `"${c.toLowerCase()}"`) : [], 'tag': [''], 'ext': [''], - 'id': [''] + 'id': [''], + 'contribute': contributeCommands } as const; const queryContains = (substr: string) => query.indexOf(substr) > -1; diff --git a/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts index 96808f4f361..d174c8c97e6 100644 --- a/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/installExtensionsTool.ts @@ -7,7 +7,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { localize } from '../../../../nls.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; -import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, IPreparedToolInvocation, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/tools/languageModelToolsService.js'; import { IExtensionsWorkbenchService } from './extensions.js'; export const InstallExtensionsToolId = 'vscode_installExtensions'; @@ -17,7 +17,7 @@ export const InstallExtensionsToolData: IToolData = { toolReferenceName: 'installExtensions', canBeReferencedInPrompt: true, displayName: localize('installExtensionsTool.displayName', 'Install Extensions'), - modelDescription: localize('installExtensionsTool.modelDescription', "This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is '\${ publisher }.\${ name }' for example: 'vscode.csharp'."), + modelDescription: 'This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is \'\${ publisher }.\${ name }\' for example: \'vscode.csharp\'.', userDescription: localize('installExtensionsTool.userDescription', 'Tool for installing extensions'), source: ToolDataSource.Internal, inputSchema: { diff --git a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts index 4d803b9f6f7..91541b6ac4d 100644 --- a/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts +++ b/src/vs/workbench/contrib/extensions/common/searchExtensionsTool.ts @@ -9,7 +9,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js'; -import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult, ToolDataSource, ToolProgress } from '../../chat/common/tools/languageModelToolsService.js'; import { ExtensionState, IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; @@ -17,10 +17,10 @@ export const SearchExtensionsToolId = 'vscode_searchExtensions_internal'; export const SearchExtensionsToolData: IToolData = { id: SearchExtensionsToolId, toolReferenceName: 'extensions', - canBeReferencedInPrompt: true, + legacyToolReferenceFullNames: ['extensions'], icon: ThemeIcon.fromId(Codicon.extensions.id), displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), - modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), + modelDescription: 'This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended.', userDescription: localize('searchExtensionsTool.userDescription', 'Search for VS Code extensions'), source: ToolDataSource.Internal, inputSchema: { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts index 375767927e2..48e567c92f3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/debugExtensionHostAction.ts @@ -9,6 +9,7 @@ import { randomPort } from '../../../../base/common/ports.js'; import * as nls from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { Action2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IExtensionHostDebugService } from '../../../../platform/debug/common/extensionHostDebug.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; @@ -17,6 +18,7 @@ import { IProgressService, ProgressLocation } from '../../../../platform/progres import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ActiveEditorContext } from '../../../common/contextkeys.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { INativeWorkbenchEnvironmentService } from '../../../services/environment/electron-browser/environmentService.js'; import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js'; import { IExtensionService, IExtensionInspectInfo } from '../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../services/host/browser/host.js'; @@ -28,6 +30,39 @@ interface IExtensionHostQuickPickItem extends IQuickPickItem { portInfo: IExtensionInspectInfo; } +// Shared helpers for debug actions +async function getExtensionHostPort( + extensionService: IExtensionService, + nativeHostService: INativeHostService, + dialogService: IDialogService, + productService: IProductService, +): Promise { + const inspectPorts = await extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false); + if (inspectPorts.length === 0) { + const res = await dialogService.confirm({ + message: nls.localize('restart1', "Debug Extensions"), + detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", productService.nameLong), + primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") + }); + if (res.confirmed) { + await nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + } + return undefined; + } + if (inspectPorts.length > 1) { + console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); + } + return inspectPorts[0].port; +} + +async function getRendererDebugPort( + extensionHostDebugService: IExtensionHostDebugService, + windowId: number, +): Promise { + const result = await extensionHostDebugService.attachToCurrentWindowRenderer(windowId); + return result.success ? result.port : undefined; +} + export class DebugExtensionHostInDevToolsAction extends Action2 { constructor() { super({ @@ -91,38 +126,86 @@ export class DebugExtensionHostInNewWindowAction extends Action2 { }); } - run(accessor: ServicesAccessor): void { + async run(accessor: ServicesAccessor): Promise { + const extensionService = accessor.get(IExtensionService); const nativeHostService = accessor.get(INativeHostService); const dialogService = accessor.get(IDialogService); - const extensionService = accessor.get(IExtensionService); const productService = accessor.get(IProductService); const instantiationService = accessor.get(IInstantiationService); const hostService = accessor.get(IHostService); - extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false).then(async inspectPorts => { - if (inspectPorts.length === 0) { - const res = await dialogService.confirm({ - message: nls.localize('restart1', "Debug Extensions"), - detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", productService.nameLong), - primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") - }); - if (res.confirmed) { - await nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); - } - return; - } + const port = await getExtensionHostPort(extensionService, nativeHostService, dialogService, productService); + if (port === undefined) { + return; + } - if (inspectPorts.length > 1) { - // TODO - console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); - } + const storage = instantiationService.createInstance(Storage); + storage.storeDebugOnNewWindow(port); + hostService.openWindow(); + } +} - const s = instantiationService.createInstance(Storage); - s.storeDebugOnNewWindow(inspectPorts[0].port); +export class DebugRendererInNewWindowAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.debugRenderer', + title: nls.localize2('debugRenderer', "Debug Renderer In New Window"), + category: Categories.Developer, + f1: true, + }); + } - hostService.openWindow(); + async run(accessor: ServicesAccessor): Promise { + const extensionHostDebugService = accessor.get(IExtensionHostDebugService); + const environmentService = accessor.get(INativeWorkbenchEnvironmentService); + const instantiationService = accessor.get(IInstantiationService); + const hostService = accessor.get(IHostService); + + const port = await getRendererDebugPort(extensionHostDebugService, environmentService.window.id); + if (port === undefined) { + return; + } + + const storage = instantiationService.createInstance(Storage); + storage.storeRendererDebugOnNewWindow(port); + hostService.openWindow(); + } +} + +export class DebugExtensionHostAndRendererAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.debugExtensionHostAndRenderer', + title: nls.localize2('debugExtensionHostAndRenderer', "Debug Extension Host and Renderer In New Window"), + category: Categories.Developer, + f1: true, }); } + + async run(accessor: ServicesAccessor): Promise { + const extensionService = accessor.get(IExtensionService); + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const productService = accessor.get(IProductService); + const extensionHostDebugService = accessor.get(IExtensionHostDebugService); + const environmentService = accessor.get(INativeWorkbenchEnvironmentService); + const instantiationService = accessor.get(IInstantiationService); + const hostService = accessor.get(IHostService); + + const [extHostPort, rendererPort] = await Promise.all([ + getExtensionHostPort(extensionService, nativeHostService, dialogService, productService), + getRendererDebugPort(extensionHostDebugService, environmentService.window.id) + ]); + + if (extHostPort === undefined || rendererPort === undefined) { + return; + } + + const storage = instantiationService.createInstance(Storage); + storage.storeDebugOnNewWindow(extHostPort); + storage.storeRendererDebugOnNewWindow(rendererPort); + hostService.openWindow(); + } } class Storage { @@ -140,8 +223,30 @@ class Storage { } return port; } + + storeRendererDebugOnNewWindow(targetPort: number) { + this._storageService.store('debugRenderer.debugPort', targetPort, StorageScope.APPLICATION, StorageTarget.MACHINE); + } + + getAndDeleteRendererDebugPortIfSet(): number | undefined { + const port = this._storageService.getNumber('debugRenderer.debugPort', StorageScope.APPLICATION); + if (port !== undefined) { + this._storageService.remove('debugRenderer.debugPort', StorageScope.APPLICATION); + } + return port; + } } +const defaultDebugConfig = { + trace: true, + resolveSourceMapLocations: null, + eagerSources: true, + timeouts: { + sourceMapMinPause: 30_000, + sourceMapCumulativePause: 300_000, + }, +}; + export class DebugExtensionsContribution extends Disposable implements IWorkbenchContribution { constructor( @IDebugService private readonly _debugService: IDebugService, @@ -151,30 +256,43 @@ export class DebugExtensionsContribution extends Disposable implements IWorkbenc super(); const storage = this._instantiationService.createInstance(Storage); - const port = storage.getAndDeleteDebugPortIfSet(); - if (port !== undefined) { - _progressService.withProgress({ + const extHostPort = storage.getAndDeleteDebugPortIfSet(); + const rendererPort = storage.getAndDeleteRendererDebugPortIfSet(); + + // Start both debug sessions in parallel + const debugPromises: Promise[] = []; + + if (extHostPort !== undefined) { + debugPromises.push(_progressService.withProgress({ location: ProgressLocation.Notification, title: nls.localize('debugExtensionHost.progress', "Attaching Debugger To Extension Host"), - }, async p => { + }, async () => { // eslint-disable-next-line local/code-no-dangerous-type-assertions await this._debugService.startDebugging(undefined, { type: 'node', name: nls.localize('debugExtensionHost.launch.name', "Attach Extension Host"), request: 'attach', - port, - trace: true, - // resolve source maps everywhere: - resolveSourceMapLocations: null, - // announces sources eagerly for the loaded scripts view: - eagerSources: true, - // source maps of published VS Code are on the CDN and can take a while to load - timeouts: { - sourceMapMinPause: 30_000, - sourceMapCumulativePause: 300_000, - }, + port: extHostPort, + ...defaultDebugConfig, } as IConfig); - }); + })); + } + + if (rendererPort !== undefined) { + debugPromises.push(_progressService.withProgress({ + location: ProgressLocation.Notification, + title: nls.localize('debugRenderer.progress', "Attaching Debugger To Renderer"), + }, async () => { + await this._debugService.startDebugging(undefined, { + type: 'chrome', + name: nls.localize('debugRenderer.launch.name', "Attach Renderer"), + request: 'attach', + port: rendererPort, + ...defaultDebugConfig, + }); + })); } + + Promise.all(debugPromises); } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index 353643784cc..4a048849b68 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -114,7 +114,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio } } - public async startProfiling(): Promise { + public async startProfiling(): Promise { if (this._state !== ProfileSessionState.None) { return null; } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 2b2c10a9418..a513f824372 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -19,7 +19,7 @@ import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../ import { EditorInput } from '../../../common/editor/editorInput.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; -import { DebugExtensionHostInNewWindowAction, DebugExtensionsContribution, DebugExtensionHostInDevToolsAction } from './debugExtensionHostAction.js'; +import { DebugExtensionHostInNewWindowAction, DebugExtensionsContribution, DebugExtensionHostInDevToolsAction, DebugRendererInNewWindowAction, DebugExtensionHostAndRendererAction } from './debugExtensionHostAction.js'; import { ExtensionHostProfileService } from './extensionProfileService.js'; import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; @@ -77,6 +77,8 @@ workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, Lif // Register Commands registerAction2(DebugExtensionHostInNewWindowAction); +registerAction2(DebugRendererInNewWindowAction); +registerAction2(DebugExtensionHostAndRendererAction); registerAction2(StartExtensionHostProfileAction); registerAction2(StopExtensionHostProfileAction); registerAction2(SaveExtensionHostProfileAction); diff --git a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts index 68495c2158d..bb3241f28ed 100644 --- a/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts +++ b/src/vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService.ts @@ -17,7 +17,7 @@ import { ILogService } from '../../../../platform/log/common/log.js'; import { IExternalOpener, IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { defaultExternalUriOpenerId, ExternalUriOpenersConfiguration, externalUriOpenersSettingId } from './configuration.js'; -import { testUrlMatchesGlob } from '../../url/common/urlGlob.js'; +import { testUrlMatchesGlob } from '../../../../platform/url/common/urlGlob.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 281fe92dfce..0a5078987db 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -163,7 +163,7 @@ async function deleteFiles(explorerService: IExplorerService, workingCopyFileSer distinctElements.length > 1 ? nls.localize('restorePlural', "You can restore these files using the Undo command.") : nls.localize('restore', "You can restore this file using the Undo command."); // Check if we need to ask for confirmation at all - if (skipConfirm || (useTrash && configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false)) { + if (skipConfirm || configurationService.getValue(CONFIRM_DELETE_SETTING_KEY) === false) { confirmation = { confirmed: true }; } diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index b15c9c52ce6..53918c80b78 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -243,7 +243,7 @@ configurationRegistry.registerConfiguration({ 'files.trimTrailingWhitespaceInRegexAndStrings': { 'type': 'boolean', 'default': true, - 'description': nls.localize('trimTrailingWhitespaceInRegexAndStrings', "When enabled, trailing whitespace will be removed from multiline strings and regexes will be removed on save or when executing 'editor.action.trimTrailingWhitespace'. This can cause whitespace to not be trimmed from lines when there isn't up-to-date token information."), + 'description': nls.localize('trimTrailingWhitespaceInRegexAndStrings', "When enabled, trailing whitespace will be removed from multiline strings and regexes on save or when executing 'editor.action.trimTrailingWhitespace'. This can cause whitespace to not be trimmed from lines when there isn't up-to-date token information."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'files.insertFinalNewline': { @@ -393,8 +393,8 @@ configurationRegistry.registerConfiguration({ ], 'enumDescriptions': [ nls.localize({ key: 'everything', comment: ['This is the description of an option'] }, "Format the whole file."), - nls.localize({ key: 'modification', comment: ['This is the description of an option'] }, "Format modifications (requires source control)."), - nls.localize({ key: 'modificationIfAvailable', comment: ['This is the description of an option'] }, "Will attempt to format modifications only (requires source control). If source control can't be used, then the whole file will be formatted."), + nls.localize({ key: 'modification', comment: ['This is the description of an option'] }, "Format modifications. Requires source control and a formatter that supports 'Format Selection'."), + nls.localize({ key: 'modificationIfAvailable', comment: ['This is the description of an option'] }, "Will attempt to format modifications only (requires source control and a formatter that supports 'Format Selection'). If source control can't be used, then the whole file will be formatted."), ], 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is enabled."), 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, @@ -483,7 +483,7 @@ configurationRegistry.registerConfiguration({ }, 'explorer.confirmDelete': { 'type': 'boolean', - 'description': nls.localize('confirmDelete', "Controls whether the Explorer should ask for confirmation when deleting a file via the trash."), + 'description': nls.localize('confirmDelete', "Controls whether the Explorer should ask for confirmation when deleting files and folders."), 'default': true }, 'explorer.enableUndo': { diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index a08c3754676..db5712fe9b0 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -9,10 +9,6 @@ height: 100%; } -.explorer-folders-view .monaco-list-row { - padding-left: 4px; /* align top level twistie with `Explorer` title label */ -} - .explorer-folders-view .explorer-folders-view.highlight .monaco-list .explorer-item:not(.explorer-item-edited), .explorer-folders-view .explorer-folders-view.highlight .monaco-list .monaco-tl-twistie { opacity: 0.3; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index bef572b125a..5b115684ac0 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -55,6 +55,7 @@ import { EditorOpenSource } from '../../../../../platform/editor/common/editor.j import { ResourceMap } from '../../../../../base/common/map.js'; import { AbstractTreePart } from '../../../../../base/browser/ui/tree/abstractTree.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; +import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree, treeInput: ExplorerItem[]): boolean { @@ -213,7 +214,8 @@ export class ExplorerView extends ViewPane implements IExplorerView { @IFileService private readonly fileService: IFileService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ICommandService private readonly commandService: ICommandService, - @IOpenerService openerService: IOpenerService + @IOpenerService openerService: IOpenerService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); @@ -448,7 +450,14 @@ export class ExplorerView extends ViewPane implements IExplorerView { this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - const isCompressionEnabled = () => this.configurationService.getValue('explorer.compactFolders'); + const isCompressionEnabled = () => { + const configValue = this.configurationService.getValue('explorer.compactFolders'); + // Disable compact folders when screen reader is optimized for better accessibility + if (this.accessibilityService.isScreenReaderOptimized()) { + return false; + } + return configValue; + }; const getFileNestingSettings = (item?: ExplorerItem) => this.configurationService.getValue({ resource: item?.root.resource }).explorer.fileNesting; @@ -511,6 +520,11 @@ export class ExplorerView extends ViewPane implements IExplorerView { const onDidChangeCompressionConfiguration = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('explorer.compactFolders')); this._register(onDidChangeCompressionConfiguration(_ => this.tree.updateOptions({ compressionEnabled: isCompressionEnabled() }))); + // Update compression when screen reader mode changes + this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { + this.tree.updateOptions({ compressionEnabled: isCompressionEnabled() }); + })); + // Bind context keys FilesExplorerFocusedContext.bindTo(this.tree.contextKeyService); ExplorerFocusedContext.bindTo(this.tree.contextKeyService); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index cdd6404a505..a4a00dfa7ea 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -9,7 +9,7 @@ import * as glob from '../../../../../base/common/glob.js'; import { IListVirtualDelegate, ListDragOverEffectPosition, ListDragOverEffectType } from '../../../../../base/browser/ui/list/list.js'; import { IProgressService, ProgressLocation, } from '../../../../../platform/progress/common/progress.js'; import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; -import { IFileService, FileKind, FileOperationError, FileOperationResult, FileChangeType } from '../../../../../platform/files/common/files.js'; +import { IFileService, FileKind, FileOperationError, FileOperationResult, FileChangeType, FileSystemProviderCapabilities } from '../../../../../platform/files/common/files.js'; import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; import { isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; @@ -591,7 +591,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { } private async searchInWorkspace(patternLowercase: string, root: ExplorerItem, rootIndex: number, isFuzzyMatch: boolean, token: CancellationToken): Promise<{ explorerRoot: ExplorerItem; files: URI[]; directories: URI[]; hitMaxResults: boolean }> { - const segmentMatchPattern = caseInsensitiveGlobPattern(isFuzzyMatch ? fuzzyMatchingGlobPattern(patternLowercase) : continousMatchingGlobPattern(patternLowercase)); + const segmentMatchPattern = isFuzzyMatch ? fuzzyMatchingGlobPattern(patternLowercase) : continousMatchingGlobPattern(patternLowercase); const searchExcludePattern = getExcludes(this.configurationService.getValue({ resource: root.resource })) || {}; const searchOptions: IFileQuery = { @@ -603,6 +603,7 @@ export class ExplorerFindProvider implements IAsyncFindProvider { shouldGlobMatchFilePattern: true, cacheKey: `explorerfindprovider:${root.name}:${rootIndex}:${this.sessionId}`, excludePattern: searchExcludePattern, + ignoreGlobCase: true, }; let fileResults: ISearchComplete | undefined; @@ -652,7 +653,7 @@ function getMatchingDirectoriesFromFiles(resources: URI[], root: ExplorerItem, s for (const dirResource of uniqueDirectories) { const stats = dirResource.path.split('/'); const dirStat = stats[stats.length - 1]; - if (!dirStat || !glob.match(segmentMatchPattern, dirStat)) { + if (!dirStat || !glob.match(segmentMatchPattern, dirStat, { ignoreCase: true })) { continue; } @@ -698,19 +699,6 @@ function continousMatchingGlobPattern(pattern: string): string { return '*' + pattern + '*'; } -function caseInsensitiveGlobPattern(pattern: string): string { - let caseInsensitiveFilePattern = ''; - for (let i = 0; i < pattern.length; i++) { - const char = pattern[i]; - if (/[a-zA-Z]/.test(char)) { - caseInsensitiveFilePattern += `[${char.toLowerCase()}${char.toUpperCase()}]`; - } else { - caseInsensitiveFilePattern += char; - } - } - return caseInsensitiveFilePattern; -} - export interface ICompressedNavigationController { readonly current: ExplorerItem; readonly currentId: string; @@ -890,13 +878,16 @@ export class FilesRenderer implements ICompressibleTreeRenderer { - try { - if (templateData.currentContext) { - this.updateWidth(templateData.currentContext); + // schedule this on the next animation frame to avoid rendering reentry + DOM.scheduleAtNextAnimationFrame(DOM.getWindow(templateData.container), () => { + try { + if (templateData.currentContext) { + this.updateWidth(templateData.currentContext); + } + } catch (e) { + // noop since the element might no longer be in the tree, no update of width necessary } - } catch (e) { - // noop since the element might no longer be in the tree, no update of width necessary - } + }); })); const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables); @@ -1376,9 +1367,10 @@ export class FilesFilter implements ITreeFilter { const ignoreFile = ignoreTree.get(dirUri); ignoreFile?.updateContents(content.value.toString()); } else { - // Otherwise we create a new ignorefile and add it to the tree + // Otherwise we create a new ignore file and add it to the tree const ignoreParent = ignoreTree.findSubstr(dirUri); - const ignoreFile = new IgnoreFile(content.value.toString(), dirUri.path, ignoreParent); + const ignoreCase = !this.fileService.hasCapability(ignoreFileResource, FileSystemProviderCapabilities.PathCaseSensitive); + const ignoreFile = new IgnoreFile(content.value.toString(), dirUri.path, ignoreParent, ignoreCase); ignoreTree.set(dirUri, ignoreFile); // If we haven't seen this resource before then we need to add it to the list of resources we're tracking if (!this.ignoreFileResourcesPerRoot.get(root)?.has(ignoreFileResource)) { diff --git a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css index 344f9790e52..d933ff97043 100644 --- a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css +++ b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css @@ -63,7 +63,7 @@ } .open-editors .monaco-list .monaco-list-row { - padding-left: 22px; + padding-left: 8px; display: flex; } diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 2eec08fd35a..292887edf07 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -64,6 +64,8 @@ export class DefaultFormatter extends Disposable implements IWorkbenchContributi this._store.add(_editorService.onDidActiveEditorChange(this._updateStatus, this)); this._store.add(_languageFeaturesService.documentFormattingEditProvider.onDidChange(this._updateStatus, this)); this._store.add(_languageFeaturesService.documentRangeFormattingEditProvider.onDidChange(this._updateStatus, this)); + this._store.add(_languageFeaturesService.documentFormattingEditProvider.onDidChange(this._updateConfigValues, this)); + this._store.add(_languageFeaturesService.documentRangeFormattingEditProvider.onDidChange(this._updateConfigValues, this)); this._store.add(_configService.onDidChangeConfiguration(e => e.affectsConfiguration(DefaultFormatter.configName) && this._updateStatus())); this._updateConfigValues(); } @@ -72,7 +74,34 @@ export class DefaultFormatter extends Disposable implements IWorkbenchContributi await this._extensionService.whenInstalledExtensionsRegistered(); let extensions = [...this._extensionService.extensions]; + // Get all formatter providers to identify which extensions actually contribute formatters + const documentFormatters = this._languageFeaturesService.documentFormattingEditProvider.allNoModel(); + const rangeFormatters = this._languageFeaturesService.documentRangeFormattingEditProvider.allNoModel(); + const formatterExtensionIds = new Set(); + + for (const formatter of documentFormatters) { + if (formatter.extensionId) { + formatterExtensionIds.add(ExtensionIdentifier.toKey(formatter.extensionId)); + } + } + for (const formatter of rangeFormatters) { + if (formatter.extensionId) { + formatterExtensionIds.add(ExtensionIdentifier.toKey(formatter.extensionId)); + } + } + extensions = extensions.sort((a, b) => { + // Ultimate boost: extensions that actually contribute formatters + const contributesFormatterA = formatterExtensionIds.has(ExtensionIdentifier.toKey(a.identifier)); + const contributesFormatterB = formatterExtensionIds.has(ExtensionIdentifier.toKey(b.identifier)); + + if (contributesFormatterA && !contributesFormatterB) { + return -1; + } else if (!contributesFormatterA && contributesFormatterB) { + return 1; + } + + // Secondary boost: category-based sorting const boostA = a.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); const boostB = b.categories?.find(cat => cat === 'Formatters' || cat === 'Programming Languages'); @@ -334,7 +363,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { }); } - async run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): Promise { + async run(accessor: ServicesAccessor, editor: ICodeEditor, args: unknown): Promise { if (!editor.hasModel()) { return; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts index 27b562a58a0..76f69758c24 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts @@ -3,44 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './inlineChatDefaultModel.js'; + import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; import { IMenuItem, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { InlineChatController, InlineChatController1, InlineChatController2 } from './inlineChatController.js'; +import { InlineChatController } from './inlineChatController.js'; import * as InlineChatActions from './inlineChatActions.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; +import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; import { InlineChatNotebookContribution } from './inlineChatNotebook.js'; import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js'; -import { InlineChatAccessibleView } from './inlineChatAccessibleView.js'; import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { InlineChatEnabler, InlineChatEscapeToolContribution, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { CancelAction, ChatSubmitAction } from '../../chat/browser/actions/chatExecuteActions.js'; import { localize } from '../../../../nls.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { InlineChatAccessibilityHelp } from './inlineChatAccessibilityHelp.js'; -import { InlineChatExpandLineAction, InlineChatHintsController, HideInlineChatHintAction, ShowInlineChatHintAction } from './inlineChatCurrentLine.js'; -registerEditorContribution(InlineChatController2.ID, InlineChatController2, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors -registerEditorContribution(INLINE_CHAT_ID, InlineChatController1, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerEditorContribution(InlineChatController.ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors registerAction2(InlineChatActions.KeepSessionAction2); +registerAction2(InlineChatActions.UndoSessionAction2); registerAction2(InlineChatActions.UndoAndCloseSessionAction2); // --- browser registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); - -registerAction2(InlineChatExpandLineAction); -registerAction2(ShowInlineChatHintAction); -registerAction2(HideInlineChatHintAction); -registerEditorContribution(InlineChatHintsController.ID, InlineChatHintsController, EditorContributionInstantiation.Eventually); - // --- MENU special --- const editActionMenuItem: IMenuItem = { @@ -94,26 +87,12 @@ MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem // --- actions --- registerAction2(InlineChatActions.StartSessionAction); -registerAction2(InlineChatActions.CloseAction); -registerAction2(InlineChatActions.ConfigureInlineChatAction); -registerAction2(InlineChatActions.UnstashSessionAction); -registerAction2(InlineChatActions.DiscardHunkAction); -registerAction2(InlineChatActions.RerunAction); -registerAction2(InlineChatActions.MoveToNextHunk); -registerAction2(InlineChatActions.MoveToPreviousHunk); - -registerAction2(InlineChatActions.ArrowOutUpAction); -registerAction2(InlineChatActions.ArrowOutDownAction); registerAction2(InlineChatActions.FocusInlineChat); -registerAction2(InlineChatActions.ViewInChatAction); -registerAction2(InlineChatActions.ToggleDiffForChange); -registerAction2(InlineChatActions.AcceptChanges); const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); registerWorkbenchContribution2(InlineChatEscapeToolContribution.Id, InlineChatEscapeToolContribution, WorkbenchPhase.AfterRestored); -AccessibleViewRegistry.register(new InlineChatAccessibleView()); AccessibleViewRegistry.register(new InlineChatAccessibilityHelp()); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts index bce9724997c..c7983f6fe1f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -9,7 +9,7 @@ import { AccessibleViewType } from '../../../../platform/accessibility/browser/a import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { getChatAccessibilityHelpProvider } from '../../chat/browser/actions/chatAccessibilityHelp.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { CTX_INLINE_CHAT_RESPONSE_FOCUSED } from '../common/inlineChat.js'; export class InlineChatAccessibilityHelp implements IAccessibleViewImplementation { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts deleted file mode 100644 index cfea2d516c1..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ /dev/null @@ -1,45 +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 { InlineChatController } from './inlineChatController.js'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from '../common/inlineChat.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { AccessibleViewProviderId, AccessibleViewType, AccessibleContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAccessibleViewImplementation } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; -import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; - -export class InlineChatAccessibleView implements IAccessibleViewImplementation { - readonly priority = 100; - readonly name = 'inlineChat'; - readonly when = ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED); - readonly type = AccessibleViewType.View; - getProvider(accessor: ServicesAccessor) { - const codeEditorService = accessor.get(ICodeEditorService); - - const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor()); - if (!editor) { - return; - } - const controller = InlineChatController.get(editor); - if (!controller) { - return; - } - const responseContent = controller.widget.responseContent; - if (!responseContent) { - return; - } - return new AccessibleContentProvider( - AccessibleViewProviderId.InlineChat, - { type: AccessibleViewType.View }, - () => renderAsPlaintext(new MarkdownString(responseContent), { includeCodeBlocksFences: true }), - () => controller.focus(), - AccessibilityVerbositySettingId.InlineChat - ); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts index b523ad5ecd3..5dd9eab5dfa 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatActions.ts @@ -10,8 +10,8 @@ import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js'; import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { InlineChatController, InlineChatController1, InlineChatController2, InlineChatRunOptions } from './inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, MENU_INLINE_CHAT_ZONE, ACTION_DISCARD_CHANGES, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, MENU_INLINE_CHAT_SIDE, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED } from '../common/inlineChat.js'; +import { InlineChatController, InlineChatRunOptions } from './inlineChatController.js'; +import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_POSSIBLE, ACTION_START, CTX_INLINE_CHAT_V2_ENABLED, CTX_INLINE_CHAT_V1_ENABLED, CTX_HOVER_MODE } from '../common/inlineChat.js'; import { ctxHasEditorModification, ctxHasRequestInProgress } from '../../chat/browser/chatEditing/chatEditingEditorContextKeys.js'; import { localize, localize2 } from '../../../../nls.js'; import { Action2, IAction2Options, MenuId } from '../../../../platform/actions/common/actions.js'; @@ -23,14 +23,8 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { IChatService } from '../../chat/common/chatService.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { HunkInformation } from './inlineChatSession.js'; -import { IChatWidgetService } from '../../chat/browser/chat.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { ChatAgentLocation } from '../../chat/common/constants.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineChat.start'); @@ -62,7 +56,8 @@ export class StartSessionAction extends Action2 { super({ id: ACTION_START, title: localize2('run', 'Open Inline Chat'), - category: AbstractInline1ChatAction.category, + shortTitle: localize2('runShort', 'Inline Chat'), + category: AbstractInlineChatAction.category, f1: true, precondition: inlineChatContextKey, keybinding: { @@ -80,6 +75,15 @@ export class StartSessionAction extends Action2 { id: MenuId.ChatTitleBarMenu, group: 'a_open', order: 3, + }, { + id: MenuId.ChatEditorInlineGutter, + group: '1_chat', + order: 1, + }, { + id: MenuId.InlineChatEditorAffordance, + group: '1_chat', + order: 1, + when: EditorContextKeys.hasNonEmptySelection }] }); } @@ -106,7 +110,7 @@ export class StartSessionAction extends Action2 { }); } - private _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { + private async _runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ...args: unknown[]) { const ctrl = InlineChatController.get(editor); if (!ctrl) { @@ -118,11 +122,11 @@ export class StartSessionAction extends Action2 { } let options: InlineChatRunOptions | undefined; - const arg = _args[0]; + const arg = args[0]; if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { options = arg; } - InlineChatController.get(editor)?.run({ ...options }); + await InlineChatController.get(editor)?.run({ ...options }); } } @@ -133,7 +137,7 @@ export class FocusInlineChat extends EditorAction2 { id: 'inlineChat.focus', title: localize2('focus', "Focus Input"), f1: true, - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), keybinding: [{ weight: KeybindingWeight.EditorCore + 10, // win against core_command @@ -152,406 +156,8 @@ export class FocusInlineChat extends EditorAction2 { } } -//#region --- VERSION 1 - -export class UnstashSessionAction extends EditorAction2 { - constructor() { - super({ - id: 'inlineChat.unstash', - title: localize2('unstash', "Resume Last Dismissed Inline Chat"), - category: AbstractInline1ChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_STASHED_SESSION, EditorContextKeys.writable), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyZ, - } - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { - const ctrl = InlineChatController1.get(editor); - if (ctrl) { - const session = ctrl.unstashLastSession(); - if (session) { - ctrl.run({ - existingSession: session, - }); - } - } - } -} - -export abstract class AbstractInline1ChatAction extends EditorAction2 { - - static readonly category = localize2('cat', "Inline Chat"); - - constructor(desc: IAction2Options) { - - const massageMenu = (menu: IAction2Options['menu'] | undefined) => { - if (Array.isArray(menu)) { - for (const entry of menu) { - entry.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, entry.when); - } - } else if (menu) { - menu.when = ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, menu.when); - } - }; - if (Array.isArray(desc.menu)) { - massageMenu(desc.menu); - } else { - massageMenu(desc.menu); - } - - super({ - ...desc, - category: AbstractInline1ChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_V1_ENABLED, desc.precondition) - }); - } - - override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: unknown[]) { - const editorService = accessor.get(IEditorService); - const logService = accessor.get(ILogService); - - let ctrl = InlineChatController1.get(editor); - if (!ctrl) { - const { activeTextEditorControl } = editorService; - if (isCodeEditor(activeTextEditorControl)) { - editor = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - editor = activeTextEditorControl.getModifiedEditor(); - } - ctrl = InlineChatController1.get(editor); - } - - if (!ctrl) { - logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); - return; - } - - if (editor instanceof EmbeddedCodeEditorWidget) { - editor = editor.getParentEditor(); - } - if (!ctrl) { - for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { - if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { - if (diffEditor instanceof EmbeddedDiffEditorWidget) { - this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); - } - } - } - return; - } - this.runInlineChatCommand(accessor, ctrl, editor, ..._args); - } - - abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void; -} - -export class ArrowOutUpAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.arrowOutUp', - title: localize('arrowUp', 'Cursor Up'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.UpArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): void { - ctrl.arrowOut(true); - } -} - -export class ArrowOutDownAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.arrowOutDown', - title: localize('arrowDown', 'Cursor Down'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_LAST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): void { - ctrl.arrowOut(false); - } -} - -export class AcceptChanges extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_ACCEPT_CHANGES, - title: localize2('apply1', "Accept Changes"), - shortTitle: localize('apply2', 'Accept'), - icon: Codicon.check, - f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE), - keybinding: [{ - weight: KeybindingWeight.WorkbenchContrib + 10, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - }], - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - ), - }, { - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - order: 1, - }] - }); - } - - override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { - ctrl.acceptHunk(hunk); - } -} - -export class DiscardHunkAction extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_DISCARD_CHANGES, - title: localize('discard', 'Discard'), - icon: Codicon.chromeClose, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: [{ - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - order: 2 - }], - keybinding: { - weight: KeybindingWeight.EditorContrib, - primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunk?: HunkInformation | any): Promise { - return ctrl.discardHunk(hunk); - } -} - -export class RerunAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: ACTION_REGENERATE_RESPONSE, - title: localize2('chat.rerun.label', "Rerun Request"), - shortTitle: localize('rerun', 'Rerun'), - f1: false, - icon: Codicon.refresh, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 5, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.None) - ) - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyR - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - const chatService = accessor.get(IChatService); - const chatWidgetService = accessor.get(IChatWidgetService); - const model = ctrl.chatWidget.viewModel?.model; - if (!model) { - return; - } - - const lastRequest = model.getRequests().at(-1); - if (lastRequest) { - const widget = chatWidgetService.getWidgetBySessionResource(model.sessionResource); - await chatService.resendRequest(lastRequest, { - noCommandDetection: false, - attempt: lastRequest.attempt + 1, - location: ctrl.chatWidget.location, - userSelectedModelId: widget?.input.currentLanguageModel - }); - } - } -} - -export class CloseAction extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.close', - title: localize('close', 'Close'), - icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.EditorContrib + 1, - primary: KeyCode.Escape, - }, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - }, { - id: MENU_INLINE_CHAT_SIDE, - group: 'navigation', - when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.None) - }] - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - ctrl.cancelSession(); - } -} - -export class ConfigureInlineChatAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: 'inlineChat.configure', - title: localize2('configure', 'Configure Inline Chat'), - icon: Codicon.settingsGear, - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'zzz', - order: 5 - } - }); - } - - async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]): Promise { - accessor.get(IPreferencesService).openSettings({ query: 'inlineChat' }); - } -} - -export class MoveToNextHunk extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.moveToNextHunk', - title: localize2('moveToNextHunk', 'Move to Next Change'), - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void { - ctrl.moveHunk(true); - } -} - -export class MoveToPreviousHunk extends AbstractInline1ChatAction { - - constructor() { - super({ - id: 'inlineChat.moveToPreviousHunk', - title: localize2('moveToPreviousHunk', 'Move to Previous Change'), - f1: true, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Shift | KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController1, editor: ICodeEditor, ...args: unknown[]): void { - ctrl.moveHunk(false); - } -} - -export class ViewInChatAction extends AbstractInline1ChatAction { - constructor() { - super({ - id: ACTION_VIEW_IN_CHAT, - title: localize('viewInChat', 'View in Chat'), - icon: Codicon.chatSparkle, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'more', - order: 1, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages) - }, { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - ChatContextKeys.inputHasText.toNegated(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - ) - }], - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - when: ChatContextKeys.inChatInput - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, ..._args: unknown[]) { - return ctrl.viewInChat(); - } -} - -export class ToggleDiffForChange extends AbstractInline1ChatAction { - - constructor() { - super({ - id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Toggle Changes'), - icon: Codicon.diffSingle, - toggled: { - condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, - }, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'zzz', - order: 1, - }, { - id: MENU_INLINE_CHAT_ZONE, - group: 'navigation', - when: CTX_INLINE_CHAT_CHANGE_HAS_DIFF, - order: 2 - }] - }); - } - - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController1, _editor: ICodeEditor, hunkInfo: HunkInformation | any): void { - ctrl.toggleDiff(hunkInfo); - } -} - -//#endregion - - //#region --- VERSION 2 -abstract class AbstractInline2ChatAction extends EditorAction2 { +export abstract class AbstractInlineChatAction extends EditorAction2 { static readonly category = localize2('cat', "Inline Chat"); @@ -573,7 +179,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { super({ ...desc, - category: AbstractInline2ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_V2_ENABLED, desc.precondition) }); } @@ -582,7 +188,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { const editorService = accessor.get(IEditorService); const logService = accessor.get(ILogService); - let ctrl = InlineChatController2.get(editor); + let ctrl = InlineChatController.get(editor); if (!ctrl) { const { activeTextEditorControl } = editorService; if (isCodeEditor(activeTextEditorControl)) { @@ -590,7 +196,7 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { } else if (isDiffEditor(activeTextEditorControl)) { editor = activeTextEditorControl.getModifiedEditor(); } - ctrl = InlineChatController2.get(editor); + ctrl = InlineChatController.get(editor); } if (!ctrl) { @@ -614,30 +220,23 @@ abstract class AbstractInline2ChatAction extends EditorAction2 { this.runInlineChatCommand(accessor, ctrl, editor, ..._args); } - abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController2, editor: ICodeEditor, ...args: unknown[]): void; + abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: unknown[]): void; } -class KeepOrUndoSessionAction extends AbstractInline2ChatAction { +class KeepOrUndoSessionAction extends AbstractInlineChatAction { constructor(private readonly _keep: boolean, desc: IAction2Options) { super(desc); } - override async runInlineChatCommand(accessor: ServicesAccessor, _ctrl: InlineChatController2, editor: ICodeEditor, ..._args: unknown[]): Promise { - const inlineChatSessions = accessor.get(IInlineChatSessionService); - if (!editor.hasModel()) { - return; + override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ..._args: unknown[]): Promise { + if (this._keep) { + await ctrl.acceptSession(); + } else { + await ctrl.rejectSession(); } - const textModel = editor.getModel(); - const session = inlineChatSessions.getSession2(textModel.uri); - if (session) { - if (this._keep) { - await session.editingSession.accept(); - } else { - await session.editingSession.reject(); - } + if (editor.hasModel()) { editor.setSelection(editor.getSelection().collapseToStart()); - session.dispose(); } } } @@ -653,7 +252,6 @@ export class KeepSessionAction2 extends KeepOrUndoSessionAction { CTX_INLINE_CHAT_VISIBLE, ctxHasRequestInProgress.negate(), ctxHasEditorModification, - ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditorInline) ), keybinding: [{ when: ContextKeyExpr.and(ChatContextKeys.inputHasFocus, ChatContextKeys.inputHasText.negate()), @@ -677,6 +275,36 @@ export class KeepSessionAction2 extends KeepOrUndoSessionAction { } } +export class UndoSessionAction2 extends KeepOrUndoSessionAction { + + constructor() { + super(false, { + id: 'inlineChat2.undo', + title: localize2('undo', "Undo"), + f1: true, + icon: Codicon.discard, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE), + keybinding: [{ + when: ContextKeyExpr.or( + ContextKeyExpr.and(EditorContextKeys.focus, ctxHasEditorModification.negate()), + ChatContextKeys.inputHasFocus, + ), + weight: KeybindingWeight.WorkbenchContrib + 1, + primary: KeyCode.Escape, + }], + menu: [{ + id: MenuId.ChatEditorInlineExecute, + group: 'navigation', + order: 100, + when: ContextKeyExpr.and( + CTX_HOVER_MODE, + ctxHasRequestInProgress.negate(), + ctxHasEditorModification, + ) + }] + }); + } +} export class UndoAndCloseSessionAction2 extends KeepOrUndoSessionAction { @@ -686,7 +314,7 @@ export class UndoAndCloseSessionAction2 extends KeepOrUndoSessionAction { title: localize2('close2', "Close"), f1: true, icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, + precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_HOVER_MODE.negate()), keybinding: [{ when: ContextKeyExpr.or( ContextKeyExpr.and(EditorContextKeys.focus, ctxHasEditorModification.negate()), @@ -698,7 +326,11 @@ export class UndoAndCloseSessionAction2 extends KeepOrUndoSessionAction { menu: [{ id: MenuId.ChatEditorInlineExecute, group: 'navigation', - order: 100 + order: 100, + when: ContextKeyExpr.or( + CTX_HOVER_MODE.negate(), + ContextKeyExpr.and(CTX_HOVER_MODE, ctxHasEditorModification.negate(), ctxHasRequestInProgress.negate()) + ) }] }); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts new file mode 100644 index 00000000000..5d53da9301b --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAffordance.ts @@ -0,0 +1,137 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { autorun, debouncedObservable, derived, observableSignalFromEvent, observableValue, runOnChange, waitForState } from '../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { ScrollType } from '../../../../editor/common/editorCommon.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { InlineChatConfigKeys } from '../common/inlineChat.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { InlineChatEditorAffordance } from './inlineChatEditorAffordance.js'; +import { InlineChatInputWidget } from './inlineChatOverlayWidget.js'; +import { InlineChatGutterAffordance } from './inlineChatGutterAffordance.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { assertType } from '../../../../base/common/types.js'; +import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; +import { IInlineChatSessionService } from './inlineChatSessionService.js'; + +export class InlineChatAffordance extends Disposable { + + private _menuData = observableValue<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>(this, undefined); + + constructor( + private readonly _editor: ICodeEditor, + private readonly _inputWidget: InlineChatInputWidget, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IChatEntitlementService chatEntiteldService: IChatEntitlementService, + @IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService, + ) { + super(); + + const editorObs = observableCodeEditor(this._editor); + const affordance = observableConfigValue<'off' | 'gutter' | 'editor'>(InlineChatConfigKeys.Affordance, 'off', configurationService); + const debouncedSelection = debouncedObservable(editorObs.cursorSelection, 500); + + const selectionData = observableValue(this, undefined); + + let explicitSelection = false; + + this._store.add(runOnChange(editorObs.selections, (value, _prev, events) => { + explicitSelection = events.every(e => e.reason === CursorChangeReason.Explicit); + if (!value || value.length !== 1 || value[0].isEmpty() || !explicitSelection) { + selectionData.set(undefined, undefined); + } + })); + + this._store.add(autorun(r => { + const value = debouncedSelection.read(r); + if (!value || value.isEmpty() || !explicitSelection || _editor.getModel()?.getValueInRange(value).match(/^\s+$/)) { + selectionData.set(undefined, undefined); + return; + } + selectionData.set(value, undefined); + })); + + this._store.add(autorun(r => { + if (chatEntiteldService.sentimentObs.read(r).hidden) { + selectionData.set(undefined, undefined); + } + })); + + const hasSessionObs = derived(r => { + observableSignalFromEvent(this, inlineChatSessionService.onDidChangeSessions).read(r); + const model = editorObs.model.read(r); + return model ? inlineChatSessionService.getSessionByTextModel(model.uri) !== undefined : false; + }); + + this._store.add(autorun(r => { + if (hasSessionObs.read(r)) { + selectionData.set(undefined, undefined); + } + })); + + this._store.add(this._instantiationService.createInstance( + InlineChatGutterAffordance, + editorObs, + derived(r => affordance.read(r) === 'gutter' ? selectionData.read(r) : undefined), + this._menuData + )); + + this._store.add(this._instantiationService.createInstance( + InlineChatEditorAffordance, + this._editor, + derived(r => affordance.read(r) === 'editor' ? selectionData.read(r) : undefined) + )); + + this._store.add(autorun(r => { + const data = this._menuData.read(r); + if (!data) { + return; + } + + // Reveal the line in case it's outside the viewport (e.g., when triggered from sticky scroll) + this._editor.revealLineInCenterIfOutsideViewport(data.lineNumber, ScrollType.Immediate); + + const editorDomNode = this._editor.getDomNode()!; + const editorRect = editorDomNode.getBoundingClientRect(); + const left = data.rect.left - editorRect.left; + + // Show the overlay widget + this._inputWidget.show(data.lineNumber, left, data.above); + })); + + this._store.add(autorun(r => { + const pos = this._inputWidget.position.read(r); + if (pos === null) { + this._menuData.set(undefined, undefined); + } + })); + } + + async showMenuAtSelection() { + assertType(this._editor.hasModel()); + + const direction = this._editor.getSelection().getDirection(); + const position = this._editor.getPosition(); + const editorDomNode = this._editor.getDomNode(); + const scrolledPosition = this._editor.getScrolledVisiblePosition(position); + const editorRect = editorDomNode.getBoundingClientRect(); + const x = editorRect.left + scrolledPosition.left; + const y = editorRect.top + scrolledPosition.top; + + this._menuData.set({ + rect: new DOMRect(x, y, 0, scrolledPosition.height), + above: direction === SelectionDirection.RTL, + lineNumber: position.lineNumber + }, undefined); + + await waitForState(this._inputWidget.position, pos => pos === null); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 0b860c31704..1790933cbb8 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -3,1249 +3,121 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as aria from '../../../../base/browser/ui/aria/aria.js'; +import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; import { alert } from '../../../../base/browser/ui/aria/aria.js'; -import { Barrier, DeferredPromise, Queue, raceCancellation } from '../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; +import { raceCancellation } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Event } from '../../../../base/common/event.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import { MovingAverage } from '../../../../base/common/numbers.js'; -import { autorun, derived, IObservable, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js'; +import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; -import { StopWatch } from '../../../../base/common/stopwatch.js'; import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; import { IPosition, Position } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ISelection, Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { ISelection, Selection } from '../../../../editor/common/core/selection.js'; import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { TextEdit, VersionedExtensionId } from '../../../../editor/common/languages.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; +import { TextEdit } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; -import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js'; import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; import { localize } from '../../../../nls.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { IChatAttachmentResolveService } from '../../chat/browser/chatAttachmentResolveService.js'; -import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatEditingSession, ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; -import { ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js'; +import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; +import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; +import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { ChatModel } from '../../chat/common/model/chatModel.js'; import { ChatMode } from '../../chat/common/chatModes.js'; -import { IChatService } from '../../chat/common/chatService.js'; -import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/chatVariableEntries.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; +import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; +import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; +import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js'; import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; -import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; +import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js'; import { INotebookService } from '../../notebook/common/notebookService.js'; -import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js'; -import { HunkInformation, Session, StashedSession } from './inlineChatSession.js'; -import { IInlineChatSession2, IInlineChatSessionService, moveToPanelChat } from './inlineChatSessionService.js'; -import { InlineChatError } from './inlineChatSessionServiceImpl.js'; -import { HunkAction, IEditObserver, IInlineChatMetadata, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js'; +import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { InlineChatAffordance } from './inlineChatAffordance.js'; +import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js'; +import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -export const enum State { - CREATE_SESSION = 'CREATE_SESSION', - INIT_UI = 'INIT_UI', - WAIT_FOR_INPUT = 'WAIT_FOR_INPUT', - SHOW_REQUEST = 'SHOW_REQUEST', - PAUSE = 'PAUSE', - CANCEL = 'CANCEL', - ACCEPT = 'DONE', -} - -const enum Message { - NONE = 0, - ACCEPT_SESSION = 1 << 0, - CANCEL_SESSION = 1 << 1, - PAUSE_SESSION = 1 << 2, - CANCEL_REQUEST = 1 << 3, - CANCEL_INPUT = 1 << 4, - ACCEPT_INPUT = 1 << 5, -} - -export abstract class InlineChatRunOptions { - initialSelection?: ISelection; - initialRange?: IRange; - message?: string; - attachments?: URI[]; - autoSend?: boolean; - existingSession?: Session; - position?: IPosition; - - static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { - const { initialSelection, initialRange, message, autoSend, position, existingSession, attachments: attachments } = options; - if ( - typeof message !== 'undefined' && typeof message !== 'string' - || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' - || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) - || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) - || typeof position !== 'undefined' && !Position.isIPosition(position) - || typeof existingSession !== 'undefined' && !(existingSession instanceof Session) - || typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI)) - ) { - return false; - } - return true; - } -} - -export class InlineChatController implements IEditorContribution { - - static ID = 'editor.contrib.inlineChatController'; - - static get(editor: ICodeEditor) { - return editor.getContribution(InlineChatController.ID); - } - - private readonly _delegate: IObservable; - - constructor( - editor: ICodeEditor, - @IConfigurationService configurationService: IConfigurationService, - @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService - ) { - const inlineChat2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configurationService); - const notebookAgent = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configurationService); - - this._delegate = derived(r => { - const isNotebookCell = !!this._notebookEditorService.getNotebookForPossibleCell(editor); - if (isNotebookCell ? notebookAgent.read(r) : inlineChat2.read(r)) { - return InlineChatController2.get(editor)!; - } else { - return InlineChatController1.get(editor)!; - } - }); - } - - dispose(): void { - - } - - get isActive(): boolean { - return this._delegate.get().isActive; - } - - async run(arg?: InlineChatRunOptions): Promise { - return this._delegate.get().run(arg); - } - - focus() { - return this._delegate.get().focus(); - } - - get widget(): EditorBasedInlineChatWidget { - return this._delegate.get().widget; - } - - getWidgetPosition() { - return this._delegate.get().getWidgetPosition(); - } - - acceptSession() { - return this._delegate.get().acceptSession(); - } -} - -/** - * @deprecated - */ -export class InlineChatController1 implements IEditorContribution { - - static get(editor: ICodeEditor) { - return editor.getContribution(INLINE_CHAT_ID); - } - - private _isDisposed: boolean = false; - private readonly _store = new DisposableStore(); - - private readonly _ui: Lazy; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxEditing: IContextKey; - private readonly _ctxResponseType: IContextKey; - private readonly _ctxRequestInProgress: IContextKey; - - private readonly _ctxResponse: IContextKey; - - private readonly _messages = this._store.add(new Emitter()); - protected readonly _onDidEnterState = this._store.add(new Emitter()); - - get chatWidget() { - return this._ui.value.widget.chatWidget; - } - - private readonly _sessionStore = this._store.add(new DisposableStore()); - private readonly _stashedSession = this._store.add(new MutableDisposable()); - private _delegateSession?: IChatEditingSession; - - private _session?: Session; - private _strategy?: LiveStrategy; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IDialogService private readonly _dialogService: IDialogService, - @IContextKeyService contextKeyService: IContextKeyService, - @IChatService private readonly _chatService: IChatService, - @IEditorService private readonly _editorService: IEditorService, - @INotebookEditorService notebookEditorService: INotebookEditorService, - @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, - @IFileService private readonly _fileService: IFileService, - @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService - ) { - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService); - this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); - this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); - - this._ctxResponse = ChatContextKeys.isResponse.bindTo(contextKeyService); - ChatContextKeys.responseHasError.bindTo(contextKeyService); - - this._ui = new Lazy(() => { - - const location: IChatWidgetLocationOptions = { - location: ChatAgentLocation.EditorInline, - resolveData: () => { - assertType(this._editor.hasModel()); - assertType(this._session); - return { - type: ChatAgentLocation.EditorInline, - selection: this._editor.getSelection(), - document: this._session.textModelN.uri, - wholeRange: this._session?.wholeRange.trackedInitialRange, - close: () => this.cancelSession(), - delegateSessionResource: this._delegateSession?.chatSessionResource, - }; - } - }; - - // inline chat in notebooks - // check if this editor is part of a notebook editor - // and iff so, use the notebook location but keep the resolveData - // talk about editor data - const notebookEditor = notebookEditorService.getNotebookForPossibleCell(this._editor); - if (!!notebookEditor) { - location.location = ChatAgentLocation.Notebook; - } - - const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, { editor: this._editor, notebookEditor }); - this._store.add(zone); - this._store.add(zone.widget.chatWidget.onDidClear(async () => { - const r = this.joinCurrentRun(); - this.cancelSession(); - await r; - this.run(); - })); - - return zone; - }); - - this._store.add(this._editor.onDidChangeModel(async e => { - if (this._session || !e.newModelUrl) { - return; - } - - const existingSession = this._inlineChatSessionService.getSession(this._editor, e.newModelUrl); - if (!existingSession) { - return; - } - - this._log('session RESUMING after model change', e); - await this.run({ existingSession }); - })); - - this._store.add(this._inlineChatSessionService.onDidEndSession(e => { - if (e.session === this._session && e.endedByExternalCause) { - this._log('session ENDED by external cause'); - this.acceptSession(); - } - })); - - this._store.add(this._inlineChatSessionService.onDidMoveSession(async e => { - if (e.editor === this._editor) { - this._log('session RESUMING after move', e); - await this.run({ existingSession: e.session }); - } - })); - - this._log(`NEW controller`); - } - - dispose(): void { - if (this._currentRun) { - this._messages.fire(this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION - : Message.CANCEL_SESSION); - } - this._store.dispose(); - this._isDisposed = true; - this._log('DISPOSED controller'); - } - - private _log(message: string | Error, ...more: unknown[]): void { - if (message instanceof Error) { - this._logService.error(message, ...more); - } else { - this._logService.trace(`[IE] (editor:${this._editor.getId()}) ${message}`, ...more); - } - } - - get widget(): EditorBasedInlineChatWidget { - return this._ui.value.widget; - } - - getId(): string { - return INLINE_CHAT_ID; - } - - getWidgetPosition(): Position | undefined { - return this._ui.value.position; - } - - private _currentRun?: Promise; - - async run(options: InlineChatRunOptions | undefined = {}): Promise { - - let lastState: State | undefined; - const d = this._onDidEnterState.event(e => lastState = e); - - try { - this.acceptSession(); - if (this._currentRun) { - await this._currentRun; - } - if (options.initialSelection) { - this._editor.setSelection(options.initialSelection); - } - this._stashedSession.clear(); - this._currentRun = this._nextState(State.CREATE_SESSION, options); - await this._currentRun; - - } catch (error) { - // this should not happen but when it does make sure to tear down the UI and everything - this._log('error during run', error); - onUnexpectedError(error); - if (this._session) { - this._inlineChatSessionService.releaseSession(this._session); - } - this[State.PAUSE](); - - } finally { - this._currentRun = undefined; - d.dispose(); - } - - return lastState !== State.CANCEL; - } - - // ---- state machine - - protected async _nextState(state: State, options: InlineChatRunOptions): Promise { - let nextState: State | void = state; - while (nextState && !this._isDisposed) { - this._log('setState to ', nextState); - const p: State | Promise | Promise = this[nextState](options); - this._onDidEnterState.fire(nextState); - nextState = await p; - } - } - - private async [State.CREATE_SESSION](options: InlineChatRunOptions): Promise { - assertType(this._session === undefined); - assertType(this._editor.hasModel()); - - let session: Session | undefined = options.existingSession; - - let initPosition: Position | undefined; - if (options.position) { - initPosition = Position.lift(options.position).delta(-1); - delete options.position; - } - - const widgetPosition = this._showWidget(session?.headless, true, initPosition); - - // this._updatePlaceholder(); - let errorMessage = localize('create.fail', "Failed to start editor chat"); - - if (!session) { - const createSessionCts = new CancellationTokenSource(); - const msgListener = Event.once(this._messages.event)(m => { - this._log('state=_createSession) message received', m); - if (m === Message.ACCEPT_INPUT) { - // user accepted the input before having a session - options.autoSend = true; - this._ui.value.widget.updateInfo(localize('welcome.2', "Getting ready...")); - } else { - createSessionCts.cancel(); - } - }); - - try { - session = await this._inlineChatSessionService.createSession( - this._editor, - { wholeRange: options.initialRange }, - createSessionCts.token - ); - } catch (error) { - // Inline chat errors are from the provider and have their error messages shown to the user - if (error instanceof InlineChatError || error?.name === InlineChatError.code) { - errorMessage = error.message; - } - } - - createSessionCts.dispose(); - msgListener.dispose(); - - if (createSessionCts.token.isCancellationRequested) { - if (session) { - this._inlineChatSessionService.releaseSession(session); - } - return State.CANCEL; - } - } - - delete options.initialRange; - delete options.existingSession; - - if (!session) { - MessageController.get(this._editor)?.showMessage(errorMessage, widgetPosition); - this._log('Failed to start editor chat'); - return State.CANCEL; - } - - // create a new strategy - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless); - - this._session = session; - return State.INIT_UI; - } - - private async [State.INIT_UI](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - // hide/cancel inline completions when invoking IE - InlineCompletionsController.get(this._editor)?.reject(); - - this._sessionStore.clear(); - - const wholeRangeDecoration = this._editor.createDecorationsCollection(); - const handleWholeRangeChange = () => { - const newDecorations = this._strategy?.getWholeRangeDecoration() ?? []; - wholeRangeDecoration.set(newDecorations); - - this._ctxEditing.set(!this._session?.wholeRange.trackedInitialRange.isEmpty()); - }; - this._sessionStore.add(toDisposable(() => { - wholeRangeDecoration.clear(); - this._ctxEditing.reset(); - })); - this._sessionStore.add(this._session.wholeRange.onDidChange(handleWholeRangeChange)); - handleWholeRangeChange(); - - this._ui.value.widget.setChatModel(this._session.chatModel); - this._updatePlaceholder(); - - const isModelEmpty = !this._session.chatModel.hasRequests; - this._ui.value.widget.updateToolbar(true); - this._ui.value.widget.toggleStatus(!isModelEmpty); - this._showWidget(this._session.headless, isModelEmpty); - - this._sessionStore.add(this._editor.onDidChangeModel((e) => { - const msg = this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange - : Message.CANCEL_SESSION; - this._log('model changed, pause or cancel session', msg, e); - this._messages.fire(msg); - })); - - const filePartOfEditSessions = this._chatService.editingSessions.filter(session => - session.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified && e.modifiedURI.toString() === this._session!.textModelN.uri.toString()) - ); - - const withinEditSession = filePartOfEditSessions.find(session => - session.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified && e.hasModificationAt({ - range: this._session!.wholeRange.trackedInitialRange, - uri: this._session!.textModelN.uri - })) - ); - - const chatWidget = this._ui.value.widget.chatWidget; - this._delegateSession = withinEditSession || filePartOfEditSessions[0]; - chatWidget.input.setIsWithinEditSession(!!withinEditSession, filePartOfEditSessions.length > 0); - - this._sessionStore.add(this._editor.onDidChangeModelContent(e => { - - - if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) { - return; - } - - const wholeRange = this._session!.wholeRange; - let shouldFinishSession = false; - if (this._configurationService.getValue(InlineChatConfigKeys.FinishOnType)) { - for (const { range } of e.changes) { - shouldFinishSession = !Range.areIntersectingOrTouching(range, wholeRange.value); - } - } - - this._session!.recordExternalEditOccurred(shouldFinishSession); - - if (shouldFinishSession) { - this._log('text changed outside of whole range, FINISH session'); - this.acceptSession(); - } - })); - - this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'removeRequest') { - // TODO@jrieken there is still some work left for when a request "in the middle" - // is removed. We will undo all changes till that point but not remove those - // later request - await this._session!.undoChangesUntil(e.requestId); - } - })); - - // apply edits from completed requests that haven't been applied yet - const editState = this._createChatTextEditGroupState(); - let didEdit = false; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response || request.response.result?.errorDetails) { - // done when seeing the first request that is still pending (no response). - break; - } - for (const part of request.response.response.value) { - if (part.kind !== 'textEditGroup' || !isEqual(part.uri, this._session.textModelN.uri)) { - continue; - } - if (part.state?.applied) { - continue; - } - for (const edit of part.edits) { - this._makeChanges(edit, undefined, !didEdit); - didEdit = true; - } - part.state ??= editState; - } - } - if (didEdit) { - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); - this._session.wholeRange.fixup(diff?.changes ?? []); - await this._session.hunkData.recompute(editState, diff); - - this._updateCtxResponseType(); - } - options.position = await this._strategy.renderChanges(); - - if (this._session.chatModel.requestInProgress) { - return State.SHOW_REQUEST; - } else { - return State.WAIT_FOR_INPUT; - } - } - - private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - this._updatePlaceholder(); - - if (options.message) { - this._updateInput(options.message); - aria.alert(options.message); - delete options.message; - this._showWidget(this._session.headless, false); - } - - let message = Message.NONE; - let request: IChatRequestModel | undefined; - - const barrier = new Barrier(); - const store = new DisposableStore(); - store.add(this._session.chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - request = e.request; - message = Message.ACCEPT_INPUT; - barrier.open(); - } - })); - store.add(this._strategy.onDidAccept(() => this.acceptSession())); - store.add(this._strategy.onDidDiscard(() => this.cancelSession())); - store.add(this.chatWidget.onDidHide(() => this.cancelSession())); - store.add(Event.once(this._messages.event)(m => { - this._log('state=_waitForInput) message received', m); - message = m; - barrier.open(); - })); - - if (options.attachments) { - await Promise.all(options.attachments.map(async attachment => { - await this._ui.value.widget.chatWidget.attachmentModel.addFile(attachment); - })); - delete options.attachments; - } - if (options.autoSend) { - delete options.autoSend; - this._showWidget(this._session.headless, false); - this._ui.value.widget.chatWidget.acceptInput(); - } - - await barrier.wait(); - store.dispose(); - - - if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) { - return State.CANCEL; - } - - if (message & Message.PAUSE_SESSION) { - return State.PAUSE; - } - - if (message & Message.ACCEPT_SESSION) { - this._ui.value.widget.selectAll(); - return State.ACCEPT; - } - - if (!request?.message.text) { - return State.WAIT_FOR_INPUT; - } - - - return State.SHOW_REQUEST; - } - - - private async [State.SHOW_REQUEST](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - assertType(this._session.chatModel.requestInProgress); - - this._ctxRequestInProgress.set(true); - - const { chatModel } = this._session; - const request = chatModel.lastRequest; - - assertType(request); - assertType(request.response); - - this._showWidget(this._session.headless, false); - this._ui.value.widget.selectAll(); - this._ui.value.widget.updateInfo(''); - this._ui.value.widget.toggleStatus(true); - - const { response } = request; - const responsePromise = new DeferredPromise(); - - const store = new DisposableStore(); - - const progressiveEditsCts = store.add(new CancellationTokenSource()); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsQueue = new Queue(); - - // disable typing and squiggles while streaming a reply - const origDeco = this._editor.getOption(EditorOption.renderValidationDecorations); - this._editor.updateOptions({ - renderValidationDecorations: 'off' - }); - store.add(toDisposable(() => { - this._editor.updateOptions({ - renderValidationDecorations: origDeco - }); - })); - - - let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT; - store.add(Event.once(this._messages.event)(message => { - this._log('state=_makeRequest) message received', message); - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); - if (message & Message.CANCEL_SESSION) { - next = State.CANCEL; - } else if (message & Message.PAUSE_SESSION) { - next = State.PAUSE; - } else if (message & Message.ACCEPT_SESSION) { - next = State.ACCEPT; - } - })); - - store.add(chatModel.onDidChange(async e => { - if (e.kind === 'removeRequest' && e.requestId === request.id) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - if (e.reason === ChatRequestRemovalReason.Resend) { - next = State.SHOW_REQUEST; - } else { - next = State.CANCEL; - } - return; - } - if (e.kind === 'move') { - assertType(this._session); - const log: typeof this._log = (msg: string, ...args: unknown[]) => this._log('state=_showRequest) moving inline chat', msg, ...args); - - log('move was requested', e.target, e.range); - - // if there's already a tab open for targetUri, show it and move inline chat to that tab - // otherwise, open the tab to the side - const initialSelection = Selection.fromRange(Range.lift(e.range), SelectionDirection.LTR); - const editorPane = await this._editorService.openEditor({ resource: e.target, options: { selection: initialSelection } }, SIDE_GROUP); - - if (!editorPane) { - log('opening editor failed'); - return; - } - - const newEditor = editorPane.getControl(); - if (!isCodeEditor(newEditor) || !newEditor.hasModel()) { - log('new editor is either missing or not a code editor or does not have a model'); - return; - } - - if (this._inlineChatSessionService.getSession(newEditor, e.target)) { - log('new editor ALREADY has a session'); - return; - } - - const newSession = await this._inlineChatSessionService.createSession( - newEditor, - { - session: this._session, - }, - CancellationToken.None); // TODO@ulugbekna: add proper cancellation? - - - InlineChatController1.get(newEditor)?.run({ existingSession: newSession }); - - next = State.CANCEL; - responsePromise.complete(); - - return; - } - })); - - // cancel the request when the user types - store.add(this._ui.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); - })); - - let lastLength = 0; - let isFirstChange = true; - - const editState = this._createChatTextEditGroupState(); - let localEditGroup: IChatTextEditGroup | undefined; - - // apply edits - const handleResponse = () => { - - this._updateCtxResponseType(); - - if (!localEditGroup) { - localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - } - - if (localEditGroup) { - - localEditGroup.state ??= editState; - - const edits = localEditGroup.edits; - const newEdits = edits.slice(lastLength); - if (newEdits.length > 0) { - - this._log(`${this._session?.textModelN.uri.toString()} received ${newEdits.length} edits`); - - // NEW changes - lastLength = edits.length; - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - - const startThen = this._session!.wholeRange.value.getStartPosition(); - - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - for (const edits of newEdits) { - await this._makeChanges(edits, { - duration: progressiveEditsAvgDuration.value, - token: progressiveEditsCts.token - }, isFirstChange); - - isFirstChange = false; - } - - // reshow the widget if the start position changed or shows at the wrong position - const startNow = this._session!.wholeRange.value.getStartPosition(); - if (!startNow.equals(startThen) || !this._ui.value.position?.equals(startNow)) { - this._showWidget(this._session!.headless, false, startNow.delta(-1)); - } - }); - } - } - - if (response.isCanceled) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - - } else if (response.isComplete) { - responsePromise.complete(); - } - }; - store.add(response.onDidChange(handleResponse)); - handleResponse(); - - // (1) we must wait for the request to finish - // (2) we must wait for all edits that came in via progress to complete - await responsePromise.p; - await progressiveEditsQueue.whenIdle(); - - if (response.result?.errorDetails && !response.result.errorDetails.responseIsFiltered) { - await this._session.undoChangesUntil(response.requestId); - } - - store.dispose(); - - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); - this._session.wholeRange.fixup(diff?.changes ?? []); - await this._session.hunkData.recompute(editState, diff); - - this._ctxRequestInProgress.set(false); - - - let newPosition: Position | undefined; - - if (response.result?.errorDetails) { - // error -> no message, errors are shown with the request - alert(response.result.errorDetails.message); - } else if (response.response.value.length === 0) { - // empty -> show message - const status = localize('empty', "No results, please refine your input and try again"); - this._ui.value.widget.updateStatus(status, { classes: ['warn'] }); - alert(status); - } else { - // real response -> no message - this._ui.value.widget.updateStatus(''); - alert(localize('responseWasEmpty', "Response was empty")); - } - - const position = await this._strategy.renderChanges(); - if (position) { - // if the selection doesn't start far off we keep the widget at its current position - // because it makes reading this nicer - const selection = this._editor.getSelection(); - if (selection?.containsPosition(position)) { - if (position.lineNumber - selection.startLineNumber > 8) { - newPosition = position; - } - } else { - newPosition = position; - } - } - this._showWidget(this._session.headless, false, newPosition); - - return next; - } - - private async[State.PAUSE]() { - - this._resetWidget(); - - this._strategy?.dispose?.(); - this._session = undefined; - } - - private async[State.ACCEPT]() { - assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - try { - await this._strategy.apply(); - } catch (err) { - this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err))); - this._log('FAILED to apply changes'); - this._log(err); - } - - this._resetWidget(); - this._inlineChatSessionService.releaseSession(this._session); - - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - private async[State.CANCEL]() { - - this._resetWidget(); - - if (this._session) { - // assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - // only stash sessions that were not unstashed, not "empty", and not interacted with - const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending; - let undoCancelEdits: IValidEditOperation[] = []; - try { - undoCancelEdits = this._strategy.cancel(); - } catch (err) { - this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err))); - this._log('FAILED to discard changes'); - this._log(err); - } - - this._stashedSession.clear(); - if (shouldStash) { - this._stashedSession.value = this._inlineChatSessionService.stashSession(this._session, this._editor, undoCancelEdits); - } else { - this._inlineChatSessionService.releaseSession(this._session); - } - } - - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - // ---- - - private _showWidget(headless: boolean = false, initialRender: boolean = false, position?: Position) { - assertType(this._editor.hasModel()); - this._ctxVisible.set(true); - - let widgetPosition: Position; - if (position) { - // explicit position wins - widgetPosition = position; - } else if (this._ui.rawValue?.position) { - // already showing - special case of line 1 - if (this._ui.rawValue?.position.lineNumber === 1) { - widgetPosition = this._ui.rawValue?.position.delta(-1); - } else { - widgetPosition = this._ui.rawValue?.position; - } - } else { - // default to ABOVE the selection - widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); - } - - if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) { - widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1); - } - - if (initialRender && (this._editor.getOption(EditorOption.stickyScroll)).enabled) { - this._editor.revealLine(widgetPosition.lineNumber); // do NOT substract `this._editor.getOption(EditorOption.stickyScroll).maxLineCount` because the editor already does that - } - - if (!headless) { - if (this._ui.rawValue?.position) { - this._ui.value.updatePositionAndHeight(widgetPosition); - } else { - this._ui.value.show(widgetPosition); - } - } - - return widgetPosition; - } - - private _resetWidget() { - - this._sessionStore.clear(); - this._ctxVisible.reset(); - - this._ui.rawValue?.hide(); - - // Return focus to the editor only if the current focus is within the editor widget - if (this._editor.hasWidgetFocus()) { - this._editor.focus(); - } - } - - private _updateCtxResponseType(): void { - - if (!this._session) { - this._ctxResponseType.set(InlineChatResponseType.None); - return; - } - - const hasLocalEdit = (response: IResponse): boolean => { - return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - }; - - let responseType = InlineChatResponseType.None; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response) { - continue; - } - responseType = InlineChatResponseType.Messages; - if (hasLocalEdit(request.response.response)) { - responseType = InlineChatResponseType.MessagesAndEdits; - break; // no need to check further - } - } - this._ctxResponseType.set(responseType); - this._ctxResponse.set(responseType !== InlineChatResponseType.None); - } - - private _createChatTextEditGroupState(): IChatTextEditGroupState { - assertType(this._session); - - const sha1 = new DefaultModelSHA1Computer(); - const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0) - ? sha1.computeSHA1(this._session.textModel0) - : generateUuid(); - - return { - sha1: textModel0Sha1, - applied: 0 - }; - } - - private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { - assertType(this._session); - assertType(this._strategy); - - const moreMinimalEdits = await raceCancellation(this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits), opts?.token || CancellationToken.None); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits); - - if (moreMinimalEdits?.length === 0) { - // nothing left to do - return; - } - - const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; - const editOperations = actualEdits.map(TextEdit.asEditOperation); - - const editsObserver: IEditObserver = { - start: () => this._session!.hunkData.ignoreTextModelNChanges = true, - stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, - }; - - const metadata = this._getMetadata(); - if (opts) { - await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore, metadata); - } else { - await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore, metadata); - } - } - - private _getMetadata(): IInlineChatMetadata { - const lastRequest = this._session?.chatModel.lastRequest; - return { - extensionId: VersionedExtensionId.tryCreate(this._session?.agent.extensionId.value, this._session?.agent.extensionVersion), - modelId: lastRequest?.modelId, - requestId: lastRequest?.id, - }; - } - - private _updatePlaceholder(): void { - this._ui.value.widget.placeholder = this._session?.agent.description ?? localize('askOrEditInContext', 'Ask or edit in context'); - } - - private _updateInput(text: string, selectAll = true): void { - - this._ui.value.widget.chatWidget.setInput(text); - if (selectAll) { - const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1); - this._ui.value.widget.chatWidget.inputEditor.setSelection(newSelection); - } - } - - // ---- controller API - - arrowOut(up: boolean): void { - if (this._ui.value.position && this._editor.hasModel()) { - const { column } = this._editor.getPosition(); - const { lineNumber } = this._ui.value.position; - const newLine = up ? lineNumber : lineNumber + 1; - this._editor.setPosition({ lineNumber: newLine, column }); - this._editor.focus(); - } - } - - focus(): void { - this._ui.value.widget.focus(); - } - - async viewInChat() { - if (!this._strategy || !this._session) { - return; - } - - let someApplied = false; - let lastEdit: IChatTextEditGroup | undefined; - - const uri = this._editor.getModel()?.uri; - const requests = this._session.chatModel.getRequests(); - for (const request of requests) { - if (!request.response) { - continue; - } - for (const part of request.response.response.value) { - if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) { - // fully or partially applied edits - someApplied = someApplied || Boolean(part.state?.applied); - lastEdit = part; - part.edits = []; - part.state = undefined; - } - } - } - - const doEdits = this._strategy.cancel(); - - if (someApplied) { - assertType(lastEdit); - lastEdit.edits = [doEdits]; - } - - await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel, false); - - this.cancelSession(); - } - - acceptSession(): void { - const response = this._session?.chatModel.getRequests().at(-1)?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { - kind: 'inlineChat', - action: 'accepted' - } - }); - } - this._messages.fire(Message.ACCEPT_SESSION); - } - - acceptHunk(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.Accept); - } - discardHunk(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.Discard); - } +export abstract class InlineChatRunOptions { - toggleDiff(hunkInfo?: HunkInformation) { - return this._strategy?.performHunkAction(hunkInfo, HunkAction.ToggleDiff); - } + initialSelection?: ISelection; + initialRange?: IRange; + message?: string; + attachments?: URI[]; + autoSend?: boolean; + position?: IPosition; + modelSelector?: ILanguageModelChatSelector; + resolveOnResponse?: boolean; - moveHunk(next: boolean) { - this.focus(); - this._strategy?.performHunkAction(undefined, next ? HunkAction.MoveNext : HunkAction.MovePrev); - } + static isInlineChatRunOptions(options: unknown): options is InlineChatRunOptions { - async cancelSession() { - const response = this._session?.chatModel.lastRequest?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { - kind: 'inlineChat', - action: 'discarded' - } - }); + if (typeof options !== 'object' || options === null) { + return false; } - this._resetWidget(); - this._messages.fire(Message.CANCEL_SESSION); - } - - reportIssue() { - const response = this._session?.chatModel.lastRequest?.response; - if (response) { - this._chatService.notifyUserAction({ - sessionResource: response.session.sessionResource, - requestId: response.requestId, - agentId: response.agent?.id, - command: response.slashCommand?.name, - result: response.result, - action: { kind: 'bug' } - }); + const { initialSelection, initialRange, message, autoSend, position, attachments, modelSelector, resolveOnResponse } = options; + if ( + typeof message !== 'undefined' && typeof message !== 'string' + || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' + || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) + || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) + || typeof position !== 'undefined' && !Position.isIPosition(position) + || typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI)) + || typeof modelSelector !== 'undefined' && !isILanguageModelChatSelector(modelSelector) + || typeof resolveOnResponse !== 'undefined' && typeof resolveOnResponse !== 'boolean' + ) { + return false; } - } - - unstashLastSession(): Session | undefined { - const result = this._stashedSession.value?.unstash(); - return result; - } - - joinCurrentRun(): Promise | undefined { - return this._currentRun; - } - get isActive() { - return Boolean(this._currentRun); + return true; } +} - async createImageAttachment(attachment: URI): Promise { - if (attachment.scheme === Schemas.file) { - if (await this._fileService.canHandleResource(attachment)) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment); - } - } else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) { - const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None); - if (extractedImages) { - return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages); - } - } - - return undefined; - } +// TODO@jrieken THIS should be shared with the code in MainThreadEditors +function getEditorId(editor: ICodeEditor, model: ITextModel): string { + return `${editor.getId()},${model.id}`; } -export class InlineChatController2 implements IEditorContribution { +export class InlineChatController implements IEditorContribution { - static readonly ID = 'editor.contrib.inlineChatController2'; + static readonly ID = 'editor.contrib.inlineChatController'; - static get(editor: ICodeEditor): InlineChatController2 | undefined { - return editor.getContribution(InlineChatController2.ID) ?? undefined; + static get(editor: ICodeEditor): InlineChatController | undefined { + return editor.getContribution(InlineChatController.ID) ?? undefined; } + /** + * Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session. + * When set, this takes priority over the inlineChat.defaultModel setting. + */ + private static _userSelectedModel: string | undefined; + private readonly _store = new DisposableStore(); private readonly _isActiveController = observableValue(this, false); + private readonly _renderMode: IObservable<'zone' | 'hover'>; private readonly _zone: Lazy; + private readonly _gutterIndicator: InlineChatAffordance; private readonly _currentSession: IObservable; @@ -1261,22 +133,32 @@ export class InlineChatController2 implements IEditorContribution { private readonly _editor: ICodeEditor, @IInstantiationService private readonly _instaService: IInstantiationService, @INotebookEditorService private readonly _notebookEditorService: INotebookEditorService, - @IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService, + @IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService, @ICodeEditorService codeEditorService: ICodeEditorService, @IContextKeyService contextKeyService: IContextKeyService, + @IConfigurationService private readonly _configurationService: IConfigurationService, @ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService, @IFileService private readonly _fileService: IFileService, @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService, @IEditorService private readonly _editorService: IEditorService, @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, - @IInlineChatSessionService inlineChatService: IInlineChatSessionService, - @IChatService chatService: IChatService, + @ILanguageModelsService private readonly _languageModelService: ILanguageModelsService, + @ILogService private readonly _logService: ILogService, ) { + const editorObs = observableCodeEditor(_editor); + const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); + const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService); + this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService); + + const overlayWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs)); + const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs)); + this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget)); this._zone = new Lazy(() => { + assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model'); const location: IChatWidgetLocationOptions = { location: ChatAgentLocation.EditorInline, @@ -1287,16 +169,10 @@ export class InlineChatController2 implements IEditorContribution { return { type: ChatAgentLocation.EditorInline, + id: getEditorId(this._editor, this._editor.getModel()), selection: this._editor.getSelection(), document, - wholeRange, - close: () => { /* TODO@jrieken */ }, - delegateSessionResource: chatService.editingSessions.find(session => - session.entries.get().some(e => e.hasModificationAt({ - range: wholeRange, - uri: document - })) - )?.chatSessionResource, + wholeRange }; } }; @@ -1307,14 +183,16 @@ export class InlineChatController2 implements IEditorContribution { const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor); if (!!notebookEditor) { location.location = ChatAgentLocation.Notebook; - location.resolveData = () => { - assertType(this._editor.hasModel()); - - return { - type: ChatAgentLocation.Notebook, - sessionInputUri: this._editor.getModel().uri, + if (notebookAgentConfig.get()) { + location.resolveData = () => { + assertType(this._editor.hasModel()); + + return { + type: ChatAgentLocation.Notebook, + sessionInputUri: this._editor.getModel().uri, + }; }; - }; + } } const result = this._instaService.createInstance(InlineChatZoneWidget, @@ -1324,7 +202,12 @@ export class InlineChatController2 implements IEditorContribution { enableImplicitContext: false, renderInputOnTop: false, renderInputToolbarBelowInput: true, - filter: _item => false, // filter ALL items + filter: item => { + if (!isResponseVM(item)) { + return false; + } + return !!item.model.isPendingConfirmation.get(); + }, menus: { telemetrySource: 'inlineChatWidget', executeToolbar: MenuId.ChatEditorInlineExecute, @@ -1333,35 +216,50 @@ export class InlineChatController2 implements IEditorContribution { defaultMode: ChatMode.Ask }, { editor: this._editor, notebookEditor }, + () => Promise.resolve(), ); + this._store.add(result); + result.domNode.classList.add('inline-chat-2'); return result; }); - const editorObs = observableCodeEditor(_editor); - const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessions.onDidChangeSessions); + const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions); this._currentSession = derived(r => { sessionsSignal.read(r); const model = editorObs.model.read(r); - const value = model && _inlineChatSessions.getSession2(model.uri); - return value ?? undefined; + const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri); + return session ?? undefined; }); + let lastSession: IInlineChatSession2 | undefined = undefined; + this._store.add(autorun(r => { const session = this._currentSession.read(r); if (!session) { this._isActiveController.set(false, undefined); + + if (lastSession && !lastSession.chatModel.hasRequests) { + const state = lastSession.chatModel.inputModel.state.read(undefined); + if (!state || (!state.inputText && state.attachments.length === 0)) { + lastSession.dispose(); + lastSession = undefined; + } + } return; } + + lastSession = session; + let foundOne = false; for (const editor of codeEditorService.listCodeEditors()) { - if (Boolean(InlineChatController2.get(editor)?._isActiveController.read(undefined))) { + if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) { foundOne = true; break; } @@ -1386,18 +284,33 @@ export class InlineChatController2 implements IEditorContribution { } })); + const defaultPlaceholderObs = visibleSessionObs.map((session, r) => { + return session?.initialSelection.isEmpty() + ? localize('placeholder', "Generate code") + : localize('placeholderWithSelection', "Modify selected code"); + }); + + this._store.add(autorun(r => { // HIDE/SHOW const session = visibleSessionObs.read(r); + const renderMode = this._renderMode.read(r); if (!session) { this._zone.rawValue?.hide(); + this._zone.rawValue?.widget.chatWidget.setModel(undefined); _editor.focus(); ctxInlineChatVisible.reset(); + } else if (renderMode === 'hover') { + // hover mode: set model but don't show zone, keep focus in editor + this._zone.value.widget.chatWidget.setModel(session.chatModel); + this._zone.rawValue?.hide(); + ctxInlineChatVisible.set(true); } else { ctxInlineChatVisible.set(true); - this._zone.value.widget.setChatModel(session.chatModel); + this._zone.value.widget.chatWidget.setModel(session.chatModel); if (!this._zone.value.position) { + this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug this._zone.value.show(session.initialPosition); } @@ -1406,25 +319,108 @@ export class InlineChatController2 implements IEditorContribution { } })); + // Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled + this._store.add(autorun(r => { + const session = visibleSessionObs.read(r); + const renderMode = this._renderMode.read(r); + if (!session || renderMode !== 'hover') { + sessionOverlayWidget.hide(); + return; + } + const lastRequest = session.chatModel.lastRequestObs.read(r); + const isInProgress = lastRequest?.response?.isInProgress.read(r); + const entry = session.editingSession.readEntry(session.uri, r); + // When there's no entry (no changes made) and the response is complete, the widget should be hidden. + // When there's an entry in Modified state, it needs to be settled (accepted/rejected). + const isNotSettled = entry ? entry.state.read(r) === ModifiedFileEntryState.Modified : false; + if (isInProgress || isNotSettled) { + sessionOverlayWidget.show(session); + } else { + sessionOverlayWidget.hide(); + } + })); + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); + if (session) { + const entries = session.editingSession.entries.read(r); + const sessionCellUri = CellUri.parse(session.uri); + const otherEntries = entries.filter(entry => { + if (isEqual(entry.modifiedURI, session.uri)) { + return false; + } + // Don't count notebooks that include the session's cell + if (!!sessionCellUri && isEqual(sessionCellUri.notebook, entry.modifiedURI)) { + return false; + } + return true; + }); + for (const entry of otherEntries) { + // OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend + // that modifies other files + this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError); + } + } + })); + + const lastResponseObs = visibleSessionObs.map((session, r) => { if (!session) { return; } + const lastRequest = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().at(-1)).read(r); + return lastRequest?.response; + }); - const entry = session.editingSession.readEntry(session.uri, r); - entry?.enableReviewModeUntilSettled(); + const lastResponseProgressObs = lastResponseObs.map((response, r) => { + if (!response) { + return; + } + return observableFromEvent(this, response.onDidChange, () => response.response.value.findLast(part => part.kind === 'progressMessage')).read(r); + }); + + + this._store.add(autorun(r => { + const response = lastResponseObs.read(r); + + this._zone.rawValue?.widget.updateInfo(''); + + if (!response?.isInProgress.read(r)) { + + if (response?.result?.errorDetails) { + // ERROR case + this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`); + alert(response.result.errorDetails.message); + } + + // no response or not in progress + this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false); + this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r)); - const inProgress = session.chatModel.requestInProgressObs.read(r); - this._zone.value.widget.domNode.classList.toggle('request-in-progress', inProgress); - if (!inProgress) { - this._zone.value.widget.chatWidget.setInputPlaceholder(localize('placeholder', "Edit, refactor, and generate code")); } else { - const prompt = session.chatModel.getRequests().at(-1)?.message.text; - this._zone.value.widget.chatWidget.setInputPlaceholder(prompt || localize('loading', "Working...")); + this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true); + let placeholder = response.request?.message.text; + const lastProgress = lastResponseProgressObs.read(r); + if (lastProgress) { + placeholder = renderAsPlaintext(lastProgress.content); + } + this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working...")); + } + + })); + + this._store.add(autorun(r => { + const session = visibleSessionObs.read(r); + if (!session) { + return; + } + + const entry = session.editingSession.readEntry(session.uri, r); + if (entry?.state.read(r) === ModifiedFileEntryState.Modified) { + entry?.enableReviewModeUntilSettled(); } })); + this._store.add(autorun(r => { const session = visibleSessionObs.read(r); @@ -1463,75 +459,187 @@ export class InlineChatController2 implements IEditorContribution { this._zone.rawValue?.widget.focus(); } - markActiveController() { - this._isActiveController.set(true, undefined); - } - async run(arg?: InlineChatRunOptions): Promise { assertType(this._editor.hasModel()); - - const uri = this._editor.getModel().uri; - const existingSession = this._inlineChatSessions.getSession2(uri); + const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri); if (existingSession) { await existingSession.editingSession.accept(); existingSession.dispose(); } - this.markActiveController(); + // use hover overlay to ask for input + if (!arg?.message && this._configurationService.getValue(InlineChatConfigKeys.RenderMode) === 'hover') { + // show menu and RETURN because the menu is re-entrant + await this._gutterIndicator.showMenuAtSelection(); + return true; + } - const session = await this._inlineChatSessions.createSession2(this._editor, uri, CancellationToken.None); + this._isActiveController.set(true, undefined); - // ADD diagnostics - const entries: IChatRequestVariableEntry[] = []; - for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { - if (range.intersectRanges(this._editor.getSelection())) { - const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); - entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter)); - } - } - if (entries.length > 0) { - this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries); - this._zone.value.widget.chatWidget.input.setValue(entries.length > 1 - ? localize('fixN', "Fix the attached problems") - : localize('fix1', "Fix the attached problem"), - true - ); - this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); - } + const session = this._inlineChatSessionService.createSession(this._editor); - // Check args - if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { - if (arg.initialRange) { - this._editor.revealRange(arg.initialRange); - } - if (arg.initialSelection) { - this._editor.setSelection(arg.initialSelection); + + // Store for tracking model changes during this session + const sessionStore = new DisposableStore(); + + try { + await this._applyModelDefaults(session, sessionStore); + + // ADD diagnostics + const entries: IChatRequestVariableEntry[] = []; + for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) { + if (range.intersectRanges(this._editor.getSelection())) { + const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker); + entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter)); + } } - if (arg.attachments) { - await Promise.all(arg.attachments.map(async attachment => { - await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); - })); - delete arg.attachments; + if (entries.length > 0) { + this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries); + this._zone.value.widget.chatWidget.input.setValue(entries.length > 1 + ? localize('fixN', "Fix the attached problems") + : localize('fix1', "Fix the attached problem"), + true + ); + this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1)); } - if (arg.message) { - this._zone.value.widget.chatWidget.setInput(arg.message); - if (arg.autoSend) { - await this._zone.value.widget.chatWidget.acceptInput(); + + // Check args + if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { + if (arg.initialRange) { + this._editor.revealRange(arg.initialRange); + } + if (arg.initialSelection) { + this._editor.setSelection(arg.initialSelection); + } + if (arg.attachments) { + await Promise.all(arg.attachments.map(async attachment => { + await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment); + })); + delete arg.attachments; + } + if (arg.modelSelector) { + const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0); + if (!id) { + throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`); + } + const model = this._languageModelService.lookupLanguageModel(id); + if (!model) { + throw new Error(`Language model not loaded: ${id}.`); + } + this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id }); + } + if (arg.message) { + this._zone.value.widget.chatWidget.setInput(arg.message); + if (arg.autoSend) { + await this._zone.value.widget.chatWidget.acceptInput(); + } } } + + if (!arg?.resolveOnResponse) { + // DEFAULT: wait for the session to be accepted or rejected + await Event.toPromise(session.editingSession.onDidDispose); + const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected; + return !rejected; + + } else { + // resolveOnResponse: ONLY wait for the file to be modified + const modifiedObs = derived(r => { + const entry = session.editingSession.readEntry(uri, r); + return entry?.state.read(r) === ModifiedFileEntryState.Modified && !entry?.isCurrentlyBeingModifiedBy.read(r); + }); + await waitForState(modifiedObs, state => state === true); + return true; + } + } finally { + sessionStore.dispose(); + } + } + + async acceptSession() { + const session = this._currentSession.get(); + if (!session) { + return; } + await session.editingSession.accept(); + session.dispose(); + } - await Event.toPromise(session.editingSession.onDidDispose); + async rejectSession() { + const session = this._currentSession.get(); + if (!session) { + return; + } + await session.editingSession.reject(); + session.dispose(); + } - const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected; - return !rejected; + private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise { + const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get(); + if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) { + const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor }); + for (const identifier of ids) { + const candidate = this._languageModelService.lookupLanguageModel(identifier); + if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) { + this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier }); + break; + } + } + } } - acceptSession() { - const value = this._currentSession.get(); - value?.editingSession.accept(); + /** + * Applies model defaults based on settings and tracks user model changes. + * Prioritization: user session choice > inlineChat.defaultModel setting > vendor default + */ + private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise { + const userSelectedModel = InlineChatController._userSelectedModel; + const defaultModelSetting = this._configurationService.getValue(InlineChatConfigKeys.DefaultModel); + + let modelApplied = false; + + // 1. Try user's explicitly chosen model from a previous inline chat in the same session + if (userSelectedModel) { + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]); + if (!modelApplied) { + // User's previously selected model is no longer available, clear it + InlineChatController._userSelectedModel = undefined; + } + } + + // 2. Try inlineChat.defaultModel setting + if (!modelApplied && defaultModelSetting) { + modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]); + if (!modelApplied) { + this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`); + } + } + + // 3. Fall back to vendor default + if (!modelApplied) { + await this._selectVendorDefaultModel(session); + } + + // Track model changes - store user's explicit choice in the given sessions. + // NOTE: This currently detects any model change, not just user-initiated ones. + let initialModelId: string | undefined; + sessionStore.add(autorun(r => { + const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r); + if (!newModel) { + return; + } + if (!initialModelId) { + initialModelId = newModel.identifier; + return; + } + if (initialModelId !== newModel.identifier) { + // User explicitly changed model, store their choice as qualified name + InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata); + initialModelId = newModel.identifier; + } + })); } async createImageAttachment(attachment: URI): Promise { @@ -1560,14 +668,13 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito const chatService = accessor.get(IChatService); const uri = editor.getModel().uri; - const chatModel = chatService.startSession(ChatAgentLocation.EditorInline, token, false); + const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; chatModel.startEditingSession(true); - const editSession = await chatModel.editingSessionObs?.promise; - const store = new DisposableStore(); - store.add(chatModel); + store.add(chatModelRef); // STREAM const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, { @@ -1591,11 +698,11 @@ export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEdito chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true }); if (!token.isCancellationRequested) { - chatModel.completeResponse(chatRequest); + chatRequest.response.complete(); } const isSettled = derived(r => { - const entry = editSession?.readEntry(uri, r); + const entry = chatModel.editingSession?.readEntry(uri, r); if (!entry) { return false; } @@ -1613,14 +720,13 @@ export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, const chatService = accessor.get(IChatService); const notebookService = accessor.get(INotebookService); const isNotebook = notebookService.hasSupportedNotebooks(uri); - const chatModel = chatService.startSession(ChatAgentLocation.EditorInline, token, false); + const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline); + const chatModel = chatModelRef.object as ChatModel; chatModel.startEditingSession(true); - const editSession = await chatModel.editingSessionObs?.promise; - const store = new DisposableStore(); - store.add(chatModel); + store.add(chatModelRef); // STREAM const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0); @@ -1653,7 +759,7 @@ export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, } const isSettled = derived(r => { - const entry = editSession?.readEntry(uri, r); + const entry = chatModel.editingSession?.readEntry(uri, r); if (!entry) { return false; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts deleted file mode 100644 index fce058db20d..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatCurrentLine.ts +++ /dev/null @@ -1,366 +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 { stringValue } from '../../../../base/browser/cssValue.js'; -import { createStyleSheet2 } from '../../../../base/browser/domStylesheets.js'; -import { IMouseEvent } from '../../../../base/browser/mouseEvent.js'; -import { toAction } from '../../../../base/common/actions.js'; -import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ICodeEditor, MouseTargetType } from '../../../../editor/browser/editorBrowser.js'; -import { EditorAction2, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; -import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; -import { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IEditorContribution } from '../../../../editor/common/editorCommon.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; -import { PLAINTEXT_LANGUAGE_ID } from '../../../../editor/common/languages/modesRegistry.js'; -import { IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { IChatAgentService } from '../../chat/common/chatAgents.js'; -import { ChatAgentLocation } from '../../chat/common/constants.js'; -import { AGENT_FILE_EXTENSION, LEGACY_MODE_FILE_EXTENSION } from '../../chat/common/promptSyntax/config/promptFileLocations.js'; -import { INSTRUCTIONS_LANGUAGE_ID, PROMPT_LANGUAGE_ID } from '../../chat/common/promptSyntax/promptTypes.js'; -import { ACTION_START, CTX_INLINE_CHAT_V1_ENABLED, CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { AbstractInline1ChatAction } from './inlineChatActions.js'; -import { InlineChatController } from './inlineChatController.js'; -import './media/inlineChat.css'; - -/** - * Set of language IDs where inline chat hints should not be shown. - */ -const IGNORED_LANGUAGE_IDS = new Set([ - PLAINTEXT_LANGUAGE_ID, - 'markdown', - 'search-result', - INSTRUCTIONS_LANGUAGE_ID, - PROMPT_LANGUAGE_ID, - LEGACY_MODE_FILE_EXTENSION, - AGENT_FILE_EXTENSION -]); - -export const CTX_INLINE_CHAT_SHOWING_HINT = new RawContextKey('inlineChatShowingHint', false, localize('inlineChatShowingHint', "Whether inline chat shows a contextual hint")); - -const _inlineChatActionId = 'inlineChat.startWithCurrentLine'; - -export class InlineChatExpandLineAction extends EditorAction2 { - - constructor() { - super({ - id: _inlineChatActionId, - category: AbstractInline1ChatAction.category, - title: localize2('startWithCurrentLine', "Start in Editor with Current Line"), - f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_V1_ENABLED, EditorContextKeys.writable), - keybinding: [{ - when: CTX_INLINE_CHAT_SHOWING_HINT, - weight: KeybindingWeight.WorkbenchContrib + 1, - primary: KeyMod.CtrlCmd | KeyCode.KeyI - }, { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI), - }] - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { - const ctrl = InlineChatController.get(editor); - if (!ctrl || !editor.hasModel()) { - return; - } - - const model = editor.getModel(); - const lineNumber = editor.getSelection().positionLineNumber; - const lineContent = model.getLineContent(lineNumber); - - const startColumn = model.getLineFirstNonWhitespaceColumn(lineNumber); - const endColumn = model.getLineMaxColumn(lineNumber); - - // clear the line - let undoEdits: IValidEditOperation[] = []; - model.pushEditOperations(null, [EditOperation.replace(new Range(lineNumber, startColumn, lineNumber, endColumn), '')], (edits) => { - undoEdits = edits; - return null; - }); - - // trigger chat - const accepted = await ctrl.run({ - autoSend: true, - message: lineContent.trim(), - position: new Position(lineNumber, startColumn) - }); - - if (!accepted) { - model.pushEditOperations(null, undoEdits, () => null); - } - } -} - -export class ShowInlineChatHintAction extends EditorAction2 { - - constructor() { - super({ - id: 'inlineChat.showHint', - category: AbstractInline1ChatAction.category, - title: localize2('showHint', "Show Inline Chat Hint"), - f1: false, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE.negate(), CTX_INLINE_CHAT_V1_ENABLED, EditorContextKeys.writable), - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ...args: [uri: URI, position: IPosition, ...rest: unknown[]]) { - if (!editor.hasModel()) { - return; - } - - const ctrl = InlineChatHintsController.get(editor); - if (!ctrl) { - return; - } - - const [uri, position] = args; - if (!URI.isUri(uri) || !Position.isIPosition(position)) { - ctrl.hide(); - return; - } - - const model = editor.getModel(); - if (!isEqual(model.uri, uri)) { - ctrl.hide(); - return; - } - - model.tokenization.forceTokenization(position.lineNumber); - const tokens = model.tokenization.getLineTokens(position.lineNumber); - - let totalLength = 0; - let specialLength = 0; - let lastTokenType: StandardTokenType | undefined; - - tokens.forEach(idx => { - const tokenType = tokens.getStandardTokenType(idx); - const startOffset = tokens.getStartOffset(idx); - const endOffset = tokens.getEndOffset(idx); - totalLength += endOffset - startOffset; - - if (tokenType !== StandardTokenType.Other) { - specialLength += endOffset - startOffset; - } - lastTokenType = tokenType; - }); - - if (specialLength / totalLength > 0.25) { - ctrl.hide(); - return; - } - if (lastTokenType === StandardTokenType.Comment) { - ctrl.hide(); - return; - } - ctrl.show(); - } -} - -export class InlineChatHintsController extends Disposable implements IEditorContribution { - - public static readonly ID = 'editor.contrib.inlineChatHints'; - - static get(editor: ICodeEditor): InlineChatHintsController | null { - return editor.getContribution(InlineChatHintsController.ID); - } - - private readonly _editor: ICodeEditor; - private readonly _ctxShowingHint: IContextKey; - private readonly _visibilityObs = observableValue(this, false); - - constructor( - editor: ICodeEditor, - @IContextKeyService contextKeyService: IContextKeyService, - @ICommandService commandService: ICommandService, - @IKeybindingService keybindingService: IKeybindingService, - @IChatAgentService chatAgentService: IChatAgentService, - @IMarkerDecorationsService markerDecorationService: IMarkerDecorationsService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(); - this._editor = editor; - this._ctxShowingHint = CTX_INLINE_CHAT_SHOWING_HINT.bindTo(contextKeyService); - - const ghostCtrl = InlineCompletionsController.get(editor); - - this._store.add(commandService.onWillExecuteCommand(e => { - if (e.commandId === _inlineChatActionId || e.commandId === ACTION_START) { - this.hide(); - } - })); - - this._store.add(this._editor.onMouseDown(e => { - if (e.target.type !== MouseTargetType.CONTENT_TEXT) { - return; - } - if (!e.target.element?.classList.contains('inline-chat-hint-text')) { - return; - } - if (e.event.leftButton) { - commandService.executeCommand(_inlineChatActionId); - this.hide(); - } else if (e.event.rightButton) { - e.event.preventDefault(); - this._showContextMenu(e.event, e.target.element?.classList.contains('whitespace') - ? InlineChatConfigKeys.LineEmptyHint - : InlineChatConfigKeys.LineNLHint - ); - } - })); - - const markerSuppression = this._store.add(new MutableDisposable()); - const decos = this._editor.createDecorationsCollection(); - - const editorObs = observableCodeEditor(editor); - const keyObs = observableFromEvent(keybindingService.onDidUpdateKeybindings, _ => keybindingService.lookupKeybinding(ACTION_START)?.getLabel()); - const configHintEmpty = observableConfigValue(InlineChatConfigKeys.LineEmptyHint, false, this._configurationService); - const configHintNL = observableConfigValue(InlineChatConfigKeys.LineNLHint, false, this._configurationService); - - const showDataObs = derived((r) => { - const ghostState = ghostCtrl?.model.read(r)?.state.read(r); - - const textFocus = editorObs.isTextFocused.read(r); - const position = editorObs.cursorPosition.read(r); - const model = editorObs.model.read(r); - - const kb = keyObs.read(r); - - if (ghostState !== undefined || !kb || !position || !model || !textFocus) { - return undefined; - } - - if (IGNORED_LANGUAGE_IDS.has(model.getLanguageId())) { - return undefined; - } - - editorObs.versionId.read(r); - - const visible = this._visibilityObs.read(r); - const isEol = model.getLineMaxColumn(position.lineNumber) === position.column; - const isWhitespace = model.getLineLastNonWhitespaceColumn(position.lineNumber) === 0 && model.getValueLength() > 0 && position.column > 1; - - if (isWhitespace) { - return configHintEmpty.read(r) - ? { isEol, isWhitespace, kb, position, model } - : undefined; - } - - if (visible && isEol && configHintNL.read(r)) { - return { isEol, isWhitespace, kb, position, model }; - } - - return undefined; - }); - - const style = createStyleSheet2(); - this._store.add(style); - - this._store.add(autorun(r => { - - const showData = showDataObs.read(r); - if (!showData) { - decos.clear(); - markerSuppression.clear(); - this._ctxShowingHint.reset(); - return; - } - - const agentName = chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)?.name ?? localize('defaultTitle', "Chat"); - const { position, isEol, isWhitespace, kb, model } = showData; - - const inlineClassName: string[] = ['a' /*HACK but sorts as we want*/, 'inline-chat-hint', 'inline-chat-hint-text']; - let content: string; - if (isWhitespace) { - content = '\u00a0' + localize('title2', "{0} to edit with {1}", kb, agentName); - } else if (isEol) { - content = '\u00a0' + localize('title1', "{0} to continue with {1}", kb, agentName); - } else { - content = '\u200a' + kb + '\u200a'; - inlineClassName.push('embedded'); - } - - style.setStyle(`.inline-chat-hint-text::after { content: ${stringValue(content)} }`); - if (isWhitespace) { - inlineClassName.push('whitespace'); - } - - this._ctxShowingHint.set(true); - - decos.set([{ - range: Range.fromPositions(position), - options: { - description: 'inline-chat-hint-line', - showIfCollapsed: true, - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - afterContentClassName: inlineClassName.join(' '), - } - }]); - - markerSuppression.value = markerDecorationService.addMarkerSuppression(model.uri, model.validateRange(new Range(position.lineNumber, 1, position.lineNumber, Number.MAX_SAFE_INTEGER))); - })); - } - - private _showContextMenu(event: IMouseEvent, setting: string): void { - this._contextMenuService.showContextMenu({ - getAnchor: () => ({ x: event.posx, y: event.posy }), - getActions: () => [ - toAction({ - id: 'inlineChat.disableHint', - label: localize('disableHint', "Disable Inline Chat Hint"), - run: async () => { - await this._configurationService.updateValue(setting, false); - } - }) - ] - }); - } - - show(): void { - this._visibilityObs.set(true, undefined); - } - - hide(): void { - this._visibilityObs.set(false, undefined); - } -} - -export class HideInlineChatHintAction extends EditorAction2 { - - constructor() { - super({ - id: 'inlineChat.hideHint', - title: localize2('hideHint', "Hide Inline Chat Hint"), - precondition: CTX_INLINE_CHAT_SHOWING_HINT, - keybinding: { - weight: KeybindingWeight.EditorContrib - 10, - primary: KeyCode.Escape - } - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { - InlineChatHintsController.get(editor)?.hide(); - } -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts new file mode 100644 index 00000000000..05514e5c594 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatDefaultModel.ts @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from '../../../../nls.js'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { ILanguageModelsService, ILanguageModelChatMetadata } from '../../chat/common/languageModels.js'; +import { InlineChatConfigKeys } from '../common/inlineChat.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { DEFAULT_MODEL_PICKER_CATEGORY } from '../../chat/common/widget/input/modelPickerWidget.js'; + +export class InlineChatDefaultModel extends Disposable { + static readonly ID = 'workbench.contrib.inlineChatDefaultModel'; + static readonly configName = InlineChatConfigKeys.DefaultModel; + + static modelIds: string[] = ['']; + static modelLabels: string[] = [localize('defaultModel', 'Auto (Vendor Default)')]; + static modelDescriptions: string[] = [localize('defaultModelDescription', 'Use the vendor\'s default model')]; + + constructor( + @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this._register(languageModelsService.onDidChangeLanguageModels(() => this._updateModelValues())); + this._updateModelValues(); + } + + private _updateModelValues(): void { + try { + // Clear arrays + InlineChatDefaultModel.modelIds.length = 0; + InlineChatDefaultModel.modelLabels.length = 0; + InlineChatDefaultModel.modelDescriptions.length = 0; + + // Add default/empty option + InlineChatDefaultModel.modelIds.push(''); + InlineChatDefaultModel.modelLabels.push(localize('defaultModel', 'Auto (Vendor Default)')); + InlineChatDefaultModel.modelDescriptions.push(localize('defaultModelDescription', 'Use the vendor\'s default model')); + + // Get all available models + const modelIds = this.languageModelsService.getLanguageModelIds(); + + const models: { identifier: string; metadata: ILanguageModelChatMetadata }[] = []; + + // Look up each model's metadata + for (const modelId of modelIds) { + try { + const metadata = this.languageModelsService.lookupLanguageModel(modelId); + if (metadata) { + models.push({ identifier: modelId, metadata }); + } else { + this.logService.warn(`[InlineChatDefaultModel] No metadata found for model ID: ${modelId}`); + } + } catch (e) { + this.logService.error(`[InlineChatDefaultModel] Error looking up model ${modelId}:`, e); + } + } + + // Filter models that are: + // 1. User selectable + // 2. Support tool calling (required for inline chat v2) + const supportedModels = models.filter(model => { + if (!model.metadata?.isUserSelectable) { + return false; + } + // Check if model supports inline chat - needs tool calling capability + if (!model.metadata.capabilities?.toolCalling) { + return false; + } + return true; + }); + + // Sort by category order, then alphabetically by name within each category + supportedModels.sort((a, b) => { + const aCategory = a.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; + const bCategory = b.metadata.modelPickerCategory ?? DEFAULT_MODEL_PICKER_CATEGORY; + + // First sort by category order + if (aCategory.order !== bCategory.order) { + return aCategory.order - bCategory.order; + } + + // Then sort by name within the same category + return a.metadata.name.localeCompare(b.metadata.name); + }); + + // Populate arrays with filtered models + for (const model of supportedModels) { + try { + const qualifiedName = `${model.metadata.name} (${model.metadata.vendor})`; + InlineChatDefaultModel.modelIds.push(qualifiedName); + InlineChatDefaultModel.modelLabels.push(model.metadata.name); + InlineChatDefaultModel.modelDescriptions.push(model.metadata.tooltip ?? model.metadata.detail ?? ''); + } catch (e) { + this.logService.error(`[InlineChatDefaultModel] Error adding model ${model.metadata.name}:`, e); + } + } + } catch (e) { + this.logService.error('[InlineChatDefaultModel] Error updating model values:', e); + } + } +} + +registerWorkbenchContribution2(InlineChatDefaultModel.ID, InlineChatDefaultModel, WorkbenchPhase.BlockRestore); + +Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + ...{ id: 'inlineChat', title: localize('inlineChatConfigurationTitle', 'Inline Chat'), order: 30, type: 'object' }, + properties: { + [InlineChatDefaultModel.configName]: { + description: localize('inlineChatDefaultModelDescription', "Select the default language model to use for inline chat from the available providers. Model names may include the provider in parentheses, for example 'Claude Haiku 4.5 (copilot)'."), + type: 'string', + default: '', + enum: InlineChatDefaultModel.modelIds, + enumItemLabels: InlineChatDefaultModel.modelLabels, + markdownEnumDescriptions: InlineChatDefaultModel.modelDescriptions + } + } +}); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts new file mode 100644 index 00000000000..568fb591e54 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatEditorAffordance.ts @@ -0,0 +1,191 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/inlineChatEditorAffordance.css'; +import { IDimension } from '../../../../base/browser/dom.js'; +import * as dom from '../../../../base/browser/dom.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { autorun, IObservable } from '../../../../base/common/observable.js'; +import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { quickFixCommandId } from '../../../../editor/contrib/codeAction/browser/codeAction.js'; +import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { MenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { Codicon } from '../../../../base/common/codicons.js'; + +class QuickFixActionViewItem extends MenuEntryActionViewItem { + + private readonly _lightBulbStore = this._store.add(new MutableDisposable()); + private _currentTitle: string | undefined; + + constructor( + action: MenuItemAction, + private readonly _editor: ICodeEditor, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IAccessibilityService accessibilityService: IAccessibilityService + ) { + super(action, { draggable: false }, keybindingService, notificationService, contextKeyService, themeService, contextMenuService, accessibilityService); + } + + override render(container: HTMLElement): void { + super.render(container); + this._updateFromLightBulb(); + } + + protected override getTooltip(): string { + return this._currentTitle ?? super.getTooltip(); + } + + private _updateFromLightBulb(): void { + const controller = CodeActionController.get(this._editor); + if (!controller) { + return; + } + + const store = new DisposableStore(); + this._lightBulbStore.value = store; + + store.add(autorun(reader => { + const info = controller.lightBulbState.read(reader); + if (this.label) { + // Update icon + const icon = info?.icon ?? Codicon.lightBulb; + const iconClasses = ThemeIcon.asClassNameArray(icon); + this.label.className = ''; + this.label.classList.add('codicon', ...iconClasses); + } + + // Update tooltip + this._currentTitle = info?.title; + this.updateTooltip(); + })); + } +} + +/** + * Content widget that shows a small sparkle icon at the cursor position. + * When clicked, it shows the overlay widget for inline chat. + */ +export class InlineChatEditorAffordance extends Disposable implements IContentWidget { + + private static _idPool = 0; + + private readonly _id = `inline-chat-content-widget-${InlineChatEditorAffordance._idPool++}`; + private readonly _domNode: HTMLElement; + private _position: IContentWidgetPosition | null = null; + private _isVisible = false; + + readonly allowEditorOverflow = true; + readonly suppressMouseDown = false; + + constructor( + private readonly _editor: ICodeEditor, + selection: IObservable, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + // Create the widget DOM + this._domNode = dom.$('.inline-chat-content-widget'); + + // Create toolbar with the inline chat start action + this._store.add(instantiationService.createInstance(MenuWorkbenchToolBar, this._domNode, MenuId.InlineChatEditorAffordance, { + telemetrySource: 'inlineChatEditorAffordance', + hiddenItemStrategy: HiddenItemStrategy.Ignore, + menuOptions: { renderShortTitle: true }, + toolbarOptions: { primaryGroup: () => true }, + actionViewItemProvider: (action: IAction) => { + if (action instanceof MenuItemAction && action.id === quickFixCommandId) { + return instantiationService.createInstance(QuickFixActionViewItem, action, this._editor); + } + return undefined; + } + })); + + this._store.add(autorun(r => { + const sel = selection.read(r); + if (sel) { + this._show(sel); + } else { + this._hide(); + } + })); + } + + private _show(selection: Selection): void { + + // Position at the cursor (active end of selection) + const cursorPosition = selection.getPosition(); + const direction = selection.getDirection(); + + // Show above for RTL (selection going up), below for LTR (selection going down) + const preference = direction === SelectionDirection.RTL + ? ContentWidgetPositionPreference.ABOVE + : ContentWidgetPositionPreference.BELOW; + + this._position = { + position: cursorPosition, + preference: [preference], + }; + + if (this._isVisible) { + this._editor.layoutContentWidget(this); + } else { + this._editor.addContentWidget(this); + this._isVisible = true; + } + } + + private _hide(): void { + if (this._isVisible) { + this._isVisible = false; + this._editor.removeContentWidget(this); + } + } + + getId(): string { + return this._id; + } + + getDomNode(): HTMLElement { + return this._domNode; + } + + getPosition(): IContentWidgetPosition | null { + return this._position; + } + + beforeRender(): IDimension | null { + const position = this._editor.getPosition(); + const lineHeight = position ? this._editor.getLineHeightForPosition(position) : this._editor.getOption(EditorOption.lineHeight); + + this._domNode.style.setProperty('--vscode-inline-chat-affordance-height', `${lineHeight}px`); + + return null; + } + + override dispose(): void { + if (this._isVisible) { + this._editor.removeContentWidget(this); + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts new file mode 100644 index 00000000000..3b3be83e50e --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatGutterAffordance.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Codicon } from '../../../../base/common/codicons.js'; +import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; +import { Selection, SelectionDirection } from '../../../../editor/common/core/selection.js'; +import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/components/gutterIndicatorView.js'; +import { InlineEditTabAction } from '../../../../editor/contrib/inlineCompletions/browser/view/inlineEdits/inlineEditsViewInterface.js'; +import { localize } from '../../../../nls.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { HoverService } from '../../../../platform/hover/browser/hoverService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { ACTION_START } from '../common/inlineChat.js'; + +export class InlineChatGutterAffordance extends InlineEditsGutterIndicator { + + constructor( + private readonly _myEditorObs: ObservableCodeEditor, + selection: IObservable, + private readonly _hover: ISettableObservable<{ rect: DOMRect; above: boolean; lineNumber: number } | undefined>, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IHoverService hoverService: HoverService, + @IInstantiationService instantiationService: IInstantiationService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IThemeService themeService: IThemeService, + ) { + const data = derived(r => { + const value = selection.read(r); + if (!value) { + return undefined; + } + + // Use the cursor position (active end of selection) to determine the line + const cursorPosition = value.getPosition(); + const lineRange = new LineRange(cursorPosition.lineNumber, cursorPosition.lineNumber + 1); + + // Create minimal gutter menu data (empty for prototype) + const gutterMenuData = new InlineSuggestionGutterMenuData( + undefined, // action + '', // displayName + [], // extensionCommands + undefined, // alternativeAction + undefined, // modelInfo + undefined, // setModelId + ); + + return new InlineEditsGutterIndicatorData( + gutterMenuData, + lineRange, + new SimpleInlineSuggestModel(() => { }, () => this._doShowHover()), + undefined, // altAction + { + icon: Codicon.sparkle, + } + ); + }); + + const focusIsInMenu = observableValue({}, false); + + super( + _myEditorObs, data, constObservable(InlineEditTabAction.Inactive), constObservable(0), constObservable(false), focusIsInMenu, + hoverService, instantiationService, accessibilityService, themeService + ); + + this._store.add(autorun(r => { + const element = _hover.read(r); + this._hoverVisible.set(!!element, undefined); + })); + } + + protected override _showHover(): void { + this._hoverService.showInstantHover({ + target: this._iconRef.element, + content: this._keybindingService.appendKeybinding(localize('inlineChatGutterHover', "Inline Chat"), ACTION_START), + // appearance: { showPointer: true } + }); + } + + private _doShowHover(): void { + if (this._hoverVisible.get()) { + return; + } + + // Use the icon element from the base class as anchor + const iconElement = this._iconRef.element; + if (!iconElement) { + this._hover.set(undefined, undefined); + return; + } + + const selection = this._myEditorObs.cursorSelection.get(); + const direction = selection?.getDirection() ?? SelectionDirection.LTR; + const lineNumber = selection?.getPosition().lineNumber ?? 1; + this._hover.set({ rect: iconElement.getBoundingClientRect(), above: direction === SelectionDirection.RTL, lineNumber }, undefined); + } + +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts index 95c38b1b78e..539e8197ee0 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatNotebook.ts @@ -3,9 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { illegalState } from '../../../../base/common/errors.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { InlineChatController } from './inlineChatController.js'; @@ -13,8 +11,6 @@ import { IInlineChatSessionService } from './inlineChatSessionService.js'; import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js'; import { CellUri } from '../../notebook/common/notebookCommon.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { NotebookTextDiffEditor } from '../../notebook/browser/diff/notebookDiffEditor.js'; -import { NotebookMultiTextDiffEditor } from '../../notebook/browser/diff/notebookMultiDiffEditor.js'; export class InlineChatNotebookContribution { @@ -26,51 +22,6 @@ export class InlineChatNotebookContribution { @INotebookEditorService notebookEditorService: INotebookEditorService, ) { - this._store.add(sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, { - getComparisonKey: (editor, uri) => { - const data = CellUri.parse(uri); - if (!data) { - throw illegalState('Expected notebook cell uri'); - } - let fallback: string | undefined; - for (const notebookEditor of notebookEditorService.listNotebookEditors()) { - if (notebookEditor.hasModel() && isEqual(notebookEditor.textModel.uri, data.notebook)) { - - const candidate = `${notebookEditor.getId()}#${uri}`; - - if (!fallback) { - fallback = candidate; - } - - // find the code editor in the list of cell-code editors - if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - return candidate; - } - - // // reveal cell and try to find code editor again - // const cell = notebookEditor.getCellByHandle(data.handle); - // if (cell) { - // notebookEditor.revealInViewAtTop(cell); - // if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - // return candidate; - // } - // } - } - } - - if (fallback) { - return fallback; - } - - const activeEditor = editorService.activeEditorPane; - if (activeEditor && (activeEditor.getId() === NotebookTextDiffEditor.ID || activeEditor.getId() === NotebookMultiTextDiffEditor.ID)) { - return `${editor.getId()}#${uri}`; - } - - throw illegalState('Expected notebook editor'); - } - })); - this._store.add(sessionService.onWillStartSession(newSessionEditor => { const candidate = CellUri.parse(newSessionEditor.getModel().uri); if (!candidate) { diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts new file mode 100644 index 00000000000..0284206c2bf --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatOverlayWidget.ts @@ -0,0 +1,503 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/inlineChatOverlayWidget.css'; +import * as dom from '../../../../base/browser/dom.js'; +import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { IAction, Separator } from '../../../../base/common/actions.js'; +import { ActionBar, ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, constObservable, derived, IObservable, observableFromEvent, observableFromEventOpts, observableValue } from '../../../../base/common/observable.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IActiveCodeEditor, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; +import { ObservableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { localize } from '../../../../nls.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ChatEditingAcceptRejectActionViewItem } from '../../chat/browser/chatEditing/chatEditingEditorOverlay.js'; +import { ACTION_START } from '../common/inlineChat.js'; +import { StickyScrollController } from '../../../../editor/contrib/stickyScroll/browser/stickyScrollController.js'; +import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { PlaceholderTextContribution } from '../../../../editor/contrib/placeholderText/browser/placeholderTextContribution.js'; +import { InlineChatRunOptions } from './inlineChatController.js'; +import { IInlineChatSession2 } from './inlineChatSessionService.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { CancelChatActionId } from '../../chat/browser/actions/chatExecuteActions.js'; +import { assertType } from '../../../../base/common/types.js'; + +/** + * Overlay widget that displays a vertical action bar menu. + */ +export class InlineChatInputWidget extends Disposable { + + private readonly _domNode: HTMLElement; + private readonly _inputContainer: HTMLElement; + private readonly _actionBar: ActionBar; + private readonly _input: IActiveCodeEditor; + private readonly _position = observableValue(this, null); + readonly position: IObservable = this._position; + + + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _stickyScrollHeight: IObservable; + private _inlineStartAction: IAction | undefined; + private _anchorLineNumber: number = 0; + private _anchorLeft: number = 0; + private _anchorAbove: boolean = false; + + + constructor( + private readonly _editorObs: ObservableCodeEditor, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IMenuService private readonly _menuService: IMenuService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService modelService: IModelService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + + // Create container + this._domNode = dom.$('.inline-chat-gutter-menu'); + + // Create input editor container + this._inputContainer = dom.append(this._domNode, dom.$('.input')); + this._inputContainer.style.width = '200px'; + this._inputContainer.style.height = '26px'; + this._inputContainer.style.display = 'flex'; + this._inputContainer.style.alignItems = 'center'; + this._inputContainer.style.justifyContent = 'center'; + + // Create editor options + const options = getSimpleEditorOptions(configurationService); + options.wordWrap = 'on'; + options.lineNumbers = 'off'; + options.glyphMargin = false; + options.lineDecorationsWidth = 0; + options.lineNumbersMinChars = 0; + options.folding = false; + options.minimap = { enabled: false }; + options.scrollbar = { vertical: 'auto', horizontal: 'hidden', alwaysConsumeMouseWheel: true, verticalSliderSize: 6 }; + options.renderLineHighlight = 'none'; + + const codeEditorWidgetOptions: ICodeEditorWidgetOptions = { + isSimpleWidget: true, + contributions: EditorExtensionsRegistry.getSomeEditorContributions([ + PlaceholderTextContribution.ID, + ]) + }; + + this._input = this._store.add(instantiationService.createInstance(CodeEditorWidget, this._inputContainer, options, codeEditorWidgetOptions)) as IActiveCodeEditor; + + const model = this._store.add(modelService.createModel('', null, URI.parse(`gutter-input:${Date.now()}`), true)); + this._input.setModel(model); + + // Initialize sticky scroll height observable + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + + // Update placeholder based on selection state + this._store.add(autorun(r => { + const selection = this._editorObs.cursorSelection.read(r); + const hasSelection = selection && !selection.isEmpty(); + const placeholderText = hasSelection + ? localize('placeholderWithSelection', "Modify selected code") + : localize('placeholderNoSelection', "Generate code"); + + this._input.updateOptions({ placeholder: this._keybindingService.appendKeybinding(placeholderText, ACTION_START) }); + })); + + // Listen to content size changes and resize the input editor (max 3 lines) + this._store.add(this._input.onDidContentSizeChange(e => { + if (e.contentHeightChanged) { + this._updateInputHeight(e.contentHeight); + } + })); + + // Handle Enter key to submit and ArrowDown to focus action bar + this._store.add(this._input.onKeyDown(e => { + if (e.keyCode === KeyCode.Enter && !e.shiftKey) { + const value = this._input.getModel().getValue() ?? ''; + // TODO@jrieken this isn't nice + if (this._inlineStartAction && value) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.actionRunner.run( + this._inlineStartAction, + { message: value, autoSend: true } satisfies InlineChatRunOptions + ); + } + } else if (e.keyCode === KeyCode.Escape) { + // Hide overlay if input is empty + const value = this._input.getModel().getValue() ?? ''; + if (!value) { + e.preventDefault(); + e.stopPropagation(); + this._hide(); + } + } else if (e.keyCode === KeyCode.DownArrow) { + // Focus first action bar item when at the end of the input + const inputModel = this._input.getModel(); + const position = this._input.getPosition(); + const lastLineNumber = inputModel.getLineCount(); + const lastLineMaxColumn = inputModel.getLineMaxColumn(lastLineNumber); + if (Position.equals(position, new Position(lastLineNumber, lastLineMaxColumn))) { + e.preventDefault(); + e.stopPropagation(); + this._actionBar.focus(); + } + } + })); + + // Create vertical action bar + this._actionBar = this._store.add(new ActionBar(this._domNode, { + orientation: ActionsOrientation.VERTICAL, + preventLoopNavigation: true, + })); + + // Handle ArrowUp on first action bar item to focus input editor + this._store.add(dom.addDisposableListener(this._actionBar.domNode, 'keydown', e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.UpArrow) && this._actionBar.isFocused(this._actionBar.viewItems.findIndex(item => item.action.id !== Separator.ID))) { + event.preventDefault(); + event.stopPropagation(); + this._input.focus(); + } + }, true)); + + // Track focus - hide when focus leaves + const focusTracker = this._store.add(dom.trackFocus(this._domNode)); + this._store.add(focusTracker.onDidBlur(() => this._hide())); + + // Handle action bar cancel (Escape key) + this._store.add(this._actionBar.onDidCancel(() => this._hide())); + this._store.add(this._actionBar.onWillRun(() => this._hide())); + } + + /** + * Show the widget at the specified line. + * @param lineNumber The line number to anchor the widget to + * @param left Left offset relative to editor + * @param anchorAbove Whether to anchor above the position (widget grows upward) + */ + show(lineNumber: number, left: number, anchorAbove: boolean): void { + this._showStore.clear(); + + // Clear input state + this._input.getModel().setValue(''); + this._updateInputHeight(this._input.getContentHeight()); + + // Refresh actions from menu + this._refreshActions(); + + // Store anchor info for scroll updates + this._anchorLineNumber = lineNumber; + this._anchorLeft = left; + this._anchorAbove = anchorAbove; + + // Set initial position + this._updatePosition(); + + // Create overlay widget via observable pattern + this._showStore.add(this._editorObs.createOverlayWidget({ + domNode: this._domNode, + position: this._position, + minContentWidthInPx: constObservable(0), + allowEditorOverflow: true, + })); + + // If anchoring above, adjust position after render to account for widget height + if (anchorAbove) { + this._updatePosition(); + } + + // Update position on scroll, hide if anchor line is out of view (only when input is empty) + this._showStore.add(this._editorObs.editor.onDidScrollChange(() => { + const visibleRanges = this._editorObs.editor.getVisibleRanges(); + const isLineVisible = visibleRanges.some(range => + this._anchorLineNumber >= range.startLineNumber && this._anchorLineNumber <= range.endLineNumber + ); + const hasContent = !!this._input.getModel().getValue(); + if (!isLineVisible && !hasContent) { + this._hide(); + } else { + this._updatePosition(); + } + })); + + // Focus the input editor + setTimeout(() => this._input.focus(), 0); + } + + private _updatePosition(): void { + const editor = this._editorObs.editor; + const lineHeight = editor.getOption(EditorOption.lineHeight); + const top = editor.getTopForLineNumber(this._anchorLineNumber) - editor.getScrollTop(); + let adjustedTop = top; + + if (this._anchorAbove) { + const widgetHeight = this._domNode.offsetHeight; + adjustedTop = top - widgetHeight; + } else { + adjustedTop = top + lineHeight; + } + + // Clamp to viewport bounds when anchor line is out of view + const stickyScrollHeight = this._stickyScrollHeight.get(); + const layoutInfo = editor.getLayoutInfo(); + const widgetHeight = this._domNode.offsetHeight; + const minTop = stickyScrollHeight; + const maxTop = layoutInfo.height - widgetHeight; + + const clampedTop = Math.max(minTop, Math.min(adjustedTop, maxTop)); + const isClamped = clampedTop !== adjustedTop; + this._domNode.classList.toggle('clamped', isClamped); + + this._position.set({ + preference: { top: clampedTop, left: this._anchorLeft }, + stackOrdinal: 10000, + }, undefined); + } + + /** + * Hide the widget (removes from editor but does not dispose). + */ + private _hide(): void { + // Focus editor if focus is still within the editor's DOM + const editorDomNode = this._editorObs.editor.getDomNode(); + if (editorDomNode && dom.isAncestorOfActiveElement(editorDomNode)) { + this._editorObs.editor.focus(); + } + this._position.set(null, undefined); + this._showStore.clear(); + } + + private _refreshActions(): void { + // Clear existing actions + this._actionBar.clear(); + this._inlineStartAction = undefined; + + // Get fresh actions from menu + const actions = getFlatActionBarActions(this._menuService.getMenuActions(MenuId.ChatEditorInlineGutter, this._contextKeyService, { shouldForwardArgs: true })); + + // Set actions with keybindings (skip ACTION_START since we have the input editor) + for (const action of actions) { + if (action.id === ACTION_START) { + this._inlineStartAction = action; + continue; + } + const keybinding = this._keybindingService.lookupKeybinding(action.id)?.getLabel(); + this._actionBar.push(action, { icon: false, label: true, keybinding }); + } + } + + private _updateInputHeight(contentHeight: number): void { + const lineHeight = this._input.getOption(EditorOption.lineHeight); + const maxHeight = 3 * lineHeight; + const clampedHeight = Math.min(contentHeight, maxHeight); + const containerPadding = 8; + + this._inputContainer.style.height = `${clampedHeight + containerPadding}px`; + this._input.layout({ width: 200, height: clampedHeight }); + } +} + +/** + * Overlay widget that displays progress messages during inline chat requests. + */ +export class InlineChatSessionOverlayWidget extends Disposable { + + private readonly _domNode: HTMLElement = document.createElement('div'); + private readonly _container: HTMLElement; + private readonly _statusNode: HTMLElement; + private readonly _icon: HTMLElement; + private readonly _message: HTMLElement; + private readonly _toolbarNode: HTMLElement; + + private readonly _showStore = this._store.add(new DisposableStore()); + private readonly _position = observableValue(this, null); + private readonly _minContentWidthInPx = constObservable(0); + + private readonly _stickyScrollHeight: IObservable; + + constructor( + private readonly _editorObs: ObservableCodeEditor, + @IInstantiationService private readonly _instaService: IInstantiationService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { + super(); + + this._domNode.classList.add('inline-chat-session-overlay-widget'); + + this._container = document.createElement('div'); + this._domNode.appendChild(this._container); + this._container.classList.add('inline-chat-session-overlay-container'); + + // Create status node with icon and message + this._statusNode = document.createElement('div'); + this._statusNode.classList.add('status'); + this._icon = dom.append(this._statusNode, dom.$('span')); + this._message = dom.append(this._statusNode, dom.$('span.message')); + this._container.appendChild(this._statusNode); + + // Create toolbar node + this._toolbarNode = document.createElement('div'); + this._toolbarNode.classList.add('toolbar'); + + // Initialize sticky scroll height observable + const stickyScrollController = StickyScrollController.get(this._editorObs.editor); + this._stickyScrollHeight = stickyScrollController ? observableFromEvent(stickyScrollController.onDidChangeStickyScrollHeight, () => stickyScrollController.stickyScrollWidgetHeight) : constObservable(0); + } + + show(session: IInlineChatSession2): void { + assertType(this._editorObs.editor.hasModel()); + this._showStore.clear(); + + // Derived entry observable for this session + const entry = derived(r => session.editingSession.readEntry(session.uri, r)); + + // Set up status message and icon observable + const requestMessage = derived(r => { + const chatModel = session?.chatModel; + if (!session || !chatModel) { + return undefined; + } + + const response = chatModel.lastRequestObs.read(r)?.response; + if (!response) { + return { message: localize('working', "Working..."), icon: ThemeIcon.modify(Codicon.loading, 'spin') }; + } + + if (response.isComplete) { + // Check for errors first + const result = response.result; + if (result?.errorDetails) { + return { + message: localize('error', "Sorry, your request failed"), + icon: Codicon.error + }; + } + + const changes = entry.read(r)?.changesCount.read(r) ?? 0; + return { + message: changes === 0 + ? localize('done', "Done") + : changes === 1 + ? localize('done1', "Done, 1 change") + : localize('doneN', "Done, {0} changes", changes), + icon: Codicon.check + }; + } + + const lastPart = observableFromEventOpts({ equalsFn: () => false }, response.onDidChange, () => response.response.value) + .read(r) + .filter(part => part.kind === 'progressMessage' || part.kind === 'toolInvocation') + .at(-1); + + if (lastPart?.kind === 'toolInvocation') { + return { message: lastPart.invocationMessage, icon: ThemeIcon.modify(Codicon.loading, 'spin') }; + } else if (lastPart?.kind === 'progressMessage') { + return { message: lastPart.content, icon: ThemeIcon.modify(Codicon.loading, 'spin') }; + } else { + return { message: localize('working', "Working..."), icon: ThemeIcon.modify(Codicon.loading, 'spin') }; + } + }); + + this._showStore.add(autorun(r => { + const value = requestMessage.read(r); + if (value) { + this._message.innerText = renderAsPlaintext(value.message); + this._icon.className = ''; + this._icon.classList.add(...ThemeIcon.asClassNameArray(value.icon)); + } else { + this._message.innerText = ''; + this._icon.className = ''; + } + })); + + // Add toolbar + this._container.appendChild(this._toolbarNode); + this._showStore.add(toDisposable(() => this._toolbarNode.remove())); + + const that = this; + + this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, MenuId.ChatEditorInlineExecute, { + telemetrySource: 'inlineChatProgress.overlayToolbar', + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: () => true, + useSeparatorsInPrimaryActions: true + }, + menuOptions: { renderShortTitle: true }, + actionViewItemProvider: (action, options) => { + const primaryActions = [CancelChatActionId, 'inlineChat2.keep']; + const labeledActions = primaryActions.concat(['inlineChat2.undo']); + + if (!labeledActions.includes(action.id)) { + return undefined; // use default action view item with label + } + + return new ChatEditingAcceptRejectActionViewItem(action, options, entry, undefined, that._keybindingService, primaryActions); + } + })); + + // Position in top right of editor, below sticky scroll + const lineHeight = this._editorObs.getOption(EditorOption.lineHeight); + + // Track widget width changes + const widgetWidth = observableValue(this, 0); + const resizeObserver = new dom.DisposableResizeObserver(() => { + widgetWidth.set(this._domNode.offsetWidth, undefined); + }); + this._showStore.add(resizeObserver); + this._showStore.add(resizeObserver.observe(this._domNode)); + + this._showStore.add(autorun(r => { + const layoutInfo = this._editorObs.layoutInfo.read(r); + const stickyScrollHeight = this._stickyScrollHeight.read(r); + const width = widgetWidth.read(r); + const padding = Math.round(lineHeight.read(r) * 2 / 3); + + // Cap max-width to the editor viewport (content area) + const maxWidth = layoutInfo.contentWidth - 2 * padding; + this._domNode.style.maxWidth = `${maxWidth}px`; + + // Position: top right, below sticky scroll with padding, left of minimap and scrollbar + const top = stickyScrollHeight + padding; + const left = layoutInfo.width - width - layoutInfo.verticalScrollbarWidth - layoutInfo.minimap.minimapWidth - padding; + + this._position.set({ + preference: { top, left }, + stackOrdinal: 10000, + }, undefined); + })); + + // Create overlay widget + this._showStore.add(this._editorObs.createOverlayWidget({ + domNode: this._domNode, + position: this._position, + minContentWidthInPx: this._minContentWidthInPx, + allowEditorOverflow: false, + })); + } + + hide(): void { + this._position.set(null, undefined); + this._showStore.clear(); + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts deleted file mode 100644 index 751fb9c1728..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts +++ /dev/null @@ -1,646 +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 { Emitter, Event } from '../../../../base/common/event.js'; -import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../common/inlineChat.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { IInlineChatSessionService } from './inlineChatSessionService.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ChatModel, IChatRequestModel, IChatTextEditGroupState } from '../../chat/common/chatModel.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { IChatAgent } from '../../chat/common/chatAgents.js'; -import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; - - -export type TelemetryData = { - extension: string; - rounds: string; - undos: string; - unstashed: number; - edits: number; - finishedByEdit: boolean; - startTime: string; - endTime: string; - acceptedHunks: number; - discardedHunks: number; - responseTypes: string; -}; - -export type TelemetryDataClassification = { - owner: 'jrieken'; - comment: 'Data about an interaction editor session'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension providing the data' }; - rounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of request that were made' }; - undos: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Requests that have been undone' }; - edits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits happen while the session was active' }; - unstashed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often did this session become stashed and resumed' }; - finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' }; - startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' }; - endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' }; - acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' }; - discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' }; - responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; -}; - - -export class SessionWholeRange { - - private static readonly _options: IModelDecorationOptions = ModelDecorationOptions.register({ description: 'inlineChat/session/wholeRange' }); - - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - - private _decorationIds: string[] = []; - - constructor(private readonly _textModel: ITextModel, wholeRange: IRange) { - this._decorationIds = _textModel.deltaDecorations([], [{ range: wholeRange, options: SessionWholeRange._options }]); - } - - dispose() { - this._onDidChange.dispose(); - if (!this._textModel.isDisposed()) { - this._textModel.deltaDecorations(this._decorationIds, []); - } - } - - fixup(changes: readonly DetailedLineRangeMapping[]): void { - const newDeco: IModelDeltaDecoration[] = []; - for (const { modified } of changes) { - const modifiedRange = this._textModel.validateRange(modified.isEmpty - ? new Range(modified.startLineNumber, 1, modified.startLineNumber, Number.MAX_SAFE_INTEGER) - : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER)); - - newDeco.push({ range: modifiedRange, options: SessionWholeRange._options }); - } - const [first, ...rest] = this._decorationIds; // first is the original whole range - const newIds = this._textModel.deltaDecorations(rest, newDeco); - this._decorationIds = [first].concat(newIds); - this._onDidChange.fire(this); - } - - get trackedInitialRange(): Range { - const [first] = this._decorationIds; - return this._textModel.getDecorationRange(first) ?? new Range(1, 1, 1, 1); - } - - get value(): Range { - let result: Range | undefined; - for (const id of this._decorationIds) { - const range = this._textModel.getDecorationRange(id); - if (range) { - if (!result) { - result = range; - } else { - result = Range.plusRange(result, range); - } - } - } - return result!; - } -} - -export class Session { - - private _isUnstashed: boolean = false; - private readonly _startTime = new Date(); - private readonly _teldata: TelemetryData; - - private readonly _versionByRequest = new Map(); - - constructor( - readonly headless: boolean, - /** - * The URI of the document which is being EditorEdit - */ - readonly targetUri: URI, - /** - * A copy of the document at the time the session was started - */ - readonly textModel0: ITextModel, - /** - * The model of the editor - */ - readonly textModelN: ITextModel, - readonly agent: IChatAgent, - readonly wholeRange: SessionWholeRange, - readonly hunkData: HunkData, - readonly chatModel: ChatModel, - versionsByRequest?: [string, number][], // DEBT? this is needed when a chat model is "reused" for a new chat session - ) { - - this._teldata = { - extension: ExtensionIdentifier.toKey(agent.extensionId), - startTime: this._startTime.toISOString(), - endTime: this._startTime.toISOString(), - edits: 0, - finishedByEdit: false, - rounds: '', - undos: '', - unstashed: 0, - acceptedHunks: 0, - discardedHunks: 0, - responseTypes: '' - }; - if (versionsByRequest) { - this._versionByRequest = new Map(versionsByRequest); - } - } - - get isUnstashed(): boolean { - return this._isUnstashed; - } - - markUnstashed() { - this._teldata.unstashed! += 1; - this._isUnstashed = true; - } - - markModelVersion(request: IChatRequestModel) { - this._versionByRequest.set(request.id, this.textModelN.getAlternativeVersionId()); - } - - get versionsByRequest() { - return Array.from(this._versionByRequest); - } - - async undoChangesUntil(requestId: string): Promise { - - const targetAltVersion = this._versionByRequest.get(requestId); - if (targetAltVersion === undefined) { - return false; - } - // undo till this point - this.hunkData.ignoreTextModelNChanges = true; - try { - while (targetAltVersion < this.textModelN.getAlternativeVersionId() && this.textModelN.canUndo()) { - await this.textModelN.undo(); - } - } finally { - this.hunkData.ignoreTextModelNChanges = false; - } - return true; - } - - get hasChangedText(): boolean { - return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer()); - } - - asChangedText(changes: readonly LineRangeMapping[]): string | undefined { - if (changes.length === 0) { - return undefined; - } - - let startLine = Number.MAX_VALUE; - let endLine = Number.MIN_VALUE; - for (const change of changes) { - startLine = Math.min(startLine, change.modified.startLineNumber); - endLine = Math.max(endLine, change.modified.endLineNumberExclusive); - } - - return this.textModelN.getValueInRange(new Range(startLine, 1, endLine, Number.MAX_VALUE)); - } - - recordExternalEditOccurred(didFinish: boolean) { - this._teldata.edits += 1; - this._teldata.finishedByEdit = didFinish; - } - - asTelemetryData(): TelemetryData { - - for (const item of this.hunkData.getInfo()) { - switch (item.getState()) { - case HunkState.Accepted: - this._teldata.acceptedHunks += 1; - break; - case HunkState.Rejected: - this._teldata.discardedHunks += 1; - break; - } - } - - this._teldata.endTime = new Date().toISOString(); - return this._teldata; - } -} - - -export class StashedSession { - - private readonly _listener: IDisposable; - private readonly _ctxHasStashedSession: IContextKey; - private _session: Session | undefined; - - constructor( - editor: ICodeEditor, - session: Session, - private readonly _undoCancelEdits: IValidEditOperation[], - @IContextKeyService contextKeyService: IContextKeyService, - @IInlineChatSessionService private readonly _sessionService: IInlineChatSessionService, - @ILogService private readonly _logService: ILogService - ) { - this._ctxHasStashedSession = CTX_INLINE_CHAT_HAS_STASHED_SESSION.bindTo(contextKeyService); - - // keep session for a little bit, only release when user continues to work (type, move cursor, etc.) - this._session = session; - this._ctxHasStashedSession.set(true); - this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel, editor.onDidBlurEditorWidget))(() => { - this._session = undefined; - this._sessionService.releaseSession(session); - this._ctxHasStashedSession.reset(); - }); - } - - dispose() { - this._listener.dispose(); - this._ctxHasStashedSession.reset(); - if (this._session) { - this._sessionService.releaseSession(this._session); - } - } - - unstash(): Session | undefined { - if (!this._session) { - return undefined; - } - this._listener.dispose(); - const result = this._session; - result.markUnstashed(); - result.hunkData.ignoreTextModelNChanges = true; - result.textModelN.pushEditOperations(null, this._undoCancelEdits, () => null); - result.hunkData.ignoreTextModelNChanges = false; - this._session = undefined; - this._logService.debug('[IE] Unstashed session'); - return result; - } -} - -// --- - -function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, Number.MAX_SAFE_INTEGER) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER); -} - -export class HunkData { - - private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({ - description: 'inline-chat-hunk-tracked-range', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - }); - - private static readonly _HUNK_THRESHOLD = 8; - - private readonly _store = new DisposableStore(); - private readonly _data = new Map(); - private _ignoreChanges: boolean = false; - - constructor( - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - private readonly _textModel0: ITextModel, - private readonly _textModelN: ITextModel, - ) { - - this._store.add(_textModelN.onDidChangeContent(e => { - if (!this._ignoreChanges) { - this._mirrorChanges(e); - } - })); - } - - dispose(): void { - if (!this._textModelN.isDisposed()) { - this._textModelN.changeDecorations(accessor => { - for (const { textModelNDecorations } of this._data.values()) { - textModelNDecorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - if (!this._textModel0.isDisposed()) { - this._textModel0.changeDecorations(accessor => { - for (const { textModel0Decorations } of this._data.values()) { - textModel0Decorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - this._data.clear(); - this._store.dispose(); - } - - set ignoreTextModelNChanges(value: boolean) { - this._ignoreChanges = value; - } - - get ignoreTextModelNChanges(): boolean { - return this._ignoreChanges; - } - - private _mirrorChanges(event: IModelContentChangedEvent) { - - // mirror textModelN changes to textModel0 execept for those that - // overlap with a hunk - - type HunkRangePair = { rangeN: Range; range0: Range; markAccepted: () => void }; - const hunkRanges: HunkRangePair[] = []; - - const ranges0: Range[] = []; - - for (const entry of this._data.values()) { - - if (entry.state === HunkState.Pending) { - // pending means the hunk's changes aren't "sync'd" yet - for (let i = 1; i < entry.textModelNDecorations.length; i++) { - const rangeN = this._textModelN.getDecorationRange(entry.textModelNDecorations[i]); - const range0 = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); - if (rangeN && range0) { - hunkRanges.push({ - rangeN, range0, - markAccepted: () => entry.state = HunkState.Accepted - }); - } - } - - } else if (entry.state === HunkState.Accepted) { - // accepted means the hunk's changes are also in textModel0 - for (let i = 1; i < entry.textModel0Decorations.length; i++) { - const range = this._textModel0.getDecorationRange(entry.textModel0Decorations[i]); - if (range) { - ranges0.push(range); - } - } - } - } - - hunkRanges.sort((a, b) => Range.compareRangesUsingStarts(a.rangeN, b.rangeN)); - ranges0.sort(Range.compareRangesUsingStarts); - - const edits: IIdentifiedSingleEditOperation[] = []; - - for (const change of event.changes) { - - let isOverlapping = false; - - let pendingChangesLen = 0; - - for (const entry of hunkRanges) { - if (entry.rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { - // pending hunk _before_ this change. When projecting into textModel0 we need to - // subtract that. Because diffing is relaxed it might include changes that are not - // actual insertions/deletions. Therefore we need to take the length of the original - // range into account. - pendingChangesLen += this._textModelN.getValueLengthInRange(entry.rangeN); - pendingChangesLen -= this._textModel0.getValueLengthInRange(entry.range0); - - } else if (Range.areIntersectingOrTouching(entry.rangeN, change.range)) { - // an edit overlaps with a (pending) hunk. We take this as a signal - // to mark the hunk as accepted and to ignore the edit. The range of the hunk - // will be up-to-date because of decorations created for them - entry.markAccepted(); - isOverlapping = true; - break; - - } else { - // hunks past this change aren't relevant - break; - } - } - - if (isOverlapping) { - // hunk overlaps, it grew - continue; - } - - const offset0 = change.rangeOffset - pendingChangesLen; - const start0 = this._textModel0.getPositionAt(offset0); - - let acceptedChangesLen = 0; - for (const range of ranges0) { - if (range.getEndPosition().isBefore(start0)) { - // accepted hunk _before_ this projected change. When projecting into textModel0 - // we need to add that - acceptedChangesLen += this._textModel0.getValueLengthInRange(range); - } - } - - const start = this._textModel0.getPositionAt(offset0 + acceptedChangesLen); - const end = this._textModel0.getPositionAt(offset0 + acceptedChangesLen + change.rangeLength); - edits.push(EditOperation.replace(Range.fromPositions(start, end), change.text)); - } - - this._textModel0.pushEditOperations(null, edits, () => null); - } - - async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) { - - diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); - - let mergedChanges: DetailedLineRangeMapping[] = []; - - if (diff && diff.changes.length > 0) { - // merge changes neighboring changes - mergedChanges = [diff.changes[0]]; - for (let i = 1; i < diff.changes.length; i++) { - const lastChange = mergedChanges[mergedChanges.length - 1]; - const thisChange = diff.changes[i]; - if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { - mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( - lastChange.original.join(thisChange.original), - lastChange.modified.join(thisChange.modified), - (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) - ); - } else { - mergedChanges.push(thisChange); - } - } - } - - const hunks = mergedChanges.map(change => new RawHunk(change.original, change.modified, change.innerChanges ?? [])); - - editState.applied = hunks.length; - - this._textModelN.changeDecorations(accessorN => { - - this._textModel0.changeDecorations(accessor0 => { - - // clean up old decorations - for (const { textModelNDecorations, textModel0Decorations } of this._data.values()) { - textModelNDecorations.forEach(accessorN.removeDecoration, accessorN); - textModel0Decorations.forEach(accessor0.removeDecoration, accessor0); - } - - this._data.clear(); - - // add new decorations - for (const hunk of hunks) { - - const textModelNDecorations: string[] = []; - const textModel0Decorations: string[] = []; - - textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); - - for (const change of hunk.changes) { - textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(change.originalRange, HunkData._HUNK_TRACKED_RANGE)); - } - - this._data.set(hunk, { - editState, - textModelNDecorations, - textModel0Decorations, - state: HunkState.Pending - }); - } - }); - }); - } - - get size(): number { - return this._data.size; - } - - get pending(): number { - return Iterable.reduce(this._data.values(), (r, { state }) => r + (state === HunkState.Pending ? 1 : 0), 0); - } - - private _discardEdits(item: HunkInformation): ISingleEditOperation[] { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < rangesN.length; i++) { - const modifiedRange = rangesN[i]; - - const originalValue = this._textModel0.getValueInRange(ranges0[i]); - edits.push(EditOperation.replace(modifiedRange, originalValue)); - } - return edits; - } - - discardAll() { - const edits: ISingleEditOperation[][] = []; - for (const item of this.getInfo()) { - if (item.getState() === HunkState.Pending) { - edits.push(this._discardEdits(item)); - } - } - const undoEdits: IValidEditOperation[][] = []; - this._textModelN.pushEditOperations(null, edits.flat(), (_undoEdits) => { - undoEdits.push(_undoEdits); - return null; - }); - return undoEdits.flat(); - } - - getInfo(): HunkInformation[] { - - const result: HunkInformation[] = []; - - for (const [hunk, data] of this._data.entries()) { - const item: HunkInformation = { - getState: () => { - return data.state; - }, - isInsertion: () => { - return hunk.original.isEmpty; - }, - getRangesN: () => { - const ranges = data.textModelNDecorations.map(id => this._textModelN.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - getRanges0: () => { - const ranges = data.textModel0Decorations.map(id => this._textModel0.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - discardChanges: () => { - // DISCARD: replace modified range with original value. The modified range is retrieved from a decoration - // which was created above so that typing in the editor keeps discard working. - if (data.state === HunkState.Pending) { - const edits = this._discardEdits(item); - this._textModelN.pushEditOperations(null, edits, () => null); - data.state = HunkState.Rejected; - if (data.editState.applied > 0) { - data.editState.applied -= 1; - } - } - }, - acceptChanges: () => { - // ACCEPT: replace original range with modified value. The modified value is retrieved from the model via - // its decoration and the original range is retrieved from the hunk. - if (data.state === HunkState.Pending) { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < ranges0.length; i++) { - const originalRange = ranges0[i]; - const modifiedValue = this._textModelN.getValueInRange(rangesN[i]); - edits.push(EditOperation.replace(originalRange, modifiedValue)); - } - this._textModel0.pushEditOperations(null, edits, () => null); - data.state = HunkState.Accepted; - } - } - }; - result.push(item); - } - - return result; - } -} - -class RawHunk { - constructor( - readonly original: LineRange, - readonly modified: LineRange, - readonly changes: RangeMapping[] - ) { } -} - -type RawHunkData = { - textModelNDecorations: string[]; - textModel0Decorations: string[]; - state: HunkState; - editState: IChatTextEditGroupState; -}; - -export const enum HunkState { - Pending = 0, - Accepted = 1, - Rejected = 2 -} - -export interface HunkInformation { - /** - * The first element [0] is the whole modified range and subsequent elements are word-level changes - */ - getRangesN(): Range[]; - - getRanges0(): Range[]; - - isInsertion(): boolean; - - discardChanges(): void; - - /** - * Accept the hunk. Applies the corresponding edits into textModel0 - */ - acceptChanges(): void; - - getState(): HunkState; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts index fca660b854d..22a55a85fcc 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionService.ts @@ -2,41 +2,24 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { raceTimeout } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; import { URI } from '../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { showChatView } from '../../chat/browser/chat.js'; -import { IChatEditingSession } from '../../chat/common/chatEditingService.js'; -import { IChatModel, IChatRequestModel } from '../../chat/common/chatModel.js'; -import { IChatService } from '../../chat/common/chatService.js'; -import { Session, StashedSession } from './inlineChatSession.js'; - -export interface ISessionKeyComputer { - getComparisonKey(editor: ICodeEditor, uri: URI): string; -} +import { ChatViewPaneTarget, IChatWidgetService } from '../../chat/browser/chat.js'; +import { IChatEditingSession } from '../../chat/common/editing/chatEditingService.js'; +import { IChatModel, IChatModelInputState, IChatRequestModel } from '../../chat/common/model/chatModel.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; +import { ChatAgentLocation } from '../../chat/common/constants.js'; -export const IInlineChatSessionService = createDecorator('IInlineChatSessionService'); -export interface IInlineChatSessionEvent { - readonly editor: ICodeEditor; - readonly session: Session; -} - -export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { - readonly endedByExternalCause: boolean; -} +export const IInlineChatSessionService = createDecorator('IInlineChatSessionService'); export interface IInlineChatSession2 { readonly initialPosition: Position; + readonly initialSelection: Selection; readonly uri: URI; readonly chatModel: IChatModel; readonly editingSession: IChatEditingSession; @@ -47,39 +30,21 @@ export interface IInlineChatSessionService { _serviceBrand: undefined; readonly onWillStartSession: Event; - readonly onDidMoveSession: Event; - readonly onDidStashSession: Event; - readonly onDidEndSession: Event; - - createSession(editor: IActiveCodeEditor, options: { wholeRange?: IRange; session?: Session; headless?: boolean }, token: CancellationToken): Promise; - - moveSession(session: Session, newEditor: ICodeEditor): void; - - getCodeEditor(session: Session): ICodeEditor; - - getSession(editor: ICodeEditor, uri: URI): Session | undefined; - - releaseSession(session: Session): void; - - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession; - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable; + readonly onDidChangeSessions: Event; dispose(): void; - createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise; - getSession2(uri: URI): IInlineChatSession2 | undefined; - getSession2(sessionId: string): IInlineChatSession2 | undefined; - readonly onDidChangeSessions: Event; + createSession(editor: ICodeEditor): IInlineChatSession2; + getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined; + getSessionBySessionUri(uri: URI): IInlineChatSession2 | undefined; } export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined, resend: boolean) { - const viewsService = accessor.get(IViewsService); const chatService = accessor.get(IChatService); - const layoutService = accessor.get(IWorkbenchLayoutService); + const widgetService = accessor.get(IChatWidgetService); - const widget = await showChatView(viewsService, layoutService); + const widget = await widgetService.revealWidget(); if (widget && widget.viewModel && model) { let lastRequest: IChatRequestModel | undefined; @@ -96,28 +61,23 @@ export async function moveToPanelChat(accessor: ServicesAccessor, model: IChatMo } } -export async function askInPanelChat(accessor: ServicesAccessor, model: IChatRequestModel) { +export async function askInPanelChat(accessor: ServicesAccessor, request: IChatRequestModel, state: IChatModelInputState | undefined) { - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); + const widgetService = accessor.get(IChatWidgetService); + const chatService = accessor.get(IChatService); - const widget = await showChatView(viewsService, layoutService); - if (!widget) { + if (!request) { return; } - if (!widget.viewModel) { - await raceTimeout(Event.toPromise(widget.onDidChangeViewModel), 1000); - } + const newModelRef = chatService.startSession(ChatAgentLocation.Chat); + const newModel = newModelRef.object; - if (model.attachedContext) { - widget.attachmentModel.addContext(...model.attachedContext); - } + newModel.inputModel.setState({ ...state }); + + const widget = await widgetService.openSession(newModelRef.object.sessionResource, ChatViewPaneTarget); - widget.acceptInput(model.message.text, { - enableImplicitContext: true, - isVoiceInput: false, - noCommandDetection: true - }); + newModelRef.dispose(); // can be freed after opening because the widget also holds a reference + widget?.acceptInput(request.message.text); } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts index ee4869c5d78..185b56de369 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts @@ -2,55 +2,32 @@ * 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 { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, dispose, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; -import { Schemas } from '../../../../base/common/network.js'; import { autorun, observableFromEvent } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; -import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; +import { IActiveCodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; -import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize, localize2 } from '../../../../nls.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js'; -import { IChatWidgetService } from '../../chat/browser/chat.js'; -import { IChatAgentService } from '../../chat/common/chatAgents.js'; -import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; -import { IChatService } from '../../chat/common/chatService.js'; +import { IChatAgentService } from '../../chat/common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../chat/common/constants.js'; -import { ILanguageModelToolsService, ToolDataSource, IToolData } from '../../chat/common/languageModelToolsService.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; -import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; -import { askInPanelChat, IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js'; - - -type SessionData = { - editor: ICodeEditor; - session: Session; - store: IDisposable; -}; +import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; +import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js'; +import { askInPanelChat, IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js'; export class InlineChatError extends Error { static readonly code = 'InlineChatError'; @@ -60,322 +37,94 @@ export class InlineChatError extends Error { } } - export class InlineChatSessionServiceImpl implements IInlineChatSessionService { declare _serviceBrand: undefined; private readonly _store = new DisposableStore(); + private readonly _sessions = new ResourceMap(); private readonly _onWillStartSession = this._store.add(new Emitter()); readonly onWillStartSession: Event = this._onWillStartSession.event; - private readonly _onDidMoveSession = this._store.add(new Emitter()); - readonly onDidMoveSession: Event = this._onDidMoveSession.event; - - private readonly _onDidEndSession = this._store.add(new Emitter()); - readonly onDidEndSession: Event = this._onDidEndSession.event; - - private readonly _onDidStashSession = this._store.add(new Emitter()); - readonly onDidStashSession: Event = this._onDidStashSession.event; - - private readonly _sessions = new Map(); - private readonly _keyComputers = new Map(); + private readonly _onDidChangeSessions = this._store.add(new Emitter()); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IModelService private readonly _modelService: IModelService, - @ITextModelService private readonly _textModelService: ITextModelService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, - @ITextFileService private readonly _textFileService: ITextFileService, - @ILanguageService private readonly _languageService: ILanguageService, @IChatService private readonly _chatService: IChatService, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, + @IChatAgentService chatAgentService: IChatAgentService, ) { - - } - - dispose() { - this._store.dispose(); - this._sessions.forEach(x => x.store.dispose()); - this._sessions.clear(); - } - - async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise { - - const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline); - - if (!agent) { - this._logService.trace('[IE] NO agent found'); - return undefined; - } - - this._onWillStartSession.fire(editor); - - const textModel = editor.getModel(); - const selection = editor.getSelection(); - - const store = new DisposableStore(); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); - - const chatModel = options.session?.chatModel ?? this._chatService.startSession(ChatAgentLocation.EditorInline, token); - if (!chatModel) { - this._logService.trace('[IE] NO chatModel found'); - return undefined; - } - - store.add(toDisposable(() => { - const doesOtherSessionUseChatModel = [...this._sessions.values()].some(data => data.session !== session && data.session.chatModel === chatModel); - - if (!doesOtherSessionUseChatModel) { - this._chatService.clearSession(chatModel.sessionResource); - chatModel.dispose(); - } - })); - - const lastResponseListener = store.add(new MutableDisposable()); - store.add(chatModel.onDidChange(e => { - if (e.kind !== 'addRequest' || !e.request.response) { - return; - } - - const { response } = e.request; - - session.markModelVersion(e.request); - lastResponseListener.value = response.onDidChange(() => { - - if (!response.isComplete) { - return; - } - - lastResponseListener.clear(); // ONCE - - // special handling for untitled files - for (const part of response.response.value) { - if (part.kind !== 'textEditGroup' || part.uri.scheme !== Schemas.untitled || isEqual(part.uri, session.textModelN.uri)) { - continue; - } - const langSelection = this._languageService.createByFilepathOrFirstLine(part.uri, undefined); - const untitledTextModel = this._textFileService.untitled.create({ - associatedResource: part.uri, - languageId: langSelection.languageId - }); - untitledTextModel.resolve(); - this._textModelService.createModelReference(part.uri).then(ref => { - store.add(ref); - }); - } - - }); - })); - - store.add(this._chatAgentService.onDidChangeAgents(e => { - if (e === undefined && (!this._chatAgentService.getAgent(agent.id) || !this._chatAgentService.getActivatedAgents().map(agent => agent.id).includes(agent.id))) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`); - this._releaseSession(session, true); + // Listen for agent changes and dispose all sessions when there is no agent + const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); + this._store.add(autorun(r => { + const agent = agentObs.read(r); + if (!agent) { + // No agent available, dispose all sessions + dispose(this._sessions.values()); + this._sessions.clear(); } })); - - const id = generateUuid(); - const targetUri = textModel.uri; - - // AI edits happen in the actual model, keep a reference but make no copy - store.add((await this._textModelService.createModelReference(textModel.uri))); - const textModelN = textModel; - - // create: keep a snapshot of the "actual" model - const textModel0 = store.add(this._modelService.createModel( - createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), - { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true - )); - - // untitled documents are special and we are releasing their session when their last editor closes - if (targetUri.scheme === Schemas.untitled) { - store.add(this._editorService.onDidCloseEditor(() => { - if (!this._editorService.isOpened({ resource: targetUri, typeId: UntitledTextEditorInput.ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })) { - this._releaseSession(session, true); - } - })); - } - - let wholeRange = options.wholeRange; - if (!wholeRange) { - wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); - } - - if (token.isCancellationRequested) { - store.dispose(); - return undefined; - } - - const session = new Session( - options.headless ?? false, - targetUri, - textModel0, - textModelN, - agent, - store.add(new SessionWholeRange(textModelN, wholeRange)), - store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), - chatModel, - options.session?.versionsByRequest, - ); - - // store: key -> session - const key = this._key(editor, session.targetUri); - if (this._sessions.has(key)) { - store.dispose(); - throw new Error(`Session already stored for ${key}`); - } - this._sessions.set(key, { session, editor, store }); - return session; - } - - moveSession(session: Session, target: ICodeEditor): void { - const newKey = this._key(target, session.targetUri); - const existing = this._sessions.get(newKey); - if (existing) { - if (existing.session !== session) { - throw new Error(`Cannot move session because the target editor already/still has one`); - } else { - // noop - return; - } - } - - let found = false; - for (const [oldKey, data] of this._sessions) { - if (data.session === session) { - found = true; - this._sessions.delete(oldKey); - this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`); - this._onDidMoveSession.fire({ session, editor: target }); - break; - } - } - if (!found) { - throw new Error(`Cannot move session because it is not stored`); - } - } - - releaseSession(session: Session): void { - this._releaseSession(session, false); - } - - private _releaseSession(session: Session, byServer: boolean): void { - - let tuple: [string, SessionData] | undefined; - - // cleanup - for (const candidate of this._sessions) { - if (candidate[1].session === session) { - // if (value.session === session) { - tuple = candidate; - break; - } - } - - if (!tuple) { - // double remove - return; - } - - this._telemetryService.publicLog2('interactiveEditor/session', session.asTelemetryData()); - - const [key, value] = tuple; - this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`); - - this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); - value.store.dispose(); } - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession { - const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); - this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`); - return result; - } - - getCodeEditor(session: Session): ICodeEditor { - for (const [, data] of this._sessions) { - if (data.session === session) { - return data.editor; - } - } - throw new Error('session not found'); - } - - getSession(editor: ICodeEditor, uri: URI): Session | undefined { - const key = this._key(editor, uri); - return this._sessions.get(key)?.session; - } - - private _key(editor: ICodeEditor, uri: URI): string { - const item = this._keyComputers.get(uri.scheme); - return item - ? item.getComparisonKey(editor, uri) - : `${editor.getId()}@${uri.toString()}`; - - } - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable { - this._keyComputers.set(scheme, value); - return toDisposable(() => this._keyComputers.delete(scheme)); + dispose() { + this._store.dispose(); } - // ---- NEW - - private readonly _sessions2 = new ResourceMap(); - - private readonly _onDidChangeSessions = this._store.add(new Emitter()); - readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; - - - async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise { - assertType(editor.hasModel()); + createSession(editor: IActiveCodeEditor): IInlineChatSession2 { + const uri = editor.getModel().uri; - if (this._sessions2.has(uri)) { + if (this._sessions.has(uri)) { throw new Error('Session already exists'); } - this._onWillStartSession.fire(editor as IActiveCodeEditor); - - const chatModel = this._chatService.startSession(ChatAgentLocation.Chat, token, false); + this._onWillStartSession.fire(editor); - const editingSession = await chatModel.editingSessionObs?.promise!; - const widget = this._chatWidgetService.getWidgetBySessionResource(chatModel.sessionResource); - await widget?.attachmentModel.addFile(uri); + const chatModelRef = this._chatService.startSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ }); + const chatModel = chatModelRef.object; + chatModel.startEditingSession(false); const store = new DisposableStore(); store.add(toDisposable(() => { this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource); - editingSession.reject(); - this._sessions2.delete(uri); + chatModel.editingSession?.reject(); + this._sessions.delete(uri); this._onDidChangeSessions.fire(this); })); - store.add(chatModel); + store.add(chatModelRef); store.add(autorun(r => { - const entries = editingSession.entries.read(r); - if (entries.length === 0) { + const entries = chatModel.editingSession?.entries.read(r); + if (!entries?.length) { return; } + const state = entries.find(entry => isEqual(entry.modifiedURI, uri))?.state.read(r); + if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) { + const response = chatModel.getRequests().at(-1)?.response; + if (response) { + this._chatService.notifyUserAction({ + sessionResource: response.session.sessionResource, + requestId: response.requestId, + agentId: response.agent?.id, + command: response.slashCommand?.name, + result: response.result, + action: { + kind: 'inlineChat', + action: state === ModifiedFileEntryState.Accepted ? 'accepted' : 'discarded' + } + }); + } + } + const allSettled = entries.every(entry => { const state = entry.state.read(r); return (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) && !entry.isCurrentlyBeingModifiedBy.read(r); }); - if (allSettled && !chatModel.requestInProgress) { + if (allSettled && !chatModel.requestInProgress.read(undefined)) { // self terminate store.dispose(); } @@ -384,35 +133,35 @@ export class InlineChatSessionServiceImpl implements IInlineChatSessionService { const result: IInlineChatSession2 = { uri, initialPosition: editor.getSelection().getStartPosition().delta(-1), /* one line above selection start */ + initialSelection: editor.getSelection(), chatModel, - editingSession, + editingSession: chatModel.editingSession!, dispose: store.dispose.bind(store) }; - this._sessions2.set(uri, result); + this._sessions.set(uri, result); this._onDidChangeSessions.fire(this); return result; } - getSession2(uriOrSessionId: URI | string): IInlineChatSession2 | undefined { - if (URI.isUri(uriOrSessionId)) { - - let result = this._sessions2.get(uriOrSessionId); - if (!result) { - // no direct session, try to find an editing session which has a file entry for the uri - for (const [_, candidate] of this._sessions2) { - const entry = candidate.editingSession.getEntry(uriOrSessionId); - if (entry) { - result = candidate; - break; - } + getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined { + let result = this._sessions.get(uri); + if (!result) { + // no direct session, try to find an editing session which has a file entry for the uri + for (const [_, candidate] of this._sessions) { + const entry = candidate.editingSession.getEntry(uri); + if (entry) { + result = candidate; + break; } } - return result; - } else { - for (const session of this._sessions2.values()) { - if (session.chatModel.sessionId === uriOrSessionId) { - return session; - } + } + return result; + } + + getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined { + for (const session of this._sessions.values()) { + if (isEqual(session.chatModel.sessionResource, sessionResource)) { + return session; } } return undefined; @@ -423,9 +172,7 @@ export class InlineChatEnabler { static Id = 'inlineChat.enabler'; - private readonly _ctxHasProvider: IContextKey; private readonly _ctxHasProvider2: IContextKey; - private readonly _ctxHasNotebookInline: IContextKey; private readonly _ctxHasNotebookProvider: IContextKey; private readonly _ctxPossible: IContextKey; @@ -437,34 +184,24 @@ export class InlineChatEnabler { @IEditorService editorService: IEditorService, @IConfigurationService configService: IConfigurationService, ) { - this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService); - this._ctxHasNotebookInline = CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE.bindTo(contextKeyService); this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService); this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService); const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline)); - const inlineChat2Obs = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configService); const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook)); const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService); this._store.add(autorun(r => { - const v2 = inlineChat2Obs.read(r); const agent = agentObs.read(r); if (!agent) { - this._ctxHasProvider.reset(); this._ctxHasProvider2.reset(); - } else if (v2) { - this._ctxHasProvider.reset(); - this._ctxHasProvider2.set(true); } else { - this._ctxHasProvider.set(true); - this._ctxHasProvider2.reset(); + this._ctxHasProvider2.set(true); } })); this._store.add(autorun(r => { - this._ctxHasNotebookInline.set(!notebookAgentConfigObs.read(r) && !!agentObs.read(r)); this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r)); })); @@ -480,7 +217,7 @@ export class InlineChatEnabler { dispose() { this._ctxPossible.reset(); - this._ctxHasProvider.reset(); + this._ctxHasProvider2.reset(); this._store.dispose(); } } @@ -517,17 +254,17 @@ export class InlineChatEscapeToolContribution extends Disposable { this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, { invoke: async (invocation, _tokenCountFn, _progress, _token) => { - const sessionId = invocation.context?.sessionId; + const sessionResource = invocation.context?.sessionResource; - if (!sessionId) { + if (!sessionResource) { logService.warn('InlineChatEscapeToolContribution: no sessionId in tool invocation context'); return { content: [{ kind: 'text', value: 'Cancel' }] }; } - const session = inlineChatSessionService.getSession2(sessionId); + const session = inlineChatSessionService.getSessionBySessionUri(sessionResource); if (!session) { - logService.warn(`InlineChatEscapeToolContribution: no session found for id ${sessionId}`); + logService.warn(`InlineChatEscapeToolContribution: no session found for id ${sessionResource}`); return { content: [{ kind: 'text', value: 'Cancel' }] }; } @@ -535,15 +272,15 @@ export class InlineChatEscapeToolContribution extends Disposable { let result: { confirmed: boolean; checkboxChecked?: boolean }; if (dontAskAgain !== undefined) { - // Use previously stored user preference: true = 'Continue in Chat', false = 'Rephrase' (Cancel) + // Use previously stored user preference: true = 'Continue in Chat view', false = 'Rephrase' (Cancel) result = { confirmed: dontAskAgain, checkboxChecked: false }; } else { result = await dialogService.confirm({ type: 'question', - title: localize('confirm.title', "Continue in Panel Chat?"), - message: localize('confirm', "Do you want to continue in panel chat or rephrase your prompt?"), - detail: localize('confirm.detail', "Inline Chat is designed for single file code changes. This task is either too complex or requires a text response. You can rephrase your prompt or continue in panel chat."), - primaryButton: localize('confirm.yes', "Continue in Chat"), + title: localize('confirm.title', "Do you want to continue in Chat view?"), + message: localize('confirm', "Do you want to continue in Chat view?"), + detail: localize('confirm.detail', "Inline chat is designed for making single-file code changes. Continue your request in the Chat view or rephrase it for inline chat."), + primaryButton: localize('confirm.yes', "Continue in Chat view"), cancelButton: localize('confirm.cancel', "Cancel"), checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false }, }); @@ -553,12 +290,14 @@ export class InlineChatEscapeToolContribution extends Disposable { if (!editor || result.confirmed) { logService.trace('InlineChatEscapeToolContribution: moving session to panel chat'); - await instaService.invokeFunction(askInPanelChat, session.chatModel.getRequests().at(-1)!); + await instaService.invokeFunction(askInPanelChat, session.chatModel.getRequests().at(-1)!, session.chatModel.inputModel.state.get()); session.dispose(); } else { logService.trace('InlineChatEscapeToolContribution: rephrase prompt'); - chatService.removeRequest(session.chatModel.sessionResource, session.chatModel.getRequests().at(-1)!.id); + const lastRequest = session.chatModel.getRequests().at(-1)!; + chatService.removeRequest(session.chatModel.sessionResource, lastRequest.id); + session.chatModel.inputModel.setState({ inputText: lastRequest.message.text }); } if (result.checkboxChecked) { @@ -576,7 +315,7 @@ registerAction2(class ResetMoveToPanelChatChoice extends Action2 { constructor() { super({ id: 'inlineChat.resetMoveToPanelChatChoice', - precondition: ContextKeyExpr.has('config.chat.disableAIFeatures').negate(), + precondition: ChatContextKeys.Setup.hidden.negate(), title: localize2('resetChoice.label', "Reset Choice for 'Move Inline Chat to Panel Chat'"), f1: true }); diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts deleted file mode 100644 index cfc65f55ef5..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatStrategies.ts +++ /dev/null @@ -1,593 +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 { WindowIntervalTimer } from '../../../../base/browser/dom.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { themeColorFromId, ThemeIcon } from '../../../../base/common/themables.js'; -import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from '../../../../editor/browser/editorBrowser.js'; -import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; -import { LineSource, RenderOptions, renderLines } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { Progress } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../common/editor.js'; -import { countWords } from '../../chat/common/chatWordCounter.js'; -import { HunkInformation, Session, HunkState } from './inlineChatSession.js'; -import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, InlineChatConfigKeys, MENU_INLINE_CHAT_ZONE, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../common/inlineChat.js'; -import { assertType } from '../../../../base/common/types.js'; -import { performAsyncTextEdit, asProgressiveEdit } from './utils.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; -import { IUntitledTextEditorModel } from '../../../services/untitled/common/untitledTextEditorModel.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { DefaultChatTextEditor } from '../../chat/browser/codeBlockPart.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { ConflictActionsFactory, IContentWidgetAction } from '../../mergeEditor/browser/view/conflictActions.js'; -import { observableValue } from '../../../../base/common/observable.js'; -import { IMenuService, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel/inlineDecorations.js'; -import { EditSources } from '../../../../editor/common/textModelEditSource.js'; -import { VersionedExtensionId } from '../../../../editor/common/languages.js'; - -export interface IEditObserver { - start(): void; - stop(): void; -} - -export const enum HunkAction { - Accept, - Discard, - MoveNext, - MovePrev, - ToggleDiff -} - -export class LiveStrategy { - - private readonly _decoInsertedText = ModelDecorationOptions.register({ - description: 'inline-modified-line', - className: 'inline-chat-inserted-range-linehighlight', - isWholeLine: true, - overviewRuler: { - position: OverviewRulerLane.Full, - color: themeColorFromId(overviewRulerInlineChatDiffInserted), - }, - minimap: { - position: MinimapPosition.Inline, - color: themeColorFromId(minimapInlineChatDiffInserted), - } - }); - - private readonly _decoInsertedTextRange = ModelDecorationOptions.register({ - description: 'inline-chat-inserted-range-linehighlight', - className: 'inline-chat-inserted-range', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }); - - protected readonly _store = new DisposableStore(); - protected readonly _onDidAccept = this._store.add(new Emitter()); - protected readonly _onDidDiscard = this._store.add(new Emitter()); - private readonly _ctxCurrentChangeHasDiff: IContextKey; - private readonly _ctxCurrentChangeShowsDiff: IContextKey; - private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; - private readonly _lensActionsFactory: ConflictActionsFactory; - private _editCount: number = 0; - private readonly _hunkData = new Map(); - - readonly onDidAccept: Event = this._onDidAccept.event; - readonly onDidDiscard: Event = this._onDidDiscard.event; - - constructor( - protected readonly _session: Session, - protected readonly _editor: ICodeEditor, - protected readonly _zone: InlineChatZoneWidget, - private readonly _showOverlayToolbar: boolean, - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IMenuService private readonly _menuService: IMenuService, - @IContextKeyService private readonly _contextService: IContextKeyService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInstantiationService protected readonly _instaService: IInstantiationService - ) { - this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService); - this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); - - this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); - this._lensActionsFactory = this._store.add(new ConflictActionsFactory(this._editor)); - } - - dispose(): void { - this._resetDiff(); - this._store.dispose(); - } - - private _resetDiff(): void { - this._ctxCurrentChangeHasDiff.reset(); - this._ctxCurrentChangeShowsDiff.reset(); - this._zone.widget.updateStatus(''); - this._progressiveEditingDecorations.clear(); - - - for (const data of this._hunkData.values()) { - data.remove(); - } - } - - async apply() { - this._resetDiff(); - if (this._editCount > 0) { - this._editor.pushUndoStop(); - } - await this._doApplyChanges(true); - } - - cancel() { - this._resetDiff(); - return this._session.hunkData.discardAll(); - } - - async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore, metadata); - } - - async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - - // add decorations once per line that got edited - const progress = new Progress(edits => { - - const newLines = new Set(); - for (const edit of edits) { - LineRange.fromRange(edit.range).forEach(line => newLines.add(line)); - } - const existingRanges = this._progressiveEditingDecorations.getRanges().map(LineRange.fromRange); - for (const existingRange of existingRanges) { - existingRange.forEach(line => newLines.delete(line)); - } - const newDecorations: IModelDeltaDecoration[] = []; - for (const line of newLines) { - newDecorations.push({ range: new Range(line, 1, line, Number.MAX_VALUE), options: this._decoInsertedText }); - } - - this._progressiveEditingDecorations.append(newDecorations); - }); - return this._makeChanges(edits, obs, opts, progress, undoStopBefore, metadata); - } - - private async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined, undoStopBefore: boolean, metadata: IInlineChatMetadata): Promise { - - // push undo stop before first edit - if (undoStopBefore) { - this._editor.pushUndoStop(); - } - - this._editCount++; - const editSource = EditSources.inlineChatApplyEdit({ - modelId: metadata.modelId, - extensionId: metadata.extensionId, - requestId: metadata.requestId, - sessionId: undefined, - languageId: this._session.textModelN.getLanguageId(), - }); - - if (opts) { - // ASYNC - const durationInSec = opts.duration / 1000; - for (const edit of edits) { - const wordCount = countWords(edit.text ?? ''); - const speed = wordCount / durationInSec; - // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); - const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); - await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs, editSource); - } - - } else { - // SYNC - obs.start(); - this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { - progress?.report(undoEdits); - return null; - }, undefined, editSource); - obs.stop(); - } - } - - performHunkAction(hunk: HunkInformation | undefined, action: HunkAction) { - const displayData = this._findDisplayData(hunk); - - if (!displayData) { - // no hunks (left or not yet) found, make sure to - // finish the sessions - if (action === HunkAction.Accept) { - this._onDidAccept.fire(); - } else if (action === HunkAction.Discard) { - this._onDidDiscard.fire(); - } - return; - } - - if (action === HunkAction.Accept) { - displayData.acceptHunk(); - } else if (action === HunkAction.Discard) { - displayData.discardHunk(); - } else if (action === HunkAction.MoveNext) { - displayData.move(true); - } else if (action === HunkAction.MovePrev) { - displayData.move(false); - } else if (action === HunkAction.ToggleDiff) { - displayData.toggleDiff?.(); - } - } - - private _findDisplayData(hunkInfo?: HunkInformation) { - let result: HunkDisplayData | undefined; - if (hunkInfo) { - // use context hunk (from tool/buttonbar) - result = this._hunkData.get(hunkInfo); - } - - if (!result && this._zone.position) { - // find nearest from zone position - const zoneLine = this._zone.position.lineNumber; - let distance: number = Number.MAX_SAFE_INTEGER; - for (const candidate of this._hunkData.values()) { - if (candidate.hunk.getState() !== HunkState.Pending) { - continue; - } - const hunkRanges = candidate.hunk.getRangesN(); - if (hunkRanges.length === 0) { - // bogous hunk - continue; - } - const myDistance = zoneLine <= hunkRanges[0].startLineNumber - ? hunkRanges[0].startLineNumber - zoneLine - : zoneLine - hunkRanges[0].endLineNumber; - - if (myDistance < distance) { - distance = myDistance; - result = candidate; - } - } - } - - if (!result) { - // fallback: first hunk that is pending - result = Iterable.first(Iterable.filter(this._hunkData.values(), candidate => candidate.hunk.getState() === HunkState.Pending)); - } - return result; - } - - async renderChanges() { - - this._progressiveEditingDecorations.clear(); - - const renderHunks = () => { - - let widgetData: HunkDisplayData | undefined; - - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - - const keysNow = new Set(this._hunkData.keys()); - widgetData = undefined; - - for (const hunkData of this._session.hunkData.getInfo()) { - - keysNow.delete(hunkData); - - const hunkRanges = hunkData.getRangesN(); - let data = this._hunkData.get(hunkData); - if (!data) { - // first time -> create decoration - const decorationIds: string[] = []; - for (let i = 0; i < hunkRanges.length; i++) { - decorationIds.push(decorationsAccessor.addDecoration(hunkRanges[i], i === 0 - ? this._decoInsertedText - : this._decoInsertedTextRange) - ); - } - - const acceptHunk = () => { - hunkData.acceptChanges(); - renderHunks(); - }; - - const discardHunk = () => { - hunkData.discardChanges(); - renderHunks(); - }; - - // original view zone - const mightContainNonBasicASCII = this._session.textModel0.mightContainNonBasicASCII(); - const mightContainRTL = this._session.textModel0.mightContainRTL(); - const renderOptions = RenderOptions.fromEditor(this._editor); - const originalRange = hunkData.getRanges0()[0]; - const source = new LineSource( - LineRange.fromRangeInclusive(originalRange).mapToLineArray(l => this._session.textModel0.tokenization.getLineTokens(l)), - [], - mightContainNonBasicASCII, - mightContainRTL, - ); - const domNode = document.createElement('div'); - domNode.className = 'inline-chat-original-zone2'; - const result = renderLines(source, renderOptions, [new InlineDecoration(new Range(originalRange.startLineNumber, 1, originalRange.startLineNumber, 1), '', InlineDecorationType.Regular)], domNode); - const viewZoneData: IViewZone = { - afterLineNumber: -1, - heightInLines: result.heightInLines, - domNode, - ordinal: 50000 + 2 // more than https://github.com/microsoft/vscode/blob/bf52a5cfb2c75a7327c9adeaefbddc06d529dcad/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts#L42 - }; - - const toggleDiff = () => { - const scrollState = StableEditorScrollState.capture(this._editor); - changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => { - assertType(data); - if (!data.diffViewZoneId) { - const [hunkRange] = hunkData.getRangesN(); - viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1; - data.diffViewZoneId = viewZoneAccessor.addZone(viewZoneData); - } else { - viewZoneAccessor.removeZone(data.diffViewZoneId!); - data.diffViewZoneId = undefined; - } - }); - this._ctxCurrentChangeShowsDiff.set(typeof data?.diffViewZoneId === 'string'); - scrollState.restore(this._editor); - }; - - - let lensActions: DisposableStore | undefined; - const lensActionsViewZoneIds: string[] = []; - - if (this._showOverlayToolbar && hunkData.getState() === HunkState.Pending) { - - lensActions = new DisposableStore(); - - const menu = this._menuService.createMenu(MENU_INLINE_CHAT_ZONE, this._contextService); - const makeActions = () => { - const actions: IContentWidgetAction[] = []; - const tuples = menu.getActions({ arg: hunkData }); - for (const [, group] of tuples) { - for (const item of group) { - if (item instanceof MenuItemAction) { - - let text = item.label; - - if (item.id === ACTION_TOGGLE_DIFF) { - text = item.checked ? 'Hide Changes' : 'Show Changes'; - } else if (ThemeIcon.isThemeIcon(item.item.icon)) { - text = `$(${item.item.icon.id}) ${text}`; - } - - actions.push({ - text, - tooltip: item.tooltip, - action: async () => item.run(), - }); - } - } - } - return actions; - }; - - const obs = observableValue(this, makeActions()); - lensActions.add(menu.onDidChange(() => obs.set(makeActions(), undefined))); - lensActions.add(menu); - - lensActions.add(this._lensActionsFactory.createWidget(viewZoneAccessor, - hunkRanges[0].startLineNumber - 1, - obs, - lensActionsViewZoneIds - )); - } - - const remove = () => { - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - assertType(data); - for (const decorationId of data.decorationIds) { - decorationsAccessor.removeDecoration(decorationId); - } - if (data.diffViewZoneId) { - viewZoneAccessor.removeZone(data.diffViewZoneId!); - } - data.decorationIds = []; - data.diffViewZoneId = undefined; - - data.lensActionsViewZoneIds?.forEach(viewZoneAccessor.removeZone); - data.lensActionsViewZoneIds = undefined; - }); - - lensActions?.dispose(); - }; - - const move = (next: boolean) => { - const keys = Array.from(this._hunkData.keys()); - const idx = keys.indexOf(hunkData); - const nextIdx = (idx + (next ? 1 : -1) + keys.length) % keys.length; - if (nextIdx !== idx) { - const nextData = this._hunkData.get(keys[nextIdx])!; - this._zone.updatePositionAndHeight(nextData?.position); - renderHunks(); - } - }; - - const zoneLineNumber = this._zone.position?.lineNumber ?? this._editor.getPosition()!.lineNumber; - const myDistance = zoneLineNumber <= hunkRanges[0].startLineNumber - ? hunkRanges[0].startLineNumber - zoneLineNumber - : zoneLineNumber - hunkRanges[0].endLineNumber; - - data = { - hunk: hunkData, - decorationIds, - diffViewZoneId: '', - diffViewZone: viewZoneData, - lensActionsViewZoneIds, - distance: myDistance, - position: hunkRanges[0].getStartPosition().delta(-1), - acceptHunk, - discardHunk, - toggleDiff: !hunkData.isInsertion() ? toggleDiff : undefined, - remove, - move, - }; - - this._hunkData.set(hunkData, data); - - } else if (hunkData.getState() !== HunkState.Pending) { - data.remove(); - - } else { - // update distance and position based on modifiedRange-decoration - const zoneLineNumber = this._zone.position?.lineNumber ?? this._editor.getPosition()!.lineNumber; - const modifiedRangeNow = hunkRanges[0]; - data.position = modifiedRangeNow.getStartPosition().delta(-1); - data.distance = zoneLineNumber <= modifiedRangeNow.startLineNumber - ? modifiedRangeNow.startLineNumber - zoneLineNumber - : zoneLineNumber - modifiedRangeNow.endLineNumber; - } - - if (hunkData.getState() === HunkState.Pending && (!widgetData || data.distance < widgetData.distance)) { - widgetData = data; - } - } - - for (const key of keysNow) { - const data = this._hunkData.get(key); - if (data) { - this._hunkData.delete(key); - data.remove(); - } - } - }); - - if (widgetData) { - this._zone.reveal(widgetData.position); - - const mode = this._configService.getValue<'on' | 'off' | 'auto'>(InlineChatConfigKeys.AccessibleDiffView); - if (mode === 'on' || mode === 'auto' && this._accessibilityService.isScreenReaderOptimized()) { - this._zone.widget.showAccessibleHunk(this._session, widgetData.hunk); - } - - this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); - - } else if (this._hunkData.size > 0) { - // everything accepted or rejected - let oneAccepted = false; - for (const hunkData of this._session.hunkData.getInfo()) { - if (hunkData.getState() === HunkState.Accepted) { - oneAccepted = true; - break; - } - } - if (oneAccepted) { - this._onDidAccept.fire(); - } else { - this._onDidDiscard.fire(); - } - } - - return widgetData; - }; - - return renderHunks()?.position; - } - - getWholeRangeDecoration(): IModelDeltaDecoration[] { - // don't render the blue in live mode - return []; - } - - private async _doApplyChanges(ignoreLocal: boolean): Promise { - - const untitledModels: IUntitledTextEditorModel[] = []; - - const editor = this._instaService.createInstance(DefaultChatTextEditor); - - - for (const request of this._session.chatModel.getRequests()) { - - if (!request.response?.response) { - continue; - } - - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup') { - continue; - } - if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { - continue; - } - - await editor.apply(request.response, item, undefined); - - if (item.uri.scheme === Schemas.untitled) { - const untitled = this._textFileService.untitled.get(item.uri); - if (untitled) { - untitledModels.push(untitled); - } - } - } - } - - for (const untitledModel of untitledModels) { - if (!untitledModel.isDisposed()) { - await untitledModel.resolve(); - await untitledModel.save({ reason: SaveReason.EXPLICIT }); - } - } - } -} - -export interface ProgressingEditsOptions { - duration: number; - token: CancellationToken; -} - -type HunkDisplayData = { - - decorationIds: string[]; - - diffViewZoneId: string | undefined; - diffViewZone: IViewZone; - - lensActionsViewZoneIds?: string[]; - - distance: number; - position: Position; - acceptHunk: () => void; - discardHunk: () => void; - toggleDiff?: () => any; - remove(): void; - move: (next: boolean) => void; - - hunk: HunkInformation; -}; - -function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { - editor.changeDecorations(decorationsAccessor => { - editor.changeViewZones(viewZoneAccessor => { - callback(decorationsAccessor, viewZoneAccessor); - }); - }); -} - -export interface IInlineChatMetadata { - modelId: string | undefined; - extensionId: VersionedExtensionId | undefined; - requestId: string | undefined; -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts index ea38c5cfcec..fcf646a5109 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts @@ -10,22 +10,15 @@ import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/icon import { IAction } from '../../../../base/common/actions.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { isEqual } from '../../../../base/common/resources.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from '../../../../editor/browser/widget/diffEditor/components/accessibleDiffViewer.js'; -import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; -import { EditorOption, IComputedEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { LineRange } from '../../../../editor/common/core/ranges/lineRange.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; import { Selection } from '../../../../editor/common/core/selection.js'; -import { DetailedLineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { ICodeEditorViewState, ScrollType } from '../../../../editor/common/editorCommon.js'; +import { ICodeEditorViewState } from '../../../../editor/common/editorCommon.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { localize } from '../../../../nls.js'; -import product from '../../../../platform/product/common/product.js'; import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js'; @@ -39,6 +32,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import product from '../../../../platform/product/common/product.js'; import { asCssVariable, asCssVariableName, editorBackground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js'; import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; @@ -46,18 +41,16 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { MarkUnhelpfulActionId } from '../../chat/browser/actions/chatTitleActions.js'; import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; -import { ChatVoteDownButton } from '../../chat/browser/chatListRenderer.js'; -import { ChatWidget, IChatViewState, IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; -import { chatRequestBackground } from '../../chat/common/chatColors.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatModel } from '../../chat/common/chatModel.js'; -import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService.js'; -import { isResponseVM } from '../../chat/common/chatViewModel.js'; +import { ChatVoteDownButton } from '../../chat/browser/widget/chatListRenderer.js'; +import { ChatWidget, IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; +import { chatRequestBackground } from '../../chat/common/widget/chatColors.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IChatModel } from '../../chat/common/model/chatModel.js'; +import { ChatMode } from '../../chat/common/chatModes.js'; +import { ChatAgentVoteDirection, IChatService } from '../../chat/common/chatService/chatService.js'; +import { isResponseVM } from '../../chat/common/model/chatViewModel.js'; import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, inlineChatForeground } from '../common/inlineChat.js'; -import { HunkInformation, Session } from './inlineChatSession.js'; import './media/inlineChat.css'; -import { ChatMode } from '../../chat/common/chatModes.js'; -import { isEqual } from '../../../../base/common/resources.js'; export interface InlineChatWidgetViewState { editorViewState: ICodeEditorViewState; @@ -209,7 +202,7 @@ export class InlineChatWidget { viewModelStore.add(viewModel.onDidChange(() => { - this._requestInProgress.set(viewModel.requestInProgress, undefined); + this._requestInProgress.set(viewModel.model.requestInProgress.get(), undefined); const last = viewModel.getItems().at(-1); toolbar2.context = last; @@ -400,7 +393,7 @@ export class InlineChatWidget { let value = this.contentHeight; value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight); + value += Math.min(this._chatWidget.input.height.get() + maxWidgetOutputHeight, this._chatWidget.contentHeight); return value; } @@ -464,8 +457,9 @@ export class InlineChatWidget { return this._chatWidget.viewModel?.model; } - setChatModel(chatModel: IChatModel, state?: IChatViewState) { - this._chatWidget.setModel(chatModel, { ...state, inputValue: undefined }); + setChatModel(chatModel: IChatModel) { + chatModel.inputModel.setState({ inputText: '', selections: [] }); + this._chatWidget.setModel(chatModel); } updateInfo(message: string): void { @@ -531,12 +525,9 @@ const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); export class EditorBasedInlineChatWidget extends InlineChatWidget { - private readonly _accessibleViewer = this._store.add(new MutableDisposable()); - - constructor( location: IChatWidgetLocationOptions, - private readonly _parentEditor: ICodeEditor, + parentEditor: ICodeEditor, options: IInlineChatWidgetConstructionOptions, @IContextKeyService contextKeyService: IContextKeyService, @IKeybindingService keybindingService: IKeybindingService, @@ -551,7 +542,7 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { @IChatEntitlementService chatEntitlementService: IChatEntitlementService, @IMarkdownRendererService markdownRendererService: IMarkdownRendererService, ) { - const overflowWidgetsNode = layoutService.getContainer(getWindow(_parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor')); + const overflowWidgetsNode = layoutService.getContainer(getWindow(parentEditor.getContainerDomNode())).appendChild($('.inline-chat-overflow.monaco-editor')); super(location, { ...options, chatWidgetViewOptions: { @@ -567,24 +558,10 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { // --- layout - override get contentHeight(): number { - let result = super.contentHeight; - - if (this._accessibleViewer.value) { - result += this._accessibleViewer.value.height + 8 /* padding */; - } - - return result; - } protected override _doLayout(dimension: Dimension): void { - let newHeight = dimension.height; - - if (this._accessibleViewer.value) { - this._accessibleViewer.value.width = dimension.width - 12; - newHeight -= this._accessibleViewer.value.height + 8; - } + const newHeight = dimension.height; super._doLayout(dimension.with(undefined, newHeight)); @@ -593,110 +570,8 @@ export class EditorBasedInlineChatWidget extends InlineChatWidget { } override reset() { - this._accessibleViewer.clear(); this.chatWidget.setInput(); super.reset(); } - // --- accessible viewer - - showAccessibleHunk(session: Session, hunkData: HunkInformation): void { - - this._elements.accessibleViewer.classList.remove('hidden'); - this._accessibleViewer.clear(); - - this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, - this._elements.accessibleViewer, - session, - hunkData, - new AccessibleHunk(this._parentEditor, session, hunkData) - ); - - this._onDidChangeHeight.fire(); - } -} - -class HunkAccessibleDiffViewer extends AccessibleDiffViewer { - - readonly height: number; - - set width(value: number) { - this._width2.set(value, undefined); - } - - private readonly _width2: ISettableObservable; - - constructor( - parentNode: HTMLElement, - session: Session, - hunk: HunkInformation, - models: IAccessibleDiffViewerModel, - @IInstantiationService instantiationService: IInstantiationService, - ) { - const width = observableValue('width', 0); - const diff = observableValue('diff', HunkAccessibleDiffViewer._asMapping(hunk)); - const diffs = derived(r => [diff.read(r)]); - const lines = Math.min(10, 8 + diff.get().changedLineCount); - const height = models.getModifiedOptions().get(EditorOption.lineHeight) * lines; - - super(parentNode, constObservable(true), () => { }, constObservable(false), width, constObservable(height), diffs, models, instantiationService); - - this.height = height; - this._width2 = width; - - this._store.add(session.textModelN.onDidChangeContent(() => { - diff.set(HunkAccessibleDiffViewer._asMapping(hunk), undefined); - })); - } - - private static _asMapping(hunk: HunkInformation): DetailedLineRangeMapping { - const ranges0 = hunk.getRanges0(); - const rangesN = hunk.getRangesN(); - const originalLineRange = LineRange.fromRangeInclusive(ranges0[0]); - const modifiedLineRange = LineRange.fromRangeInclusive(rangesN[0]); - const innerChanges: RangeMapping[] = []; - for (let i = 1; i < ranges0.length; i++) { - innerChanges.push(new RangeMapping(ranges0[i], rangesN[i])); - } - return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, innerChanges); - } - -} - -class AccessibleHunk implements IAccessibleDiffViewerModel { - - constructor( - private readonly _editor: ICodeEditor, - private readonly _session: Session, - private readonly _hunk: HunkInformation - ) { } - - getOriginalModel(): ITextModel { - return this._session.textModel0; - } - getModifiedModel(): ITextModel { - return this._session.textModelN; - } - getOriginalOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - getModifiedOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - originalReveal(range: Range): void { - // throw new Error('Method not implemented.'); - } - modifiedReveal(range?: Range | undefined): void { - this._editor.revealRangeInCenterIfOutsideViewport(range || this._hunk.getRangesN()[0], ScrollType.Smooth); - } - modifiedSetSelection(range: Range): void { - // this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); - // this._editor.setSelection(range); - } - modifiedFocus(): void { - this._editor.focus(); - } - getModifiedPosition(): Position | undefined { - return this._hunk.getRangesN()[0].getStartPosition(); - } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts index 024f3d9aa02..21113b9d0de 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatZoneWidget.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { addDisposableListener, Dimension } from '../../../../base/browser/dom.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; -import { MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { toDisposable } from '../../../../base/common/lifecycle.js'; import { autorun } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; import { assertType } from '../../../../base/common/types.js'; @@ -20,9 +20,8 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IChatWidgetViewOptions } from '../../chat/browser/chat.js'; -import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js'; +import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js'; import { ChatMode } from '../../chat/common/chatModes.js'; -import { isResponseVM } from '../../chat/common/chatViewModel.js'; import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js'; import { ACTION_REGENERATE_RESPONSE, ACTION_REPORT_ISSUE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, MENU_INLINE_CHAT_SIDE, MENU_INLINE_CHAT_WIDGET_SECONDARY, MENU_INLINE_CHAT_WIDGET_STATUS } from '../common/inlineChat.js'; import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; @@ -44,7 +43,6 @@ export class InlineChatZoneWidget extends ZoneWidget { readonly widget: EditorBasedInlineChatWidget; - private readonly _scrollUp = this._disposables.add(new ScrollUpState(this.editor)); private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; private _dimension?: Dimension; private notebookEditor?: INotebookEditor; @@ -53,6 +51,8 @@ export class InlineChatZoneWidget extends ZoneWidget { location: IChatWidgetLocationOptions, options: IChatWidgetViewOptions | undefined, editors: { editor: ICodeEditor; notebookEditor?: INotebookEditor }, + /** @deprecated should go away with inline2 */ + clearDelegate: () => Promise, @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private _logService: ILogService, @IContextKeyService contextKeyService: IContextKeyService, @@ -87,6 +87,7 @@ export class InlineChatZoneWidget extends ZoneWidget { telemetrySource: 'interactiveEditorWidget-toolbar', inputSideToolbar: MENU_INLINE_CHAT_SIDE }, + clear: clearDelegate, ...options, rendererOptions: { renderTextEditsAsSummary: (uri) => { @@ -207,7 +208,6 @@ export class InlineChatZoneWidget extends ZoneWidget { this.widget.focus(); revealZone(); - this._scrollUp.enable(); } private _updatePadding() { @@ -222,7 +222,6 @@ export class InlineChatZoneWidget extends ZoneWidget { const stickyScroll = this.editor.getOption(EditorOption.stickyScroll); const magicValue = stickyScroll.enabled ? stickyScroll.maxLineCount : 0; this.editor.revealLines(position.lineNumber + magicValue, position.lineNumber + magicValue, ScrollType.Immediate); - this._scrollUp.reset(); this.updatePositionAndHeight(position); } @@ -237,23 +236,8 @@ export class InlineChatZoneWidget extends ZoneWidget { const scrollState = StableEditorBottomScrollState.capture(this.editor); const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber; - const scrollTop = this.editor.getScrollTop(); - const lineTop = this.editor.getTopForLineNumber(lineNumber); - const zoneTop = lineTop - this._computeHeight().pixelsValue; - const hasResponse = this.widget.chatWidget.viewModel?.getItems().find(candidate => { - return isResponseVM(candidate) && candidate.response.value.length > 0; - }); - - if (hasResponse && zoneTop < scrollTop || this._scrollUp.didScrollUpOrDown) { - // don't reveal the zone if it is already out of view (unless we are still getting ready) - // or if an outside scroll-up happened (e.g the user scrolled up/down to see the new content) - return this._scrollUp.runIgnored(() => { - scrollState.restore(this.editor); - }); - } - - return this._scrollUp.runIgnored(() => { + return () => { scrollState.restore(this.editor); const scrollTop = this.editor.getScrollTop(); @@ -276,7 +260,7 @@ export class InlineChatZoneWidget extends ZoneWidget { this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); } - }); + }; } protected override revealRange(range: Range, isLastLine: boolean): void { @@ -285,62 +269,10 @@ export class InlineChatZoneWidget extends ZoneWidget { override hide(): void { const scrollState = StableEditorBottomScrollState.capture(this.editor); - this._scrollUp.disable(); this._ctxCursorPosition.reset(); - this.widget.reset(); this.widget.chatWidget.setVisible(false); super.hide(); aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); scrollState.restore(this.editor); } } - -class ScrollUpState { - - private _didScrollUpOrDown?: boolean; - private _ignoreEvents = false; - - private readonly _listener = new MutableDisposable(); - - constructor(private readonly _editor: ICodeEditor) { } - - dispose(): void { - this._listener.dispose(); - } - - reset(): void { - this._didScrollUpOrDown = undefined; - } - - enable(): void { - this._didScrollUpOrDown = undefined; - this._listener.value = this._editor.onDidScrollChange(e => { - if (!e.scrollTopChanged || this._ignoreEvents) { - return; - } - this._listener.clear(); - this._didScrollUpOrDown = true; - }); - } - - disable(): void { - this._listener.clear(); - this._didScrollUpOrDown = undefined; - } - - runIgnored(callback: () => void): () => void { - return () => { - this._ignoreEvents = true; - try { - return callback(); - } finally { - this._ignoreEvents = false; - } - }; - } - - get didScrollUpOrDown(): boolean | undefined { - return this._didScrollUpOrDown; - } - -} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css index fdb73019c9f..2215f0f84dd 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChat.css @@ -96,15 +96,18 @@ .monaco-workbench .zone-widget.inline-chat-widget.inline-chat-2 { .inline-chat .chat-widget .interactive-session .interactive-input-part { - padding: 8px 0 0 0; + padding: 8px 0 4px 0; } + .interactive-session .chat-input-container.focused, .interactive-session .chat-input-container { - border-color: var(--vscode-input-border, transparent); + border-color: var(--vscode-inlineChat-background); + background-color: var(--vscode-inlineChat-background); + padding-left: 0; } - .interactive-session .chat-input-container:focus-within { - border-color: var(--vscode-input-border, transparent); + .chat-attachments-container { + margin-right: 0; } .chat-attachments-container > .chat-input-toolbar { @@ -116,6 +119,10 @@ .request-in-progress .monaco-editor [class^="ced-chat-session-detail"]::after { animation: pulse-opacity 2.5s ease-in-out infinite; } + + .chat-editor-container .interactive-input-editor .monaco-editor .monaco-editor-background { + background-color: var(--vscode-inlineChat-background); + } } @@ -175,7 +182,7 @@ max-width: 66%; } -.monaco-workbench .inline-chat .chat-widget .interactive-session .chat-input-toolbars > .chat-execute-toolbar .chat-modelPicker-item { +.monaco-workbench .inline-chat .chat-widget .interactive-session .chat-input-toolbars > .chat-execute-toolbar .chat-input-picker-item { min-width: 40px; max-width: 132px; } @@ -191,7 +198,7 @@ .monaco-workbench .inline-chat > .status { .label, .actions { - padding-top: 8px; + padding: 4px 0; } } @@ -202,14 +209,13 @@ .monaco-workbench .inline-chat .status .label { overflow: hidden; color: var(--vscode-descriptionForeground); - font-size: 11px; + font-size: 12px; display: flex; white-space: nowrap; } .monaco-workbench .inline-chat .status .label.info { margin-right: auto; - padding-left: 2px; } .monaco-workbench .inline-chat .status .label.status { @@ -237,27 +243,7 @@ line-height: 18px; } -.monaco-workbench .inline-chat .status .rerun { - display: inline-flex; -} - -.monaco-workbench .inline-chat .status .rerun:not(:empty) { - padding-top: 8px; - padding-left: 4px; -} - -.monaco-workbench .inline-chat .status .rerun .agentOrSlashCommandDetected A { - cursor: pointer; - color: var(--vscode-textLink-foreground); -} - -.monaco-workbench .inline-chat .interactive-item-container.interactive-response .detail-container .detail .agentOrSlashCommandDetected, -.monaco-workbench .inline-chat .interactive-item-container.interactive-response .detail-container .chat-animated-ellipsis { - display: none; -} - -.monaco-workbench .inline-chat .status .actions, -.monaco-workbench .inline-chat-diff-overlay { +.monaco-workbench .inline-chat .status .actions { display: flex; height: 18px; @@ -308,31 +294,6 @@ display: inherit; } -.monaco-workbench .inline-chat-diff-overlay { - - .monaco-button { - border-radius: 0; - } - - .monaco-button.secondary.checked { - background-color: var(--vscode-button-secondaryHoverBackground); - } - - .monaco-button:first-child { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; - } - - .monaco-button:last-child { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - } - - .monaco-button:not(:last-child) { - border-right: 1px solid var(--vscode-button-foreground); - } -} - .monaco-workbench .inline-chat .status .disclaimer { a { color: var(--vscode-textLink-foreground); @@ -355,71 +316,50 @@ background-color: var(--vscode-button-hoverBackground); } -/* accessible diff viewer */ -.monaco-workbench .inline-chat .diff-review { - padding: 4px 6px; - background-color: unset; -} - -.monaco-workbench .inline-chat .diff-review.hidden { - display: none; -} - -/* decoration styles */ - -.monaco-workbench .inline-chat-inserted-range { - background-color: var(--vscode-inlineChatDiff-inserted); -} - -.monaco-workbench .inline-chat-inserted-range-linehighlight { - background-color: var(--vscode-diffEditor-insertedLineBackground); -} - -.monaco-workbench .inline-chat-original-zone2 { - background-color: var(--vscode-diffEditor-removedLineBackground); - opacity: 0.8; -} - -.monaco-workbench .inline-chat-lines-inserted-range { - background-color: var(--vscode-diffEditor-insertedTextBackground); -} -/* gutter decoration */ - -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { - display: block; - cursor: pointer; - transition: opacity .2s ease-in-out; +.monaco-workbench .inline-chat .chat-attached-context { + padding: 2px 0px; } -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { - opacity: 0.5; +/* Gutter menu overlay widget */ +.inline-chat-gutter-menu { + background: var(--vscode-menu-background); + border: 1px solid var(--vscode-menu-border, var(--vscode-widget-border)); + border-radius: 4px; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + padding: 4px 0; + min-width: 160px; + z-index: 10000; } -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { - opacity: 0; +.inline-chat-gutter-menu .input { + padding: 0 8px; } -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { - opacity: 1; +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item { + display: flex; + justify-content: space-between; + border-radius: 3px; + margin: 0 4px; } -.monaco-workbench .inline-chat .chat-attached-context { - padding: 3px 0px; +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item .action-label { + font-size: 13px; + width: 100%; } - -/* HINT */ - -.monaco-workbench .monaco-editor .inline-chat-hint { - cursor: pointer; - color: var(--vscode-editorGhostText-foreground); +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within { + background-color: var(--vscode-list-activeSelectionBackground); + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; } -.monaco-workbench .monaco-editor .inline-chat-hint.embedded { - border: 1px solid var(--vscode-editorSuggestWidget-border); - border-radius: 3px; +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):hover .action-label, +.inline-chat-gutter-menu .monaco-action-bar.vertical .action-item:not(.disabled):focus-within .action-label { + color: var(--vscode-list-activeSelectionForeground); + outline: 1px solid var(--vscode-menu-selectionBorder, transparent); + outline-offset: -1px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css new file mode 100644 index 00000000000..5eaa356dcfa --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatEditorAffordance.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.inline-chat-content-widget { + background-color: var(--vscode-editor-background); + padding: 2px; + border-radius: 8px; + display: flex; + align-items: center; + box-shadow: 0 4px 8px var(--vscode-widget-shadow); + cursor: pointer; + min-width: var(--vscode-inline-chat-affordance-height); + min-height: var(--vscode-inline-chat-affordance-height); + line-height: var(--vscode-inline-chat-affordance-height); +} + +.inline-chat-content-widget .icon.codicon { + margin: 0; + color: var(--vscode-editorLightBulb-foreground); +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css new file mode 100644 index 00000000000..1af3ff339a1 --- /dev/null +++ b/src/vs/workbench/contrib/inlineChat/browser/media/inlineChatOverlayWidget.css @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.inline-chat-gutter-menu.clamped { + transition: top 100ms; +} + +.inline-chat-gutter-menu .input .monaco-editor-background { + background-color: var(--vscode-menu-background); +} + +.inline-chat-session-overlay-widget { + z-index: 1; + transition: top 100ms; +} + +.inline-chat-session-overlay-container { + padding: 2px 4px; + color: var(--vscode-foreground); + background-color: var(--vscode-editorWidget-background); + border-radius: 6px; + border: 1px solid var(--vscode-contrastBorder); + display: flex; + align-items: center; + justify-content: center; + gap: 4px; + z-index: 10; + box-shadow: 0 2px 8px var(--vscode-widget-shadow); + overflow: hidden; +} + +.inline-chat-session-overlay-container .status { + align-items: center; + display: inline-flex; + padding: 5px 0 5px 5px; + font-size: 12px; + overflow: hidden; + gap: 6px; +} + +.inline-chat-session-overlay-container .status .message { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.inline-chat-session-overlay-container .status .message:not(:empty) { + padding-right: 2em; +} + +.inline-chat-session-overlay-container .status .codicon { + color: var(--vscode-foreground); +} + +.inline-chat-session-overlay-container .action-item > .action-label { + padding: 4px 6px; + font-size: 11px; + line-height: 14px; + border-radius: 4px; +} + +.inline-chat-session-overlay-container .monaco-action-bar .actions-container { + gap: 4px; +} + +.inline-chat-session-overlay-container .action-item.primary > .action-label { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); +} + +.monaco-workbench .inline-chat-session-overlay-container .monaco-action-bar .action-item.primary > .action-label:hover { + background-color: var(--vscode-button-hoverBackground); +} + +.inline-chat-session-overlay-container .action-item > .action-label.codicon:not(.separator) { + color: var(--vscode-foreground); + width: 22px; + height: 22px; + padding: 0; + font-size: 16px; + line-height: 22px; + display: flex; + align-items: center; + justify-content: center; +} + +.inline-chat-session-overlay-container .monaco-action-bar .action-item.disabled { + + > .action-label.codicon::before, + > .action-label.codicon, + > .action-label, + > .action-label:hover { + color: var(--vscode-button-separator); + opacity: 1; + } +} diff --git a/src/vs/workbench/contrib/inlineChat/browser/utils.ts b/src/vs/workbench/contrib/inlineChat/browser/utils.ts deleted file mode 100644 index df93be8528c..00000000000 --- a/src/vs/workbench/contrib/inlineChat/browser/utils.ts +++ /dev/null @@ -1,95 +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 { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IIdentifiedSingleEditOperation, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { IEditObserver } from './inlineChatStrategies.js'; -import { IProgress } from '../../../../platform/progress/common/progress.js'; -import { IntervalTimer, AsyncIterableSource } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { getNWords } from '../../chat/common/chatWordCounter.js'; -import { TextModelEditSource } from '../../../../editor/common/textModelEditSource.js'; - - - -// --- async edit - -export interface AsyncTextEdit { - readonly range: IRange; - readonly newText: AsyncIterable; -} - -export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdit, progress?: IProgress, obs?: IEditObserver, editSource?: TextModelEditSource) { - - const [id] = model.deltaDecorations([], [{ - range: edit.range, - options: { - description: 'asyncTextEdit', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - } - }]); - - let first = true; - for await (const part of edit.newText) { - - if (model.isDisposed()) { - break; - } - - const range = model.getDecorationRange(id); - if (!range) { - throw new Error('FAILED to perform async replace edit because the anchor decoration was removed'); - } - - const edit = first - ? EditOperation.replace(range, part) // first edit needs to override the "anchor" - : EditOperation.insert(range.getEndPosition(), part); - obs?.start(); - - model.pushEditOperations(null, [edit], (undoEdits) => { - progress?.report(undoEdits); - return null; - }, undefined, editSource); - - obs?.stop(); - first = false; - } -} - -export function asProgressiveEdit(interval: IntervalTimer, edit: IIdentifiedSingleEditOperation, wordsPerSec: number, token: CancellationToken): AsyncTextEdit { - - wordsPerSec = Math.max(30, wordsPerSec); - - const stream = new AsyncIterableSource(); - let newText = edit.text ?? ''; - - interval.cancelAndSet(() => { - if (token.isCancellationRequested) { - return; - } - const r = getNWords(newText, 1); - stream.emitOne(r.value); - newText = newText.substring(r.value.length); - if (r.isFullString) { - interval.cancel(); - stream.resolve(); - d.dispose(); - } - - }, 1000 / wordsPerSec); - - // cancel ASAP - const d = token.onCancellationRequested(() => { - interval.cancel(); - stream.resolve(); - d.dispose(); - }); - - return { - range: edit.range, - newText: stream.asyncIterable - }; -} diff --git a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts index 64d678e8fea..62726dedfcc 100644 --- a/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts +++ b/src/vs/workbench/contrib/inlineChat/common/inlineChat.ts @@ -15,13 +15,13 @@ import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../notebook/common/notebookContext export const enum InlineChatConfigKeys { FinishOnType = 'inlineChat.finishOnType', - StartWithOverlayWidget = 'inlineChat.startWithOverlayWidget', HoldToSpeech = 'inlineChat.holdToSpeech', - AccessibleDiffView = 'inlineChat.accessibleDiffView', - LineEmptyHint = 'inlineChat.lineEmptyHint', - LineNLHint = 'inlineChat.lineNaturalLanguageHint', + /** @deprecated do not read on client */ EnableV2 = 'inlineChat.enableV2', notebookAgent = 'inlineChat.notebookAgent', + DefaultModel = 'inlineChat.defaultModel', + Affordance = 'inlineChat.affordance', + RenderMode = 'inlineChat.renderMode', } Registry.as(Extensions.Configuration).registerConfiguration({ @@ -37,29 +37,6 @@ Registry.as(Extensions.Configuration).registerConfigurat default: true, type: 'boolean' }, - [InlineChatConfigKeys.AccessibleDiffView]: { - description: localize('accessibleDiffView', "Whether the inline chat also renders an accessible diff viewer for its changes."), - default: 'auto', - type: 'string', - enum: ['auto', 'on', 'off'], - markdownEnumDescriptions: [ - localize('accessibleDiffView.auto', "The accessible diff viewer is based on screen reader mode being enabled."), - localize('accessibleDiffView.on', "The accessible diff viewer is always enabled."), - localize('accessibleDiffView.off', "The accessible diff viewer is never enabled."), - ], - }, - [InlineChatConfigKeys.LineEmptyHint]: { - description: localize('emptyLineHint', "Whether empty lines show a hint to generate code with inline chat."), - default: false, - type: 'boolean', - tags: ['experimental'], - }, - [InlineChatConfigKeys.LineNLHint]: { - markdownDescription: localize('lineSuffixHint', "Whether lines that are dominated by natural language or pseudo code show a hint to continue with inline chat. For instance, `class Person with name and hobbies` would show a hint to continue with chat."), - default: true, - type: 'boolean', - tags: ['experimental'], - }, [InlineChatConfigKeys.EnableV2]: { description: localize('enableV2', "Whether to use the next version of inline chat."), default: false, @@ -77,6 +54,29 @@ Registry.as(Extensions.Configuration).registerConfigurat experiment: { mode: 'startup' } + }, + [InlineChatConfigKeys.Affordance]: { + description: localize('affordance', "Controls whether an inline chat affordance is shown when text is selected."), + default: 'off', + type: 'string', + enum: ['off', 'gutter', 'editor'], + enumDescriptions: [ + localize('affordance.off', "No affordance is shown."), + localize('affordance.gutter', "Show an affordance in the gutter."), + localize('affordance.editor', "Show an affordance in the editor at the cursor position."), + ], + tags: ['experimental'] + }, + [InlineChatConfigKeys.RenderMode]: { + description: localize('renderMode', "Controls how inline chat is rendered."), + default: 'zone', + type: 'string', + enum: ['zone', 'hover'], + enumDescriptions: [ + localize('renderMode.zone', "Render inline chat as a zone widget below the current line."), + localize('renderMode.hover', "Render inline chat as a hover overlay."), + ], + tags: ['experimental'] } } }); @@ -94,7 +94,6 @@ export const enum InlineChatResponseType { } export const CTX_INLINE_CHAT_POSSIBLE = new RawContextKey('inlineChatPossible', false, localize('inlineChatHasPossible', "Whether a provider for inline chat exists and whether an editor for inline chat is open")); -export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists")); export const CTX_INLINE_CHAT_HAS_AGENT2 = new RawContextKey('inlineChatHasEditsAgent', false, localize('inlineChatHasEditsAgent', "Whether an agent for inline for interactive editors exists")); export const CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE = new RawContextKey('inlineChatHasNotebookInline', false, localize('inlineChatHasNotebookInline', "Whether an agent for notebook cells exists")); export const CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT = new RawContextKey('inlineChatHasNotebookAgent', false, localize('inlineChatHasNotebookAgent', "Whether an agent for notebook cells exists")); @@ -113,15 +112,16 @@ export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('i export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineChatResponseType', InlineChatResponseType.None, localize('inlineChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); export const CTX_INLINE_CHAT_V1_ENABLED = ContextKeyExpr.or( - ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR.negate(), CTX_INLINE_CHAT_HAS_AGENT), ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_INLINE) ); export const CTX_INLINE_CHAT_V2_ENABLED = ContextKeyExpr.or( - ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR.negate(), CTX_INLINE_CHAT_HAS_AGENT2), + CTX_INLINE_CHAT_HAS_AGENT2, ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT) ); +export const CTX_HOVER_MODE = ContextKeyExpr.equals('config.inlineChat.renderMode', 'hover'); + // --- (selected) action identifier export const ACTION_START = 'inlineChat.start'; diff --git a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts index 914993f57ae..0a9c91d1859 100644 --- a/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts +++ b/src/vs/workbench/contrib/inlineChat/electron-browser/inlineChatActions.ts @@ -8,7 +8,7 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { InlineChatController } from '../browser/inlineChatController.js'; -import { AbstractInline1ChatAction, setHoldForSpeech } from '../browser/inlineChatActions.js'; +import { AbstractInlineChatAction, setHoldForSpeech } from '../browser/inlineChatActions.js'; import { disposableTimeout } from '../../../../base/common/async.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -27,7 +27,7 @@ export class HoldToSpeak extends EditorAction2 { constructor() { super({ id: 'inlineChat.holdForSpeech', - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and(HasSpeechProvider, CTX_INLINE_CHAT_VISIBLE), title: localize2('holdForSpeech', "Hold for Speech"), keybinding: { diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap b/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap deleted file mode 100644 index a0379e041b9..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.1.snap +++ /dev/null @@ -1,13 +0,0 @@ -export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - - let a = 0, b = 1, c; - for (let i = 3; i <= n; i++) { - c = a + b; - a = b; - b = c; - } - return b; -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap b/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap deleted file mode 100644 index 3d44a421300..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/__snapshots__/InlineChatSession_Apply_Code_s_preview_should_be_easier_to_undo_esc__7537.2.snap +++ /dev/null @@ -1,6 +0,0 @@ -export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - return fib(n - 1) + fib(n - 2); -} \ No newline at end of file diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts deleted file mode 100644 index 6f29da36b92..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts +++ /dev/null @@ -1,1078 +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 { equals } from '../../../../../base/common/arrays.js'; -import { DeferredPromise, raceCancellation, timeout } from '../../../../../base/common/async.js'; -import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { constObservable, IObservable } from '../../../../../base/common/observable.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js'; -import { IActiveCodeEditor, ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js'; -import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { EndOfLineSequence, ITextModel } from '../../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js'; -import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js'; -import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; -import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IView, IViewDescriptorService } from '../../../../common/views.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { TestChatEntitlementService, TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../../../chat/browser/chat.js'; -import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js'; -import { ChatLayoutService } from '../../../chat/browser/chatLayoutService.js'; -import { ChatVariablesService } from '../../../chat/browser/chatVariables.js'; -import { ChatWidget, ChatWidgetService } from '../../../chat/browser/chatWidget.js'; -import { ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; -import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IChatLayoutService } from '../../../chat/common/chatLayoutService.js'; -import { IChatModeService } from '../../../chat/common/chatModes.js'; -import { IChatTodo, IChatTodoListService } from '../../../chat/common/chatTodoListService.js'; -import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; -import { ChatService } from '../../../chat/common/chatServiceImpl.js'; -import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/chatSlashCommands.js'; -import { ChatTransferService, IChatTransferService } from '../../../chat/common/chatTransferService.js'; -import { IChatVariablesService } from '../../../chat/common/chatVariables.js'; -import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; -import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js'; -import { ILanguageModelsService, LanguageModelsService } from '../../../chat/common/languageModels.js'; -import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; -import { PromptsType } from '../../../chat/common/promptSyntax/promptTypes.js'; -import { IPromptPath, IPromptsService } from '../../../chat/common/promptSyntax/service/promptsService.js'; -import { MockChatModeService } from '../../../chat/test/common/mockChatModeService.js'; -import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js'; -import { RerunAction } from '../../browser/inlineChatActions.js'; -import { InlineChatController1, State } from '../../browser/inlineChatController.js'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js'; -import { TestWorkerService } from './testWorkerService.js'; -import { URI } from '../../../../../base/common/uri.js'; - -suite('InlineChatController', function () { - - const agentData = { - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - // id: 'testEditorAgent', - name: 'testEditorAgent', - isDefault: true, - locations: [ChatAgentLocation.EditorInline], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }; - - class TestController extends InlineChatController1 { - - static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT]; - static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]; - - - readonly onDidChangeState: Event = this._onDidEnterState.event; - - readonly states: readonly State[] = []; - - awaitStates(states: readonly State[]): Promise { - const actual: State[] = []; - - return new Promise((resolve, reject) => { - const d = this.onDidChangeState(state => { - actual.push(state); - if (equals(states, actual)) { - d.dispose(); - resolve(undefined); - } - }); - - setTimeout(() => { - d.dispose(); - resolve(`[${states.join(',')}] <> [${actual.join(',')}]`); - }, 1000); - }); - } - } - - const store = new DisposableStore(); - let configurationService: TestConfigurationService; - let editor: IActiveCodeEditor; - let model: ITextModel; - let ctrl: TestController; - let contextKeyService: MockContextKeyService; - let chatService: IChatService; - let chatAgentService: IChatAgentService; - let inlineChatSessionService: IInlineChatSessionService; - let instaService: TestInstantiationService; - - let chatWidget: IChatWidget; - - setup(function () { - - const serviceCollection = new ServiceCollection( - [IConfigurationService, new TestConfigurationService()], - [IChatVariablesService, new SyncDescriptor(ChatVariablesService)], - [ILogService, new NullLogService()], - [ITelemetryService, NullTelemetryService], - [IHoverService, NullHoverService], - [IExtensionService, new TestExtensionService()], - [IContextKeyService, new MockContextKeyService()], - [IViewsService, new class extends TestViewsService { - override async openView(id: string, focus?: boolean | undefined): Promise { - // eslint-disable-next-line local/code-no-any-casts - return { widget: chatWidget ?? null } as any; - } - }()], - [IWorkspaceContextService, new TestContextService()], - [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], - [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], - [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], - [IChatTransferService, new SyncDescriptor(ChatTransferService)], - [IChatService, new SyncDescriptor(ChatService)], - [IMcpService, new TestMcpService()], - [IChatAgentNameService, new class extends mock() { - override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { - return false; - } - }], - [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], - [IContextKeyService, contextKeyService], - [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], - [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], - [ICommandService, new SyncDescriptor(TestCommandService)], - [IChatEditingService, new class extends mock() { - override editingSessionsObs: IObservable = constObservable([]); - }], - [IEditorProgressService, new class extends mock() { - override show(total: unknown, delay?: unknown): IProgressRunner { - return { - total() { }, - worked(value) { }, - done() { }, - }; - } - }], - [IChatAccessibilityService, new class extends mock() { - override acceptResponse(widget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | undefined, requestId: number): void { } - override acceptRequest(): number { return -1; } - override acceptElicitation(): void { } - }], - [IAccessibleViewService, new class extends mock() { - override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { - return null; - } - }], - [IConfigurationService, configurationService], - [IViewDescriptorService, new class extends mock() { - override onDidChangeLocation = Event.None; - }], - [INotebookEditorService, new class extends mock() { - override listNotebookEditors() { return []; } - override getNotebookForPossibleCell(editor: ICodeEditor) { - return undefined; - } - }], - [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()], - [ILanguageModelsService, new SyncDescriptor(LanguageModelsService)], - [ITextModelService, new SyncDescriptor(TextModelResolverService)], - [ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)], - [IPromptsService, new class extends mock() { - override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise { - return []; - } - }], - [IChatEntitlementService, new class extends mock() { }], - [IChatModeService, new SyncDescriptor(MockChatModeService)], - [IChatLayoutService, new SyncDescriptor(ChatLayoutService)], - [IChatTodoListService, new class extends mock() { - override onDidUpdateTodos = Event.None; - override getTodos(sessionResource: URI): IChatTodo[] { return []; } - override setTodos(sessionResource: URI, todos: IChatTodo[]): void { } - }], - [IChatEntitlementService, new SyncDescriptor(TestChatEntitlementService)], - ); - - instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection)); - - configurationService = instaService.get(IConfigurationService) as TestConfigurationService; - configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } }); - - configurationService.setUserConfiguration('editor', {}); - - contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService; - chatService = instaService.get(IChatService); - chatAgentService = instaService.get(IChatAgentService); - - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - - store.add(instaService.get(ILanguageModelsService) as LanguageModelsService); - store.add(instaService.get(IEditorWorkerService) as TestWorkerService); - - store.add(instaService.createInstance(ChatInputBoxContentProvider)); - - model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null)); - model.setEOL(EndOfLineSequence.LF); - editor = store.add(instantiateTestCodeEditor(instaService, model)); - - store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { - async invoke(request, progress, history, token) { - progress([{ - kind: 'textEdit', - uri: model.uri, - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.message - }] - }]); - return {}; - }, - })); - - }); - - teardown(function () { - store.clear(); - ctrl?.dispose(); - }); - - // TODO@jrieken re-enable, looks like List/ChatWidget is leaking - // ensureNoDisposablesAreLeakedInTestSuite(); - - test('creation, not showing anything', function () { - ctrl = instaService.createInstance(TestController, editor); - assert.ok(ctrl); - assert.strictEqual(ctrl.getWidgetPosition(), undefined); - }); - - test('run (show/hide)', async function () { - ctrl = instaService.createInstance(TestController, editor); - const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); - const run = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await actualStates, undefined); - assert.ok(ctrl.getWidgetPosition() !== undefined); - await ctrl.cancelSession(); - - await run; - - assert.ok(ctrl.getWidgetPosition() === undefined); - }); - - test('wholeRange does not expand to whole lines, editor selection default', async function () { - - editor.setSelection(new Range(1, 1, 1, 3)); - ctrl = instaService.createInstance(TestController, editor); - - ctrl.run({}); - await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT)); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3)); - - await ctrl.cancelSession(); - }); - - test('typing outside of wholeRange finishes session', async function () { - - configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true); - - ctrl = instaService.createInstance(TestController, editor); - const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await actualStates, undefined); - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 11 /* line length */)); - - editor.setSelection(new Range(2, 1, 2, 1)); - editor.trigger('test', 'type', { text: 'a' }); - - assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined); - await r; - }); - - test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () { - - editor.setSelection(new Range(3, 1, 3, 3)); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ - kind: 'textEdit', - uri: editor.getModel().uri, - edits: [{ - range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range - text: `${request.message}\n${request.message}` - }] - }]); - - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates(TestController.INIT_SEQUENCE); - const r = ctrl.run({ message: 'GENGEN', autoSend: false }); - - assert.strictEqual(await p, undefined); - - - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assert.ok(session); - assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial - - ctrl.chatWidget.setInput('GENGEN'); - ctrl.chatWidget.acceptInput(); - assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]), undefined); - - assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3)); - - await ctrl.cancelSession(); - await r; - }); - - test('Stuck inline chat widget #211', async function () { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - return new Promise(() => { }); - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await p, undefined); - - ctrl.acceptSession(); - - await r; - assert.strictEqual(ctrl.getWidgetPosition(), undefined); - }); - - test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }]); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }]); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }]); - - return {}; - }, - })); - - const valueThen = editor.getModel().getValue(); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await p, undefined); - ctrl.acceptSession(); - await r; - - assert.strictEqual(editor.getModel().getValue(), 'Hello1\nHello2\n'); - - editor.getModel().undo(); - assert.strictEqual(editor.getModel().getValue(), valueThen); - }); - - - - test.skip('UI is streaming edits minutes after the response is finished #3345', async function () { - - - return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => { - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n'; - - await timeout(10); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }]); - - await timeout(10); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }]); - - throw new Error('Too long'); - }, - })); - - - // let modelChangeCounter = 0; - // store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; })); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'Hello', autoSend: true }); - assert.strictEqual(await p, undefined); - - // assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made - // const modelChangeCounterNow = modelChangeCounter; - - assert.ok(!editor.getModel().getValue().includes('DONE')); - await timeout(10); - - // assert.strictEqual(modelChangeCounterNow, modelChangeCounter); - assert.ok(!editor.getModel().getValue().includes('DONE')); - - await ctrl.cancelSession(); - await r; - }); - }); - - test('escape doesn\'t remove code added from inline editor chat #3523 1/2', async function () { - - - // NO manual edits -> cancel - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.ok(model.getValue().includes('GENERATED')); - ctrl.cancelSession(); - await r; - assert.ok(!model.getValue().includes('GENERATED')); - - }); - - test('escape doesn\'t remove code added from inline editor chat #3523, 2/2', async function () { - - // manual edits -> finish - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'GENERATED', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.ok(model.getValue().includes('GENERATED')); - - editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]); - - ctrl.acceptSession(); - await r; - assert.ok(model.getValue().includes('GENERATED')); - assert.ok(model.getValue().includes('MANUAL')); - - }); - - test('cancel while applying streamed edits should close the widget', async function () { - - const workerService = instaService.get(IEditorWorkerService) as TestWorkerService; - const originalCompute = workerService.computeMoreMinimalEdits.bind(workerService); - const editsBarrier = new DeferredPromise(); - let computeInvoked = false; - workerService.computeMoreMinimalEdits = async (resource, edits, pretty) => { - computeInvoked = true; - await editsBarrier.p; - return originalCompute(resource, edits, pretty); - }; - store.add({ dispose: () => { workerService.computeMoreMinimalEdits = originalCompute; } }); - - const progressBarrier = new DeferredPromise(); - store.add(chatAgentService.registerDynamicAgent({ - id: 'pendingEditsAgent', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message }] }]); - await progressBarrier.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const states = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const run = ctrl.run({ message: 'BLOCK', autoSend: true }); - assert.strictEqual(await states, undefined); - assert.ok(computeInvoked); - - ctrl.cancelSession(); - assert.strictEqual(await states, undefined); - - await run; - }); - - test('re-run should discard pending edits', async function () { - - let count = 1; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const rerun = new RerunAction(); - - model.setValue(''); - - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: 'PROMPT_', autoSend: true }); - assert.strictEqual(await p, undefined); - - - assert.strictEqual(model.getValue(), 'PROMPT_1'); - - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'PROMPT_2'); - ctrl.acceptSession(); - await r; - }); - - test('Retry undoes all changes, not just those from the request#5736', async function () { - - const text = [ - 'eins-', - 'zwei-', - 'drei-' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - const rerun = new RerunAction(); - - model.setValue(''); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const r = ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins-'); - - // REQUEST 2 - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.chatWidget.setInput('1'); - await ctrl.chatWidget.acceptInput(); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'zwei-eins-'); - - // REQUEST 2 - RERUN - const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - assert.strictEqual(await p3, undefined); - - assert.strictEqual(model.getValue(), 'drei-eins-'); - - ctrl.acceptSession(); - await r; - - }); - - test('moving inline chat to another model undoes changes', async function () { - const text = [ - 'eins\n', - 'zwei\n' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); - - const targetModel = chatService.startSession(ChatAgentLocation.EditorInline, CancellationToken.None)!; - store.add(targetModel); - chatWidget = new class extends mock() { - override get viewModel() { - // eslint-disable-next-line local/code-no-any-casts - return { model: targetModel } as any; - } - override focusResponseItem() { } - }; - - const r = ctrl.joinCurrentRun(); - await ctrl.viewInChat(); - - assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); - await r; - }); - - test('moving inline chat to another model undoes changes (2 requests)', async function () { - const text = [ - 'eins\n', - 'zwei\n' - ]; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: '1', autoSend: true }); - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n'); - - // REQUEST 2 - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.chatWidget.setInput('1'); - await ctrl.chatWidget.acceptInput(); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n'); - - const targetModel = chatService.startSession(ChatAgentLocation.EditorInline, CancellationToken.None)!; - store.add(targetModel); - chatWidget = new class extends mock() { - override get viewModel() { - // eslint-disable-next-line local/code-no-any-casts - return { model: targetModel } as any; - } - override focusResponseItem() { } - }; - - const r = ctrl.joinCurrentRun(); - - await ctrl.viewInChat(); - - assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n'); - - await r; - }); - - // TODO@jrieken https://github.com/microsoft/vscode/issues/251429 - test.skip('Clicking "re-run without /doc" while a request is in progress closes the widget #5997', async function () { - - model.setValue(''); - - let count = 0; - const commandDetection: (boolean | undefined)[] = []; - - const onDidInvoke = new Emitter(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - queueMicrotask(() => onDidInvoke.fire()); - commandDetection.push(request.enableCommandDetection); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - - if (count === 1) { - // FIRST call waits for cancellation - await raceCancellation(new Promise(() => { }), token); - } else { - await timeout(10); - } - - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - // const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - const p = Event.toPromise(onDidInvoke.event); - ctrl.run({ message: 'Hello-', autoSend: true }); - - await p; - - // assert.strictEqual(await p, undefined); - - // resend pending request without command detection - const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); - assertType(request); - const p2 = Event.toPromise(onDidInvoke.event); - const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.EditorInline }); - - await p2; - assert.strictEqual(await p3, undefined); - - assert.deepStrictEqual(commandDetection, [true, false]); - assert.strictEqual(model.getValue(), 'Hello-1'); - }); - - test('Re-run without after request is done', async function () { - - model.setValue(''); - - let count = 0; - const commandDetection: (boolean | undefined)[] = []; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - commandDetection.push(request.enableCommandDetection); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]); - return {}; - }, - })); - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: 'Hello-', autoSend: true }); - assert.strictEqual(await p, undefined); - - // resend pending request without command detection - const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1); - assertType(request); - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.EditorInline }); - - assert.strictEqual(await p2, undefined); - - assert.deepStrictEqual(commandDetection, [true, false]); - assert.strictEqual(model.getValue(), 'Hello-1'); - }); - - - test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () { - - model.setValue('two\none\n'); - - const attempts: (number | undefined)[] = []; - - const deferred = new DeferredPromise(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - attempts.push(request.attempt); - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }]); - await raceCancellation(deferred.p, token); - deferred.complete(); - await timeout(10); - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello-', autoSend: true }); - assert.strictEqual(await p, undefined); - await timeout(10); - assert.deepStrictEqual(attempts, [0]); - - // RERUN (cancel, undo, redo) - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const rerun = new RerunAction(); - await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor); - assert.strictEqual(await p2, undefined); - - assert.deepStrictEqual(attempts, [0, 1]); - - assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n'); - - }); - - test('Stopping/cancelling a request should NOT undo its changes', async function () { - - model.setValue('World'); - - const deferred = new DeferredPromise(); - let progress: ((parts: IChatProgress[]) => void) | undefined; - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, _progress, history, token) { - - progress = _progress; - await deferred.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello', autoSend: true }); - await timeout(10); - assert.strictEqual(await p, undefined); - - assertType(progress); - - const modelChange = new Promise(resolve => model.onDidChangeContent(() => resolve())); - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]); - - await modelChange; - assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed - - const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]); - chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionResource); - assert.strictEqual(await p2, undefined); - - assert.strictEqual(model.getValue(), 'HelloWorld'); // CANCEL just stops the request and progressive typing but doesn't undo - - }); - - test('Apply Edits from existing session w/ edits', async function () { - - model.setValue(''); - - const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(newSession); - - await (await chatService.sendRequest(newSession.chatModel.sessionResource, 'Existing', { location: ChatAgentLocation.EditorInline }))?.responseCreatedPromise; - - assert.strictEqual(newSession.chatModel.requestInProgress, true); - - const response = newSession.chatModel.lastRequest?.response; - assertType(response); - - await new Promise(resolve => { - if (response.isComplete) { - resolve(undefined); - } - const d = response.onDidChange(() => { - if (response.isComplete) { - d.dispose(); - resolve(undefined); - } - }); - }); - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE]); - ctrl.run({ existingSession: newSession }); - - assert.strictEqual(await p, undefined); - - assert.strictEqual(model.getValue(), 'Existing'); - - }); - - test('Undo on error (2 rounds)', async function () { - - return runWithFakedTimers({}, async () => { - - - store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, { - async invoke(request, progress, history, token) { - - progress([{ - kind: 'textEdit', - uri: model.uri, - edits: [{ - range: new Range(1, 1, 1, 1), - text: request.message - }] - }]); - - if (request.message === 'two') { - await timeout(100); // give edit a chance - return { - errorDetails: { message: 'FAILED' } - }; - } - return {}; - }, - })); - - model.setValue(''); - - // ROUND 1 - - ctrl = instaService.createInstance(TestController, editor); - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ autoSend: true, message: 'one' }); - assert.strictEqual(await p, undefined); - assert.strictEqual(model.getValue(), 'one'); - - - // ROUND 2 - - const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - const values = new Set(); - store.add(model.onDidChangeContent(() => values.add(model.getValue()))); - ctrl.chatWidget.acceptInput('two'); // WILL Trigger a failure - assert.strictEqual(await p2, undefined); - assert.strictEqual(model.getValue(), 'one'); // undone - assert.ok(values.has('twoone')); // we had but the change got undone - }); - }); - - test('Inline chat "discard" button does not always appear if response is stopped #228030', async function () { - - model.setValue('World'); - - const deferred = new DeferredPromise(); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]); - await deferred.p; - return {}; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]); - ctrl.run({ message: 'Hello', autoSend: true }); - - - assert.strictEqual(await p, undefined); - - const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]); - chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionResource); - assert.strictEqual(await p2, undefined); - - - const value = contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_RESPONSE_TYPE.key); - assert.notStrictEqual(value, InlineChatResponseType.None); - }); - - test('Restore doesn\'t edit on errored result', async function () { - return runWithFakedTimers({ useFakeTimers: true }, async () => { - - const model2 = store.add(instaService.get(IModelService).createModel('ABC', null)); - - model.setValue('World'); - - store.add(chatAgentService.registerDynamicAgent({ - id: 'testEditorAgent2', - ...agentData - }, { - async invoke(request, progress, history, token) { - - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello1' }] }]); - await timeout(100); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello2' }] }]); - await timeout(100); - progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello3' }] }]); - await timeout(100); - - return { - errorDetails: { message: 'FAILED' } - }; - }, - })); - - ctrl = instaService.createInstance(TestController, editor); - - // REQUEST 1 - const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]); - ctrl.run({ message: 'Hello', autoSend: true }); - - assert.strictEqual(await p, undefined); - - const p2 = ctrl.awaitStates([State.PAUSE]); - editor.setModel(model2); - assert.strictEqual(await p2, undefined); - - const p3 = ctrl.awaitStates([...TestController.INIT_SEQUENCE]); - editor.setModel(model); - assert.strictEqual(await p3, undefined); - - assert.strictEqual(model.getValue(), 'World'); - }); - }); -}); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts deleted file mode 100644 index 5972527290c..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatSession.test.ts +++ /dev/null @@ -1,593 +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 { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { Event } from '../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { IObservable, constObservable } from '../../../../../base/common/observable.js'; -import { assertType } from '../../../../../base/common/types.js'; -import { mock } from '../../../../../base/test/common/mock.js'; -import { assertSnapshot } from '../../../../../base/test/common/snapshot.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { IActiveCodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js'; -import { EditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { Position } from '../../../../../editor/common/core/position.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js'; -import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js'; -import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js'; -import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js'; -import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js'; -import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js'; -import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IViewDescriptorService } from '../../../../common/views.js'; -import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js'; -import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js'; -import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; -import { TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAccessibilityService, IChatWidgetService } from '../../../chat/browser/chat.js'; -import { ChatSessionsService } from '../../../chat/browser/chatSessions.contribution.js'; -import { ChatVariablesService } from '../../../chat/browser/chatVariables.js'; -import { ChatWidget, ChatWidgetService } from '../../../chat/browser/chatWidget.js'; -import { ChatAgentService, IChatAgentService } from '../../../chat/common/chatAgents.js'; -import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js'; -import { IChatRequestModel } from '../../../chat/common/chatModel.js'; -import { IChatService } from '../../../chat/common/chatService.js'; -import { ChatService } from '../../../chat/common/chatServiceImpl.js'; -import { IChatSessionsService } from '../../../chat/common/chatSessionsService.js'; -import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/chatSlashCommands.js'; -import { ChatTransferService, IChatTransferService } from '../../../chat/common/chatTransferService.js'; -import { IChatVariablesService } from '../../../chat/common/chatVariables.js'; -import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js'; -import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/chatWidgetHistoryService.js'; -import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js'; -import { ILanguageModelsService } from '../../../chat/common/languageModels.js'; -import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; -import { NullLanguageModelsService } from '../../../chat/test/common/languageModels.js'; -import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js'; -import { IMcpService } from '../../../mcp/common/mcpTypes.js'; -import { TestMcpService } from '../../../mcp/test/common/testMcpService.js'; -import { HunkState } from '../../browser/inlineChatSession.js'; -import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js'; -import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js'; -import { TestWorkerService } from './testWorkerService.js'; - -suite('InlineChatSession', function () { - - const store = new DisposableStore(); - let editor: IActiveCodeEditor; - let model: ITextModel; - let instaService: TestInstantiationService; - - let inlineChatSessionService: IInlineChatSessionService; - - setup(function () { - const contextKeyService = new MockContextKeyService(); - - - const serviceCollection = new ServiceCollection( - [IConfigurationService, new TestConfigurationService()], - [IChatVariablesService, new SyncDescriptor(ChatVariablesService)], - [ILogService, new NullLogService()], - [ITelemetryService, NullTelemetryService], - [IExtensionService, new TestExtensionService()], - [IContextKeyService, new MockContextKeyService()], - [IViewsService, new TestExtensionService()], - [IWorkspaceContextService, new TestContextService()], - [IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)], - [IChatWidgetService, new SyncDescriptor(ChatWidgetService)], - [IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)], - [IChatTransferService, new SyncDescriptor(ChatTransferService)], - [IChatSessionsService, new SyncDescriptor(ChatSessionsService)], - [IChatService, new SyncDescriptor(ChatService)], - [IEditorWorkerService, new SyncDescriptor(TestWorkerService)], - [IChatAgentService, new SyncDescriptor(ChatAgentService)], - [IContextKeyService, contextKeyService], - [IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)], - [ILanguageModelsService, new SyncDescriptor(NullLanguageModelsService)], - [IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)], - [ICommandService, new SyncDescriptor(TestCommandService)], - [ILanguageModelToolsService, new MockLanguageModelToolsService()], - [IMcpService, new TestMcpService()], - [IEditorProgressService, new class extends mock() { - override show(total: unknown, delay?: unknown): IProgressRunner { - return { - total() { }, - worked(value) { }, - done() { }, - }; - } - }], - [IChatEditingService, new class extends mock() { - override editingSessionsObs: IObservable = constObservable([]); - }], - [IChatAccessibilityService, new class extends mock() { - override acceptResponse(chatWidget: ChatWidget, container: HTMLElement, response: IChatResponseViewModel | undefined, requestId: number): void { } - override acceptRequest(): number { return -1; } - override acceptElicitation(): void { } - }], - [IAccessibleViewService, new class extends mock() { - override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { - return null; - } - }], - [IConfigurationService, new TestConfigurationService()], - [IViewDescriptorService, new class extends mock() { - override onDidChangeLocation = Event.None; - }], - [IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()] - ); - - - - instaService = store.add(workbenchInstantiationService(undefined, store).createChild(serviceCollection)); - inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService)); - store.add(instaService.get(IChatSessionsService) as ChatSessionsService); // Needs to be disposed in between test runs to clear extensionPoint contribution - - instaService.get(IChatAgentService).registerDynamicAgent({ - extensionId: nullExtensionDescription.identifier, - extensionVersion: undefined, - publisherDisplayName: '', - extensionDisplayName: '', - extensionPublisherId: '', - id: 'testAgent', - name: 'testAgent', - isDefault: true, - locations: [ChatAgentLocation.EditorInline], - modes: [ChatModeKind.Ask], - metadata: {}, - slashCommands: [], - disambiguation: [], - }, { - async invoke() { - return {}; - } - }); - - - store.add(instaService.get(IEditorWorkerService) as TestWorkerService); - model = store.add(instaService.get(IModelService).createModel('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven', null)); - editor = store.add(instantiateTestCodeEditor(instaService, model)); - }); - - teardown(function () { - store.clear(); - }); - - ensureNoDisposablesAreLeakedInTestSuite(); - - async function makeEditAsAi(edit: EditOperation | EditOperation[]) { - const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri); - assertType(session); - session.hunkData.ignoreTextModelNChanges = true; - try { - editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]); - } finally { - session.hunkData.ignoreTextModelNChanges = false; - } - await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }); - } - - function makeEdit(edit: EditOperation | EditOperation[]) { - editor.executeEdits('test', Array.isArray(edit) ? edit : [edit]); - } - - test('Create, release', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, info', async function () { - - const decorationCountThen = model.getAllDecorations().length; - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - assert.ok(session.textModelN === model); - - await makeEditAsAi(EditOperation.insert(new Position(1, 1), 'AI_EDIT\n')); - - - assert.strictEqual(session.hunkData.size, 1); - let [hunk] = session.hunkData.getInfo(); - assertType(hunk); - - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - assert.strictEqual(hunk.getState(), HunkState.Pending); - assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 8 })); - - await makeEditAsAi(EditOperation.insert(new Position(1, 3), 'foobar')); - [hunk] = session.hunkData.getInfo(); - assert.ok(hunk.getRangesN()[0].equalsRange({ startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 14 })); - - inlineChatSessionService.releaseSession(session); - - assert.strictEqual(model.getAllDecorations().length, decorationCountThen); // no leaked decorations! - }); - - test('HunkData, accept', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - for (const hunk of session.hunkData.getInfo()) { - assertType(hunk); - assert.strictEqual(hunk.getState(), HunkState.Pending); - hunk.acceptChanges(); - assert.strictEqual(hunk.getState(), HunkState.Accepted); - } - - assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue()); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, reject', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - for (const hunk of session.hunkData.getInfo()) { - assertType(hunk); - assert.strictEqual(hunk.getState(), HunkState.Pending); - hunk.discardChanges(); - assert.strictEqual(hunk.getState(), HunkState.Rejected); - } - - assert.strictEqual(session.textModel0.getValue(), session.textModelN.getValue()); - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, N rounds', async function () { - - model.setValue('one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\ntwelwe\nthirteen\nfourteen\nfifteen\nsixteen\nseventeen\neighteen\nnineteen\n'); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - assert.ok(session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - assert.strictEqual(session.hunkData.size, 0); - - // ROUND #1 - await makeEditAsAi([ - EditOperation.insert(new Position(1, 1), 'AI1'), - EditOperation.insert(new Position(4, 1), 'AI2'), - EditOperation.insert(new Position(19, 1), 'AI3') - ]); - - assert.strictEqual(session.hunkData.size, 2); // AI1, AI2 are merged into one hunk, AI3 is a separate hunk - - let [first, second] = session.hunkData.getInfo(); - - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(!session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - first.acceptChanges(); - assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI1')); - assert.ok(session.textModel0.getValueInRange(first.getRangesN()[0]).includes('AI2')); - assert.ok(!session.textModel0.getValueInRange(second.getRangesN()[0]).includes('AI3')); - - - // ROUND #2 - await makeEditAsAi([ - EditOperation.insert(new Position(7, 1), 'AI4'), - ]); - assert.strictEqual(session.hunkData.size, 2); - - [first, second] = session.hunkData.getInfo(); - assert.ok(model.getValueInRange(first.getRangesN()[0]).includes('AI4')); // the new hunk (in line-order) - assert.ok(model.getValueInRange(second.getRangesN()[0]).includes('AI3')); // the previous hunk remains - - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, (mirror) edit before', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(1, 1, 1, 4), 'ONE')]); - assert.strictEqual(session.textModelN.getValue(), ['ONE', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['ONE', 'two', 'three'].join('\n')); - }); - - test('HunkData, (mirror) edit after', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - const [hunk] = session.hunkData.getInfo(); - - makeEdit([EditOperation.insert(new Position(1, 1), 'USER1')]); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'four', 'five'].join('\n')); - - makeEdit([EditOperation.insert(new Position(5, 1), 'USER2')]); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'three', 'USER2four', 'five'].join('\n')); - - hunk.acceptChanges(); - assert.strictEqual(session.textModelN.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['USER1one', 'two', 'AI_EDIT', 'three', 'USER2four', 'five'].join('\n')); - }); - - test('HunkData, (mirror) edit inside ', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(3, 4, 3, 7), 'wwaaassss')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI wwaaassss HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three'].join('\n')); - }); - - test('HunkData, (mirror) edit after dicard ', async function () { - - const lines = ['one', 'two', 'three']; - model.setValue(lines.join('\n')); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI WAS HERE\n')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI WAS HERE', 'three'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - assert.strictEqual(session.hunkData.size, 1); - const [hunk] = session.hunkData.getInfo(); - hunk.discardChanges(); - assert.strictEqual(session.textModelN.getValue(), lines.join('\n')); - assert.strictEqual(session.textModel0.getValue(), lines.join('\n')); - - makeEdit([EditOperation.replace(new Range(3, 4, 3, 6), '3333')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'thr3333'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'thr3333'].join('\n')); - }); - - test('HunkData, (mirror) edit after, multi turn', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - - makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - await makeEditAsAi([EditOperation.insert(new Position(2, 4), ' zwei')]); - assert.strictEqual(session.hunkData.size, 1); - - assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two zwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - }); - - test('HunkData, (mirror) edit after, multi turn 2', async function () { - - const lines = ['one', 'two', 'three', 'four', 'five']; - model.setValue(lines.join('\n')); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(3, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 1); - - makeEdit([EditOperation.insert(new Position(5, 1), 'FOO')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'two', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - await makeEditAsAi([EditOperation.insert(new Position(2, 4), 'zwei')]); - assert.strictEqual(session.hunkData.size, 1); - - assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'five'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'five'].join('\n')); - - makeEdit([EditOperation.replace(new Range(6, 3, 6, 5), 'vefivefi')]); - assert.strictEqual(session.textModelN.getValue(), ['one', 'twozwei', 'AI_EDIT', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - assert.strictEqual(session.textModel0.getValue(), ['one', 'two', 'three', 'FOOfour', 'fivefivefi'].join('\n')); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - makeEdit([EditOperation.replace(new Range(1, 1, 1, 1), 'done')]); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - }); - - test('HunkData, accept, discardAll', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - const textModeNNow = session.textModelN.getValue(); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - session.hunkData.discardAll(); // all remaining - assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - inlineChatSessionService.releaseSession(session); - }); - - test('HunkData, discardAll return undo edits', async function () { - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.insert(new Position(1, 1), 'AI_EDIT\n'), EditOperation.insert(new Position(10, 1), 'AI_EDIT\n')]); - - assert.strictEqual(session.hunkData.size, 2); - assert.ok(!session.textModel0.equalsTextBuffer(session.textModelN.getTextBuffer())); - - const textModeNNow = session.textModelN.getValue(); - - session.hunkData.getInfo()[0].acceptChanges(); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - const undoEdits = session.hunkData.discardAll(); // all remaining - assert.strictEqual(session.textModelN.getValue(), 'AI_EDIT\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven'); - assert.strictEqual(session.textModelN.getValue(), session.textModel0.getValue()); - - // undo the discards - session.textModelN.pushEditOperations(null, undoEdits, () => null); - assert.strictEqual(textModeNNow, session.textModelN.getValue()); - - inlineChatSessionService.releaseSession(session); - }); - - test('Pressing Escape after inline chat errored with "response filtered" leaves document dirty #7764', async function () { - - const origValue = `class Foo { - private onError(error: string): void { - if (/The request timed out|The network connection was lost/i.test(error)) { - return; - } - - error = error.replace(/See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information'); - - this.notificationService.notify({ - severity: Severity.Error, - message: error, - source: nls.localize('update service', "Update Service"), - }); - } -}`; - model.setValue(origValue); - - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - const fakeRequest = new class extends mock() { - override get id() { return 'one'; } - }; - session.markModelVersion(fakeRequest); - - assert.strictEqual(editor.getModel().getLineCount(), 15); - - await makeEditAsAi([EditOperation.replace(new Range(7, 1, 7, Number.MAX_SAFE_INTEGER), `error = error.replace( - /See https:\/\/github\.com\/Squirrel\/Squirrel\.Mac\/issues\/182 for more information/, - 'This might mean the application was put on quarantine by macOS. See [this link](https://github.com/microsoft/vscode/issues/7426#issuecomment-425093469) for more information' - );`)]); - - assert.strictEqual(editor.getModel().getLineCount(), 18); - - // called when a response errors out - await session.undoChangesUntil(fakeRequest.id); - await session.hunkData.recompute({ applied: 0, sha1: 'fakeSha1' }, undefined); - - assert.strictEqual(editor.getModel().getValue(), origValue); - - session.hunkData.discardAll(); // called when dimissing the session - assert.strictEqual(editor.getModel().getValue(), origValue); - }); - - test('Apply Code\'s preview should be easier to undo/esc #7537', async function () { - model.setValue(`export function fib(n) { - if (n <= 0) return 0; - if (n === 1) return 0; - if (n === 2) return 1; - return fib(n - 1) + fib(n - 2); -}`); - const session = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None); - assertType(session); - - await makeEditAsAi([EditOperation.replace(new Range(5, 1, 6, Number.MAX_SAFE_INTEGER), ` - let a = 0, b = 1, c; - for (let i = 3; i <= n; i++) { - c = a + b; - a = b; - b = c; - } - return b; -}`)]); - - assert.strictEqual(session.hunkData.size, 1); - assert.strictEqual(session.hunkData.pending, 1); - assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Pending)); - - await assertSnapshot(editor.getModel().getValue(), { name: '1' }); - - await model.undo(); - await assertSnapshot(editor.getModel().getValue(), { name: '2' }); - - // overlapping edits (even UNDO) mark edits as accepted - assert.strictEqual(session.hunkData.size, 1); - assert.strictEqual(session.hunkData.pending, 0); - assert.ok(session.hunkData.getInfo().every(d => d.getState() === HunkState.Accepted)); - - // no further change when discarding - session.hunkData.discardAll(); // CANCEL - await assertSnapshot(editor.getModel().getValue(), { name: '2' }); - }); - -}); diff --git a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts b/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts deleted file mode 100644 index df51a99ed0d..00000000000 --- a/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatStrategies.test.ts +++ /dev/null @@ -1,75 +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 { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { IntervalTimer } from '../../../../../base/common/async.js'; -import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { asProgressiveEdit } from '../../browser/utils.js'; -import assert from 'assert'; - - -suite('AsyncEdit', () => { - - ensureNoDisposablesAreLeakedInTestSuite(); - - test('asProgressiveEdit', async () => { - const interval = new IntervalTimer(); - const edit = { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: 'Hello, world!' - }; - - const cts = new CancellationTokenSource(); - const result = asProgressiveEdit(interval, edit, 5, cts.token); - - // Verify the range - assert.deepStrictEqual(result.range, edit.range); - - const iter = result.newText[Symbol.asyncIterator](); - - // Verify the newText - const a = await iter.next(); - assert.strictEqual(a.value, 'Hello,'); - assert.strictEqual(a.done, false); - - // Verify the next word - const b = await iter.next(); - assert.strictEqual(b.value, ' world!'); - assert.strictEqual(b.done, false); - - const c = await iter.next(); - assert.strictEqual(c.value, undefined); - assert.strictEqual(c.done, true); - - cts.dispose(); - }); - - test('asProgressiveEdit - cancellation', async () => { - const interval = new IntervalTimer(); - const edit = { - range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }, - text: 'Hello, world!' - }; - - const cts = new CancellationTokenSource(); - const result = asProgressiveEdit(interval, edit, 5, cts.token); - - // Verify the range - assert.deepStrictEqual(result.range, edit.range); - - const iter = result.newText[Symbol.asyncIterator](); - - // Verify the newText - const a = await iter.next(); - assert.strictEqual(a.value, 'Hello,'); - assert.strictEqual(a.done, false); - - cts.dispose(true); - - const c = await iter.next(); - assert.strictEqual(c.value, undefined); - assert.strictEqual(c.done, true); - }); -}); diff --git a/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts b/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts new file mode 100644 index 00000000000..2ac90546d65 --- /dev/null +++ b/src/vs/workbench/contrib/inlineCompletions/browser/renameSymbolTrackerService.ts @@ -0,0 +1,316 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, derived, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; +import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IRenameSymbolTrackerService, type ITrackedWord } from '../../../../editor/browser/services/renameSymbolTrackerService.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { Range } from '../../../../editor/common/core/range.js'; +import { StandardTokenType } from '../../../../editor/common/encodedTokenAttributes.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; +import { TextModelEditSource } from '../../../../editor/common/textModelEditSource.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; + +/** + * Checks if a model content change event was caused only by typing or pasting. + * Returns false for AI edits, refactorings, undo/redo, etc. + */ +function isUserEdit(event: IModelContentChangedEvent): boolean { + if (event.isUndoing || event.isRedoing || event.isFlush) { + return false; + } + + for (const source of event.detailedReasons) { + if (!isUserEditSource(source)) { + return false; + } + } + + return event.detailedReasons.length > 0; +} + +const userEditKinds = new Set(['type', 'paste', 'cut', 'executeCommands', 'executeCommand', 'compositionType', 'compositionEnd']); +function isUserEditSource(source: TextModelEditSource): boolean { + const metadata = source.metadata; + if (metadata.source !== 'cursor') { + return false; + } + const kind = metadata.kind; + return userEditKinds.has(kind); +} + +type WordState = { + word: string; + range: Range; + position: Position; +}; + +/** + * Tracks symbol edits for a single ITextModel. + * + * Receives cursor position updates from external sources (e.g., focused code editors). + * Only tracks edits done by typing or paste. Resets when: + * - A non-typing/paste edit occurs (AI, refactoring, undo/redo, etc.) + */ +class ModelSymbolRenameTracker extends Disposable { + private readonly _trackedWord = observableValue(this, undefined); + public readonly trackedWord: IObservable = this._trackedWord; + + private _capturedWord: WordState | undefined = undefined; + private _lastWordBeforeEdit: WordState | undefined = undefined; + private _pendingContentChange: boolean = false; + private _lastCursorPosition: Position | undefined = undefined; + + constructor( + private readonly _model: ITextModel + ) { + super(); + + // Listen to content changes - only reset on non-typing/paste edits + this._register(this._model.onDidChangeContent(e => { + if (!isUserEdit(e)) { + // Non-user edit has occurred - reset rename tracking at + // the current cursor position (if any) + const position = this._lastCursorPosition; + this.reset(); + if (position !== undefined) { + this.updateCursorPosition(position); + } + return; + } + // Valid typing/paste edit - mark that content changed, cursor update will handle tracking + this._pendingContentChange = true; + })); + } + + /** + * Called by the service when the cursor position changes in an editor showing this model. + * Updates tracking based on the word under cursor and whether content has changed. + */ + public updateCursorPosition(position: Position): void { + this._lastCursorPosition = position; + const wordAtPosition = this._model.getWordAtPosition(position); + if (!wordAtPosition) { + // Not on a word - just clear lastWordBeforeEdit + this._lastWordBeforeEdit = undefined; + this._pendingContentChange = false; + return; + } + + // Check if the position is in a comment + if (this._isPositionInComment(position)) { + this._lastWordBeforeEdit = undefined; + this._pendingContentChange = false; + return; + } + + const currentWord: WordState = { + word: wordAtPosition.word, + range: new Range( + position.lineNumber, + wordAtPosition.startColumn, + position.lineNumber, + wordAtPosition.endColumn + ), + position + }; + + const contentChanged = this._pendingContentChange; + this._pendingContentChange = false; + + if (!contentChanged) { + // Just cursor movement - remember this word for later + this._lastWordBeforeEdit = currentWord; + return; + } + + // Content changed - update tracking + if (!this._capturedWord) { + // First edit on a word - use the word from before the edit as original + const originalWord = this._lastWordBeforeEdit ?? currentWord; + this._capturedWord = { ...originalWord }; + this._trackedWord.set({ + model: this._model, + originalWord: originalWord.word, + originalPosition: originalWord.position, + originalRange: originalWord.range, + currentWord: currentWord.word, + currentRange: currentWord.range, + }, undefined); + this._lastWordBeforeEdit = currentWord; + return; + } + + const capturedWord = this._capturedWord; + // Check if we're still on the same word (by position overlap or adjacency) + const isOnSameWord = this._rangesOverlap(capturedWord.range, currentWord.range) || + this._isAdjacent(capturedWord.range, currentWord.range); + + if (isOnSameWord) { + // Word has been edited - update the tracked word + this._trackedWord.set({ + model: this._model, + originalWord: capturedWord.word, + originalPosition: capturedWord.position, + originalRange: capturedWord.range, + currentWord: currentWord.word, + currentRange: currentWord.range, + }, undefined); + } else { + // User started typing in a different word - use the word from before the edit as original + const originalWord = this._lastWordBeforeEdit ?? currentWord; + this._capturedWord = { ...originalWord }; + this._trackedWord.set({ + model: this._model, + originalWord: originalWord.word, + originalPosition: originalWord.position, + originalRange: originalWord.range, + currentWord: currentWord.word, + currentRange: currentWord.range, + }, undefined); + } + // Update lastWordBeforeEdit for the next iteration + this._lastWordBeforeEdit = currentWord; + } + + private reset(): void { + this._trackedWord.set(undefined, undefined); + this._capturedWord = undefined; + this._lastWordBeforeEdit = undefined; + this._pendingContentChange = false; + this._lastCursorPosition = undefined; + } + + private _isPositionInComment(position: Position): boolean { + this._model.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = this._model.tokenization.getLineTokens(position.lineNumber); + const tokenIndex = tokens.findTokenIndexAtOffset(position.column - 1); + const tokenType = tokens.getStandardTokenType(tokenIndex); + return tokenType === StandardTokenType.Comment; + } + + private _rangesOverlap(a: Range, b: Range): boolean { + if (a.startLineNumber !== b.startLineNumber) { + return false; + } + return !(a.endColumn < b.startColumn || b.endColumn < a.startColumn); + } + + private _isAdjacent(a: Range, b: Range): boolean { + if (a.startLineNumber !== b.startLineNumber) { + return false; + } + return a.endColumn === b.startColumn || b.endColumn === a.startColumn; + } +} + +class RenameSymbolTrackerService extends Disposable implements IRenameSymbolTrackerService { + public _serviceBrand: undefined; + + private readonly _modelTrackers = new Map(); + private readonly _editorFocusTrackingDisposables = new Map(); + + private readonly _focusedModelTracker = observableValue(this, undefined); + + public readonly trackedWord: IObservable = derived(this, reader => { + const tracker = this._focusedModelTracker.read(reader); + return tracker?.trackedWord.read(reader); + }); + + constructor( + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IModelService private readonly _modelService: IModelService + ) { + super(); + + // Setup tracking for existing editors + for (const editor of this._codeEditorService.listCodeEditors()) { + this._setupEditorTracking(editor); + } + + // Track editor additions + this._register(this._codeEditorService.onCodeEditorAdd(editor => { + this._setupEditorTracking(editor); + })); + + // Clean up editor focus tracking when editors are removed + this._register(this._codeEditorService.onCodeEditorRemove(editor => { + const focusDisposable = this._editorFocusTrackingDisposables.get(editor); + if (focusDisposable) { + focusDisposable.dispose(); + this._editorFocusTrackingDisposables.delete(editor); + } + })); + + // Clean up model trackers when models are removed + this._register(this._modelService.onModelRemoved(model => { + const tracker = this._modelTrackers.get(model); + if (tracker) { + tracker.dispose(); + this._modelTrackers.delete(model); + } + })); + } + + private _setupEditorTracking(editor: ICodeEditor): void { + if (editor.isSimpleWidget) { + return; + } + + // Setup focus and cursor tracking + if (!this._editorFocusTrackingDisposables.has(editor)) { + const obsEditor = observableCodeEditor(editor); + + const focusDisposable = autorun(reader => { + /** @description track focused editor and forward cursor to model tracker */ + const isFocused = obsEditor.isFocused.read(reader); + const model = obsEditor.model.read(reader); + const cursorPosition = obsEditor.cursorPosition.read(reader); + + if (!isFocused || !model) { + return; + } + + // Ensure we have a tracker for this model + let tracker = this._modelTrackers.get(model); + if (!tracker) { + tracker = new ModelSymbolRenameTracker(model); + this._modelTrackers.set(model, tracker); + } + + // Update the focused tracker + if (this._focusedModelTracker.read(undefined) !== tracker) { + this._focusedModelTracker.set(tracker, undefined); + } + + // Forward cursor position to the model tracker + if (cursorPosition) { + tracker.updateCursorPosition(cursorPosition); + } + }); + + this._editorFocusTrackingDisposables.set(editor, focusDisposable); + } + } + + override dispose(): void { + for (const tracker of this._modelTrackers.values()) { + tracker.dispose(); + } + this._modelTrackers.clear(); + for (const disposable of this._editorFocusTrackingDisposables.values()) { + disposable.dispose(); + } + this._editorFocusTrackingDisposables.clear(); + super.dispose(); + } +} + +registerSingleton(IRenameSymbolTrackerService, RenameSymbolTrackerService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index caa2ab618cc..9a7466a8722 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -174,7 +174,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, true, { cellToolbarInteraction: 'hover', globalToolbar: true, stickyScrollEnabled: false, dragAndDropEnabled: false, disableRulers: true }); this._editorMemento = this.getEditorMemento(editorGroupService, textResourceConfigurationService, INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY); - codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {}); + this._register(codeEditorService.registerDecorationType('interactive-decoration', DECORATION_KEY, {})); this._register(this._keybindingService.onDidUpdateKeybindings(this._updateInputHint, this)); this._register(this._notebookExecutionStateService.onDidChangeExecution((e) => { if (e.type === NotebookExecutionType.cell && isEqual(e.notebook, this._notebookWidget.value?.viewModel?.notebookDocument.uri)) { @@ -296,7 +296,7 @@ export class InteractiveEditor extends EditorPane implements IEditorPaneWithScro bottom: INPUT_EDITOR_PADDING }, hover: { - enabled: true + enabled: 'on' as const }, rulers: [] } diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts b/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts index 0bbd8acf09a..139ef875c36 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterModel.ts @@ -7,13 +7,18 @@ import { mainWindow } from '../../../../base/browser/window.js'; import { isRemoteDiagnosticError, SystemInfo } from '../../../../platform/diagnostics/common/diagnostics.js'; import { ISettingSearchResult, IssueReporterExtensionData, IssueType } from '../common/issue.js'; +interface VersionInfo { + vscodeVersion: string; + os: string; +} + export interface IssueReporterData { issueType: IssueType; issueDescription?: string; issueTitle?: string; extensionData?: string; - versionInfo?: any; + versionInfo?: VersionInfo; systemInfo?: SystemInfo; systemInfoWeb?: string; processInfo?: string; diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css index bc997b189eb..d24fe259aa8 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporter.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporter.css @@ -80,9 +80,7 @@ .issue-reporter-body .monaco-text-button { display: block; width: auto; - padding: 4px 10px; align-self: flex-end; - font-size: 13px; } .issue-reporter-body .monaco-button-dropdown { @@ -603,10 +601,6 @@ body.issue-reporter-body { line-height: 15px; /* approximate button height for vertical centering */ } -.issue-reporter-body .internal-elements .monaco-text-button { - font-size: 10px; - padding: 2px 8px; -} .issue-reporter-body .internal-elements #show-private-repo-name { align-self: flex-end; diff --git a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts index 729a6932d6e..1e6a0cb4184 100644 --- a/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts +++ b/src/vs/workbench/contrib/languageDetection/browser/languageDetection.contribution.ts @@ -88,12 +88,10 @@ class LanguageDetectionStatusContribution implements IWorkbenchContribution { const existing = editorModel.getLanguageId(); if (lang && lang !== existing && skip[existing] !== lang) { const detectedName = this._languageService.getLanguageName(lang) || lang; - let tooltip = localize('status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); - const keybinding = this._keybindingService.lookupKeybinding(detectLanguageCommandId); - const label = keybinding?.getLabel(); - if (label) { - tooltip += ` (${label})`; - } + const tooltip = this._keybindingService.appendKeybinding( + localize('status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName), + detectLanguageCommandId + ); const props: IStatusbarEntry = { name: localize('langDetection.name', "Language Detection"), diff --git a/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts b/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts index 1df2bb82a03..40bee5caa19 100644 --- a/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts +++ b/src/vs/workbench/contrib/list/browser/tableColumnResizeQuickPick.ts @@ -15,7 +15,7 @@ interface IColumnResizeQuickPickItem extends IQuickPickItem { export class TableColumnResizeQuickPick extends Disposable { constructor( - private readonly _table: Table, + private readonly _table: Table, @IQuickInputService private readonly _quickInputService: IQuickInputService, ) { super(); diff --git a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts b/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts deleted file mode 100644 index 4d6ba9f5e01..00000000000 --- a/src/vs/workbench/contrib/logs/common/defaultLogLevels.ts +++ /dev/null @@ -1,178 +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 { ILogService, ILoggerService, LogLevel, LogLevelToString, getLogLevel, parseLogLevel } from '../../../../platform/log/common/log.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { FileOperationResult, IFileService, toFileOperationResult } from '../../../../platform/files/common/files.js'; -import { IJSONEditingService } from '../../../services/configuration/common/jsonEditing.js'; -import { isString, isUndefined } from '../../../../base/common/types.js'; -import { EXTENSION_IDENTIFIER_WITH_LOG_REGEX } from '../../../../platform/environment/common/environmentService.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { parse } from '../../../../base/common/json.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; - -interface ParsedArgvLogLevels { - default?: LogLevel; - extensions?: [string, LogLevel][]; -} - -export type DefaultLogLevels = Required>; - -export const IDefaultLogLevelsService = createDecorator('IDefaultLogLevelsService'); - -export interface IDefaultLogLevelsService { - - readonly _serviceBrand: undefined; - - /** - * An event which fires when default log levels are changed - */ - readonly onDidChangeDefaultLogLevels: Event; - - getDefaultLogLevels(): Promise; - - getDefaultLogLevel(extensionId?: string): Promise; - - setDefaultLogLevel(logLevel: LogLevel, extensionId?: string): Promise; -} - -class DefaultLogLevelsService extends Disposable implements IDefaultLogLevelsService { - - _serviceBrand: undefined; - - private _onDidChangeDefaultLogLevels = this._register(new Emitter); - readonly onDidChangeDefaultLogLevels = this._onDidChangeDefaultLogLevels.event; - - constructor( - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IFileService private readonly fileService: IFileService, - @IJSONEditingService private readonly jsonEditingService: IJSONEditingService, - @ILogService private readonly logService: ILogService, - @ILoggerService private readonly loggerService: ILoggerService, - ) { - super(); - } - - async getDefaultLogLevels(): Promise { - const argvLogLevel = await this._parseLogLevelsFromArgv(); - return { - default: argvLogLevel?.default ?? this._getDefaultLogLevelFromEnv(), - extensions: argvLogLevel?.extensions ?? this._getExtensionsDefaultLogLevelsFromEnv() - }; - } - - async getDefaultLogLevel(extensionId?: string): Promise { - const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; - if (extensionId) { - extensionId = extensionId.toLowerCase(); - return this._getDefaultLogLevel(argvLogLevel, extensionId); - } else { - return this._getDefaultLogLevel(argvLogLevel); - } - } - - async setDefaultLogLevel(defaultLogLevel: LogLevel, extensionId?: string): Promise { - const argvLogLevel = await this._parseLogLevelsFromArgv() ?? {}; - if (extensionId) { - extensionId = extensionId.toLowerCase(); - const currentDefaultLogLevel = this._getDefaultLogLevel(argvLogLevel, extensionId); - argvLogLevel.extensions = argvLogLevel.extensions ?? []; - const extension = argvLogLevel.extensions.find(([extension]) => extension === extensionId); - if (extension) { - extension[1] = defaultLogLevel; - } else { - argvLogLevel.extensions.push([extensionId, defaultLogLevel]); - } - await this._writeLogLevelsToArgv(argvLogLevel); - const extensionLoggers = [...this.loggerService.getRegisteredLoggers()].filter(logger => logger.extensionId && logger.extensionId.toLowerCase() === extensionId); - for (const { resource } of extensionLoggers) { - if (this.loggerService.getLogLevel(resource) === currentDefaultLogLevel) { - this.loggerService.setLogLevel(resource, defaultLogLevel); - } - } - } else { - const currentLogLevel = this._getDefaultLogLevel(argvLogLevel); - argvLogLevel.default = defaultLogLevel; - await this._writeLogLevelsToArgv(argvLogLevel); - if (this.loggerService.getLogLevel() === currentLogLevel) { - this.loggerService.setLogLevel(defaultLogLevel); - } - } - this._onDidChangeDefaultLogLevels.fire(); - } - - private _getDefaultLogLevel(argvLogLevels: ParsedArgvLogLevels, extension?: string): LogLevel { - if (extension) { - const extensionLogLevel = argvLogLevels.extensions?.find(([extensionId]) => extensionId === extension); - if (extensionLogLevel) { - return extensionLogLevel[1]; - } - } - return argvLogLevels.default ?? getLogLevel(this.environmentService); - } - - private async _writeLogLevelsToArgv(logLevels: ParsedArgvLogLevels): Promise { - const logLevelsValue: string[] = []; - if (!isUndefined(logLevels.default)) { - logLevelsValue.push(LogLevelToString(logLevels.default)); - } - for (const [extension, logLevel] of logLevels.extensions ?? []) { - logLevelsValue.push(`${extension}=${LogLevelToString(logLevel)}`); - } - await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['log-level'], value: logLevelsValue.length ? logLevelsValue : undefined }], true); - } - - private async _parseLogLevelsFromArgv(): Promise { - const result: ParsedArgvLogLevels = { extensions: [] }; - const logLevels = await this._readLogLevelsFromArgv(); - for (const extensionLogLevel of logLevels) { - const matches = EXTENSION_IDENTIFIER_WITH_LOG_REGEX.exec(extensionLogLevel); - if (matches && matches[1] && matches[2]) { - const logLevel = parseLogLevel(matches[2]); - if (!isUndefined(logLevel)) { - result.extensions?.push([matches[1].toLowerCase(), logLevel]); - } - } else { - const logLevel = parseLogLevel(extensionLogLevel); - if (!isUndefined(logLevel)) { - result.default = logLevel; - } - } - } - return !isUndefined(result.default) || result.extensions?.length ? result : undefined; - } - - private async _readLogLevelsFromArgv(): Promise { - try { - const content = await this.fileService.readFile(this.environmentService.argvResource); - const argv: { 'log-level'?: string | string[] } = parse(content.value.toString()); - return isString(argv['log-level']) ? [argv['log-level']] : Array.isArray(argv['log-level']) ? argv['log-level'] : []; - } catch (error) { - if (toFileOperationResult(error) !== FileOperationResult.FILE_NOT_FOUND) { - this.logService.error(error); - } - } - return []; - } - - private _getDefaultLogLevelFromEnv(): LogLevel { - return getLogLevel(this.environmentService); - } - - private _getExtensionsDefaultLogLevelsFromEnv(): [string, LogLevel][] { - const result: [string, LogLevel][] = []; - for (const [extension, logLevelValue] of this.environmentService.extensionLogLevel ?? []) { - const logLevel = parseLogLevel(logLevelValue); - if (!isUndefined(logLevel)) { - result.push([extension, logLevel]); - } - } - return result; - } -} - -registerSingleton(IDefaultLogLevelsService, DefaultLogLevelsService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index 9326a7bd421..f6df64c81a7 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -16,11 +16,11 @@ import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js' import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Event } from '../../../../base/common/event.js'; import { windowLogId, showWindowLogActionId } from '../../../services/log/common/logConstants.js'; -import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { CounterSet } from '../../../../base/common/map.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { Schemas } from '../../../../base/common/network.js'; +import { IDefaultLogLevelsService } from '../../../services/log/common/defaultLogLevels.js'; registerAction2(class extends Action2 { constructor() { diff --git a/src/vs/workbench/contrib/logs/common/logsActions.ts b/src/vs/workbench/contrib/logs/common/logsActions.ts index 23bf63df891..bd1e37011b4 100644 --- a/src/vs/workbench/contrib/logs/common/logsActions.ts +++ b/src/vs/workbench/contrib/logs/common/logsActions.ts @@ -13,10 +13,10 @@ import { IWorkbenchEnvironmentService } from '../../../services/environment/comm import { dirname, basename, isEqual } from '../../../../base/common/resources.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IOutputChannelDescriptor, IOutputService, isMultiSourceOutputChannelDescriptor, isSingleSourceOutputChannelDescriptor } from '../../../services/output/common/output.js'; -import { IDefaultLogLevelsService } from './defaultLogLevels.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { IDefaultLogLevelsService } from '../../../services/log/common/defaultLogLevels.js'; type LogLevelQuickPickItem = IQuickPickItem & { level: LogLevel }; type LogChannelQuickPickItem = IQuickPickItem & { id: string; channel: IOutputChannelDescriptor }; @@ -47,7 +47,7 @@ export class SetLogLevelAction extends Action { } private async selectLogLevelOrChannel(): Promise { - const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); + const defaultLogLevels = this.defaultLogLevelsService.defaultLogLevels; const extensionLogs: LogChannelQuickPickItem[] = [], logs: LogChannelQuickPickItem[] = []; const logLevel = this.loggerService.getLogLevel(); for (const channel of this.outputService.getChannelDescriptors()) { @@ -105,7 +105,7 @@ export class SetLogLevelAction extends Action { } private async setLogLevelForChannel(logChannel: LogChannelQuickPickItem): Promise { - const defaultLogLevels = await this.defaultLogLevelsService.getDefaultLogLevels(); + const defaultLogLevels = this.defaultLogLevelsService.defaultLogLevels; const defaultLogLevel = defaultLogLevels.extensions.find(e => e[0] === logChannel.channel.extensionId?.toLowerCase())?.[1] ?? defaultLogLevels.default; const entries = this.getLogLevelEntries(defaultLogLevel, this.outputService.getLogLevel(logChannel.channel) ?? defaultLogLevel, !!logChannel.channel.extensionId); diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index 5cf78c4d475..24d5f8d2107 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -20,9 +20,9 @@ export class SimpleSettingRenderer { private readonly codeSettingAnchorRegex: RegExp; private readonly codeSettingSimpleRegex: RegExp; - private _updatedSettings = new Map(); // setting ID to user's original setting value + private _updatedSettings = new Map(); // setting ID to user's original setting value private _encounteredSettings = new Map(); // setting ID to setting - private _featuredSettings = new Map(); // setting ID to feature value + private _featuredSettings = new Map(); // setting ID to feature value constructor( @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -87,7 +87,7 @@ export class SimpleSettingRenderer { }; } - settingToUriString(settingId: string, value?: any): string { + settingToUriString(settingId: string, value?: unknown): string { return `${Schemas.codeSetting}://${settingId}${value ? `/${value}` : ''}`; } @@ -208,7 +208,7 @@ export class SimpleSettingRenderer { return this._configurationService.updateValue(settingId, userOriginalSettingValue, ConfigurationTarget.USER); } - async setSetting(settingId: string, currentSettingValue: any, newSettingValue: any): Promise { + async setSetting(settingId: string, currentSettingValue: unknown, newSettingValue: unknown): Promise { this._updatedSettings.set(settingId, currentSettingValue); return this._configurationService.updateValue(settingId, newSettingValue, ConfigurationTarget.USER); } diff --git a/src/vs/workbench/contrib/markdown/common/markedKatexExtension.ts b/src/vs/workbench/contrib/markdown/common/markedKatexExtension.ts index 01b35c39d86..2df695b19c5 100644 --- a/src/vs/workbench/contrib/markdown/common/markedKatexExtension.ts +++ b/src/vs/workbench/contrib/markdown/common/markedKatexExtension.ts @@ -5,13 +5,12 @@ import type * as marked from '../../../../base/common/marked/marked.js'; import { htmlAttributeEncodeValue } from '../../../../base/common/strings.js'; -export const mathInlineRegExp = /(?\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\k(?![a-zA-Z0-9])/; // Non-standard, but ensure opening $ is not preceded and closing $ is not followed by word/number characters +export const mathInlineRegExp = /(?\${1,2})(?!\.|\(["'])((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\k(?![a-zA-Z0-9])/; // Non-standard, but ensure opening $ is not preceded and closing $ is not followed by word/number characters, opening $ not followed by ., (", (' export const katexContainerClassName = 'vscode-katex-container'; export const katexContainerLatexAttributeName = 'data-latex'; const inlineRule = new RegExp('^' + mathInlineRegExp.source); - export namespace MarkedKatexExtension { type KatexOptions = import('katex').KatexOptions; diff --git a/src/vs/workbench/contrib/markdown/test/browser/__snapshots__/Markdown_Katex_Support_Test_Should_not_render_math_when_dollar_signs_appear_in_jQuery_expressions.0.snap b/src/vs/workbench/contrib/markdown/test/browser/__snapshots__/Markdown_Katex_Support_Test_Should_not_render_math_when_dollar_signs_appear_in_jQuery_expressions.0.snap new file mode 100644 index 00000000000..20c3532e7b2 --- /dev/null +++ b/src/vs/workbench/contrib/markdown/test/browser/__snapshots__/Markdown_Katex_Support_Test_Should_not_render_math_when_dollar_signs_appear_in_jQuery_expressions.0.snap @@ -0,0 +1 @@ +

$.getJSON, $.ajax, $.get and $("#dialogDetalleZona").dialog(...) / $("#dialogDetallePDC").dialog(...)

\ No newline at end of file diff --git a/src/vs/workbench/contrib/markdown/test/browser/markdownKatexSupport.test.ts b/src/vs/workbench/contrib/markdown/test/browser/markdownKatexSupport.test.ts index f970b34b58f..ca9d6434c4b 100644 --- a/src/vs/workbench/contrib/markdown/test/browser/markdownKatexSupport.test.ts +++ b/src/vs/workbench/contrib/markdown/test/browser/markdownKatexSupport.test.ts @@ -73,5 +73,11 @@ suite('Markdown Katex Support Test', () => { assert.ok(rendered.element.innerHTML.includes('katex')); await assertSnapshot(rendered.element.innerHTML); }); + + test('Should not render math when dollar signs appear in jQuery expressions', async () => { + const rendered = await renderMarkdownWithKatex('$.getJSON, $.ajax, $.get and $("#dialogDetalleZona").dialog(...) / $("#dialogDetallePDC").dialog(...)'); + assert.ok(!rendered.element.innerHTML.includes('katex')); + await assertSnapshot(rendered.element.innerHTML); + }); }); diff --git a/src/vs/workbench/contrib/markers/browser/markersChatContext.ts b/src/vs/workbench/contrib/markers/browser/markersChatContext.ts index 1d862978895..1df4e1967bb 100644 --- a/src/vs/workbench/contrib/markers/browser/markersChatContext.ts +++ b/src/vs/workbench/contrib/markers/browser/markersChatContext.ts @@ -17,8 +17,8 @@ import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quic import { EditorResourceAccessor } from '../../../common/editor.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextPicker, picksWithPromiseFn } from '../../chat/browser/chatContextPickService.js'; -import { IDiagnosticVariableEntryFilterData } from '../../chat/common/chatVariableEntries.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextPicker, picksWithPromiseFn } from '../../chat/browser/attachments/chatContextPickService.js'; +import { IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js'; import { IChatWidget } from '../../chat/browser/chat.js'; class MarkerChatContextPick implements IChatContextPickerItem { diff --git a/src/vs/workbench/contrib/markers/browser/markersFilterOptions.ts b/src/vs/workbench/contrib/markers/browser/markersFilterOptions.ts index 0b1bec81c46..7bab6f15510 100644 --- a/src/vs/workbench/contrib/markers/browser/markersFilterOptions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersFilterOptions.ts @@ -11,6 +11,8 @@ import { relativePath } from '../../../../base/common/resources.js'; import { TernarySearchTree } from '../../../../base/common/ternarySearchTree.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +const SOURCE_FILTER_REGEX = /(!)?@source:("[^"]*"|[^\s,]+)(\s*)/i; + export class ResourceGlobMatcher { private readonly globalExpression: ParsedExpression; @@ -52,6 +54,9 @@ export class FilterOptions { readonly excludesMatcher: ResourceGlobMatcher; readonly includesMatcher: ResourceGlobMatcher; + readonly includeSourceFilters: string[]; + readonly excludeSourceFilters: string[]; + static EMPTY(uriIdentityService: IUriIdentityService) { return new FilterOptions('', [], false, false, false, uriIdentityService); } constructor( @@ -79,6 +84,27 @@ export class FilterOptions { } } + const includeSourceFilters: string[] = []; + const excludeSourceFilters: string[] = []; + let sourceMatch; + while ((sourceMatch = SOURCE_FILTER_REGEX.exec(filter)) !== null) { + const negate = !!sourceMatch[1]; + let source = sourceMatch[2]; + // Remove quotes if present + if (source.startsWith('"') && source.endsWith('"')) { + source = source.slice(1, -1); + } + if (negate) { + excludeSourceFilters.push(source.toLowerCase()); + } else { + includeSourceFilters.push(source.toLowerCase()); + } + // Remove the entire match (including trailing whitespace) + filter = (filter.substring(0, sourceMatch.index) + filter.substring(sourceMatch.index + sourceMatch[0].length)).trim(); + } + this.includeSourceFilters = includeSourceFilters; + this.excludeSourceFilters = excludeSourceFilters; + const negate = filter.startsWith('!'); this.textFilter = { text: (negate ? strings.ltrim(filter, '!') : filter).trim(), negate }; const includeExpression: IExpression = getEmptyExpression(); @@ -101,6 +127,26 @@ export class FilterOptions { this.includesMatcher = new ResourceGlobMatcher(includeExpression, [], uriIdentityService); } + matchesSourceFilters(markerSource: string | undefined): boolean { + if (this.includeSourceFilters.length === 0 && this.excludeSourceFilters.length === 0) { + return true; + } + + const source = markerSource?.toLowerCase(); + + // Check negative filters first - if any match, exclude + if (source && this.excludeSourceFilters.includes(source)) { + return false; + } + + // If there are positive filters, check if any match + if (this.includeSourceFilters.length > 0) { + return source ? this.includeSourceFilters.includes(source) : false; + } + + return true; + } + private setPattern(expression: IExpression, pattern: string) { if (pattern[0] === '.') { pattern = '*' + pattern; // convert ".js" to "*.js" diff --git a/src/vs/workbench/contrib/markers/browser/markersTable.ts b/src/vs/workbench/contrib/markers/browser/markersTable.ts index 3f058e518ad..2065d259604 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTable.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTable.ts @@ -252,12 +252,12 @@ class MarkerSourceColumnRenderer implements ITableRenderer { +class MarkersTableVirtualDelegate implements ITableVirtualDelegate { static readonly HEADER_ROW_HEIGHT = 24; static readonly ROW_HEIGHT = 24; readonly headerRowHeight = MarkersTableVirtualDelegate.HEADER_ROW_HEIGHT; - getHeight(item: any) { + getHeight(item: MarkerTableItem) { return MarkersTableVirtualDelegate.ROW_HEIGHT; } } @@ -341,8 +341,7 @@ export class MarkersTable extends Disposable implements IProblemsWidget { // mouseover/mouseleave event handlers const onRowHover = Event.chain(this._register(new DomEmitter(list, 'mouseover')).event, $ => $.map(e => DOM.findParentWithClass(e.target as HTMLElement, 'monaco-list-row', 'monaco-list-rows')) - // eslint-disable-next-line local/code-no-any-casts - .filter(((e: HTMLElement | null) => !!e) as any) + .filter(e => !!e) .map(e => parseInt(e.getAttribute('data-index')!)) ); @@ -450,6 +449,11 @@ export class MarkersTable extends Disposable implements IProblemsWidget { continue; } + // Source filters + if (!this.filterOptions.matchesSourceFilters(marker.marker.source)) { + continue; + } + // Text filter if (this.filterOptions.textFilter.text) { const sourceMatches = marker.marker.source ? FilterOptions._filter(this.filterOptions.textFilter.text, marker.marker.source) ?? undefined : undefined; diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 4ed27e35584..081478c7596 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -506,6 +506,11 @@ export class Filter implements ITreeFilter { return false; } + // Check source filters if present + if (!this.options.matchesSourceFilters(marker.marker.source)) { + return false; + } + if (!this.options.textFilter.text) { return true; } diff --git a/src/vs/workbench/contrib/markers/browser/messages.ts b/src/vs/workbench/contrib/markers/browser/messages.ts index 89d14380dfe..a99be1dbf4b 100644 --- a/src/vs/workbench/contrib/markers/browser/messages.ts +++ b/src/vs/workbench/contrib/markers/browser/messages.ts @@ -37,7 +37,7 @@ export default class Messages { public static MARKERS_PANEL_ACTION_TOOLTIP_FILTER: string = nls.localize('markers.panel.action.filter', "Filter Problems"); public static MARKERS_PANEL_ACTION_TOOLTIP_QUICKFIX: string = nls.localize('markers.panel.action.quickfix', "Show fixes"); public static MARKERS_PANEL_FILTER_ARIA_LABEL: string = nls.localize('markers.panel.filter.ariaLabel', "Filter Problems"); - public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**)"); + public static MARKERS_PANEL_FILTER_PLACEHOLDER: string = nls.localize('markers.panel.filter.placeholder', "Filter (e.g. text, **/*.ts, !**/node_modules/**, @source:ts)"); public static MARKERS_PANEL_FILTER_ERRORS: string = nls.localize('markers.panel.filter.errors', "errors"); public static MARKERS_PANEL_FILTER_WARNINGS: string = nls.localize('markers.panel.filter.warnings', "warnings"); public static MARKERS_PANEL_FILTER_INFOS: string = nls.localize('markers.panel.filter.infos', "infos"); diff --git a/src/vs/workbench/contrib/markers/test/browser/markersFilterOptions.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersFilterOptions.test.ts new file mode 100644 index 00000000000..65ed4f9584c --- /dev/null +++ b/src/vs/workbench/contrib/markers/test/browser/markersFilterOptions.test.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { FilterOptions } from '../../browser/markersFilterOptions.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { UriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentityService.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { FileService } from '../../../../../platform/files/common/fileService.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; + +suite('MarkersFilterOptions', () => { + + let instantiationService: TestInstantiationService; + let uriIdentityService: IUriIdentityService; + + setup(() => { + instantiationService = new TestInstantiationService(); + const fileService = new FileService(new NullLogService()); + instantiationService.stub(IFileService, fileService); + uriIdentityService = instantiationService.createInstance(UriIdentityService); + }); + + ensureNoDisposablesAreLeakedInTestSuite(); + + test('source filter', () => { + const filterOptions = new FilterOptions('@source:ts', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['ts']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('source filter with negation', () => { + const filterOptions = new FilterOptions('!@source:eslint', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['eslint']); + assert.deepStrictEqual(filterOptions.includeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('multiple source filters', () => { + const filterOptions = new FilterOptions('@source:eslint @source:ts', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint', 'ts']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('source filter combined with text filter', () => { + const filterOptions = new FilterOptions('@source:ts error', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['ts']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, 'error'); + }); + + test('negated source filter combined with text filter', () => { + const filterOptions = new FilterOptions('!@source:ts error', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['ts']); + assert.deepStrictEqual(filterOptions.includeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, 'error'); + }); + + test('no source filter when not specified', () => { + const filterOptions = new FilterOptions('some text', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, []); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, 'some text'); + }); + + test('source filter case insensitive', () => { + const filterOptions = new FilterOptions('@SOURCE:TypeScript', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['typescript']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + }); + + test('complex filter with multiple source filters and text', () => { + const filterOptions = new FilterOptions('text1 @source:eslint @source:ts text2', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint', 'ts']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, 'text1 text2'); + }); + + test('source filter at the beginning', () => { + const filterOptions = new FilterOptions('@source:eslint foo', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('source filter at the end', () => { + const filterOptions = new FilterOptions('foo @source:eslint', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('source filter in the middle', () => { + const filterOptions = new FilterOptions('foo @source:eslint bar', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, 'foo bar'); + }); + + test('source filter with leading spaces', () => { + const filterOptions = new FilterOptions(' @source:eslint foo', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('source filter with trailing spaces', () => { + const filterOptions = new FilterOptions('foo @source:eslint ', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('multiple consecutive source filters', () => { + const filterOptions = new FilterOptions('@source:eslint @source:ts foo', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint', 'ts']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('only source filter with no text', () => { + const filterOptions = new FilterOptions('@source:eslint', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('multiple source filters with no text', () => { + const filterOptions = new FilterOptions('@source:eslint @source:ts', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint', 'ts']); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('negated source filter at different positions', () => { + const filterOptions = new FilterOptions('foo !@source:eslint bar', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['eslint']); + assert.deepStrictEqual(filterOptions.includeSourceFilters, []); + assert.strictEqual(filterOptions.textFilter.text, 'foo bar'); + }); + + test('mixed negated and positive source filters', () => { + const filterOptions = new FilterOptions('@source:eslint !@source:ts foo', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['eslint']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['ts']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('single quoted source with spaces', () => { + const filterOptions = new FilterOptions('@source:"hello world"', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['hello world']); + assert.strictEqual(filterOptions.textFilter.text, ''); + }); + + test('quoted source combined with text filter', () => { + const filterOptions = new FilterOptions('@source:"hello world" foo', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['hello world']); + assert.strictEqual(filterOptions.textFilter.text, 'foo'); + }); + + test('mixed quoted and unquoted sources (OR logic)', () => { + const filterOptions = new FilterOptions('@source:"hello world" @source:eslint @source:ts', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['hello world', 'eslint', 'ts']); + }); + + test('multiple quoted sources (OR logic)', () => { + const filterOptions = new FilterOptions('@source:"hello world" @source:"foo bar"', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['hello world', 'foo bar']); + }); + + test('quoted source with negation', () => { + const filterOptions = new FilterOptions('!@source:"hello world"', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['hello world']); + }); + + test('quoted source in the middle of filter', () => { + const filterOptions = new FilterOptions('foo @source:"hello world" bar', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['hello world']); + assert.strictEqual(filterOptions.textFilter.text, 'foo bar'); + }); + + test('complex filter with quoted and unquoted mixed', () => { + const filterOptions = new FilterOptions('@source:"TypeScript Compiler" @source:eslint !@source:"My Extension" text', [], true, true, true, uriIdentityService); + assert.deepStrictEqual(filterOptions.includeSourceFilters, ['typescript compiler', 'eslint']); + assert.deepStrictEqual(filterOptions.excludeSourceFilters, ['my extension']); + assert.strictEqual(filterOptions.textFilter.text, 'text'); + }); + + test('no filters - always matches', () => { + const filterOptions = new FilterOptions('foo', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters(undefined), true); + }); + + test('positive filter - exact match only', () => { + const filterOptions = new FilterOptions('@source:eslint', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('ESLint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('ts'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint-plugin'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('es'), false); + }); + + test('positive filter - no source in marker', () => { + const filterOptions = new FilterOptions('@source:eslint', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters(undefined), false); + }); + + test('negative filter - excludes exact source', () => { + const filterOptions = new FilterOptions('!@source:eslint', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('ts'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint-plugin'), true); + }); + + test('negative filter - no source in marker', () => { + const filterOptions = new FilterOptions('!@source:eslint', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters(undefined), true); + }); + + test('OR logic - multiple @source filters', () => { + const filterOptions = new FilterOptions('@source:eslint @source:ts', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('ts'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('python'), false); + }); + + test('OR logic with negation', () => { + const filterOptions = new FilterOptions('@source:eslint @source:ts !@source:error', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('ts'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('error'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('python'), false); + }); + + test('only negative filters - excludes specified sources', () => { + const filterOptions = new FilterOptions('!@source:eslint !@source:ts', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('ts'), false); + assert.strictEqual(filterOptions.matchesSourceFilters('python'), true); + assert.strictEqual(filterOptions.matchesSourceFilters(undefined), true); + }); + + test('case insensitivity', () => { + const filterOptions = new FilterOptions('@source:ESLint', [], true, true, true, uriIdentityService); + assert.strictEqual(filterOptions.matchesSourceFilters('eslint'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('ESLINT'), true); + assert.strictEqual(filterOptions.matchesSourceFilters('EsLiNt'), true); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts index 433046be5f9..d0d8260bb51 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcp.contribution.ts @@ -16,6 +16,7 @@ import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationEx import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { EditorExtensions } from '../../../common/editor.js'; import { mcpSchemaId } from '../../../services/configuration/common/configuration.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js'; import { InstalledMcpServersDiscovery } from '../common/discovery/installedMcpServersDiscovery.js'; import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js'; @@ -33,7 +34,7 @@ import { McpSamplingService } from '../common/mcpSamplingService.js'; import { McpService } from '../common/mcpService.js'; import { IMcpElicitationService, IMcpSamplingService, IMcpService, IMcpWorkbenchService } from '../common/mcpTypes.js'; import { McpAddContextContribution } from './mcpAddContextContribution.js'; -import { AddConfigurationAction, EditStoredInput, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, McpConfirmationServerOptionsCommand, MCPServerActionRendering, McpServerOptionsCommand, McpSkipCurrentAutostartCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; +import { AddConfigurationAction, EditStoredInput, InstallFromManifestAction, ListMcpServerCommand, McpBrowseCommand, McpBrowseResourcesCommand, McpConfigureSamplingModels, McpConfirmationServerOptionsCommand, MCPServerActionRendering, McpServerOptionsCommand, McpSkipCurrentAutostartCommand, McpStartPromptingServerCommand, OpenRemoteUserMcpResourceCommand, OpenUserMcpResourceCommand, OpenWorkspaceFolderMcpResourceCommand, OpenWorkspaceMcpResourceCommand, RemoveStoredInput, ResetMcpCachedTools, ResetMcpTrustCommand, RestartServer, ShowConfiguration, ShowInstalledMcpServersCommand, ShowOutput, StartServer, StopServer } from './mcpCommands.js'; import { McpDiscovery } from './mcpDiscovery.js'; import { McpElicitationService } from './mcpElicitationService.js'; import { McpLanguageFeatures } from './mcpLanguageFeatures.js'; @@ -68,6 +69,7 @@ registerAction2(McpConfirmationServerOptionsCommand); registerAction2(ResetMcpTrustCommand); registerAction2(ResetMcpCachedTools); registerAction2(AddConfigurationAction); +registerAction2(InstallFromManifestAction); registerAction2(RemoveStoredInput); registerAction2(EditStoredInput); registerAction2(StartServer); @@ -108,6 +110,7 @@ Registry.as(EditorExtensions.EditorPane).registerEditorPane Registry.as(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({ ctor: McpResourceQuickAccess, prefix: McpResourceQuickAccess.PREFIX, + when: ChatContextKeys.enabled, placeholder: localize('mcp.quickaccess.placeholder', "Filter to an MCP resource"), helpEntries: [{ description: localize('mcp.quickaccess.add', "MCP Server Resources"), diff --git a/src/vs/workbench/contrib/mcp/browser/mcpAddContextContribution.ts b/src/vs/workbench/contrib/mcp/browser/mcpAddContextContribution.ts index ec78181f5b7..ea735f845e9 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpAddContextContribution.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpAddContextContribution.ts @@ -5,27 +5,41 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { CancellationError } from '../../../../base/common/errors.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, observableValue } from '../../../../base/common/observable.js'; +import { autorun, derived } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ChatContextPick, IChatContextPickService } from '../../chat/browser/chatContextPickService.js'; +import { ChatContextPick, IChatContextPickService } from '../../chat/browser/attachments/chatContextPickService.js'; +import { IMcpService, McpCapability } from '../common/mcpTypes.js'; import { McpResourcePickHelper } from './mcpResourceQuickAccess.js'; export class McpAddContextContribution extends Disposable implements IWorkbenchContribution { - private readonly _helper: McpResourcePickHelper; private readonly _addContextMenu = this._register(new MutableDisposable()); constructor( @IChatContextPickService private readonly _chatContextPickService: IChatContextPickService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IMcpService mcpService: IMcpService ) { super(); - this._helper = instantiationService.createInstance(McpResourcePickHelper); + const hasServersWithResources = derived(reader => { + let enabled = false; + for (const server of mcpService.servers.read(reader)) { + const cap = server.capabilities.read(undefined); + if (cap === undefined) { + enabled = true; // until we know more + } else if (cap & McpCapability.Resources) { + enabled = true; + break; + } + } + + return enabled; + }); + this._register(autorun(reader => { - const enabled = this._helper.hasServersWithResources.read(reader); + const enabled = hasServersWithResources.read(reader); if (enabled && !this._addContextMenu.value) { this._registerAddContextMenu(); } else { @@ -42,42 +56,43 @@ export class McpAddContextContribution extends Disposable implements IWorkbenchC isEnabled(widget) { return !!widget.attachmentCapabilities.supportsMCPAttachments; }, - asPicker: () => ({ - placeholder: localize('mcp.addContext.placeholder', "Select MCP Resource..."), - picks: (_query, token) => this._getResourcePicks(token), - }), + asPicker: () => { + const helper = this._instantiationService.createInstance(McpResourcePickHelper); + return { + placeholder: localize('mcp.addContext.placeholder', "Select MCP Resource..."), + picks: (_query, token) => this._getResourcePicks(token, helper), + goBack: () => { + return helper.navigateBack(); + }, + dispose: () => { + helper.dispose(); + } + }; + }, }); } - private _getResourcePicks(token: CancellationToken) { - const observable = observableValue<{ busy: boolean; picks: ChatContextPick[] }>(this, { busy: true, picks: [] }); + private _getResourcePicks(token: CancellationToken, helper: McpResourcePickHelper) { + const picksObservable = helper.getPicks(token); + + return derived(this, reader => { - this._helper.getPicks(servers => { + const pickItems = picksObservable.read(reader); const picks: ChatContextPick[] = []; - for (const [server, resources] of servers) { + + for (const [server, resources] of pickItems.picks) { if (resources.length === 0) { continue; } - picks.push(McpResourcePickHelper.sep(server)); for (const resource of resources) { picks.push({ ...McpResourcePickHelper.item(resource), - asAttachment: () => this._helper.toAttachment(resource).then(r => { - if (!r) { - throw new CancellationError(); - } else { - return r; - } - }), + asAttachment: () => helper.toAttachment(resource, server) }); } } - observable.set({ picks, busy: true }, undefined); - }, token).finally(() => { - observable.set({ busy: false, picks: observable.get().picks }, undefined); + return { picks, busy: pickItems.isBusy }; }); - - return observable; } } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts index 5fa3f85a76b..71fd9bcd6c8 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommands.ts @@ -14,7 +14,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { groupBy } from '../../../../base/common/collections.js'; import { Event } from '../../../../base/common/event.js'; -import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { autorun, derived, derivedObservableWithCache, observableValue } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; @@ -50,11 +50,11 @@ import { IUserDataProfileService } from '../../../services/userDataProfile/commo import { IViewsService } from '../../../services/views/common/viewsService.js'; import { CHAT_CONFIG_MENU_ID } from '../../chat/browser/actions/chatActions.js'; import { ChatViewId, IChatWidgetService } from '../../chat/browser/chat.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService.js'; -import { ChatModeKind } from '../../chat/common/constants.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IChatElicitationRequest, IChatToolInvocation } from '../../chat/common/chatService/chatService.js'; +import { ChatAgentLocation, ChatModeKind } from '../../chat/common/constants.js'; import { ILanguageModelsService } from '../../chat/common/languageModels.js'; -import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js'; +import { ILanguageModelToolsService } from '../../chat/common/tools/languageModelToolsService.js'; import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; import { extensionsFilterSubMenu, IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { TEXT_FILE_EDITOR_ID } from '../../files/common/files.js'; @@ -62,8 +62,9 @@ import { McpCommandIds } from '../common/mcpCommandIds.js'; import { McpContextKeys } from '../common/mcpContextKeys.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { HasInstalledMcpServersContext, IMcpSamplingService, IMcpServer, IMcpServerStartOpts, IMcpService, InstalledMcpServersViewId, LazyCollectionState, McpCapability, McpCollectionDefinition, McpConnectionState, McpDefinitionReference, mcpPromptPrefix, McpServerCacheState, McpStartServerInteraction } from '../common/mcpTypes.js'; -import { McpAddConfigurationCommand } from './mcpCommandsAddConfiguration.js'; +import { McpAddConfigurationCommand, McpInstallFromManifestCommand } from './mcpCommandsAddConfiguration.js'; import { McpResourceQuickAccess, McpResourceQuickPick } from './mcpResourceQuickAccess.js'; +import { startServerAndWaitForLiveTools } from '../common/mcpTypesUtils.js'; import './media/mcpServerAction.css'; import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js'; @@ -195,7 +196,7 @@ export class McpConfirmationServerOptionsCommand extends Action2 { if (tool?.source.type === 'mcp') { accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, tool.source.definitionId); } - } else if (arg.kind === 'elicitation') { + } else if (arg.kind === 'elicitation2') { if (arg.source?.type === 'mcp') { accessor.get(ICommandService).executeCommand(McpCommandIds.ServerOptions, arg.source.definitionId); } @@ -548,7 +549,7 @@ export class MCPServerActionRendering extends Disposable implements IWorkbenchCo } protected override getHoverContents({ state, servers } = displayedStateCurrent.get()): string | undefined | IManagedHoverTooltipHTMLElement { - const link = (s: IMcpServer) => markdownCommandLink({ + const link = (s: IMcpServer) => createMarkdownCommandLink({ title: s.definition.label, id: McpCommandIds.ServerOptions, arguments: [s.definition.id], @@ -657,7 +658,7 @@ export class ResetMcpTrustCommand extends Action2 { title: localize2('mcp.resetTrust', "Reset Trust"), category, f1: true, - precondition: McpContextKeys.toolsCount.greater(0), + precondition: ContextKeyExpr.and(McpContextKeys.toolsCount.greater(0), ChatContextKeys.Setup.hidden.negate()), }); } @@ -715,6 +716,26 @@ export class AddConfigurationAction extends Action2 { } } +export class InstallFromManifestAction extends Action2 { + constructor() { + super({ + id: McpCommandIds.InstallFromManifest, + title: localize2('mcp.installFromManifest', "Install Server from Manifest..."), + metadata: { + description: localize2('mcp.installFromManifest.description', "Install an MCP server from a JSON manifest file"), + }, + category, + f1: true, + precondition: ChatContextKeys.Setup.hidden.negate(), + }); + } + + async run(accessor: ServicesAccessor): Promise { + const instantiationService = accessor.get(IInstantiationService); + return instantiationService.createInstance(McpInstallFromManifestCommand).run(); + } +} + export class RemoveStoredInput extends Action2 { constructor() { @@ -821,9 +842,18 @@ export class StartServer extends Action2 { }); } - async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts) { - const s = accessor.get(IMcpService).servers.get().find(s => s.definition.id === serverId); - await s?.start({ promptType: 'all-untrusted', ...opts }); + async run(accessor: ServicesAccessor, serverId: string, opts?: IMcpServerStartOpts & { waitForLiveTools?: boolean }) { + let servers = accessor.get(IMcpService).servers.get(); + if (serverId !== '*') { + servers = servers.filter(s => s.definition.id === serverId); + } + + const startOpts: IMcpServerStartOpts = { promptType: 'all-untrusted', ...opts }; + if (opts?.waitForLiveTools) { + await Promise.all(servers.map(s => startServerAndWaitForLiveTools(s, startOpts))); + } else { + await Promise.all(servers.map(s => s.start(startOpts))); + } } } @@ -885,7 +915,7 @@ export class ShowInstalledMcpServersCommand extends Action2 { id: McpCommandIds.ShowInstalled, title: localize2('mcp.command.show.installed', "Show Installed Servers"), category, - precondition: HasInstalledMcpServersContext, + precondition: ContextKeyExpr.and(HasInstalledMcpServersContext, ChatContextKeys.Setup.hidden.negate()), f1: true, }); } @@ -1019,7 +1049,7 @@ export class McpBrowseResourcesCommand extends Action2 { id: McpCommandIds.BrowseResources, title: localize2('mcp.browseResources', "Browse Resources..."), category, - precondition: McpContextKeys.serverCount.greater(0), + precondition: ContextKeyExpr.and(McpContextKeys.serverCount.greater(0), ChatContextKeys.Setup.hidden.negate()), f1: true, }); } @@ -1057,7 +1087,7 @@ export class McpConfigureSamplingModels extends Action2 { label: model.name, description: model.tooltip, id, - picked: existingIds.size ? existingIds.has(id) : model.isDefault, + picked: existingIds.size ? existingIds.has(id) : model.isDefaultForLocation[ChatAgentLocation.Chat], }; }).filter(isDefined); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts index acdc7d98954..0f842a00418 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpCommandsAddConfiguration.ts @@ -18,13 +18,14 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; -import { IGalleryMcpServerConfiguration, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js'; import { IMcpRemoteServerConfiguration, IMcpServerConfiguration, IMcpServerVariable, IMcpStdioServerConfiguration, McpServerType } from '../../../../platform/mcp/common/mcpPlatformTypes.js'; +import { IGalleryMcpServerConfiguration, RegistryType } from '../../../../platform/mcp/common/mcpManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IWorkbenchMcpManagementService } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; @@ -32,6 +33,7 @@ import { McpCommandIds } from '../common/mcpCommandIds.js'; import { allDiscoverySources, DiscoverySource, mcpDiscoverySection, mcpStdioServerSchema } from '../common/mcpConfiguration.js'; import { IMcpRegistry } from '../common/mcpRegistryTypes.js'; import { IMcpService, McpConnectionState } from '../common/mcpTypes.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export const enum AddConfigurationType { Stdio, @@ -113,15 +115,16 @@ type AddServerCompletedClassification = { }; type AssistedServerConfiguration = { - type?: 'vscode'; + type?: 'assisted'; name?: string; server: Omit; inputs?: IMcpServerVariable[]; inputValues?: Record; } | { - type: 'server.json'; + type: 'mapped'; name?: string; - server: IGalleryMcpServerConfiguration; + server: Omit; + inputs?: IMcpServerVariable[]; }; export class McpAddConfigurationCommand { @@ -406,21 +409,13 @@ export class McpAddConfigurationCommand { } ); - if (config?.type === 'server.json') { - const packageType = this.getPackageTypeEnum(type); - if (!packageType) { - throw new Error(`Unsupported assisted package type ${type}`); - } - const { mcpServerConfiguration } = this._mcpManagementService.getMcpServerConfigurationFromManifest(config.server, packageType); - if (mcpServerConfiguration.config.type !== McpServerType.LOCAL) { - throw new Error(`Unexpected server type ${mcpServerConfiguration.config.type} for assisted configuration from server.json.`); - } + if (config?.type === 'mapped') { return { name: config.name, - server: mcpServerConfiguration.config, - inputs: mcpServerConfiguration.inputs, + server: config.server, + inputs: config.inputs, }; - } else if (config?.type === 'vscode' || !config?.type) { + } else if (config?.type === 'assisted' || !config?.type) { return config; } else { assertNever(config?.type); @@ -584,21 +579,6 @@ export class McpAddConfigurationCommand { } } - private getPackageTypeEnum(type: AddConfigurationType): RegistryType | undefined { - switch (type) { - case AddConfigurationType.NpmPackage: - return RegistryType.NODE; - case AddConfigurationType.PipPackage: - return RegistryType.PYTHON; - case AddConfigurationType.NuGetPackage: - return RegistryType.NUGET; - case AddConfigurationType.DockerImage: - return RegistryType.DOCKER; - default: - return undefined; - } - } - private getPackageType(serverType: AddConfigurationType): string | undefined { switch (serverType) { case AddConfigurationType.NpmPackage: @@ -618,3 +598,98 @@ export class McpAddConfigurationCommand { } } } + +export class McpInstallFromManifestCommand { + constructor( + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + @IFileService private readonly _fileService: IFileService, + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @INotificationService private readonly _notificationService: INotificationService, + @IWorkbenchMcpManagementService private readonly _mcpManagementService: IWorkbenchMcpManagementService, + @ILogService private readonly _logService: ILogService, + ) { } + + async run(): Promise { + // Step 1: Open file dialog to select the manifest file + const result = await this._fileDialogService.showOpenDialog({ + title: localize('mcp.installFromManifest.title', "Select MCP Server Manifest"), + filters: [{ name: localize('mcp.installFromManifest.filter', "MCP Manifest"), extensions: ['json'] }], + canSelectFiles: true, + canSelectMany: false, + openLabel: localize({ key: 'mcp.installFromManifest.openLabel', comment: ['&& denotes a mnemonic'] }, "&&Install") + }); + + if (!result?.[0]) { + return; + } + + const manifestUri = result[0]; + + // Step 2: Read and parse the manifest file + let manifest: unknown; + try { + const contents = await this._fileService.readFile(manifestUri); + manifest = parseJsonc(contents.value.toString()); + } catch (e) { + this._notificationService.error(localize('mcp.installFromManifest.readError', "Failed to read manifest file: {0}", e.message)); + return; + } + + if (!manifest || typeof manifest !== 'object') { + this._notificationService.error(localize('mcp.installFromManifest.invalidJson', "Invalid manifest file: expected a JSON object")); + return; + } + + // Step 3: Validate and extract configuration from gallery manifest + const galleryManifest = manifest as IGalleryMcpServerConfiguration & { name?: string }; + + // Determine package type from manifest + let packageType: RegistryType; + if (Array.isArray(galleryManifest.packages) && galleryManifest.packages.length > 0) { + packageType = galleryManifest.packages[0].registryType; + } else if (Array.isArray(galleryManifest.remotes) && galleryManifest.remotes.length > 0) { + packageType = RegistryType.REMOTE; + } else { + this._notificationService.error(localize('mcp.installFromManifest.invalidManifest', "Invalid manifest: expected 'packages' or 'remotes' with at least one entry")); + return; + } + + let config: IMcpServerConfiguration; + let inputs: IMcpServerVariable[] | undefined; + try { + const { mcpServerConfiguration, notices } = this._mcpManagementService.getMcpServerConfigurationFromManifest(galleryManifest, packageType); + config = mcpServerConfiguration.config; + inputs = mcpServerConfiguration.inputs; + + if (notices.length > 0) { + this._logService.warn(`MCP Management Service: Warnings while installing the MCP server from ${manifestUri.path}`, notices); + } + } catch (e) { + this._notificationService.error(localize('mcp.installFromManifest.parseError', "Failed to parse manifest: {0}", e.message)); + return; + } + + // Step 4: Get server name from manifest or prompt user + let name = galleryManifest.name; + if (!name) { + name = await this._quickInputService.input({ + title: localize('mcp.installFromManifest.serverId.title', "Enter Server ID"), + placeHolder: localize('mcp.installFromManifest.serverId.placeholder', "Unique identifier for this server"), + value: basename(manifestUri).replace(/\.json$/i, ''), + ignoreFocusLost: true, + }); + + if (!name) { + return; + } + } + + // Step 5: Install to user settings + try { + await this._mcpManagementService.install({ name, config, inputs }); + this._notificationService.info(localize('mcp.installFromManifest.success', "MCP server '{0}' installed successfully", name)); + } catch (e) { + this._notificationService.error(localize('mcp.installFromManifest.installError', "Failed to install MCP server: {0}", e.message)); + } + } +} diff --git a/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts b/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts index 443a76f81e7..2b1b21e5bb0 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts @@ -4,22 +4,63 @@ *--------------------------------------------------------------------------------------------*/ import { Action } from '../../../../base/common/actions.js'; -import { assertNever } from '../../../../base/common/assert.js'; +import { assertNever, softAssertNever } from '../../../../base/common/assert.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { autorun } from '../../../../base/common/observable.js'; +import { isDefined } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; -import { ChatElicitationRequestPart } from '../../chat/browser/chatElicitationRequestPart.js'; -import { ChatModel } from '../../chat/common/chatModel.js'; -import { IChatService } from '../../chat/common/chatService.js'; -import { LocalChatSessionUri } from '../../chat/common/chatUri.js'; -import { IMcpElicitationService, IMcpServer, IMcpToolCallContext } from '../common/mcpTypes.js'; +import { ChatElicitationRequestPart } from '../../chat/common/model/chatProgressTypes/chatElicitationRequestPart.js'; +import { ChatModel } from '../../chat/common/model/chatModel.js'; +import { ElicitationState, IChatService } from '../../chat/common/chatService/chatService.js'; +import { LocalChatSessionUri } from '../../chat/common/model/chatUri.js'; +import { ElicitationKind, ElicitResult, IFormModeElicitResult, IMcpElicitationService, IMcpServer, IMcpToolCallContext, IUrlModeElicitResult, McpConnectionState, MpcResponseError } from '../common/mcpTypes.js'; import { mcpServerToSourceData } from '../common/mcpTypesUtils.js'; import { MCP } from '../common/modelContextProtocol.js'; const noneItem: IQuickPickItem = { id: undefined, label: localize('mcp.elicit.enum.none', 'None'), description: localize('mcp.elicit.enum.none.description', 'No selection'), alwaysShow: true }; +type Pre20251125ElicitationParams = Omit & { mode?: undefined }; + +function isFormElicitation(params: MCP.ElicitRequest['params'] | Pre20251125ElicitationParams): params is (MCP.ElicitRequestFormParams | Pre20251125ElicitationParams) { + return params.mode === 'form' || (params.mode === undefined && !!(params as Pre20251125ElicitationParams).requestedSchema); +} + +function isUrlElicitation(params: MCP.ElicitRequest['params']): params is MCP.ElicitRequestURLParams { + return params.mode === 'url'; +} + +function isLegacyTitledEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.LegacyTitledEnumSchema & { enumNames: string[] } { + const cast = schema as MCP.LegacyTitledEnumSchema; + return cast.type === 'string' && Array.isArray(cast.enum) && Array.isArray(cast.enumNames); +} + +function isUntitledEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.LegacyTitledEnumSchema | MCP.UntitledSingleSelectEnumSchema { + const cast = schema as MCP.LegacyTitledEnumSchema | MCP.UntitledSingleSelectEnumSchema; + return cast.type === 'string' && Array.isArray(cast.enum); +} + +function isTitledSingleEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.TitledSingleSelectEnumSchema { + const cast = schema as MCP.TitledSingleSelectEnumSchema; + return cast.type === 'string' && Array.isArray(cast.oneOf); +} + +function isUntitledMultiEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.UntitledMultiSelectEnumSchema { + const cast = schema as MCP.UntitledMultiSelectEnumSchema; + return cast.type === 'array' && !!cast.items?.enum; +} + +function isTitledMultiEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.TitledMultiSelectEnumSchema { + const cast = schema as MCP.TitledMultiSelectEnumSchema; + return cast.type === 'array' && !!cast.items?.anyOf; +} + export class McpElicitationService implements IMcpElicitationService { declare readonly _serviceBrand: undefined; @@ -27,11 +68,23 @@ export class McpElicitationService implements IMcpElicitationService { @INotificationService private readonly _notificationService: INotificationService, @IQuickInputService private readonly _quickInputService: IQuickInputService, @IChatService private readonly _chatService: IChatService, + @IOpenerService private readonly _openerService: IOpenerService, ) { } - public elicit(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequest['params'], token: CancellationToken): Promise { + public elicit(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequest['params'], token: CancellationToken): Promise { + if (isFormElicitation(elicitation)) { + return this._elicitForm(server, context, elicitation, token); + } else if (isUrlElicitation(elicitation)) { + return this._elicitUrl(server, context, elicitation, token); + } else { + softAssertNever(elicitation); + return Promise.reject(new MpcResponseError('Unsupported elicitation type', MCP.INVALID_PARAMS, undefined)); + } + } + + private async _elicitForm(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequestFormParams | Pre20251125ElicitationParams, token: CancellationToken): Promise { const store = new DisposableStore(); - return new Promise(resolve => { + const value = await new Promise(resolve => { const chatModel = context?.chatSessionId && this._chatService.getSession(LocalChatSessionUri.forSession(context.chatSessionId)); if (chatModel instanceof ChatModel) { const request = chatModel.getRequests().at(-1); @@ -43,16 +96,15 @@ export class McpElicitationService implements IMcpElicitationService { localize('mcp.elicit.accept', 'Respond'), localize('mcp.elicit.reject', 'Cancel'), async () => { - const p = this._doElicit(elicitation, token); + const p = this._doElicitForm(elicitation, token); resolve(p); const result = await p; - part.state = result.action === 'accept' ? 'accepted' : 'rejected'; part.acceptedResult = result.content; + return result.action === 'accept' ? ElicitationState.Accepted : ElicitationState.Rejected; }, () => { resolve({ action: 'decline' }); - part.state = 'rejected'; - return Promise.resolve(); + return Promise.resolve(ElicitationState.Rejected); }, mcpServerToSourceData(server), ); @@ -64,7 +116,7 @@ export class McpElicitationService implements IMcpElicitationService { source: localize('mcp.elicit.source', 'MCP Server ({0})', server.definition.label), severity: Severity.Info, actions: { - primary: [store.add(new Action('mcp.elicit.give', localize('mcp.elicit.give', 'Respond'), undefined, true, () => resolve(this._doElicit(elicitation, token))))], + primary: [store.add(new Action('mcp.elicit.give', localize('mcp.elicit.give', 'Respond'), undefined, true, () => resolve(this._doElicitForm(elicitation, token))))], secondary: [store.add(new Action('mcp.elicit.cancel', localize('mcp.elicit.cancel', 'Cancel'), undefined, true, () => resolve({ action: 'decline' })))], } }); @@ -73,16 +125,106 @@ export class McpElicitationService implements IMcpElicitationService { } }).finally(() => store.dispose()); + + return { kind: ElicitationKind.Form, value, dispose: () => { } }; + } + + private async _elicitUrl(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequestURLParams, token: CancellationToken): Promise { + const promiseStore = new DisposableStore(); + + // We create this ahead of time in case e.g. a user manually opens the URL beforehand + const completePromise = new Promise((resolve, reject) => { + promiseStore.add(token.onCancellationRequested(() => reject(new CancellationError()))); + promiseStore.add(autorun(reader => { + const cnx = server.connection.read(reader); + const handler = cnx?.handler.read(reader); + if (handler) { + reader.store.add(handler.onDidReceiveElicitationCompleteNotification(e => { + if (e.params.elicitationId === elicitation.elicitationId) { + resolve(); + } + })); + } else if (!McpConnectionState.isRunning(server.connectionState.read(reader))) { + reject(new CancellationError()); + } + })); + }).finally(() => promiseStore.dispose()); + + const store = new DisposableStore(); + const value = await new Promise(resolve => { + const chatModel = context?.chatSessionId && this._chatService.getSession(LocalChatSessionUri.forSession(context.chatSessionId)); + if (chatModel instanceof ChatModel) { + const request = chatModel.getRequests().at(-1); + if (request) { + const part = new ChatElicitationRequestPart( + localize('mcp.elicit.url.title', 'Authorization Required'), + new MarkdownString().appendText(elicitation.message) + .appendMarkdown('\n\n' + localize('mcp.elicit.url.instruction', 'Open this URL?')) + .appendCodeblock('', elicitation.url), + localize('msg.subtitle', "{0} (MCP Server)", server.definition.label), + localize('mcp.elicit.url.open', 'Open {0}', URI.parse(elicitation.url).authority), + localize('mcp.elicit.reject', 'Cancel'), + async () => { + const result = await this._doElicitUrl(elicitation, token); + resolve(result); + completePromise.then(() => part.hide()); + return result.action === 'accept' ? ElicitationState.Accepted : ElicitationState.Rejected; + }, + () => { + resolve({ action: 'decline' }); + return Promise.resolve(ElicitationState.Rejected); + }, + mcpServerToSourceData(server), + ); + chatModel.acceptResponseProgress(request, part); + } + } else { + const handle = this._notificationService.notify({ + message: elicitation.message + ' ' + localize('mcp.elicit.url.instruction2', 'This will open {0}', elicitation.url), + source: localize('mcp.elicit.source', 'MCP Server ({0})', server.definition.label), + severity: Severity.Info, + actions: { + primary: [store.add(new Action('mcp.elicit.url.open2', localize('mcp.elicit.url.open2', 'Open URL'), undefined, true, () => resolve(this._doElicitUrl(elicitation, token))))], + secondary: [store.add(new Action('mcp.elicit.cancel', localize('mcp.elicit.cancel', 'Cancel'), undefined, true, () => resolve({ action: 'decline' })))], + } + }); + store.add(handle.onDidClose(() => resolve({ action: 'cancel' }))); + store.add(token.onCancellationRequested(() => resolve({ action: 'cancel' }))); + } + }).finally(() => store.dispose()); + + return { + kind: ElicitationKind.URL, + value, + wait: completePromise, + dispose: () => promiseStore.dispose(), + }; + } + + private async _doElicitUrl(elicitation: MCP.ElicitRequestURLParams, token: CancellationToken): Promise { + if (token.isCancellationRequested) { + return { action: 'cancel' }; + } + + try { + if (await this._openerService.open(elicitation.url, { allowCommands: false })) { + return { action: 'accept' }; + } + } catch { + // ignored + } + + return { action: 'decline' }; } - private async _doElicit(elicitation: MCP.ElicitRequest['params'], token: CancellationToken): Promise { + private async _doElicitForm(elicitation: MCP.ElicitRequestFormParams | Pre20251125ElicitationParams, token: CancellationToken): Promise { const quickPick = this._quickInputService.createQuickPick(); const store = new DisposableStore(); try { const properties = Object.entries(elicitation.requestedSchema.properties); const requiredFields = new Set(elicitation.requestedSchema.required || []); - const results: Record = {}; + const results: Record = {}; const backSnapshots: { value: string; validationMessage?: string }[] = []; quickPick.title = elicitation.message; @@ -102,12 +244,20 @@ export class McpElicitationService implements IMcpElicitationService { quickPick.validationMessage = ''; quickPick.buttons = i > 0 ? [this._quickInputService.backButton] : []; - let result: { type: 'value'; value: string | number | boolean | undefined } | { type: 'back' } | { type: 'cancel' }; + let result: { type: 'value'; value: string | number | boolean | undefined | string[] } | { type: 'back' } | { type: 'cancel' }; if (schema.type === 'boolean') { - result = await this._handleEnumField(quickPick, { ...schema, type: 'string', enum: ['true', 'false'], default: schema.default ? String(schema.default) : undefined }, isRequired, store, token); + result = await this._handleEnumField(quickPick, { enum: [{ const: 'true' }, { const: 'false' }], default: schema.default ? String(schema.default) : undefined }, isRequired, store, token); if (result.type === 'value') { result.value = result.value === 'true' ? true : false; } - } else if (schema.type === 'string' && 'enum' in schema) { - result = await this._handleEnumField(quickPick, schema, isRequired, store, token); + } else if (isLegacyTitledEnumSchema(schema)) { + result = await this._handleEnumField(quickPick, { enum: schema.enum.map((v, i) => ({ const: v, title: schema.enumNames[i] })), default: schema.default }, isRequired, store, token); + } else if (isUntitledEnumSchema(schema)) { + result = await this._handleEnumField(quickPick, { enum: schema.enum.map(v => ({ const: v })), default: schema.default }, isRequired, store, token); + } else if (isTitledSingleEnumSchema(schema)) { + result = await this._handleEnumField(quickPick, { enum: schema.oneOf, default: schema.default }, isRequired, store, token); + } else if (isTitledMultiEnumSchema(schema)) { + result = await this._handleMultiEnumField(quickPick, { enum: schema.items.anyOf, default: schema.default }, isRequired, store, token); + } else if (isUntitledMultiEnumSchema(schema)) { + result = await this._handleMultiEnumField(quickPick, { enum: schema.items.enum.map(v => ({ const: v })), default: schema.default }, isRequired, store, token); } else { result = await this._handleInputField(quickPick, schema, isRequired, store, token); if (result.type === 'value' && (schema.type === 'number' || schema.type === 'integer')) { @@ -152,23 +302,23 @@ export class McpElicitationService implements IMcpElicitationService { private async _handleEnumField( quickPick: IQuickPick, - schema: MCP.EnumSchema, + schema: { default?: string; enum: { const: string; title?: string }[] }, required: boolean, store: DisposableStore, token: CancellationToken ) { - const items: IQuickPickItem[] = schema.enum.map((value, index) => ({ + const items: IQuickPickItem[] = schema.enum.map(({ const: value, title }) => ({ id: value, label: value, - description: schema.enumNames?.[index], + description: title, })); if (!required) { items.push(noneItem); } - quickPick.items = items; quickPick.canSelectMany = false; + quickPick.items = items; if (schema.default !== undefined) { quickPick.activeItems = items.filter(item => item.id === schema.default); } @@ -188,6 +338,45 @@ export class McpElicitationService implements IMcpElicitationService { }); } + private async _handleMultiEnumField( + quickPick: IQuickPick, + schema: { default?: string[]; enum: { const: string; title?: string }[] }, + required: boolean, + store: DisposableStore, + token: CancellationToken + ) { + const items: IQuickPickItem[] = schema.enum.map(({ const: value, title }) => ({ + id: value, + label: value, + description: title, + picked: !!schema.default?.includes(value), + pickable: true, + })); + + if (!required) { + items.push(noneItem); + } + + quickPick.canSelectMany = true; + quickPick.items = items; + + return new Promise<{ type: 'value'; value: string[] | undefined } | { type: 'back' } | { type: 'cancel' }>(resolve => { + store.add(token.onCancellationRequested(() => resolve({ type: 'cancel' }))); + store.add(quickPick.onDidAccept(() => { + const selected = quickPick.selectedItems[0]; + if (selected.id === undefined) { + resolve({ type: 'value', value: undefined }); + } else { + resolve({ type: 'value', value: quickPick.selectedItems.map(i => i.id).filter(isDefined) }); + } + })); + store.add(quickPick.onDidTriggerButton(() => resolve({ type: 'back' }))); + store.add(quickPick.onDidHide(() => resolve({ type: 'cancel' }))); + + quickPick.show(); + }); + } + private async _handleInputField( quickPick: IQuickPick, schema: MCP.NumberSchema | MCP.StringSchema, diff --git a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts index 05b9eccfead..254537d67c9 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpLanguageFeatures.ts @@ -5,7 +5,7 @@ import { computeLevenshteinDistance } from '../../../../base/common/diff/diff.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; import { findNodeAtLocation, Node, parseTree } from '../../../../base/common/json.js'; import { Disposable, DisposableStore, dispose, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { IObservable } from '../../../../base/common/observable.js'; @@ -388,9 +388,9 @@ export class McpLanguageFeatures extends Disposable implements IWorkbenchContrib function pushAnnotation(savedId: string, offset: number, saved: IResolvedValue): InlayHint { const tooltip = new MarkdownString([ - markdownCommandLink({ id: McpCommandIds.EditStoredInput, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), - markdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), - markdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), + createMarkdownCommandLink({ id: McpCommandIds.EditStoredInput, title: localize('edit', 'Edit'), arguments: [savedId, model.uri, mcpConfigurationSection, inConfig!.target] }), + createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clear', 'Clear'), arguments: [inConfig!.scope, savedId] }), + createMarkdownCommandLink({ id: McpCommandIds.RemoveStoredInput, title: localize('clearAll', 'Clear All'), arguments: [inConfig!.scope] }), ].join(' | '), { isTrusted: true }); const hint: InlayHint = { diff --git a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts index 1adc3657382..054d2edfa18 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpResourceQuickAccess.ts @@ -7,13 +7,13 @@ import { DeferredPromise, disposableTimeout, RunOnceScheduler } from '../../../. import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Event } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../base/common/observable.js'; +import { DisposableStore, IDisposable, toDisposable, Disposable } from '../../../../base/common/lifecycle.js'; +import { autorun, derived, observableValue, IObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { localize } from '../../../../nls.js'; -import { ByteSize, IFileService } from '../../../../platform/files/common/files.js'; +import { ByteSize, IFileService, IFileStat } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { DefaultQuickAccessFilterValue, IQuickAccessProvider, IQuickAccessProviderRunOptions } from '../../../../platform/quickinput/common/quickAccess.js'; @@ -21,13 +21,20 @@ import { IQuickInputService, IQuickPick, IQuickPickItem, IQuickPickSeparator } f import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatWidgetService } from '../../chat/browser/chat.js'; -import { IChatAttachmentResolveService } from '../../chat/browser/chatAttachmentResolveService.js'; -import { IChatRequestVariableEntry } from '../../chat/common/chatVariableEntries.js'; +import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js'; +import { IChatRequestVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; import { IMcpResource, IMcpResourceTemplate, IMcpServer, IMcpService, isMcpResourceTemplate, McpCapability, McpConnectionState, McpResourceURI } from '../common/mcpTypes.js'; +import { McpIcons } from '../common/mcpIcons.js'; import { IUriTemplateVariable } from '../common/uriTemplate.js'; import { openPanelChatAndGetWidget } from './openPanelChatAndGetWidget.js'; - -export class McpResourcePickHelper { +import { LinkedList } from '../../../../base/common/linkedList.js'; +import { ChatContextPickAttachment } from '../../chat/browser/attachments/chatContextPickService.js'; +import { asArray } from '../../../../base/common/arrays.js'; + +export class McpResourcePickHelper extends Disposable { + private _resources = observableValue<{ picks: Map; isBusy: boolean }>(this, { picks: new Map(), isBusy: true }); + private _pickItemsStack: LinkedList<{ server: IMcpServer; resources: (IMcpResource | IMcpResourceTemplate)[] }> = new LinkedList(); + private _inDirectory = observableValue(this, undefined); public static sep(server: IMcpServer): IQuickPickSeparator { return { id: server.definition.id, @@ -36,6 +43,33 @@ export class McpResourcePickHelper { }; } + public addCurrentMCPQuickPickItemLevel(server: IMcpServer, resources: (IMcpResource | IMcpResourceTemplate)[]): void { + let isValidPush: boolean = false; + isValidPush = this._pickItemsStack.isEmpty(); + if (!isValidPush) { + const stackedItem = this._pickItemsStack.peek(); + if (stackedItem?.server === server && stackedItem.resources === resources) { + isValidPush = false; + } else { + isValidPush = true; + } + } + if (isValidPush) { + this._pickItemsStack.push({ server, resources }); + } + + } + + public navigateBack(): boolean { + const items = this._pickItemsStack.pop(); + if (items) { + this._inDirectory.set({ server: items.server, resources: items.resources }, undefined); + return true; + } else { + return false; + } + } + public static item(resource: IMcpResource | IMcpResourceTemplate): IQuickPickItem { const iconPath = resource.icons.getUrl(22); if (isMcpResourceTemplate(resource)) { @@ -80,13 +114,75 @@ export class McpResourcePickHelper { @IQuickInputService private readonly _quickInputService: IQuickInputService, @INotificationService private readonly _notificationService: INotificationService, @IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService - ) { } + ) { + super(); + } - public async toAttachment(resource: IMcpResource | IMcpResourceTemplate): Promise { + /** + * Navigate to a resource if it's a directory. + * Returns true if the resource is a directory with children (navigation succeeded). + * Returns false if the resource is a leaf file (no navigation). + * When returning true, statefully updates the picker state to display directory contents. + */ + public async navigate(resource: IMcpResource | IMcpResourceTemplate, server: IMcpServer): Promise { if (isMcpResourceTemplate(resource)) { - return this._resourceTemplateToAttachment(resource); + return false; + } + + const uri = resource.uri; + let stat: IFileStat | undefined = undefined; + try { + stat = await this._fileService.resolve(uri, { resolveMetadata: false }); + } catch (e) { + return false; + } + + if (stat && this._isDirectoryResource(resource) && (stat.children?.length ?? 0) > 0) { + // Save current state to stack before navigating + const currentResources = this._resources.get().picks.get(server); + if (currentResources) { + this.addCurrentMCPQuickPickItemLevel(server, currentResources); + } + + // Convert all the children to IMcpResource objects + const childResources: IMcpResource[] = stat.children!.map(child => { + const mcpUri = McpResourceURI.fromServer(server.definition, child.resource.toString()); + return { + uri: mcpUri, + mcpUri: child.resource.path, + name: child.name, + title: child.name, + description: resource.description, + mimeType: undefined, + sizeInBytes: child.size, + icons: McpIcons.fromParsed(undefined) + }; + }); + this._inDirectory.set({ server, resources: childResources }, undefined); + return true; + } + return false; + } + + public toAttachment(resource: IMcpResource | IMcpResourceTemplate, server: IMcpServer): Promise | 'noop' { + const noop = 'noop'; + if (this._isDirectoryResource(resource)) { + //Check if directory + this.checkIfDirectoryAndPopulate(resource, server); + return noop; + } + if (isMcpResourceTemplate(resource)) { + return this._resourceTemplateToAttachment(resource).then(val => val || noop); } else { - return this._resourceToAttachment(resource); + return this._resourceToAttachment(resource).then(val => val || noop); + } + } + + public async checkIfDirectoryAndPopulate(resource: IMcpResource | IMcpResourceTemplate, server: IMcpServer): Promise { + try { + return !await this.navigate(resource, server); + } catch (error) { + return false; } } @@ -99,6 +195,8 @@ export class McpResourcePickHelper { } } + public checkIfNestedResources = () => !this._pickItemsStack.isEmpty(); + private async _resourceToAttachment(resource: { uri: URI; name: string; mimeType?: string }): Promise { const asImage = await this._chatAttachmentResolveService.resolveImageEditorAttachContext(resource.uri, undefined, resource.mimeType); if (asImage) { @@ -121,6 +219,7 @@ export class McpResourcePickHelper { name: rt.name, mimeType: rt.mimeType, }); + } private async _verifyUriIfNeeded({ uri, needsVerification }: { uri: URI; needsVerification: boolean }): Promise { @@ -199,7 +298,9 @@ export class McpResourcePickHelper { input.items = items; }; - let changeCancellation = store.add(new CancellationTokenSource()); + let changeCancellation = new CancellationTokenSource(); + store.add(toDisposable(() => changeCancellation.dispose(true))); + const getCompletionItems = () => { const inputValue = input.value; let promise = completions.get(inputValue); @@ -239,8 +340,7 @@ export class McpResourcePickHelper { store.add(input.onDidChangeValue(value => { input.busy = true; changeCancellation.dispose(true); - store.delete(changeCancellation); - changeCancellation = store.add(new CancellationTokenSource()); + changeCancellation = new CancellationTokenSource(); getCompletionItemsScheduler.cancel(); setItems(value); @@ -255,15 +355,25 @@ export class McpResourcePickHelper { }).finally(() => store.dispose()); } - public getPicks(onChange: (value: Map) => void, token?: CancellationToken) { - const cts = new CancellationTokenSource(token); - const store = new DisposableStore(); - store.add(toDisposable(() => cts.dispose(true))); + private _isDirectoryResource(resource: IMcpResource | IMcpResourceTemplate): boolean { + + if (resource.mimeType && resource.mimeType === 'inode/directory') { + return true; + } else if (isMcpResourceTemplate(resource)) { + return resource.template.template.endsWith('/'); + } else { + return resource.uri.path.endsWith('/'); + } + } + public getPicks(token?: CancellationToken): IObservable<{ picks: Map; isBusy: boolean }> { + const cts = new CancellationTokenSource(token); + let isBusyLoadingPicks = true; + this._register(toDisposable(() => cts.dispose(true))); // We try to show everything in-sequence to avoid flickering (#250411) as long as // it loads within 5 seconds. Otherwise we just show things as the load in parallel. let showInSequence = true; - store.add(disposableTimeout(() => { + this._register(disposableTimeout(() => { showInSequence = false; publish(); }, 5_000)); @@ -284,14 +394,14 @@ export class McpResourcePickHelper { break; } } - onChange(output); + this._resources.set({ picks: output, isBusy: isBusyLoadingPicks }, undefined); }; type Rec = { templates: DeferredPromise; resourcesSoFar: IMcpResource[]; resources: DeferredPromise }; const servers = new Map(); // Enumerate servers and start servers that need to be started to get capabilities - return Promise.all((this.explicitServers || this._mcpService.servers.get()).map(async server => { + Promise.all((this.explicitServers || this._mcpService.servers.get()).map(async server => { let cap = server.capabilities.get(); const rec: Rec = { templates: new DeferredPromise(), @@ -307,8 +417,8 @@ export class McpResourcePickHelper { resolve(undefined); } }); - store.add(cts.token.onCancellationRequested(() => resolve(undefined))); - store.add(autorun(reader => { + this._register(cts.token.onCancellationRequested(() => resolve(undefined))); + this._register(autorun(reader => { const cap2 = server.capabilities.read(reader); if (cap2 !== undefined) { resolve(cap2); @@ -331,14 +441,21 @@ export class McpResourcePickHelper { rec.templates.complete([]); rec.resources.complete([]); } - publish(); })).finally(() => { - store.dispose(); + isBusyLoadingPicks = false; + publish(); + }); + + // Use derived to compute the appropriate resource map based on directory navigation state + return derived(this, reader => { + const directoryResource = this._inDirectory.read(reader); + return directoryResource + ? { picks: new Map([[directoryResource.server, directoryResource.resources]]), isBusy: false } + : this._resources.read(reader); }); } } - export abstract class AbstractMcpResourceAccessPick { constructor( private readonly _scopeTo: IMcpServer | undefined, @@ -346,66 +463,98 @@ export abstract class AbstractMcpResourceAccessPick { @IEditorService private readonly _editorService: IEditorService, @IChatWidgetService protected readonly _chatWidgetService: IChatWidgetService, @IViewsService private readonly _viewsService: IViewsService, - ) { } + ) { + } protected applyToPick(picker: IQuickPick, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions) { picker.canAcceptInBackground = true; picker.busy = true; picker.keepScrollPosition = true; + const store = new DisposableStore(); + const goBackId = '_goback_'; - type ResourceQuickPickItem = IQuickPickItem & { resource: IMcpResource | IMcpResourceTemplate }; + type ResourceQuickPickItem = IQuickPickItem & { resource: IMcpResource | IMcpResourceTemplate; server: IMcpServer }; const attachButton = localize('mcp.quickaccess.attach', "Attach to chat"); - const helper = this._instantiationService.createInstance(McpResourcePickHelper); + const helper = store.add(this._instantiationService.createInstance(McpResourcePickHelper)); if (this._scopeTo) { helper.explicitServers = [this._scopeTo]; } - helper.getPicks(servers => { - const items: (ResourceQuickPickItem | IQuickPickSeparator)[] = []; - for (const [server, resources] of servers) { + const picksObservable = helper.getPicks(token); + store.add(autorun(reader => { + const pickItems = picksObservable.read(reader); + const isBusy = pickItems.isBusy; + const items: (ResourceQuickPickItem | IQuickPickSeparator | IQuickPickItem)[] = []; + for (const [server, resources] of pickItems.picks) { items.push(McpResourcePickHelper.sep(server)); for (const resource of resources) { const pickItem = McpResourcePickHelper.item(resource); pickItem.buttons = [{ iconClass: ThemeIcon.asClassName(Codicon.attach), tooltip: attachButton }]; - items.push({ ...pickItem, resource }); + items.push({ ...pickItem, resource, server }); } } + if (helper.checkIfNestedResources()) { + // Add go back item + const goBackItem: IQuickPickItem = { + id: goBackId, + label: localize('goBack', 'Go back ↩'), + alwaysShow: true + }; + items.push(goBackItem); + } picker.items = items; - }, token).finally(() => { - picker.busy = false; - }); + picker.busy = isBusy; + })); - const store = new DisposableStore(); store.add(picker.onDidTriggerItemButton(event => { if (event.button.tooltip === attachButton) { picker.busy = true; - helper.toAttachment((event.item as ResourceQuickPickItem).resource).then(async a => { - if (a) { - const widget = await openPanelChatAndGetWidget(this._viewsService, this._chatWidgetService); - widget?.attachmentModel.addContext(a); - } - picker.hide(); - }); + const resourceItem = event.item as ResourceQuickPickItem; + const attachment = helper.toAttachment(resourceItem.resource, resourceItem.server); + if (attachment instanceof Promise) { + attachment.then(async a => { + if (a !== 'noop') { + const widget = await openPanelChatAndGetWidget(this._viewsService, this._chatWidgetService); + widget?.attachmentModel.addContext(...asArray(a)); + } + picker.hide(); + }); + } } })); - store.add(picker.onDidAccept(async event => { - if (!event.inBackground) { - picker.hide(); // hide picker unless we accept in background - } + store.add(picker.onDidHide(() => { + helper.dispose(); + })); - if (runOptions?.handleAccept) { - runOptions.handleAccept?.(picker.activeItems[0], event.inBackground); - } else { + store.add(picker.onDidAccept(async event => { + try { + picker.busy = true; const [item] = picker.selectedItems; - const uri = await helper.toURI((item as ResourceQuickPickItem).resource); - if (uri) { - this._editorService.openEditor({ resource: uri, options: { preserveFocus: event.inBackground } }); + + // Check if go back item was selected + if (item.id === goBackId) { + helper.navigateBack(); + picker.busy = false; + return; + } + + const resourceItem = item as ResourceQuickPickItem; + const resource = resourceItem.resource; + // Try to navigate into the resource if it's a directory + const isNested = await helper.navigate(resource, resourceItem.server); + if (!isNested) { + const uri = await helper.toURI(resource); + if (uri) { + picker.hide(); + this._editorService.openEditor({ resource: uri, options: { preserveFocus: event.inBackground } }); + } } + } finally { + picker.busy = false; } })); - return store; } } @@ -442,7 +591,7 @@ export class McpResourceQuickAccess extends AbstractMcpResourceAccessPick implem @IInstantiationService instantiationService: IInstantiationService, @IEditorService editorService: IEditorService, @IChatWidgetService chatWidgetService: IChatWidgetService, - @IViewsService viewsService: IViewsService, + @IViewsService viewsService: IViewsService ) { super(undefined, instantiationService, editorService, chatWidgetService, viewsService); } diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts index 54a382ebf98..034985bdf44 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServerEditor.ts @@ -622,7 +622,7 @@ export class McpServerEditor extends EditorPane { private async openDetails(extension: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { const details = append(template.content, $('.details')); - const readmeContainer = append(details, $('.readme-container')); + const readmeContainer = append(details, $('.content-container')); const additionalDetailsContainer = append(details, $('.additional-details-container')); const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500); @@ -652,7 +652,7 @@ export class McpServerEditor extends EditorPane { private async openManifestWithAdditionalDetails(mcpServer: IWorkbenchMcpServer, template: IExtensionEditorTemplate, token: CancellationToken): Promise { const details = append(template.content, $('.details')); - const readmeContainer = append(details, $('.readme-container')); + const readmeContainer = append(details, $('.content-container')); const additionalDetailsContainer = append(details, $('.additional-details-container')); const layout = () => details.classList.toggle('narrow', this.dimension && this.dimension.width < 500); diff --git a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts index 7eb368978f8..7f4dfea45e3 100644 --- a/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts +++ b/src/vs/workbench/contrib/mcp/browser/mcpServersView.ts @@ -8,7 +8,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { markdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js'; import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js'; import { DelayedPagedModel, IPagedModel, PagedModel, IterativePagedModel } from '../../../../base/common/paging.js'; import { localize, localize2 } from '../../../../nls.js'; @@ -39,7 +39,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { DefaultViewsContext, SearchMcpServersContext } from '../../extensions/common/extensions.js'; import { VIEW_CONTAINER } from '../../extensions/browser/extensions.contribution.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { AbstractExtensionsListView } from '../../extensions/browser/extensionsViews.js'; @@ -256,7 +256,7 @@ export class McpServersListView extends AbstractExtensionsListView; + + constructor( + private readonly _uiData: IMcpToolCallUIData, + @IMcpService private readonly _mcpService: IMcpService, + @IThemeService themeService: IThemeService, + ) { + super(); + + const colorTheme = observableFromEvent( + themeService.onDidColorThemeChange, + () => { + const type = themeService.getColorTheme().type; + return type === ColorScheme.DARK || type === ColorScheme.HIGH_CONTRAST_DARK ? 'dark' : 'light'; + } + ); + + this.hostContext = derived((reader): McpApps.McpUiHostContext => { + return { + theme: colorTheme.read(reader), + styles: { + variables: { + '--color-background-primary': 'var(--vscode-editor-background)', + '--color-background-secondary': 'var(--vscode-sideBar-background)', + '--color-background-tertiary': 'var(--vscode-activityBar-background)', + '--color-background-inverse': 'var(--vscode-editor-foreground)', + '--color-background-ghost': 'transparent', + '--color-background-info': 'var(--vscode-inputValidation-infoBackground)', + '--color-background-danger': 'var(--vscode-inputValidation-errorBackground)', + '--color-background-success': 'var(--vscode-diffEditor-insertedTextBackground)', + '--color-background-warning': 'var(--vscode-inputValidation-warningBackground)', + '--color-background-disabled': 'var(--vscode-editor-inactiveSelectionBackground)', + + '--color-text-primary': 'var(--vscode-foreground)', + '--color-text-secondary': 'var(--vscode-descriptionForeground)', + '--color-text-tertiary': 'var(--vscode-disabledForeground)', + '--color-text-inverse': 'var(--vscode-editor-background)', + '--color-text-info': 'var(--vscode-textLink-foreground)', + '--color-text-danger': 'var(--vscode-errorForeground)', + '--color-text-success': 'var(--vscode-testing-iconPassed)', + '--color-text-warning': 'var(--vscode-editorWarning-foreground)', + '--color-text-disabled': 'var(--vscode-disabledForeground)', + '--color-text-ghost': 'var(--vscode-descriptionForeground)', + + '--color-border-primary': 'var(--vscode-widget-border)', + '--color-border-secondary': 'var(--vscode-editorWidget-border)', + '--color-border-tertiary': 'var(--vscode-panel-border)', + '--color-border-inverse': 'var(--vscode-foreground)', + '--color-border-ghost': 'transparent', + '--color-border-info': 'var(--vscode-inputValidation-infoBorder)', + '--color-border-danger': 'var(--vscode-inputValidation-errorBorder)', + '--color-border-success': 'var(--vscode-testing-iconPassed)', + '--color-border-warning': 'var(--vscode-inputValidation-warningBorder)', + '--color-border-disabled': 'var(--vscode-disabledForeground)', + + '--color-ring-primary': 'var(--vscode-focusBorder)', + '--color-ring-secondary': 'var(--vscode-focusBorder)', + '--color-ring-inverse': 'var(--vscode-focusBorder)', + '--color-ring-info': 'var(--vscode-inputValidation-infoBorder)', + '--color-ring-danger': 'var(--vscode-inputValidation-errorBorder)', + '--color-ring-success': 'var(--vscode-testing-iconPassed)', + '--color-ring-warning': 'var(--vscode-inputValidation-warningBorder)', + + '--font-sans': 'var(--vscode-font-family)', + '--font-mono': 'var(--vscode-editor-font-family)', + + '--font-weight-normal': 'normal', + '--font-weight-medium': '500', + '--font-weight-semibold': '600', + '--font-weight-bold': 'bold', + + '--font-text-xs-size': '10px', + '--font-text-sm-size': '11px', + '--font-text-md-size': '13px', + '--font-text-lg-size': '14px', + + '--font-heading-xs-size': '16px', + '--font-heading-sm-size': '18px', + '--font-heading-md-size': '20px', + '--font-heading-lg-size': '24px', + '--font-heading-xl-size': '32px', + '--font-heading-2xl-size': '40px', + '--font-heading-3xl-size': '48px', + + '--border-radius-xs': '2px', + '--border-radius-sm': '3px', + '--border-radius-md': '4px', + '--border-radius-lg': '6px', + '--border-radius-xl': '8px', + '--border-radius-full': '9999px', + + '--border-width-regular': '1px', + + '--font-text-xs-line-height': '1.5', + '--font-text-sm-line-height': '1.5', + '--font-text-md-line-height': '1.5', + '--font-text-lg-line-height': '1.5', + + '--font-heading-xs-line-height': '1.25', + '--font-heading-sm-line-height': '1.25', + '--font-heading-md-line-height': '1.25', + '--font-heading-lg-line-height': '1.25', + '--font-heading-xl-line-height': '1.25', + '--font-heading-2xl-line-height': '1.25', + '--font-heading-3xl-line-height': '1.25', + + '--shadow-hairline': '0 0 0 1px var(--vscode-widget-shadow)', + '--shadow-sm': '0 1px 2px 0 var(--vscode-widget-shadow)', + '--shadow-md': '0 4px 6px -1px var(--vscode-widget-shadow)', + '--shadow-lg': '0 10px 15px -3px var(--vscode-widget-shadow)', + } + }, + displayMode: 'inline', + availableDisplayModes: ['inline'], + locale: locale, + platform: isWeb ? 'web' : isMobile ? 'mobile' : 'desktop', + deviceCapabilities: { + touch: Gesture.isTouchDevice(), + hover: Gesture.isHoverDevice(), + }, + }; + }); + } + + /** + * Gets the underlying UI data. + */ + public get uiData(): IMcpToolCallUIData { + return this._uiData; + } + + /** + * Logs a message to the MCP server's logger. + */ + public async log(log: MCP.LoggingMessageNotificationParams) { + const server = await this._getServer(CancellationToken.None); + if (server) { + translateMcpLogMessage((server as McpServer).logger, log, `[App UI]`); + } + } + + /** + * Gets or finds the MCP server for this UI. + */ + private async _getServer(token: CancellationToken): Promise { + return findMcpServer(this._mcpService, s => + s.definition.id === this._uiData.serverDefinitionId && + s.collection.id === this._uiData.collectionId, + token + ); + } + + /** + * Loads the UI resource from the MCP server. + * @param token Cancellation token + * @returns The HTML content and CSP configuration + */ + public async loadResource(token: CancellationToken): Promise { + const server = await this._getServer(token); + if (!server) { + throw new Error('MCP server not found for UI resource'); + } + + const resourceResult = await McpServer.callOn(server, h => h.readResource({ uri: this._uiData.resourceUri }, token), token); + if (!resourceResult.contents || resourceResult.contents.length === 0) { + throw new Error('UI resource not found on server'); + } + + const content = resourceResult.contents[0]; + let html: string; + const mimeType = content.mimeType || 'text/html'; + + if (hasKey(content, { text: true })) { + html = content.text; + } else if (hasKey(content, { blob: true })) { + html = decodeBase64(content.blob).toString(); + } else { + throw new Error('UI resource has no content'); + } + + const meta = content._meta?.ui as McpApps.McpUiResourceMeta | undefined; + + return { + ...meta, + html, + mimeType, + }; + } + + /** + * Calls a tool on the MCP server. + * @param name Tool name + * @param params Tool parameters + * @param token Cancellation token + * @returns The tool call result + */ + public async callTool(name: string, params: Record, token: CancellationToken): Promise { + const server = await this._getServer(token); + if (!server) { + throw new Error('MCP server not found for tool call'); + } + + await startServerAndWaitForLiveTools(server, undefined, token); + + const tool = server.tools.get().find(t => t.definition.name === name); + if (!tool || !(tool.visibility & McpToolVisibility.App)) { + throw new Error(`Tool not found on server: ${name}`); + } + + const res = await tool.call(params, undefined, token); + return { + content: res.content, + isError: res.isError, + _meta: res._meta, + structuredContent: res.structuredContent, + }; + } + + /** + * Reads a resource from the MCP server. + * @param uri Resource URI + * @param token Cancellation token + * @returns The resource content + */ + public async readResource(uri: string, token: CancellationToken): Promise { + const server = await this._getServer(token); + if (!server) { + throw new Error('MCP server not found'); + } + + return await McpServer.callOn(server, h => h.readResource({ uri }, token), token); + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts index c68b5acedbc..44bbf99a82d 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpCommandIds.ts @@ -9,6 +9,7 @@ export const enum McpCommandIds { AddConfiguration = 'workbench.mcp.addConfiguration', Browse = 'workbench.mcp.browseServers', + InstallFromManifest = 'workbench.mcp.installFromManifest', BrowsePage = 'workbench.mcp.browseServersPage', BrowseResources = 'workbench.mcp.browseResources', ConfigureSamplingModels = 'workbench.mcp.configureSamplingModels', diff --git a/src/vs/workbench/contrib/mcp/common/mcpDevMode.ts b/src/vs/workbench/contrib/mcp/common/mcpDevMode.ts index 51af84c6ff4..ddfd644fcb8 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpDevMode.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpDevMode.ts @@ -12,7 +12,7 @@ import { equals as objectsEqual } from '../../../../base/common/objects.js'; import { autorun, autorunDelta, derivedOpts } from '../../../../base/common/observable.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { FileSystemProviderCapabilities, IFileService } from '../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IConfig, IDebugService, IDebugSessionOptions } from '../../debug/common/debug.js'; @@ -88,8 +88,9 @@ export class McpDevModeServerAttache extends Disposable { const excludes = pattern.filter(p => p.startsWith('!')).map(p => p.slice(1)); reader.store.add(fileService.watch(wf, { includes, excludes, recursive: true })); - const includeParse = includes.map(p => glob.parse({ base: wf.fsPath, pattern: p })); - const excludeParse = excludes.map(p => glob.parse({ base: wf.fsPath, pattern: p })); + const ignoreCase = !fileService.hasCapability(wf, FileSystemProviderCapabilities.PathCaseSensitive); + const includeParse = includes.map(p => glob.parse({ base: wf.fsPath, pattern: p }, { ignoreCase })); + const excludeParse = excludes.map(p => glob.parse({ base: wf.fsPath, pattern: p }, { ignoreCase })); reader.store.add(fileService.onDidFilesChange(e => { for (const change of [e.rawAdded, e.rawDeleted, e.rawUpdated]) { for (const uri of change) { diff --git a/src/vs/workbench/contrib/mcp/common/mcpIcons.ts b/src/vs/workbench/contrib/mcp/common/mcpIcons.ts index 8bb7e7b112e..d763c19270f 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpIcons.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpIcons.ts @@ -85,7 +85,8 @@ export function parseAndValidateMcpIcon(icons: MCP.Icons, launch: McpServerLaunc continue; } - const sizesArr = typeof icon.sizes === 'string' ? icon.sizes.split(' ') : Array.isArray(icon.sizes) ? icon.sizes : []; + // check for sizes as string for back-compat with early 2025-11-25 drafts + const sizesArr = typeof icon.sizes === 'string' ? (icon.sizes as string).split(' ') : Array.isArray(icon.sizes) ? icon.sizes : []; result.push({ src: uri, theme: icon.theme === 'light' ? IconTheme.Light : icon.theme === 'dark' ? IconTheme.Dark : IconTheme.Any, diff --git a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts index b98a48fb8c5..25e9080dd4c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpLanguageModelToolContribution.ts @@ -8,25 +8,28 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Lazy } from '../../../../base/common/lazy.js'; -import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { equals } from '../../../../base/common/objects.js'; import { autorun } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; -import { isDefined } from '../../../../base/common/types.js'; +import { isDefined, Mutable } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { IImageResizeService } from '../../../../platform/imageResize/common/imageResizeService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { mcpAppsEnabledConfig } from '../../../../platform/mcp/common/mcpManagement.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { ChatResponseResource, getAttachableImageExtension } from '../../chat/common/chatModel.js'; +import { ChatResponseResource, getAttachableImageExtension } from '../../chat/common/model/chatModel.js'; import { LanguageModelPartAudience } from '../../chat/common/languageModels.js'; -import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/languageModelToolsService.js'; +import { CountTokensCallback, ILanguageModelToolsService, IPreparedToolInvocation, IToolConfirmationMessages, IToolData, IToolImpl, IToolInvocation, IToolInvocationPreparationContext, IToolResult, IToolResultInputOutputDetails, ToolDataSource, ToolProgress, ToolSet } from '../../chat/common/tools/languageModelToolsService.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; -import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType } from './mcpTypes.js'; +import { IMcpServer, IMcpService, IMcpTool, IMcpToolResourceLinkContents, McpResourceURI, McpToolResourceLinkMimeType, McpToolVisibility } from './mcpTypes.js'; import { mcpServerToSourceData } from './mcpTypesUtils.js'; +import { ILifecycleService } from '../../../services/lifecycle/common/lifecycle.js'; interface ISyncedToolData { toolData: IToolData; @@ -42,27 +45,38 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor @IMcpService mcpService: IMcpService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IMcpRegistry private readonly _mcpRegistry: IMcpRegistry, + @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); + type Rec = { source?: ToolDataSource } & IDisposable; + // Keep tools in sync with the tools service. - const previous = this._register(new DisposableMap()); + const previous = this._register(new DisposableMap()); this._register(autorun(reader => { const servers = mcpService.servers.read(reader); const toDelete = new Set(previous.keys()); for (const server of servers) { - if (previous.has(server)) { + const previousRec = previous.get(server); + if (previousRec) { toDelete.delete(server); - continue; + if (!previousRec.source || equals(previousRec.source, mcpServerToSourceData(server, reader))) { + continue; // same definition, no need to update + } + + previousRec.dispose(); } const store = new DisposableStore(); + const rec: Rec = { dispose: () => store.dispose() }; const toolSet = new Lazy(() => { - const source = mcpServerToSourceData(server); + const source = rec.source = mcpServerToSourceData(server); + const referenceName = server.definition.label.toLowerCase().replace(/\s+/g, '-'); // see issue https://github.com/microsoft/vscode/issues/278152 const toolSet = store.add(this._toolsService.createToolSet( source, - server.definition.id, server.definition.label, + server.definition.id, + referenceName, { icon: Codicon.mcp, description: localize('mcp.toolset', "{0}: All Tools", server.definition.label) @@ -73,7 +87,7 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor }); this._syncTools(server, toolSet, store); - previous.set(server, store); + previous.set(server, rec); } for (const key of toDelete) { @@ -99,8 +113,24 @@ export class McpLanguageModelToolContribution extends Disposable implements IWor store.add(collectionData.value.toolSet.addTool(toolData)); }; + // Don't bother cleaning up tools internally during shutdown. This just costs time for no benefit. + if (this.lifecycleService.willShutdown) { + return; + } + const collection = collectionObservable.read(reader); + if (!collection) { + tools.forEach(t => t.store.dispose()); + tools.clear(); + return; + } + for (const tool of server.tools.read(reader)) { + // Skip app-only tools - they should not be registered with the language model tools service + if (!(tool.visibility & McpToolVisibility.Model)) { + continue; + } + const existing = tools.get(tool.id); const icons = tool.icons.getUrl(22); const toolData: IToolData = { @@ -166,6 +196,7 @@ class McpToolImplementation implements IToolImpl { constructor( private readonly _tool: IMcpTool, private readonly _server: IMcpServer, + @IConfigurationService private readonly _configurationService: IConfigurationService, @IProductService private readonly _productService: IProductService, @IFileService private readonly _fileService: IFileService, @IImageResizeService private readonly _imageResizeService: IImageResizeService, @@ -195,6 +226,8 @@ class McpToolImplementation implements IToolImpl { confirm.confirmResults = true; } + const mcpUiEnabled = this._configurationService.getValue(mcpAppsEnabledConfig); + return { confirmationMessages: confirm, invocationMessage: new MarkdownString(localize('msg.run', "Running {0}", title)), @@ -202,7 +235,12 @@ class McpToolImplementation implements IToolImpl { originMessage: localize('msg.subtitle', "{0} (MCP Server)", server.definition.label), toolSpecificData: { kind: 'input', - rawInput: context.parameters + rawInput: context.parameters, + mcpAppData: mcpUiEnabled && tool.uiResourceUri ? { + resourceUri: tool.uiResourceUri, + serverDefinitionId: server.definition.id, + collectionId: server.collection.id, + } : undefined, } }; } @@ -214,7 +252,7 @@ class McpToolImplementation implements IToolImpl { }; const callResult = await this._tool.callWithProgress(invocation.parameters as Record, progress, { chatRequestId: invocation.chatRequestId, chatSessionId: invocation.context?.sessionId }, token); - const details: IToolResultInputOutputDetails = { + const details: Mutable = { input: JSON.stringify(invocation.parameters, undefined, 2), output: [], isError: callResult.isError === true, @@ -319,7 +357,7 @@ class McpToolImplementation implements IToolImpl { }); if (isForModel) { - const permalink = invocation.context && ChatResponseResource.createUri(invocation.context.sessionId, invocation.callId, result.content.length, basename(uri)); + const permalink = invocation.context && ChatResponseResource.createUri(invocation.context.sessionResource, invocation.callId, result.content.length, basename(uri)); addAsLinkedResource(permalink || uri, item.resource.mimeType); } } @@ -331,6 +369,11 @@ class McpToolImplementation implements IToolImpl { result.content.push({ kind: 'text', value: JSON.stringify(callResult.structuredContent), audience: [LanguageModelPartAudience.Assistant] }); } + // Add raw MCP output for MCP App UI rendering if this tool has UI + if (this._tool.uiResourceUri) { + details.mcpOutput = callResult; + } + result.toolResultDetails = details; return result; } diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts index 6d95d0f068a..084d2f5126c 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistry.ts @@ -106,10 +106,12 @@ export class McpRegistry extends Disposable implements IMcpRegistry { public registerCollection(collection: McpCollectionDefinition): IDisposable { const currentCollections = this._collections.get(); - const toReplace = currentCollections.find(c => c.lazy && c.id === collection.id); + const toReplace = currentCollections.find(c => c.id === collection.id); // Incoming collections replace the "lazy" versions. See `ExtensionMcpDiscovery` for an example. - if (toReplace) { + if (toReplace && !toReplace.lazy) { + return Disposable.None; + } else if (toReplace) { this._collections.set(currentCollections.map(c => c === toReplace ? collection : c), undefined); } else { this._collections.set([...currentCollections, collection] @@ -542,6 +544,7 @@ export class McpRegistry extends Disposable implements IMcpRegistry { launch, logger, opts.errorOnUserInteraction, + opts.taskManager, ); } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts index 6e365d30ccb..6b589320fa6 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpRegistryTypes.ts @@ -12,6 +12,7 @@ import { ILogger, LogLevel } from '../../../../platform/log/common/log.js'; import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { IResolvedValue } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; +import { McpTaskManager } from './mcpTaskManager.js'; import { IMcpServerConnection, LazyCollectionState, McpCollectionDefinition, McpCollectionReference, McpConnectionState, McpDefinitionReference, McpServerDefinition, McpServerLaunch, McpStartServerInteraction } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; @@ -61,6 +62,9 @@ export interface IMcpResolveConnectionOptions { /** If true, throw an error if any user interaction would be required during startup. */ errorOnUserInteraction?: boolean; + + /** Shared task manager for server-side MCP tasks (survives reconnections) */ + taskManager: McpTaskManager; } export interface IMcpRegistry { diff --git a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts index ee3b966e05a..474344c93bd 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpResourceFilesystem.ts @@ -17,10 +17,12 @@ import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; import { createFileSystemProviderError, FileChangeType, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, IFileChange, IFileDeleteOptions, IFileOverwriteOptions, IFileReadStreamOptions, IFileService, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileWriteOptions, IStat, IWatchOptions } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { McpServer } from './mcpServer.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { IMcpService, McpCapability, McpResourceURI } from './mcpTypes.js'; +import { canLoadMcpNetworkResourceDirectly } from './mcpTypesUtils.js'; import { MCP } from './modelContextProtocol.js'; const MOMENTARY_CACHE_DURATION = 3000; @@ -65,6 +67,7 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @IFileService private readonly _fileService: IFileService, + @IWebContentExtractorService private readonly _webContentExtractorService: IWebContentExtractorService, ) { super(); this._register(this._fileService.registerProvider(McpResourceURI.scheme, this)); @@ -164,7 +167,6 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr if (forSameURI.length > 0) { throw createFileSystemProviderError(`File is not a directory`, FileSystemProviderErrorCode.FileNotADirectory); } - const resourcePathParts = resourceURI.pathname.split('/'); const output = new Map(); @@ -273,12 +275,26 @@ export class McpResourceFilesystem extends Disposable implements IWorkbenchContr private async _readURIInner(uri: URI, token?: CancellationToken): Promise { const { resourceURI, server } = this._decodeURI(uri); - const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token); + const matchedServer = this._mcpService.servers.get().find(s => s.definition.id === server.definition.id); + + //check for http/https resources and use web content extractor service to fetch the contents. + if (canLoadMcpNetworkResourceDirectly(resourceURI, matchedServer)) { + const extractURI = URI.parse(resourceURI.toString()); + const result = (await this._webContentExtractorService.extract([extractURI], { followRedirects: false })).at(0); + if (result?.status === 'ok') { + return { + contents: [{ uri: resourceURI.toString(), text: result.result }], + resourceURI, + forSameURI: [{ uri: resourceURI.toString(), text: result.result }] + }; + } + } + const res = await McpServer.callOn(server, r => r.readResource({ uri: resourceURI.toString() }, token), token); return { contents: res.contents, resourceURI, - forSameURI: res.contents.filter(c => equalsUrlPath(c.uri, resourceURI)), + forSameURI: res.contents.filter(c => equalsUrlPath(c.uri, resourceURI)) }; } } diff --git a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts index adfb958e818..317b4a0473b 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpSamplingService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { asArray } from '../../../../base/common/arrays.js'; import { mapFindFirst } from '../../../../base/common/arraysFind.js'; import { Sequencer } from '../../../../base/common/async.js'; import { decodeBase64 } from '../../../../base/common/buffer.js'; @@ -17,6 +18,7 @@ import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../chat/common/constants.js'; import { ChatImageMimeType, ChatMessageRole, IChatMessage, IChatMessagePart, ILanguageModelsService } from '../../chat/common/languageModels.js'; import { McpCommandIds } from './mcpCommandIds.js'; import { IMcpServerSamplingConfiguration, mcpServerSamplingSection } from './mcpConfiguration.js'; @@ -57,17 +59,19 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic async sample(opts: ISamplingOptions, token = CancellationToken.None): Promise { const messages = opts.params.messages.map((message): IChatMessage | undefined => { - const content: IChatMessagePart | undefined = message.content.type === 'text' - ? { type: 'text', value: message.content.text } - : message.content.type === 'image' || message.content.type === 'audio' - ? { type: 'image_url', value: { mimeType: message.content.mimeType as ChatImageMimeType, data: decodeBase64(message.content.data) } } - : undefined; - if (!content) { + const content: IChatMessagePart[] = asArray(message.content).map((part): IChatMessagePart | undefined => part.type === 'text' + ? { type: 'text', value: part.text } + : part.type === 'image' || part.type === 'audio' + ? { type: 'image_url', value: { mimeType: part.mimeType as ChatImageMimeType, data: decodeBase64(part.data) } } + : undefined + ).filter(isDefined); + + if (!content.length) { return undefined; } return { role: message.role === 'assistant' ? ChatMessageRole.Assistant : ChatMessageRole.User, - content: [content] + content, }; }).filter(isDefined); @@ -122,8 +126,14 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic private async _getMatchingModel(opts: ISamplingOptions): Promise { const model = await this._getMatchingModelInner(opts.server, opts.isDuringToolCall, opts.params.modelPreferences); + const globalAutoApprove = this._configurationService.getValue(ChatConfiguration.GlobalAutoApprove); if (model === ModelMatch.UnsureAllowedDuringChat) { + // In YOLO mode, auto-approve MCP sampling requests without prompting + if (globalAutoApprove) { + this._sessionSets.allowedDuringChat.set(opts.server.definition.id, true); + return this._getMatchingModel(opts); + } const retry = await this._showContextual( opts.isDuringToolCall, localize('mcp.sampling.allowDuringChat.title', 'Allow MCP tools from "{0}" to make LLM requests?', opts.server.definition.label), @@ -135,6 +145,11 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic } throw McpError.notAllowed(); } else if (model === ModelMatch.UnsureAllowedOutsideChat) { + // In YOLO mode, auto-approve MCP sampling requests without prompting + if (globalAutoApprove) { + this._sessionSets.allowedOutsideChat.set(opts.server.definition.id, true); + return this._getMatchingModel(opts); + } const retry = await this._showContextual( opts.isDuringToolCall, localize('mcp.sampling.allowOutsideChat.title', 'Allow MCP server "{0}" to make LLM requests?', opts.server.definition.label), @@ -229,7 +244,7 @@ export class McpSamplingService extends Disposable implements IMcpSamplingServic } // 2. Get the configured models, or the default model(s) - const foundModelIdsDeep = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._languageModelsService.getLanguageModelIds().filter(m => this._languageModelsService.lookupLanguageModel(m)?.isDefault); + const foundModelIdsDeep = config.allowedModels?.filter(m => !!this._languageModelsService.lookupLanguageModel(m)) || this._languageModelsService.getLanguageModelIds().filter(m => this._languageModelsService.lookupLanguageModel(m)?.isDefaultForLocation[ChatAgentLocation.Chat]); const foundModelIds = foundModelIdsDeep.flat().sort((a, b) => b.length - a.length); // Sort by length to prefer most specific diff --git a/src/vs/workbench/contrib/mcp/common/mcpServer.ts b/src/vs/workbench/contrib/mcp/common/mcpServer.ts index 07cc7064901..0c55918b43b 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServer.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServer.ts @@ -7,8 +7,10 @@ import { AsyncIterableProducer, raceCancellationError, Sequencer } from '../../. import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Iterable } from '../../../../base/common/iterator.js'; import * as json from '../../../../base/common/json.js'; +import { normalizeDriveLetter } from '../../../../base/common/labels.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { LRUCache } from '../../../../base/common/map.js'; +import { Schemas } from '../../../../base/common/network.js'; import { mapValues } from '../../../../base/common/objects.js'; import { autorun, autorunSelfDisposable, derived, disposableObservableValue, IDerivedReader, IObservable, IReader, ITransaction, observableFromEvent, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { basename } from '../../../../base/common/resources.js'; @@ -28,14 +30,16 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IOutputService } from '../../../services/output/common/output.js'; -import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js'; import { mcpActivationEvent } from './mcpConfiguration.js'; import { McpDevModeServerAttache } from './mcpDevMode.js'; import { McpIcons, parseAndValidateMcpIcon, StoredMcpIcons } from './mcpIcons.js'; import { IMcpRegistry } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; -import { extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, UserInteractionRequiredError } from './mcpTypes.js'; +import { McpTaskManager } from './mcpTaskManager.js'; +import { ElicitationKind, extensionMcpCollectionPrefix, IMcpElicitationService, IMcpIcons, IMcpPrompt, IMcpPromptMessage, IMcpResource, IMcpResourceTemplate, IMcpSamplingService, IMcpServer, IMcpServerConnection, IMcpServerStartOpts, IMcpTool, IMcpToolCallContext, McpCapability, McpCollectionDefinition, McpCollectionReference, McpConnectionFailedError, McpConnectionState, McpDefinitionReference, mcpPromptReplaceSpecialChars, McpResourceURI, McpServerCacheState, McpServerDefinition, McpServerStaticToolAvailability, McpServerTransportType, McpToolName, McpToolVisibility, MpcResponseError, UserInteractionRequiredError } from './mcpTypes.js'; import { MCP } from './modelContextProtocol.js'; +import { McpApps } from './modelContextProtocolApps.js'; import { UriTemplate } from './uriTemplate.js'; type ServerBootData = { @@ -215,6 +219,18 @@ type ValidatedMcpTool = MCP.Tool & { * in {@link McpServer._getValidatedTools}. */ serverToolName: string; + + /** + * Visibility of the tool, parsed from `_meta.ui.visibility`. + * Defaults to Model | App if not specified. + */ + visibility: McpToolVisibility; + + /** + * UI resource URI if this tool has an associated MCP App UI. + * Parsed from `_meta.ui.resourceUri`. + */ + uiResourceUri?: string; }; interface StoredServerMetadata { @@ -273,6 +289,9 @@ class CachedPrimitive { } export class McpServer extends Disposable implements IMcpServer { + /** Shared task manager that survives reconnections */ + private readonly _taskManager = this._register(new McpTaskManager()); + /** * Helper function to call the function on the handler once it's online. The * connection started if it is not already. @@ -388,6 +407,10 @@ export class McpServer extends Disposable implements IMcpServer { return fromServerResult.data?.nonce === currentNonce() ? McpServerCacheState.Live : McpServerCacheState.Outdated; }); + public get logger(): ILogger { + return this._logger; + } + private readonly _loggerId: string; private readonly _logger: ILogger; private _lastModeDebugged = false; @@ -448,10 +471,14 @@ export class McpServer extends Disposable implements IMcpServer { cnx.roots = workspaces.read(reader) .filter(w => w.uri.authority === (initialCollection.remoteAuthority || '')) - .map(w => ({ - name: w.name, - uri: URI.from(uriTransformer?.transformIncoming(w.uri) ?? w.uri).toString() - })); + .map(w => { + let uri = URI.from(uriTransformer?.transformIncoming(w.uri) ?? w.uri); + if (uri.scheme === Schemas.file) { // #271812 + uri = URI.file(normalizeDriveLetter(uri.fsPath, true)); + } + + return { name: w.name, uri: uri.toString() }; + }); })); // 2. Populate this.tools when we connect to a server. @@ -481,7 +508,7 @@ export class McpServer extends Disposable implements IMcpServer { }) .map((o, reader) => o?.promiseResult.read(reader)?.data), (entry) => entry.tools, - (entry) => entry.map(def => new McpTool(this, toolPrefix, def)).sort((a, b) => a.compare(b)), + (entry) => entry.map(def => this._instantiationService.createInstance(McpTool, this, toolPrefix, def)).sort((a, b) => a.compare(b)), [], ); @@ -583,6 +610,7 @@ export class McpServer extends Disposable implements IMcpServer { definitionRef: this.definition, debug, errorOnUserInteraction, + taskManager: this._taskManager, }); if (!connection) { return { state: McpConnectionState.Kind.Stopped }; @@ -602,12 +630,12 @@ export class McpServer extends Disposable implements IMcpServer { const start = Date.now(); let state = await connection.start({ - createMessageRequestHandler: params => this._samplingService.sample({ + createMessageRequestHandler: (params, token) => this._samplingService.sample({ isDuringToolCall: this.runningToolCalls.size > 0, server: this, params, - }).then(r => r.sample), - elicitationRequestHandler: req => { + }, token).then(r => r.sample), + elicitationRequestHandler: async (req, token) => { const serverInfo = connection.handler.get()?.serverInfo; if (serverInfo) { this._telemetryService.publicLog2('mcp.elicitationRequested', { @@ -616,7 +644,9 @@ export class McpServer extends Disposable implements IMcpServer { }); } - return this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, CancellationToken.None); + const r = await this._elicitationService.elicit(this, Iterable.first(this.runningToolCalls), req, token || CancellationToken.None); + r.dispose(); + return r.value; } }); @@ -728,10 +758,28 @@ export class McpServer extends Disposable implements IMcpServer { } private async _normalizeTool(originalTool: MCP.Tool): Promise { + // Parse MCP Apps UI metadata from _meta.ui + const uiMeta = originalTool._meta?.ui as McpApps.McpUiToolMeta | undefined; + + // Compute visibility from _meta.ui.visibility, defaulting to Model | App + let visibility: McpToolVisibility = McpToolVisibility.Model | McpToolVisibility.App; + if (uiMeta?.visibility && Array.isArray(uiMeta.visibility)) { + visibility &= 0; + + if (uiMeta.visibility.includes('model')) { + visibility |= McpToolVisibility.Model; + } + if (uiMeta.visibility.includes('app')) { + visibility |= McpToolVisibility.App; + } + } + const tool: ValidatedMcpTool = { ...originalTool, serverToolName: originalTool.name, _icons: this._parseIcons(originalTool), + visibility, + uiResourceUri: uiMeta?.resourceUri, }; if (!tool.description) { // Ensure a description is provided for each tool, #243919 @@ -967,42 +1015,27 @@ export class McpTool implements IMcpTool { readonly id: string; readonly referenceName: string; readonly icons: IMcpIcons; + readonly visibility: McpToolVisibility; public get definition(): MCP.Tool { return this._definition; } + public get uiResourceUri(): string | undefined { return this._definition.uiResourceUri; } constructor( private readonly _server: McpServer, idPrefix: string, private readonly _definition: ValidatedMcpTool, + @IMcpElicitationService private readonly _elicitationService: IMcpElicitationService, ) { this.referenceName = _definition.name.replaceAll('.', '_'); this.id = (idPrefix + _definition.name).replaceAll('.', '_').slice(0, McpToolName.MaxLength); this.icons = McpIcons.fromStored(this._definition._icons); + this.visibility = _definition.visibility ?? (McpToolVisibility.Model | McpToolVisibility.App); } async call(params: Record, context?: IMcpToolCallContext, token?: CancellationToken): Promise { - // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. - const name = this._definition.serverToolName ?? this._definition.name; if (context) { this._server.runningToolCalls.add(context); } try { - const meta: Record = {}; - if (context?.chatSessionId) { - meta['vscode.conversationId'] = context.chatSessionId; - } - if (context?.chatRequestId) { - meta['vscode.requestId'] = context.chatRequestId; - } - - const result = await McpServer.callOn(this._server, h => h.callTool({ - name, - arguments: params, - _meta: Object.keys(meta).length > 0 ? meta : undefined - }, token), token); - - // Wait for tools to refresh for dynamic servers (#261611) - await this._server.awaitToolRefresh(); - - return result; + return await this._callWithProgress(params, undefined, context, token); } finally { if (context) { this._server.runningToolCalls.delete(context); } } @@ -1017,20 +1050,23 @@ export class McpTool implements IMcpTool { } } - _callWithProgress(params: Record, progress: ToolProgress, context?: IMcpToolCallContext, token?: CancellationToken, allowRetry = true): Promise { + _callWithProgress(params: Record, progress: ToolProgress | undefined, context?: IMcpToolCallContext, token = CancellationToken.None, allowRetry = true): Promise { // serverToolName is always set now, but older cache entries (from 1.99-Insiders) may not have it. const name = this._definition.serverToolName ?? this._definition.name; - const progressToken = generateUuid(); + const progressToken = progress ? generateUuid() : undefined; + const store = new DisposableStore(); return McpServer.callOn(this._server, async h => { - const listener = h.onDidReceiveProgressNotification((e) => { - if (e.params.progressToken === progressToken) { - progress.report({ - message: e.params.message, - progress: e.params.total !== undefined && e.params.progress !== undefined ? e.params.progress / e.params.total : undefined, - }); - } - }); + if (progress) { + store.add(h.onDidReceiveProgressNotification((e) => { + if (e.params.progressToken === progressToken) { + progress.report({ + message: e.params.message, + progress: e.params.total !== undefined && e.params.progress !== undefined ? e.params.progress / e.params.total : undefined, + }); + } + })); + } const meta: Record = { progressToken }; if (context?.chatSessionId) { @@ -1040,13 +1076,29 @@ export class McpTool implements IMcpTool { meta['vscode.requestId'] = context.chatRequestId; } + const taskHint = this._definition.execution?.taskSupport; + const serverSupportsTasksForTools = h.capabilities.tasks?.requests?.tools?.call !== undefined; + const shouldUseTask = serverSupportsTasksForTools && (taskHint === 'required' || taskHint === 'optional'); + try { - const result = await h.callTool({ name, arguments: params, _meta: meta }, token); + const result = await h.callTool({ + name, + arguments: params, + task: shouldUseTask ? {} : undefined, + _meta: meta, + }, token); + // Wait for tools to refresh for dynamic servers (#261611) await this._server.awaitToolRefresh(); return result; } catch (err) { + // Handle URL elicitation required error + if (err instanceof MpcResponseError && err.code === MCP.URL_ELICITATION_REQUIRED && allowRetry) { + await this._handleElicitationErr(err, context, token); + return this._callWithProgress(params, progress, context, token, false); + } + const state = this._server.connectionState.get(); if (allowRetry && state.state === McpConnectionState.Kind.Error && state.shouldRetry) { return this._callWithProgress(params, progress, context, token, false); @@ -1054,11 +1106,32 @@ export class McpTool implements IMcpTool { throw err; } } finally { - listener.dispose(); + store.dispose(); } }, token); } + private async _handleElicitationErr(err: MpcResponseError, context: IMcpToolCallContext | undefined, token: CancellationToken) { + const elicitations = (err.data as MCP.URLElicitationRequiredError['error']['data'])?.elicitations; + if (Array.isArray(elicitations) && elicitations.length > 0) { + for (const elicitation of elicitations) { + const elicitResult = await this._elicitationService.elicit(this._server, context, elicitation, token); + + try { + if (elicitResult.value.action !== 'accept') { + throw err; + } + + if (elicitResult.kind === ElicitationKind.URL) { + await elicitResult.wait; + } + } finally { + elicitResult.dispose(); + } + } + } + } + compare(other: IMcpTool): number { return this._definition.name.localeCompare(other.definition.name); } diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts index f300f2de866..7adde629336 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerConnection.ts @@ -12,6 +12,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogger, log, LogLevel } from '../../../../platform/log/common/log.js'; import { IMcpHostDelegate, IMcpMessageTransport } from './mcpRegistryTypes.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { McpTaskManager } from './mcpTaskManager.js'; import { IMcpClientMethods, IMcpServerConnection, McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from './mcpTypes.js'; export class McpServerConnection extends Disposable implements IMcpServerConnection { @@ -29,6 +30,7 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect public readonly launchDefinition: McpServerLaunch, private readonly _logger: ILogger, private readonly _errorOnUserInteraction: boolean | undefined, + private readonly _taskManager: McpTaskManager, @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); @@ -78,10 +80,11 @@ export class McpServerConnection extends Disposable implements IMcpServerConnect if (state.state === McpConnectionState.Kind.Running && !didStart) { didStart = true; McpServerRequestHandler.create(this._instantiationService, { + ...methods, launch, logger: this._logger, requestLogLevel: this.definition.devMode ? LogLevel.Info : LogLevel.Debug, - ...methods, + taskManager: this._taskManager, }, cts.token).then( handler => { if (!store.isDisposed) { diff --git a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts index ca09440ec4a..b71f9791274 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpServerRequestHandler.ts @@ -4,19 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { equals } from '../../../../base/common/arrays.js'; -import { assertNever } from '../../../../base/common/assert.js'; -import { DeferredPromise, IntervalTimer } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { assertNever, softAssertNever } from '../../../../base/common/assert.js'; +import { DeferredPromise, disposableTimeout, IntervalTimer } from '../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../base/common/observable.js'; +import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; +import { autorun, ISettableObservable, ObservablePromise, observableValue, transaction } from '../../../../base/common/observable.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { canLog, ILogger, log, LogLevel } from '../../../../platform/log/common/log.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IMcpMessageTransport } from './mcpRegistryTypes.js'; +import { IMcpTaskInternal, McpTaskManager } from './mcpTaskManager.js'; import { IMcpClientMethods, McpConnectionState, McpError, MpcResponseError } from './mcpTypes.js'; +import { isTaskResult, translateMcpLogMessage } from './mcpTypesUtils.js'; import { MCP } from './modelContextProtocol.js'; /** @@ -38,6 +40,8 @@ export interface IMcpServerRequestHandlerOptions extends IMcpClientMethods { logger: ILogger; /** Log level MCP messages is logged at */ requestLogLevel?: LogLevel; + /** Task manager for server-side MCP tasks (shared across reconnections) */ + taskManager: McpTaskManager; } /** @@ -83,6 +87,9 @@ export class McpServerRequestHandler extends Disposable { private readonly _onDidReceiveProgressNotification = this._register(new Emitter()); readonly onDidReceiveProgressNotification = this._onDidReceiveProgressNotification.event; + private readonly _onDidReceiveElicitationCompleteNotification = this._register(new Emitter()); + readonly onDidReceiveElicitationCompleteNotification = this._onDidReceiveElicitationCompleteNotification.event; + private readonly _onDidChangeResourceList = this._register(new Emitter()); readonly onDidChangeResourceList = this._onDidChangeResourceList.event; @@ -117,7 +124,20 @@ export class McpServerRequestHandler extends Disposable { capabilities: { roots: { listChanged: true }, sampling: opts.createMessageRequestHandler ? {} : undefined, - elicitation: opts.elicitationRequestHandler ? {} : undefined, + elicitation: opts.elicitationRequestHandler ? { form: {}, url: {} } : undefined, + tasks: { + list: {}, + cancel: {}, + requests: { + sampling: opts.createMessageRequestHandler ? { createMessage: {} } : undefined, + elicitation: opts.elicitationRequestHandler ? { create: {} } : undefined, + }, + }, + extensions: { + 'io.modelcontextprotocol/ui': { + mimeTypes: ['text/html;profile=mcp-app'] + } + } }, clientInfo: { name: productService.nameLong, @@ -125,7 +145,6 @@ export class McpServerRequestHandler extends Disposable { } } }, token); - mcp._serverInit = initialized; mcp._sendLogLevelToServer(opts.logger.getLevel()); @@ -148,6 +167,7 @@ export class McpServerRequestHandler extends Disposable { private readonly _requestLogLevel: LogLevel; private readonly _createMessageRequestHandler: IMcpServerRequestHandlerOptions['createMessageRequestHandler']; private readonly _elicitationRequestHandler: IMcpServerRequestHandlerOptions['elicitationRequestHandler']; + private readonly _taskManager: McpTaskManager; protected constructor({ launch, @@ -155,6 +175,7 @@ export class McpServerRequestHandler extends Disposable { createMessageRequestHandler, elicitationRequestHandler, requestLogLevel = LogLevel.Debug, + taskManager, }: IMcpServerRequestHandlerOptions) { super(); this._launch = launch; @@ -162,6 +183,18 @@ export class McpServerRequestHandler extends Disposable { this._requestLogLevel = requestLogLevel; this._createMessageRequestHandler = createMessageRequestHandler; this._elicitationRequestHandler = elicitationRequestHandler; + this._taskManager = taskManager; + + // Attach this handler to the task manager + this._taskManager.setHandler(this); + this._register(this._taskManager.onDidUpdateTask(task => { + this.send({ + jsonrpc: MCP.JSONRPC_VERSION, + method: 'notifications/tasks/status', + params: task + } satisfies MCP.TaskStatusNotification); + })); + this._register(toDisposable(() => this._taskManager.setHandler(undefined))); this._register(launch.onDidReceiveMessage(message => this.handleMessage(message))); this._register(autorun(reader => { @@ -293,22 +326,26 @@ export class McpServerRequestHandler extends Disposable { /** * Handle successful responses */ - private handleResult(response: MCP.JSONRPCResponse): void { - const request = this._pendingRequests.get(response.id); - if (request) { - this._pendingRequests.delete(response.id); - request.promise.complete(response.result); + private handleResult(response: MCP.JSONRPCResultResponse): void { + if (response.id !== undefined) { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.complete(response.result); + } } } /** * Handle error responses */ - private handleError(response: MCP.JSONRPCError): void { - const request = this._pendingRequests.get(response.id); - if (request) { - this._pendingRequests.delete(response.id); - request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + private handleError(response: MCP.JSONRPCErrorResponse): void { + if (response.id !== undefined) { + const request = this._pendingRequests.get(response.id); + if (request) { + this._pendingRequests.delete(response.id); + request.promise.error(new MpcResponseError(response.error.message, response.error.code, response.error.data)); + } } } @@ -323,9 +360,39 @@ export class McpServerRequestHandler extends Disposable { } else if (request.method === 'roots/list') { response = this.handleRootsList(request); } else if (request.method === 'sampling/createMessage' && this._createMessageRequestHandler) { - response = await this._createMessageRequestHandler(request.params as MCP.CreateMessageRequest['params']); + // Check if this is a task-augmented request + if (request.params.task) { + const taskResult = this._taskManager.createTask( + request.params.task.ttl ?? null, + (token) => this._createMessageRequestHandler!(request.params, token) + ); + taskResult._meta ??= {}; + taskResult._meta['io.modelcontextprotocol/related-task'] = { taskId: taskResult.task.taskId }; + response = taskResult; + } else { + response = await this._createMessageRequestHandler(request.params); + } } else if (request.method === 'elicitation/create' && this._elicitationRequestHandler) { - response = await this._elicitationRequestHandler(request.params as MCP.ElicitRequest['params']); + // Check if this is a task-augmented request + if (request.params.task) { + const taskResult = this._taskManager.createTask( + request.params.task.ttl ?? null, + (token) => this._elicitationRequestHandler!(request.params, token) + ); + taskResult._meta ??= {}; + taskResult._meta['io.modelcontextprotocol/related-task'] = { taskId: taskResult.task.taskId }; + response = taskResult; + } else { + response = await this._elicitationRequestHandler(request.params); + } + } else if (request.method === 'tasks/get') { + response = this._taskManager.getTask(request.params.taskId); + } else if (request.method === 'tasks/result') { + response = await this._taskManager.getTaskResult(request.params.taskId); + } else if (request.method === 'tasks/cancel') { + response = this._taskManager.cancelTask(request.params.taskId); + } else if (request.method === 'tasks/list') { + response = this._taskManager.listTasks(); } else { throw McpError.methodNotFound(request.method); } @@ -336,7 +403,7 @@ export class McpServerRequestHandler extends Disposable { e = McpError.unknown(e); } - const errorResponse: MCP.JSONRPCError = { + const errorResponse: MCP.JSONRPCErrorResponse = { jsonrpc: MCP.JSONRPC_VERSION, id: request.id, error: { @@ -374,44 +441,29 @@ export class McpServerRequestHandler extends Disposable { case 'notifications/prompts/list_changed': this._onDidChangePromptList.fire(); return; + case 'notifications/elicitation/complete': + this._onDidReceiveElicitationCompleteNotification.fire(request); + return; + case 'notifications/tasks/status': + this._taskManager.getClientTask(request.params.taskId)?.onDidUpdateState(request.params); + return; + default: + softAssertNever(request); } } private handleCancelledNotification(request: MCP.CancelledNotification): void { - const pendingRequest = this._pendingRequests.get(request.params.requestId); - if (pendingRequest) { - this._pendingRequests.delete(request.params.requestId); - pendingRequest.promise.cancel(); + if (request.params.requestId) { + const pendingRequest = this._pendingRequests.get(request.params.requestId); + if (pendingRequest) { + this._pendingRequests.delete(request.params.requestId); + pendingRequest.promise.cancel(); + } } } private handleLoggingNotification(request: MCP.LoggingMessageNotification): void { - let contents = typeof request.params.data === 'string' ? request.params.data : JSON.stringify(request.params.data); - if (request.params.logger) { - contents = `${request.params.logger}: ${contents}`; - } - - switch (request.params?.level) { - case 'debug': - this.logger.debug(contents); - break; - case 'info': - case 'notice': - this.logger.info(contents); - break; - case 'warning': - this.logger.warn(contents); - break; - case 'error': - case 'critical': - case 'alert': - case 'emergency': - this.logger.error(contents); - break; - default: - this.logger.info(contents); - break; - } + translateMcpLogMessage(this.logger, request.params); } /** @@ -538,10 +590,22 @@ export class McpServerRequestHandler extends Disposable { } /** - * Call a specific tool + * Call a specific tool. Supports tasks automatically if `task` is set on the request. */ - callTool(params: MCP.CallToolRequest['params'] & MCP.Request['params'], token?: CancellationToken): Promise { - return this.sendRequest({ method: 'tools/call', params }, token); + async callTool(params: MCP.CallToolRequest['params'] & MCP.Request['params'], token?: CancellationToken): Promise { + const response = await this.sendRequest({ method: 'tools/call', params }, token); + + if (isTaskResult(response)) { + const task = new McpTask(response.task, token); + this._taskManager.adoptClientTask(task); + task.setHandler(this); + return task.result.finally(() => { + this._taskManager.abandonClientTask(task.id); + }); + } + + return response; + } /** @@ -557,8 +621,204 @@ export class McpServerRequestHandler extends Disposable { complete(params: MCP.CompleteRequest['params'], token?: CancellationToken): Promise { return this.sendRequest({ method: 'completion/complete', params }, token); } + + /** + * Get task status + */ + getTask(params: { taskId: string }, token?: CancellationToken): Promise { + return this.sendRequest({ method: 'tasks/get', params }, token); + } + + /** + * Get task result + */ + getTaskResult(params: { taskId: string }, token?: CancellationToken): Promise { + return this.sendRequest({ method: 'tasks/result', params }, token); + } + + /** + * Cancel a task + */ + cancelTask(params: { taskId: string }, token?: CancellationToken): Promise { + return this.sendRequest({ method: 'tasks/cancel', params }, token); + } + + /** + * List all tasks + */ + listTasks(params?: MCP.ListTasksRequest['params'], token?: CancellationToken): Promise { + return Iterable.asyncToArrayFlat( + this.sendRequestPaginated( + 'tasks/list', result => result.tasks, params, token + ) + ); + } +} + +function isTaskInTerminalState(task: MCP.Task): boolean { + return task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled'; } +/** + * Implementation of a task that handles polling, status notifications, and handler reconnections. It implements the task polling loop internally and can also be + * updated externally via `onDidUpdateState`, when notifications are received + * for example. + * @internal + */ +export class McpTask extends Disposable implements IMcpTaskInternal { + private readonly promise = new DeferredPromise(); + + public get result(): Promise { + return this.promise.p; + } + + public get id() { + return this._task.taskId; + } + + private _lastTaskState: ISettableObservable; + private _handler = observableValue('mcpTaskHandler', undefined); + + constructor( + private readonly _task: MCP.Task, + _token: CancellationToken = CancellationToken.None + ) { + super(); + + const expiresAt = _task.ttl ? (Date.now() + _task.ttl) : undefined; + this._lastTaskState = observableValue('lastTaskState', this._task); + + const store = this._register(new DisposableStore()); + + // Handle external cancellation token + if (_token.isCancellationRequested) { + this._lastTaskState.set({ ...this._task, status: 'cancelled' }, undefined); + } else { + store.add(_token.onCancellationRequested(() => { + const current = this._lastTaskState.get(); + if (!isTaskInTerminalState(current)) { + this._lastTaskState.set({ ...current, status: 'cancelled' }, undefined); + } + })); + } + + // Handle TTL expiration with an explicit timeout + if (expiresAt) { + const ttlTimeout = expiresAt - Date.now(); + if (ttlTimeout <= 0) { + this._lastTaskState.set({ ...this._task, status: 'cancelled', statusMessage: 'Task timed out.' }, undefined); + } else { + store.add(disposableTimeout(() => { + const current = this._lastTaskState.get(); + if (!isTaskInTerminalState(current)) { + this._lastTaskState.set({ ...current, status: 'cancelled', statusMessage: 'Task timed out.' }, undefined); + } + }, ttlTimeout)); + } + } + + // A `tasks/result` call triggered by an input_required state. + const inputRequiredLookup = observableValue | undefined>('activeResultLookup', undefined); + + // 1. Poll for task updates when the task isn't in a terminal state + store.add(autorun(reader => { + const current = this._lastTaskState.read(reader); + if (isTaskInTerminalState(current)) { + return; + } + + // When a task goes into the input_required state, by spec we should call + // `tasks/result` which can return an SSE stream of task updates. No need + // to poll while such a lookup is going on, but once it resolves we should + // clear and update our state. + const lookup = inputRequiredLookup.read(reader); + if (lookup) { + const result = lookup.promiseResult.read(reader); + return transaction(tx => { + if (!result) { + // still ongoing + } else if (result.data) { + inputRequiredLookup.set(undefined, tx); + this._lastTaskState.set(result.data, tx); + } else { + inputRequiredLookup.set(undefined, tx); + if (result.error instanceof McpError && result.error.code === MCP.INVALID_PARAMS) { + this._lastTaskState.set({ ...current, status: 'cancelled' }, undefined); + } else { + // Maybe a connection error -- start polling again + this._lastTaskState.set({ ...current, status: 'working' }, undefined); + } + } + }); + } + + const handler = this._handler.read(reader); + if (!handler) { + return; + } + + const pollInterval = _task.pollInterval ?? 2000; + const cts = new CancellationTokenSource(_token); + reader.store.add(toDisposable(() => cts.dispose(true))); + reader.store.add(disposableTimeout(() => { + handler.getTask({ taskId: current.taskId }, cts.token) + .catch((e): MCP.Task | undefined => { + if (e instanceof McpError && e.code === MCP.INVALID_PARAMS) { + return { ...current, status: 'cancelled' }; + } else { + return { ...current }; // errors are already logged, keep in current state + } + }) + .then(r => { + if (r && !cts.token.isCancellationRequested) { + this._lastTaskState.set(r, undefined); + } + }); + }, pollInterval)); + })); + + // 2. Get the result once it's available (or propagate errors). Trigger + // input_required handling as needed. Only react when the status itself changes. + const lastStatus = this._lastTaskState.map(task => task.status); + store.add(autorun(reader => { + const status = lastStatus.read(reader); + if (status === 'failed') { + const current = this._lastTaskState.read(undefined); + this.promise.error(new Error(`Task ${current.taskId} failed: ${current.statusMessage ?? 'unknown error'}`)); + store.dispose(); + } else if (status === 'cancelled') { + this.promise.cancel(); + store.dispose(); + } else if (status === 'input_required') { + const handler = this._handler.read(reader); + if (handler) { + const current = this._lastTaskState.read(undefined); + const cts = new CancellationTokenSource(_token); + reader.store.add(toDisposable(() => cts.dispose(true))); + inputRequiredLookup.set(new ObservablePromise(handler.getTask({ taskId: current.taskId }, cts.token)), undefined); + } + } else if (status === 'completed') { + const handler = this._handler.read(reader); + if (handler) { + this.promise.settleWith(handler.getTaskResult({ taskId: _task.taskId }, _token) as Promise); + store.dispose(); + } + } else if (status === 'working') { + // no-op + } else { + softAssertNever(status); + } + })); + } + + onDidUpdateState(task: MCP.Task) { + this._lastTaskState.set(task, undefined); + } + + setHandler(handler: McpServerRequestHandler | undefined): void { + this._handler.set(handler, undefined); + } +} /** * Maps VSCode LogLevel to MCP LoggingLevel diff --git a/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts b/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts new file mode 100644 index 00000000000..689e457469c --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/mcpTaskManager.ts @@ -0,0 +1,267 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { disposableTimeout } from '../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; +import type { McpServerRequestHandler } from './mcpServerRequestHandler.js'; +import { McpError } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + +export interface IMcpTaskInternal extends IDisposable { + readonly id: string; + onDidUpdateState(task: MCP.Task): void; + setHandler(handler: McpServerRequestHandler | undefined): void; +} + +interface TaskEntry extends IDisposable { + task: MCP.Task; + result?: MCP.Result; + error?: MCP.Error; + cts: CancellationTokenSource; + /** Time when the task was created (client time), used to calculate TTL expiration */ + createdAtTime: number; + /** Promise that resolves when the task execution completes */ + executionPromise: Promise; +} + +/** + * Manages in-memory task state for server-side MCP tasks (sampling and elicitation). + * Also tracks client-side tasks to survive handler reconnections. + * Lifecycle is tied to the McpServer instance. + */ +export class McpTaskManager extends Disposable { + private readonly _serverTasks = this._register(new DisposableMap()); + private readonly _clientTasks = this._register(new DisposableMap()); + private readonly _onDidUpdateTask = this._register(new Emitter()); + public readonly onDidUpdateTask = this._onDidUpdateTask.event; + + /** + * Attach a new handler to this task manager. + * Updates all client tasks to use the new handler. + */ + setHandler(handler: McpServerRequestHandler | undefined): void { + for (const task of this._clientTasks.values()) { + task.setHandler(handler); + } + } + + /** + * Get a client task by ID for status notification handling. + */ + getClientTask(taskId: string): IMcpTaskInternal | undefined { + return this._clientTasks.get(taskId); + } + + /** + * Track a new client task. + */ + adoptClientTask(task: IMcpTaskInternal): void { + this._clientTasks.set(task.id, task); + } + + /** + * Untracks a client task. + */ + abandonClientTask(taskId: string): void { + this._clientTasks.deleteAndDispose(taskId); + } + + /** + * Create a new task and execute it asynchronously. + * Returns the task immediately while execution continues in the background. + */ + public createTask( + ttl: number | null, + executor: (token: CancellationToken) => Promise + ): MCP.CreateTaskResult { + const taskId = generateUuid(); + const createdAt = new Date().toISOString(); + const createdAtTime = Date.now(); + + const task: MCP.Task = { + taskId, + status: 'working', + createdAt, + ttl, + lastUpdatedAt: new Date().toISOString(), + pollInterval: 1000, // Suggest 1 second polling interval + }; + + const store = new DisposableStore(); + const cts = new CancellationTokenSource(); + store.add(toDisposable(() => cts.dispose(true))); + + const executionPromise = this._executeTask(taskId, executor, cts.token); + + // Delete the task after its TTL. Or, if no TTL is given, delete it shortly after the task completes. + if (ttl) { + store.add(disposableTimeout(() => this._serverTasks.deleteAndDispose(taskId), ttl)); + } else { + executionPromise.finally(() => { + const timeout = this._register(disposableTimeout(() => { + this._serverTasks.deleteAndDispose(taskId); + this._store.delete(timeout); + }, 60_000)); + }); + } + + this._serverTasks.set(taskId, { + task, + cts, + dispose: () => store.dispose(), + createdAtTime, + executionPromise, + }); + + return { task }; + } + + /** + * Execute a task asynchronously and update its state. + */ + private async _executeTask( + taskId: string, + executor: (token: CancellationToken) => Promise, + token: CancellationToken + ): Promise { + try { + const result = await executor(token); + this._updateTaskStatus(taskId, 'completed', undefined, result); + } catch (error) { + if (error instanceof CancellationError) { + this._updateTaskStatus(taskId, 'cancelled', 'Task was cancelled by the client'); + } else if (error instanceof McpError) { + this._updateTaskStatus(taskId, 'failed', error.message, undefined, { + code: error.code, + message: error.message, + data: error.data, + }); + } else if (error instanceof Error) { + this._updateTaskStatus(taskId, 'failed', error.message, undefined, { + code: MCP.INTERNAL_ERROR, + message: error.message, + }); + } else { + this._updateTaskStatus(taskId, 'failed', 'Unknown error', undefined, { + code: MCP.INTERNAL_ERROR, + message: 'Unknown error', + }); + } + } + } + + /** + * Update task status and optionally store result or error. + */ + private _updateTaskStatus( + taskId: string, + status: MCP.TaskStatus, + statusMessage?: string, + result?: MCP.Result, + error?: MCP.Error + ): void { + const entry = this._serverTasks.get(taskId); + if (!entry) { + return; + } + + entry.task.status = status; + entry.task.lastUpdatedAt = new Date().toISOString(); + + if (statusMessage !== undefined) { + entry.task.statusMessage = statusMessage; + } + if (result !== undefined) { + entry.result = result; + } + if (error !== undefined) { + entry.error = error; + } + + this._onDidUpdateTask.fire({ ...entry.task }); + } + + /** + * Get the current state of a task. + * Returns an error if the task doesn't exist or has expired. + */ + public getTask(taskId: string): MCP.GetTaskResult { + const entry = this._serverTasks.get(taskId); + if (!entry) { + throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`); + } + + return { ...entry.task }; + } + + /** + * Get the result of a completed task. + * Blocks until the task completes if it's still in progress. + */ + public async getTaskResult(taskId: string): Promise { + const entry = this._serverTasks.get(taskId); + if (!entry) { + throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`); + } + + if (entry.task.status === 'working' || entry.task.status === 'input_required') { + await entry.executionPromise; + } + + // Refresh entry after waiting + const updatedEntry = this._serverTasks.get(taskId); + if (!updatedEntry) { + throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`); + } + + if (updatedEntry.error) { + throw new McpError(updatedEntry.error.code, updatedEntry.error.message, updatedEntry.error.data); + } + + if (!updatedEntry.result) { + throw new McpError(MCP.INTERNAL_ERROR, 'Task completed but no result available'); + } + + return updatedEntry.result; + } + + /** + * Cancel a task. + */ + public cancelTask(taskId: string): MCP.CancelTaskResult { + const entry = this._serverTasks.get(taskId); + if (!entry) { + throw new McpError(MCP.INVALID_PARAMS, `Task not found: ${taskId}`); + } + + // Check if already in terminal status + if (entry.task.status === 'completed' || entry.task.status === 'failed' || entry.task.status === 'cancelled') { + throw new McpError(MCP.INVALID_PARAMS, `Cannot cancel task in ${entry.task.status} status`); + } + + entry.task.status = 'cancelled'; + entry.task.statusMessage = 'Task was cancelled by the client'; + entry.cts.cancel(); + + return { ...entry.task }; + } + + /** + * List all tasks. + */ + public listTasks(): MCP.ListTasksResult { + const tasks: MCP.Task[] = []; + + for (const entry of this._serverTasks.values()) { + tasks.push({ ...entry.task }); + } + + return { tasks }; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts index 2b1c0fc8f0d..9a38b10c35f 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypes.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypes.ts @@ -28,7 +28,7 @@ import { IMcpDevModeConfig, IMcpServerConfiguration } from '../../../../platform import { StorageScope } from '../../../../platform/storage/common/storage.js'; import { IWorkspaceFolder, IWorkspaceFolderData } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchLocalMcpServer, IWorkbencMcpServerInstallOptions } from '../../../services/mcp/common/mcpWorkbenchManagementService.js'; -import { ToolProgress } from '../../chat/common/languageModelToolsService.js'; +import { ToolProgress } from '../../chat/common/tools/languageModelToolsService.js'; import { IMcpServerSamplingConfiguration } from './mcpConfiguration.js'; import { McpServerRequestHandler } from './mcpServerRequestHandler.js'; import { MCP } from './modelContextProtocol.js'; @@ -184,6 +184,7 @@ export namespace McpServerDefinition { export function equals(a: McpServerDefinition, b: McpServerDefinition): boolean { return a.id === b.id && a.label === b.label + && a.cacheNonce === b.cacheNonce && arraysEqual(a.roots, b.roots, (a, b) => a.toString() === b.toString()) && objectsEqual(a.launch, b.launch) && objectsEqual(a.presentation, b.presentation) @@ -315,7 +316,6 @@ export namespace McpServerTrust { } } - export interface IMcpServer extends IDisposable { readonly collection: McpCollectionReference; readonly definition: McpDefinitionReference; @@ -438,6 +438,30 @@ export interface IMcpToolCallContext { chatRequestId?: string; } +/** + * Visibility of an MCP tool, based on the MCP Apps `_meta.ui.visibility` field. + * @see https://github.com/anthropics/mcp/blob/main/apps.md + */ +export const enum McpToolVisibility { + /** Tool is visible to and callable by the language model */ + Model = 1 << 0, + /** Tool is callable by the MCP App UI */ + App = 1 << 1, +} + +/** + * Serializable data for MCP App UI rendering. + * This contains all the information needed to render an MCP App webview. + */ +export interface IMcpToolCallUIData { + /** URI of the UI resource for rendering (e.g., "ui://weather-server/dashboard") */ + readonly resourceUri: string; + /** Reference to the server definition for reconnection */ + readonly serverDefinitionId: string; + /** Reference to the collection containing the server */ + readonly collectionId: string; +} + export interface IMcpTool { readonly id: string; @@ -445,6 +469,10 @@ export interface IMcpTool { readonly referenceName: string; readonly icons: IMcpIcons; readonly definition: MCP.Tool; + /** Visibility of the tool (Model, App, or both). Defaults to Model | App. */ + readonly visibility: McpToolVisibility; + /** Optional UI resource URI for MCP App rendering */ + readonly uiResourceUri?: string; /** * Calls a tool @@ -479,6 +507,17 @@ export interface McpServerTransportStdio { readonly envFile: string | undefined; } +export interface McpServerTransportHTTPAuthentication { + /** + * Authentication provider ID to use to get a session for the initial MCP server connection. + */ + readonly providerId: string; + /** + * Scopes to use to get a session for the initial MCP server connection. + */ + readonly scopes: string[]; +} + /** * MCP server launched on the command line which communicated over SSE or Streamable HTTP. * https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/transports/#http-with-sse @@ -488,6 +527,7 @@ export interface McpServerTransportHTTP { readonly type: McpServerTransportType.HTTP; readonly uri: URI; readonly headers: [string, string][]; + readonly authentication?: McpServerTransportHTTPAuthentication; } export type McpServerLaunch = @@ -496,7 +536,7 @@ export type McpServerLaunch = export namespace McpServerLaunch { export type Serialized = - | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][] } + | { type: McpServerTransportType.HTTP; uri: UriComponents; headers: [string, string][]; authentication?: McpServerTransportHTTPAuthentication } | { type: McpServerTransportType.Stdio; cwd: string | undefined; command: string; args: readonly string[]; env: Record; envFile: string | undefined }; export function toSerialized(launch: McpServerLaunch): McpServerLaunch.Serialized { @@ -506,7 +546,7 @@ export namespace McpServerLaunch { export function fromSerialized(launch: McpServerLaunch.Serialized): McpServerLaunch { switch (launch.type) { case McpServerTransportType.HTTP: - return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers }; + return { type: launch.type, uri: URI.revive(launch.uri), headers: launch.headers, authentication: launch.authentication }; case McpServerTransportType.Stdio: return { type: launch.type, @@ -556,9 +596,9 @@ export interface IMcpServerConnection extends IDisposable { /** Client methods whose implementations are passed through the server connection. */ export interface IMcpClientMethods { /** Handler for `sampling/createMessage` */ - createMessageRequestHandler?(req: MCP.CreateMessageRequest['params']): Promise; + createMessageRequestHandler?(req: MCP.CreateMessageRequest['params'], token?: CancellationToken): Promise; /** Handler for `elicitation/create` */ - elicitationRequestHandler?(req: MCP.ElicitRequest['params']): Promise; + elicitationRequestHandler?(req: MCP.ElicitRequest['params'], token?: CancellationToken): Promise; } /** @@ -851,7 +891,7 @@ export interface ISamplingResult { export interface IMcpSamplingService { _serviceBrand: undefined; - sample(opts: ISamplingOptions): Promise; + sample(opts: ISamplingOptions, token?: CancellationToken): Promise; /** Whether MCP sampling logs are available for this server */ hasLogs(server: IMcpServer): boolean; @@ -906,9 +946,32 @@ export interface IMcpElicitationService { * @param elicitation Request to elicit a response. * @returns A promise that resolves to an {@link ElicitationResult}. */ - elicit(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequest['params'], token: CancellationToken): Promise; + elicit(server: IMcpServer, context: IMcpToolCallContext | undefined, elicitation: MCP.ElicitRequest['params'], token: CancellationToken): Promise; +} + +export const enum ElicitationKind { + Form, + URL, +} + +export interface IUrlModeElicitResult extends IDisposable { + kind: ElicitationKind.URL; + value: MCP.ElicitResult; + /** + * Waits until the server tells us the elicitation is completed before resolving. + * Rejects with a CancellationError if the server stops before elicitation is + * complete, or if the token is cancelled. + */ + wait: Promise; } +export interface IFormModeElicitResult extends IDisposable { + kind: ElicitationKind.Form; + value: MCP.ElicitResult; +} + +export type ElicitResult = IUrlModeElicitResult | IFormModeElicitResult; + export const IMcpElicitationService = createDecorator('IMcpElicitationService'); export const McpToolResourceLinkMimeType = 'application/vnd.code.resource-link'; diff --git a/src/vs/workbench/contrib/mcp/common/mcpTypesUtils.ts b/src/vs/workbench/contrib/mcp/common/mcpTypesUtils.ts index bbc69d6aa3a..cdd61709f59 100644 --- a/src/vs/workbench/contrib/mcp/common/mcpTypesUtils.ts +++ b/src/vs/workbench/contrib/mcp/common/mcpTypesUtils.ts @@ -7,9 +7,12 @@ import { disposableTimeout, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { ToolDataSource } from '../../chat/common/languageModelToolsService.js'; -import { IMcpServer, IMcpServerStartOpts, IMcpService, McpConnectionState, McpServerCacheState } from './mcpTypes.js'; +import { autorun, autorunSelfDisposable, IReader } from '../../../../base/common/observable.js'; +import { ILogger } from '../../../../platform/log/common/log.js'; +import { ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js'; +import { IMcpServer, IMcpServerStartOpts, IMcpService, McpConnectionState, McpServerCacheState, McpServerTransportType } from './mcpTypes.js'; +import { MCP } from './modelContextProtocol.js'; + /** * Waits up to `timeout` for a server passing the filter to be discovered, @@ -80,8 +83,8 @@ export async function startServerAndWaitForLiveTools(server: IMcpServer, opts?: return ok; } -export function mcpServerToSourceData(server: IMcpServer): ToolDataSource { - const metadata = server.serverMetadata.get(); +export function mcpServerToSourceData(server: IMcpServer, reader?: IReader): ToolDataSource { + const metadata = server.serverMetadata.read(reader); return { type: 'mcp', serverLabel: metadata?.serverName, @@ -91,3 +94,86 @@ export function mcpServerToSourceData(server: IMcpServer): ToolDataSource { definitionId: server.definition.id }; } + + +/** + * Validates whether the given HTTP or HTTPS resource is allowed for the specified MCP server. + * + * @param resource The URI of the resource to validate. + * @param server The MCP server instance to validate against, or undefined. + * @returns True if the resource request is valid for the server, false otherwise. + */ +export function canLoadMcpNetworkResourceDirectly(resource: URL, server: IMcpServer | undefined) { + let isResourceRequestValid = false; + if (resource.protocol === 'http:') { + const launch = server?.connection.get()?.launchDefinition; + if (launch && launch.type === McpServerTransportType.HTTP && launch.uri.authority.toLowerCase() === resource.host.toLowerCase()) { + isResourceRequestValid = true; + } + } else if (resource.protocol === 'https:') { + isResourceRequestValid = true; + } + return isResourceRequestValid; +} + +export function isTaskResult(obj: MCP.Result | MCP.CreateTaskResult): obj is MCP.CreateTaskResult { + return (obj as MCP.CreateTaskResult).task !== undefined; +} + +export function findMcpServer(mcpService: IMcpService, filter: (s: IMcpServer) => boolean, token?: CancellationToken) { + return new Promise((resolve) => { + autorunSelfDisposable(reader => { + if (token) { + if (token.isCancellationRequested) { + reader.dispose(); + resolve(undefined); + return; + } + + reader.store.add(token.onCancellationRequested(() => { + reader.dispose(); + resolve(undefined); + })); + } + + const servers = mcpService.servers.read(reader); + const server = servers.find(filter); + if (server) { + resolve(server); + reader.dispose(); + } + }); + }); +} + +export function translateMcpLogMessage(logger: ILogger, params: MCP.LoggingMessageNotificationParams, prefix = '') { + let contents = typeof params.data === 'string' ? params.data : JSON.stringify(params.data); + if (params.logger) { + contents = `${params.logger}: ${contents}`; + } + if (prefix) { + contents = `${prefix} ${contents}`; + } + + switch (params?.level) { + case 'debug': + logger.debug(contents); + break; + case 'info': + case 'notice': + logger.info(contents); + break; + case 'warning': + logger.warn(contents); + break; + case 'error': + case 'critical': + case 'alert': + case 'emergency': + logger.error(contents); + break; + default: + logger.info(contents); + break; + } +} diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts index b4e617b0ba4..db33783efc6 100644 --- a/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts @@ -26,72 +26,137 @@ export namespace MCP { * * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ */ -export namespace MCP {/* JSON-RPC types */ +export namespace MCP { + /* JSON-RPC types */ /** * Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. * - * @internal + * @category JSON-RPC */ export type JSONRPCMessage = | JSONRPCRequest | JSONRPCNotification - | JSONRPCResponse - | JSONRPCError; + | JSONRPCResponse; /** @internal */ - export const LATEST_PROTOCOL_VERSION = "2025-06-18"; + export const LATEST_PROTOCOL_VERSION = "2025-11-25"; /** @internal */ export const JSONRPC_VERSION = "2.0"; + /** + * Represents the contents of a `_meta` field, which clients and servers use to attach additional metadata to their interactions. + * + * Certain key names are reserved by MCP for protocol-level metadata; implementations MUST NOT make assumptions about values at these keys. Additionally, specific schema definitions may reserve particular names for purpose-specific metadata, as declared in those definitions. + * + * Valid keys have two segments: + * + * **Prefix:** + * - Optional - if specified, MUST be a series of _labels_ separated by dots (`.`), followed by a slash (`/`). + * - Labels MUST start with a letter and end with a letter or digit. Interior characters may be letters, digits, or hyphens (`-`). + * - Any prefix consisting of zero or more labels, followed by `modelcontextprotocol` or `mcp`, followed by any label, is **reserved** for MCP use. For example: `modelcontextprotocol.io/`, `mcp.dev/`, `api.modelcontextprotocol.org/`, and `tools.mcp.com/` are all reserved. + * + * **Name:** + * - Unless empty, MUST start and end with an alphanumeric character (`[a-z0-9A-Z]`). + * - Interior characters may be alphanumeric, hyphens (`-`), underscores (`_`), or dots (`.`). + * + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ + export type MetaObject = Record; + + /** + * Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + * + * @see {@link MetaObject} for key naming rules and reserved prefixes. + * @see [General fields: `_meta`](/specification/draft/basic/index#meta) for more details. + * @category Common Types + */ + export interface RequestMetaObject extends MetaObject { + /** + * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotification | notifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + */ + progressToken?: ProgressToken; + } + /** * A progress token, used to associate progress notifications with the original request. + * + * @category Common Types */ export type ProgressToken = string | number; /** * An opaque token used to represent a cursor for pagination. + * + * @category Common Types */ export type Cursor = string; + /** + * Common params for any task-augmented request. + * + * @internal + */ + export interface TaskAugmentedRequestParams extends RequestParams { + /** + * If specified, the caller is requesting task-augmented execution for this request. + * The request will return a {@link CreateTaskResult} immediately, and the actual result can be + * retrieved later via {@link GetTaskPayloadRequest | tasks/result}. + * + * Task augmentation is subject to capability negotiation - receivers MUST declare support + * for task augmentation of specific request types in their capabilities. + */ + task?: TaskMetadata; + } + + /** + * Common params for any request. + * + * @category Common Types + */ + export interface RequestParams { + _meta?: RequestMetaObject; + } + /** @internal */ export interface Request { method: string; - params?: { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { - /** - * If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - */ - progressToken?: ProgressToken; - [key: string]: unknown; - }; - [key: string]: unknown; - }; + // Allow unofficial extensions of `Request.params` without impacting `RequestParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; + } + + /** + * Common params for any notification. + * + * @category Common Types + */ + export interface NotificationParams { + _meta?: MetaObject; } /** @internal */ export interface Notification { method: string; - params?: { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; - [key: string]: unknown; - }; + // Allow unofficial extensions of `Notification.params` without impacting `NotificationParams`. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: { [key: string]: any }; } + /** + * Common result fields. + * + * @category Common Types + */ export interface Result { - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; [key: string]: unknown; } + /** + * @category Errors + */ export interface Error { /** * The error type that occurred. @@ -109,11 +174,15 @@ export namespace MCP {/* JSON-RPC types */ /** * A uniquely identifying ID for a request in JSON-RPC. + * + * @category Common Types */ export type RequestId = string | number; /** * A request that expects a response. + * + * @category JSON-RPC */ export interface JSONRPCRequest extends Request { jsonrpc: typeof JSONRPC_VERSION; @@ -122,6 +191,8 @@ export namespace MCP {/* JSON-RPC types */ /** * A notification which does not expect a response. + * + * @category JSON-RPC */ export interface JSONRPCNotification extends Notification { jsonrpc: typeof JSONRPC_VERSION; @@ -129,41 +200,189 @@ export namespace MCP {/* JSON-RPC types */ /** * A successful (non-error) response to a request. + * + * @category JSON-RPC */ - export interface JSONRPCResponse { + export interface JSONRPCResultResponse { jsonrpc: typeof JSONRPC_VERSION; id: RequestId; result: Result; } + /** + * A response to a request that indicates an error occurred. + * + * @category JSON-RPC + */ + export interface JSONRPCErrorResponse { + jsonrpc: typeof JSONRPC_VERSION; + id?: RequestId; + error: Error; + } + + /** + * A response to a request, containing either the result or error. + * + * @category JSON-RPC + */ + export type JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse; + // Standard JSON-RPC error codes - /** @internal */ export const PARSE_ERROR = -32700; - /** @internal */ export const INVALID_REQUEST = -32600; - /** @internal */ export const METHOD_NOT_FOUND = -32601; - /** @internal */ export const INVALID_PARAMS = -32602; - /** @internal */ export const INTERNAL_ERROR = -32603; /** - * A response to a request that indicates an error occurred. + * A JSON-RPC error indicating that invalid JSON was received by the server. This error is returned when the server cannot parse the JSON text of a message. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Invalid JSON + * {@includeCode ./examples/ParseError/invalid-json.json} + * + * @category Errors */ - export interface JSONRPCError { - jsonrpc: typeof JSONRPC_VERSION; - id: RequestId; - error: Error; + export interface ParseError extends Error { + code: typeof PARSE_ERROR; + } + + /** + * A JSON-RPC error indicating that the request is not a valid request object. This error is returned when the message structure does not conform to the JSON-RPC 2.0 specification requirements for a request (e.g., missing required fields like `jsonrpc` or `method`, or using invalid types for these fields). + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @category Errors + */ + export interface InvalidRequestError extends Error { + code: typeof INVALID_REQUEST; + } + + /** + * A JSON-RPC error indicating that the requested method does not exist or is not available. + * + * In MCP, this error is returned when a request is made for a method that requires a capability that has not been declared. This can occur in either direction: + * + * - A server returning this error when the client requests a capability it doesn't support (e.g., requesting completions when the `completions` capability was not advertised) + * - A client returning this error when the server requests a capability it doesn't support (e.g., requesting roots when the client did not declare the `roots` capability) + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Roots not supported + * {@includeCode ./examples/MethodNotFoundError/roots-not-supported.json} + * + * @category Errors + */ + export interface MethodNotFoundError extends Error { + code: typeof METHOD_NOT_FOUND; + } + + /** + * A JSON-RPC error indicating that the method parameters are invalid or malformed. + * + * In MCP, this error is returned in various contexts when request parameters fail validation: + * + * - **Tools**: Unknown tool name or invalid tool arguments + * - **Prompts**: Unknown prompt name or missing required arguments + * - **Pagination**: Invalid or expired cursor values + * - **Logging**: Invalid log level + * - **Tasks**: Invalid or nonexistent task ID, invalid cursor, or attempting to cancel a task already in a terminal status + * - **Elicitation**: Server requests an elicitation mode not declared in client capabilities + * - **Sampling**: Missing tool result or tool results mixed with other content + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unknown tool + * {@includeCode ./examples/InvalidParamsError/unknown-tool.json} + * + * @example Invalid tool arguments + * {@includeCode ./examples/InvalidParamsError/invalid-tool-arguments.json} + * + * @example Unknown prompt + * {@includeCode ./examples/InvalidParamsError/unknown-prompt.json} + * + * @example Invalid cursor + * {@includeCode ./examples/InvalidParamsError/invalid-cursor.json} + * + * @category Errors + */ + export interface InvalidParamsError extends Error { + code: typeof INVALID_PARAMS; + } + + /** + * A JSON-RPC error indicating that an internal error occurred on the receiver. This error is returned when the receiver encounters an unexpected condition that prevents it from fulfilling the request. + * + * @see {@link https://www.jsonrpc.org/specification#error_object | JSON-RPC 2.0 Error Object} + * + * @example Unexpected error + * {@includeCode ./examples/InternalError/unexpected-error.json} + * + * @category Errors + */ + export interface InternalError extends Error { + code: typeof INTERNAL_ERROR; + } + + // Implementation-specific JSON-RPC error codes [-32000, -32099] + /** @internal */ + export const URL_ELICITATION_REQUIRED = -32042; + + /** + * An error response that indicates that the server requires the client to provide additional information via an elicitation request. + * + * @example Authorization required + * {@includeCode ./examples/URLElicitationRequiredError/authorization-required.json} + * + * @internal + */ + export interface URLElicitationRequiredError extends Omit< + JSONRPCErrorResponse, + "error" + > { + error: Error & { + code: typeof URL_ELICITATION_REQUIRED; + data: { + elicitations: ElicitRequestURLParams[]; + [key: string]: unknown; + }; + }; } /* Empty result */ /** - * A response that indicates success but carries no data. + * A result that indicates success but carries no data. + * + * @category Common Types */ export type EmptyResult = Result; /* Cancellation */ + /** + * Parameters for a `notifications/cancelled` notification. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotificationParams/user-requested-cancellation.json} + * + * @category `notifications/cancelled` + */ + export interface CancelledNotificationParams extends NotificationParams { + /** + * The ID of the request to cancel. + * + * This MUST correspond to the ID of a request previously issued in the same direction. + * This MUST be provided for cancelling non-task requests. + * This MUST NOT be used for cancelling tasks (use the {@link CancelTaskRequest | tasks/cancel} request instead). + */ + requestId?: RequestId; + + /** + * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. + */ + reason?: string; + } + /** * This notification can be sent by either side to indicate that it is cancelling a previously-issued request. * @@ -173,47 +392,56 @@ export namespace MCP {/* JSON-RPC types */ * * A client MUST NOT attempt to cancel its `initialize` request. * - * @category notifications/cancelled + * For task cancellation, use the {@link CancelTaskRequest | tasks/cancel} request instead of this notification. + * + * @example User-requested cancellation + * {@includeCode ./examples/CancelledNotification/user-requested-cancellation.json} + * + * @category `notifications/cancelled` */ export interface CancelledNotification extends JSONRPCNotification { method: "notifications/cancelled"; - params: { - /** - * The ID of the request to cancel. - * - * This MUST correspond to the ID of a request previously issued in the same direction. - */ - requestId: RequestId; - - /** - * An optional string describing the reason for the cancellation. This MAY be logged or presented to the user. - */ - reason?: string; - }; + params: CancelledNotificationParams; } /* Initialization */ + /** + * Parameters for an `initialize` request. + * + * @example Full client capabilities + * {@includeCode ./examples/InitializeRequestParams/full-client-capabilities.json} + * + * @category `initialize` + */ + export interface InitializeRequestParams extends RequestParams { + /** + * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. + */ + protocolVersion: string; + capabilities: ClientCapabilities; + clientInfo: Implementation; + } + /** * This request is sent from the client to the server when it first connects, asking it to begin initialization. * - * @category initialize + * @example Initialize request + * {@includeCode ./examples/InitializeRequest/initialize-request.json} + * + * @category `initialize` */ export interface InitializeRequest extends JSONRPCRequest { method: "initialize"; - params: { - /** - * The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well. - */ - protocolVersion: string; - capabilities: ClientCapabilities; - clientInfo: Implementation; - }; + params: InitializeRequestParams; } /** - * After receiving an initialize request from the client, the server sends this response. + * The result returned by the server for an {@link InitializeRequest | initialize} request. + * + * @example Full server capabilities + * {@includeCode ./examples/InitializeResult/full-server-capabilities.json} * - * @category initialize + * @category `initialize` */ export interface InitializeResult extends Result { /** @@ -231,17 +459,35 @@ export namespace MCP {/* JSON-RPC types */ instructions?: string; } + /** + * A successful response from the server for a {@link InitializeRequest | initialize} request. + * + * @example Initialize result response + * {@includeCode ./examples/InitializeResultResponse/initialize-result-response.json} + * + * @category `initialize` + */ + export interface InitializeResultResponse extends JSONRPCResultResponse { + result: InitializeResult; + } + /** * This notification is sent from the client to the server after initialization has finished. * - * @category notifications/initialized + * @example Initialized notification + * {@includeCode ./examples/InitializedNotification/initialized-notification.json} + * + * @category `notifications/initialized` */ export interface InitializedNotification extends JSONRPCNotification { method: "notifications/initialized"; + params?: NotificationParams; } /** * Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + * + * @category `initialize` */ export interface ClientCapabilities { /** @@ -250,6 +496,12 @@ export namespace MCP {/* JSON-RPC types */ experimental?: { [key: string]: object }; /** * Present if the client supports listing roots. + * + * @example Roots - minimum baseline support + * {@includeCode ./examples/ClientCapabilities/roots-minimum-baseline-support.json} + * + * @example Roots - list changed notifications + * {@includeCode ./examples/ClientCapabilities/roots-list-changed-notifications.json} */ roots?: { /** @@ -259,16 +511,89 @@ export namespace MCP {/* JSON-RPC types */ }; /** * Present if the client supports sampling from an LLM. + * + * @example Sampling - minimum baseline support + * {@includeCode ./examples/ClientCapabilities/sampling-minimum-baseline-support.json} + * + * @example Sampling - tool use support + * {@includeCode ./examples/ClientCapabilities/sampling-tool-use-support.json} + * + * @example Sampling - context inclusion support (soft-deprecated) + * {@includeCode ./examples/ClientCapabilities/sampling-context-inclusion-support-soft-deprecated.json} */ - sampling?: object; + sampling?: { + /** + * Whether the client supports context inclusion via `includeContext` parameter. + * If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + */ + context?: object; + /** + * Whether the client supports tool use via `tools` and `toolChoice` parameters. + */ + tools?: object; + }; /** * Present if the client supports elicitation from the server. + * + * @example Elicitation - form and URL mode support + * {@includeCode ./examples/ClientCapabilities/elicitation-form-and-url-mode-support.json} + * + * @example Elicitation - form mode only (implicit) + * {@includeCode ./examples/ClientCapabilities/elicitation-form-only-implicit.json} + */ + elicitation?: { form?: object; url?: object }; + + /** + * Present if the client supports task-augmented requests. + */ + tasks?: { + /** + * Whether this client supports {@link ListTasksRequest | tasks/list}. + */ + list?: object; + /** + * Whether this client supports {@link CancelTaskRequest | tasks/cancel}. + */ + cancel?: object; + /** + * Specifies which request types can be augmented with tasks. + */ + requests?: { + /** + * Task support for sampling-related requests. + */ + sampling?: { + /** + * Whether the client supports task-augmented `sampling/createMessage` requests. + */ + createMessage?: object; + }; + /** + * Task support for elicitation-related requests. + */ + elicitation?: { + /** + * Whether the client supports task-augmented {@link ElicitRequest | elicitation/create} requests. + */ + create?: object; + }; + }; + }; + /** + * Optional MCP extensions that the client supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + * per-extension settings objects. An empty object indicates support with no settings. + * + * @example Extensions - UI extension with MIME type support + * {@includeCode ./examples/ClientCapabilities/extensions-ui-mime-types.json} */ - elicitation?: object; + extensions?: { [key: string]: object }; } /** * Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + * + * @category `initialize` */ export interface ServerCapabilities { /** @@ -277,14 +602,26 @@ export namespace MCP {/* JSON-RPC types */ experimental?: { [key: string]: object }; /** * Present if the server supports sending log messages to the client. + * + * @example Logging - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/logging-minimum-baseline-support.json} */ logging?: object; /** * Present if the server supports argument autocompletion suggestions. + * + * @example Completions - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/completions-minimum-baseline-support.json} */ completions?: object; /** * Present if the server offers any prompt templates. + * + * @example Prompts - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/prompts-minimum-baseline-support.json} + * + * @example Prompts - list changed notifications + * {@includeCode ./examples/ServerCapabilities/prompts-list-changed-notifications.json} */ prompts?: { /** @@ -294,6 +631,18 @@ export namespace MCP {/* JSON-RPC types */ }; /** * Present if the server offers any resources to read. + * + * @example Resources - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/resources-minimum-baseline-support.json} + * + * @example Resources - subscription to individual resource updates (only) + * {@includeCode ./examples/ServerCapabilities/resources-subscription-to-individual-resource-updates-only.json} + * + * @example Resources - list changed notifications (only) + * {@includeCode ./examples/ServerCapabilities/resources-list-changed-notifications-only.json} + * + * @example Resources - all notifications + * {@includeCode ./examples/ServerCapabilities/resources-all-notifications.json} */ resources?: { /** @@ -307,6 +656,12 @@ export namespace MCP {/* JSON-RPC types */ }; /** * Present if the server offers any tools to call. + * + * @example Tools - minimum baseline support + * {@includeCode ./examples/ServerCapabilities/tools-minimum-baseline-support.json} + * + * @example Tools - list changed notifications + * {@includeCode ./examples/ServerCapabilities/tools-list-changed-notifications.json} */ tools?: { /** @@ -314,17 +669,55 @@ export namespace MCP {/* JSON-RPC types */ */ listChanged?: boolean; }; + /** + * Present if the server supports task-augmented requests. + */ + tasks?: { + /** + * Whether this server supports {@link ListTasksRequest | tasks/list}. + */ + list?: object; + /** + * Whether this server supports {@link CancelTaskRequest | tasks/cancel}. + */ + cancel?: object; + /** + * Specifies which request types can be augmented with tasks. + */ + requests?: { + /** + * Task support for tool-related requests. + */ + tools?: { + /** + * Whether the server supports task-augmented {@link CallToolRequest | tools/call} requests. + */ + call?: object; + }; + }; + }; + /** + * Optional MCP extensions that the server supports. Keys are extension identifiers + * (e.g., "io.modelcontextprotocol/apps"), and values are per-extension settings + * objects. An empty object indicates support with no settings. + * + * @example Extensions - UI extension support + * {@includeCode ./examples/ServerCapabilities/extensions-ui.json} + */ + extensions?: { [key: string]: object }; } /** * An optionally-sized icon that can be displayed in a user interface. + * + * @category Common Types */ export interface Icon { /** * A standard URI pointing to an icon resource. May be an HTTP/HTTPS URL or a * `data:` URI with Base64-encoded image data. * - * Consumers SHOULD takes steps to ensure URLs serving icons are from the + * Consumers SHOULD take steps to ensure URLs serving icons are from the * same domain as the client/server or a trusted domain. * * Consumers SHOULD take appropriate precautions when consuming SVGs as they can contain @@ -341,21 +734,21 @@ export namespace MCP {/* JSON-RPC types */ mimeType?: string; /** - * Optional string that specifies one or more sizes at which the icon can be used. - * For example: `"48x48"`, `"48x48 96x96"`, or `"any"` for scalable formats like SVG. + * Optional array of strings that specify sizes at which the icon can be used. + * Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for scalable formats like SVG. * * If not provided, the client should assume that the icon can be used at any size. */ - sizes?: string; + sizes?: string[]; /** - * Optional specifier for the theme this icon is designed for. `light` indicates - * the icon is designed to be used with a light background, and `dark` indicates + * Optional specifier for the theme this icon is designed for. `"light"` indicates + * the icon is designed to be used with a light background, and `"dark"` indicates * the icon is designed to be used with a dark background. * * If not provided, the client should assume the icon can be used with any theme. */ - theme?: 'light' | 'dark'; + theme?: "light" | "dark"; } /** @@ -393,7 +786,7 @@ export namespace MCP {/* JSON-RPC types */ * Intended for UI and end-user contexts - optimized to be human-readable and easily understood, * even by those unfamiliar with domain-specific terminology. * - * If not provided, the name should be used for display (except for Tool, + * If not provided, the name should be used for display (except for {@link Tool}, * where `annotations.title` should be given precedence over using `name`, * if present). */ @@ -401,11 +794,25 @@ export namespace MCP {/* JSON-RPC types */ } /** - * Describes the MCP implementation + * Describes the MCP implementation. + * + * @category `initialize` */ export interface Implementation extends BaseMetadata, Icons { + /** + * The version of this implementation. + */ version: string; + /** + * An optional human-readable description of what this implementation does. + * + * This can be used by clients or servers to provide context about their purpose + * and capabilities. For example, a server might describe the types of resources + * or tools it provides, while a client might describe its intended use case. + */ + description?: string; + /** * An optional URL of the website for this implementation. * @@ -418,54 +825,94 @@ export namespace MCP {/* JSON-RPC types */ /** * A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected. * - * @category ping + * @example Ping request + * {@includeCode ./examples/PingRequest/ping-request.json} + * + * @category `ping` */ export interface PingRequest extends JSONRPCRequest { method: "ping"; + params?: RequestParams; + } + + /** + * A successful response for a {@link PingRequest | ping} request. + * + * @example Ping result response + * {@includeCode ./examples/PingResultResponse/ping-result-response.json} + * + * @category `ping` + */ + export interface PingResultResponse extends JSONRPCResultResponse { + result: EmptyResult; } /* Progress notifications */ + /** - * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * Parameters for a {@link ProgressNotification | notifications/progress} notification. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotificationParams/progress-message.json} * - * @category notifications/progress + * @category `notifications/progress` */ - export interface ProgressNotification extends JSONRPCNotification { - method: "notifications/progress"; - params: { - /** - * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. - */ - progressToken: ProgressToken; - /** - * The progress thus far. This should increase every time progress is made, even if the total is unknown. - * - * @TJS-type number - */ - progress: number; - /** - * Total number of items to process (or total progress required), if known. - * - * @TJS-type number - */ - total?: number; - /** - * An optional message describing the current progress. - */ - message?: string; - }; + export interface ProgressNotificationParams extends NotificationParams { + /** + * The progress token which was given in the initial request, used to associate this notification with the request that is proceeding. + */ + progressToken: ProgressToken; + /** + * The progress thus far. This should increase every time progress is made, even if the total is unknown. + * + * @TJS-type number + */ + progress: number; + /** + * Total number of items to process (or total progress required), if known. + * + * @TJS-type number + */ + total?: number; + /** + * An optional message describing the current progress. + */ + message?: string; + } + + /** + * An out-of-band notification used to inform the receiver of a progress update for a long-running request. + * + * @example Progress message + * {@includeCode ./examples/ProgressNotification/progress-message.json} + * + * @category `notifications/progress` + */ + export interface ProgressNotification extends JSONRPCNotification { + method: "notifications/progress"; + params: ProgressNotificationParams; } /* Pagination */ + /** + * Common params for paginated requests. + * + * @example List request with cursor + * {@includeCode ./examples/PaginatedRequestParams/list-with-cursor.json} + * + * @category Common Types + */ + export interface PaginatedRequestParams extends RequestParams { + /** + * An opaque token representing the current pagination position. + * If provided, the server should return results starting after this cursor. + */ + cursor?: Cursor; + } + /** @internal */ export interface PaginatedRequest extends JSONRPCRequest { - params?: { - /** - * An opaque token representing the current pagination position. - * If provided, the server should return results starting after this cursor. - */ - cursor?: Cursor; - }; + params?: PaginatedRequestParams; } /** @internal */ @@ -481,127 +928,250 @@ export namespace MCP {/* JSON-RPC types */ /** * Sent from the client to request a list of resources the server has. * - * @category resources/list + * @example List resources request + * {@includeCode ./examples/ListResourcesRequest/list-resources-request.json} + * + * @category `resources/list` */ export interface ListResourcesRequest extends PaginatedRequest { method: "resources/list"; } /** - * The server's response to a resources/list request from the client. + * The result returned by the server for a {@link ListResourcesRequest | resources/list} request. * - * @category resources/list + * @example Resources list with cursor + * {@includeCode ./examples/ListResourcesResult/resources-list-with-cursor.json} + * + * @category `resources/list` */ export interface ListResourcesResult extends PaginatedResult { resources: Resource[]; } + /** + * A successful response from the server for a {@link ListResourcesRequest | resources/list} request. + * + * @example List resources result response + * {@includeCode ./examples/ListResourcesResultResponse/list-resources-result-response.json} + * + * @category `resources/list` + */ + export interface ListResourcesResultResponse extends JSONRPCResultResponse { + result: ListResourcesResult; + } + /** * Sent from the client to request a list of resource templates the server has. * - * @category resources/templates/list + * @example List resource templates request + * {@includeCode ./examples/ListResourceTemplatesRequest/list-resource-templates-request.json} + * + * @category `resources/templates/list` */ export interface ListResourceTemplatesRequest extends PaginatedRequest { method: "resources/templates/list"; } /** - * The server's response to a resources/templates/list request from the client. + * The result returned by the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. * - * @category resources/templates/list + * @example Resource templates list + * {@includeCode ./examples/ListResourceTemplatesResult/resource-templates-list.json} + * + * @category `resources/templates/list` */ export interface ListResourceTemplatesResult extends PaginatedResult { resourceTemplates: ResourceTemplate[]; } + /** + * A successful response from the server for a {@link ListResourceTemplatesRequest | resources/templates/list} request. + * + * @example List resource templates result response + * {@includeCode ./examples/ListResourceTemplatesResultResponse/list-resource-templates-result-response.json} + * + * @category `resources/templates/list` + */ + export interface ListResourceTemplatesResultResponse extends JSONRPCResultResponse { + result: ListResourceTemplatesResult; + } + + /** + * Common params for resource-related requests. + * + * @internal + */ + export interface ResourceRequestParams extends RequestParams { + /** + * The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + * + * @format uri + */ + uri: string; + } + + /** + * Parameters for a `resources/read` request. + * + * @category `resources/read` + */ + export interface ReadResourceRequestParams extends ResourceRequestParams { } + /** * Sent from the client to the server, to read a specific resource URI. * - * @category resources/read + * @example Read resource request + * {@includeCode ./examples/ReadResourceRequest/read-resource-request.json} + * + * @category `resources/read` */ export interface ReadResourceRequest extends JSONRPCRequest { method: "resources/read"; - params: { - /** - * The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. - * - * @format uri - */ - uri: string; - }; + params: ReadResourceRequestParams; } /** - * The server's response to a resources/read request from the client. + * The result returned by the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example File resource contents + * {@includeCode ./examples/ReadResourceResult/file-resource-contents.json} * - * @category resources/read + * @category `resources/read` */ export interface ReadResourceResult extends Result { contents: (TextResourceContents | BlobResourceContents)[]; } + /** + * A successful response from the server for a {@link ReadResourceRequest | resources/read} request. + * + * @example Read resource result response + * {@includeCode ./examples/ReadResourceResultResponse/read-resource-result-response.json} + * + * @category `resources/read` + */ + export interface ReadResourceResultResponse extends JSONRPCResultResponse { + result: ReadResourceResult; + } + /** * An optional notification from the server to the client, informing it that the list of resources it can read from has changed. This may be issued by servers without any previous subscription from the client. * - * @category notifications/resources/list_changed + * @example Resources list changed + * {@includeCode ./examples/ResourceListChangedNotification/resources-list-changed.json} + * + * @category `notifications/resources/list_changed` */ export interface ResourceListChangedNotification extends JSONRPCNotification { method: "notifications/resources/list_changed"; + params?: NotificationParams; } /** - * Sent from the client to request resources/updated notifications from the server whenever a particular resource changes. + * Parameters for a `resources/subscribe` request. + * + * @example Subscribe to file resource + * {@includeCode ./examples/SubscribeRequestParams/subscribe-to-file-resource.json} + * + * @category `resources/subscribe` + */ + export interface SubscribeRequestParams extends ResourceRequestParams { } + + /** + * Sent from the client to request {@link ResourceUpdatedNotification | resources/updated} notifications from the server whenever a particular resource changes. * - * @category resources/subscribe + * @example Subscribe request + * {@includeCode ./examples/SubscribeRequest/subscribe-request.json} + * + * @category `resources/subscribe` */ export interface SubscribeRequest extends JSONRPCRequest { method: "resources/subscribe"; - params: { - /** - * The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. - * - * @format uri - */ - uri: string; - }; + params: SubscribeRequestParams; + } + + /** + * A successful response from the server for a {@link SubscribeRequest | resources/subscribe} request. + * + * @example Subscribe result response + * {@includeCode ./examples/SubscribeResultResponse/subscribe-result-response.json} + * + * @category `resources/subscribe` + */ + export interface SubscribeResultResponse extends JSONRPCResultResponse { + result: EmptyResult; } /** - * Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request. + * Parameters for a `resources/unsubscribe` request. + * + * @category `resources/unsubscribe` + */ + export interface UnsubscribeRequestParams extends ResourceRequestParams { } + + /** + * Sent from the client to request cancellation of {@link ResourceUpdatedNotification | resources/updated} notifications from the server. This should follow a previous {@link SubscribeRequest | resources/subscribe} request. + * + * @example Unsubscribe request + * {@includeCode ./examples/UnsubscribeRequest/unsubscribe-request.json} * - * @category resources/unsubscribe + * @category `resources/unsubscribe` */ export interface UnsubscribeRequest extends JSONRPCRequest { method: "resources/unsubscribe"; - params: { - /** - * The URI of the resource to unsubscribe from. - * - * @format uri - */ - uri: string; - }; + params: UnsubscribeRequestParams; + } + + /** + * A successful response from the server for a {@link UnsubscribeRequest | resources/unsubscribe} request. + * + * @example Unsubscribe result response + * {@includeCode ./examples/UnsubscribeResultResponse/unsubscribe-result-response.json} + * + * @category `resources/unsubscribe` + */ + export interface UnsubscribeResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + + /** + * Parameters for a `notifications/resources/updated` notification. + * + * @example File resource updated + * {@includeCode ./examples/ResourceUpdatedNotificationParams/file-resource-updated.json} + * + * @category `notifications/resources/updated` + */ + export interface ResourceUpdatedNotificationParams extends NotificationParams { + /** + * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. + * + * @format uri + */ + uri: string; } /** - * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a resources/subscribe request. + * A notification from the server to the client, informing it that a resource has changed and may need to be read again. This should only be sent if the client previously sent a {@link SubscribeRequest | resources/subscribe} request. + * + * @example File resource updated notification + * {@includeCode ./examples/ResourceUpdatedNotification/file-resource-updated-notification.json} * - * @category notifications/resources/updated + * @category `notifications/resources/updated` */ export interface ResourceUpdatedNotification extends JSONRPCNotification { method: "notifications/resources/updated"; - params: { - /** - * The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. - * - * @format uri - */ - uri: string; - }; + params: ResourceUpdatedNotificationParams; } /** * A known resource that the server is capable of reading. + * + * @example File resource with annotations + * {@includeCode ./examples/Resource/file-resource-with-annotations.json} + * + * @category `resources/list` */ export interface Resource extends BaseMetadata, Icons { /** @@ -635,14 +1205,13 @@ export namespace MCP {/* JSON-RPC types */ */ size?: number; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * A template description for resources available on the server. + * + * @category `resources/templates/list` */ export interface ResourceTemplate extends BaseMetadata, Icons { /** @@ -669,14 +1238,13 @@ export namespace MCP {/* JSON-RPC types */ */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * The contents of a specific resource or sub-resource. + * + * @internal */ export interface ResourceContents { /** @@ -690,12 +1258,15 @@ export namespace MCP {/* JSON-RPC types */ */ mimeType?: string; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } + /** + * @example Text file contents + * {@includeCode ./examples/TextResourceContents/text-file-contents.json} + * + * @category Content + */ export interface TextResourceContents extends ResourceContents { /** * The text of the item. This must only be set if the item can actually be represented as text (not binary data). @@ -703,6 +1274,12 @@ export namespace MCP {/* JSON-RPC types */ text: string; } + /** + * @example Image file contents + * {@includeCode ./examples/BlobResourceContents/image-file-contents.json} + * + * @category Content + */ export interface BlobResourceContents extends ResourceContents { /** * A base64-encoded string representing the binary data of the item. @@ -716,44 +1293,78 @@ export namespace MCP {/* JSON-RPC types */ /** * Sent from the client to request a list of prompts and prompt templates the server has. * - * @category prompts/list + * @example List prompts request + * {@includeCode ./examples/ListPromptsRequest/list-prompts-request.json} + * + * @category `prompts/list` */ export interface ListPromptsRequest extends PaginatedRequest { method: "prompts/list"; } /** - * The server's response to a prompts/list request from the client. + * The result returned by the server for a {@link ListPromptsRequest | prompts/list} request. * - * @category prompts/list + * @example Prompts list with cursor + * {@includeCode ./examples/ListPromptsResult/prompts-list-with-cursor.json} + * + * @category `prompts/list` */ export interface ListPromptsResult extends PaginatedResult { prompts: Prompt[]; } + /** + * A successful response from the server for a {@link ListPromptsRequest | prompts/list} request. + * + * @example List prompts result response + * {@includeCode ./examples/ListPromptsResultResponse/list-prompts-result-response.json} + * + * @category `prompts/list` + */ + export interface ListPromptsResultResponse extends JSONRPCResultResponse { + result: ListPromptsResult; + } + + /** + * Parameters for a `prompts/get` request. + * + * @example Get code review prompt + * {@includeCode ./examples/GetPromptRequestParams/get-code-review-prompt.json} + * + * @category `prompts/get` + */ + export interface GetPromptRequestParams extends RequestParams { + /** + * The name of the prompt or prompt template. + */ + name: string; + /** + * Arguments to use for templating the prompt. + */ + arguments?: { [key: string]: string }; + } + /** * Used by the client to get a prompt provided by the server. * - * @category prompts/get + * @example Get prompt request + * {@includeCode ./examples/GetPromptRequest/get-prompt-request.json} + * + * @category `prompts/get` */ export interface GetPromptRequest extends JSONRPCRequest { method: "prompts/get"; - params: { - /** - * The name of the prompt or prompt template. - */ - name: string; - /** - * Arguments to use for templating the prompt. - */ - arguments?: { [key: string]: string }; - }; + params: GetPromptRequestParams; } /** - * The server's response to a prompts/get request from the client. + * The result returned by the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Code review prompt + * {@includeCode ./examples/GetPromptResult/code-review-prompt.json} * - * @category prompts/get + * @category `prompts/get` */ export interface GetPromptResult extends Result { /** @@ -763,8 +1374,22 @@ export namespace MCP {/* JSON-RPC types */ messages: PromptMessage[]; } + /** + * A successful response from the server for a {@link GetPromptRequest | prompts/get} request. + * + * @example Get prompt result response + * {@includeCode ./examples/GetPromptResultResponse/get-prompt-result-response.json} + * + * @category `prompts/get` + */ + export interface GetPromptResultResponse extends JSONRPCResultResponse { + result: GetPromptResult; + } + /** * A prompt or prompt template that the server offers. + * + * @category `prompts/list` */ export interface Prompt extends BaseMetadata, Icons { /** @@ -777,14 +1402,13 @@ export namespace MCP {/* JSON-RPC types */ */ arguments?: PromptArgument[]; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * Describes an argument that a prompt can accept. + * + * @category `prompts/list` */ export interface PromptArgument extends BaseMetadata { /** @@ -799,14 +1423,18 @@ export namespace MCP {/* JSON-RPC types */ /** * The sender or recipient of messages and data in a conversation. + * + * @category Common Types */ export type Role = "user" | "assistant"; /** * Describes a message returned as part of a prompt. * - * This is similar to `SamplingMessage`, but also supports the embedding of + * This is similar to {@link SamplingMessage}, but also supports the embedding of * resources from the MCP server. + * + * @category `prompts/get` */ export interface PromptMessage { role: Role; @@ -816,7 +1444,12 @@ export namespace MCP {/* JSON-RPC types */ /** * A resource that the server is capable of reading, included in a prompt or tool call result. * - * Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests. + * Note: resource links returned by tools are not guaranteed to appear in the results of {@link ListResourcesRequest | resources/list} requests. + * + * @example File resource link + * {@includeCode ./examples/ResourceLink/file-resource-link.json} + * + * @category Content */ export interface ResourceLink extends Resource { type: "resource_link"; @@ -827,6 +1460,11 @@ export namespace MCP {/* JSON-RPC types */ * * It is up to the client how best to render embedded resources for the benefit * of the LLM and/or the user. + * + * @example Embedded file resource with annotations + * {@includeCode ./examples/EmbeddedResource/embedded-file-resource-with-annotations.json} + * + * @category Content */ export interface EmbeddedResource { type: "resource"; @@ -837,43 +1475,71 @@ export namespace MCP {/* JSON-RPC types */ */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * An optional notification from the server to the client, informing it that the list of prompts it offers has changed. This may be issued by servers without any previous subscription from the client. * - * @category notifications/prompts/list_changed + * @example Prompts list changed + * {@includeCode ./examples/PromptListChangedNotification/prompts-list-changed.json} + * + * @category `notifications/prompts/list_changed` */ export interface PromptListChangedNotification extends JSONRPCNotification { method: "notifications/prompts/list_changed"; + params?: NotificationParams; } /* Tools */ /** * Sent from the client to request a list of tools the server has. * - * @category tools/list + * @example List tools request + * {@includeCode ./examples/ListToolsRequest/list-tools-request.json} + * + * @category `tools/list` */ export interface ListToolsRequest extends PaginatedRequest { method: "tools/list"; } /** - * The server's response to a tools/list request from the client. + * The result returned by the server for a {@link ListToolsRequest | tools/list} request. * - * @category tools/list + * @example Tools list with cursor + * {@includeCode ./examples/ListToolsResult/tools-list-with-cursor.json} + * + * @category `tools/list` */ export interface ListToolsResult extends PaginatedResult { tools: Tool[]; } /** - * The server's response to a tool call. + * A successful response from the server for a {@link ListToolsRequest | tools/list} request. + * + * @example List tools result response + * {@includeCode ./examples/ListToolsResultResponse/list-tools-result-response.json} + * + * @category `tools/list` + */ + export interface ListToolsResultResponse extends JSONRPCResultResponse { + result: ListToolsResult; + } + + /** + * The result returned by the server for a {@link CallToolRequest | tools/call} request. + * + * @example Result with unstructured text + * {@includeCode ./examples/CallToolResult/result-with-unstructured-text.json} + * + * @example Result with structured content + * {@includeCode ./examples/CallToolResult/result-with-structured-content.json} + * + * @example Invalid tool input error + * {@includeCode ./examples/CallToolResult/invalid-tool-input-error.json} * - * @category tools/call + * @category `tools/call` */ export interface CallToolResult extends Result { /** @@ -903,37 +1569,77 @@ export namespace MCP {/* JSON-RPC types */ isError?: boolean; } + /** + * A successful response from the server for a {@link CallToolRequest | tools/call} request. + * + * @example Call tool result response + * {@includeCode ./examples/CallToolResultResponse/call-tool-result-response.json} + * + * @category `tools/call` + */ + export interface CallToolResultResponse extends JSONRPCResultResponse { + result: CallToolResult; + } + + /** + * Parameters for a `tools/call` request. + * + * @example `get_weather` tool call params + * {@includeCode ./examples/CallToolRequestParams/get-weather-tool-call-params.json} + * + * @example Tool call params with progress token + * {@includeCode ./examples/CallToolRequestParams/tool-call-params-with-progress-token.json} + * + * @category `tools/call` + */ + export interface CallToolRequestParams extends TaskAugmentedRequestParams { + /** + * The name of the tool. + */ + name: string; + /** + * Arguments to use for the tool call. + */ + arguments?: { [key: string]: unknown }; + } + /** * Used by the client to invoke a tool provided by the server. * - * @category tools/call + * @example Call tool request + * {@includeCode ./examples/CallToolRequest/call-tool-request.json} + * + * @category `tools/call` */ export interface CallToolRequest extends JSONRPCRequest { method: "tools/call"; - params: { - name: string; - arguments?: { [key: string]: unknown }; - }; + params: CallToolRequestParams; } /** * An optional notification from the server to the client, informing it that the list of tools it offers has changed. This may be issued by servers without any previous subscription from the client. * - * @category notifications/tools/list_changed + * @example Tools list changed + * {@includeCode ./examples/ToolListChangedNotification/tools-list-changed.json} + * + * @category `notifications/tools/list_changed` */ export interface ToolListChangedNotification extends JSONRPCNotification { method: "notifications/tools/list_changed"; + params?: NotificationParams; } /** - * Additional properties describing a Tool to clients. + * Additional properties describing a {@link Tool} to clients. * - * NOTE: all properties in ToolAnnotations are **hints**. + * NOTE: all properties in `ToolAnnotations` are **hints**. * They are not guaranteed to provide a faithful description of * tool behavior (including descriptive properties like `title`). * - * Clients should never make tool use decisions based on ToolAnnotations + * Clients should never make tool use decisions based on `ToolAnnotations` * received from untrusted servers. + * + * @category `tools/list` */ export interface ToolAnnotations { /** @@ -960,7 +1666,7 @@ export namespace MCP {/* JSON-RPC types */ /** * If true, calling the tool repeatedly with the same arguments - * will have no additional effect on the its environment. + * will have no additional effect on its environment. * * (This property is meaningful only when `readOnlyHint == false`) * @@ -979,8 +1685,42 @@ export namespace MCP {/* JSON-RPC types */ openWorldHint?: boolean; } + /** + * Execution-related properties for a tool. + * + * @category `tools/list` + */ + export interface ToolExecution { + /** + * Indicates whether this tool supports task-augmented execution. + * This allows clients to handle long-running operations through polling + * the task system. + * + * - `"forbidden"`: Tool does not support task-augmented execution (default when absent) + * - `"optional"`: Tool may support task-augmented execution + * - `"required"`: Tool requires task-augmented execution + * + * Default: `"forbidden"` + */ + taskSupport?: "forbidden" | "optional" | "required"; + } + /** * Definition for a tool the client can call. + * + * @example With default 2020-12 input schema + * {@includeCode ./examples/Tool/with-default-2020-12-input-schema.json} + * + * @example With explicit draft-07 input schema + * {@includeCode ./examples/Tool/with-explicit-draft-07-input-schema.json} + * + * @example With no parameters + * {@includeCode ./examples/Tool/with-no-parameters.json} + * + * @example With output schema for structured content + * {@includeCode ./examples/Tool/with-output-schema-for-structured-content.json} + * + * @category `tools/list` */ export interface Tool extends BaseMetadata, Icons { /** @@ -994,16 +1734,26 @@ export namespace MCP {/* JSON-RPC types */ * A JSON Schema object defining the expected parameters for the tool. */ inputSchema: { + $schema?: string; type: "object"; properties?: { [key: string]: object }; required?: string[]; }; + /** + * Execution-related properties for this tool. + */ + execution?: ToolExecution; + /** * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. + * the structuredContent field of a {@link CallToolResult}. + * + * Defaults to JSON Schema 2020-12 when no explicit `$schema` is provided. + * Currently restricted to `type: "object"` at the root level. */ outputSchema?: { + $schema?: string; type: "object"; properties?: { [key: string]: object }; required?: string[]; @@ -1012,60 +1762,343 @@ export namespace MCP {/* JSON-RPC types */ /** * Optional additional tool information. * - * Display name precedence order is: title, annotations.title, then name. + * Display name precedence order is: `title`, `annotations.title`, then `name`. */ annotations?: ToolAnnotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } - /* Logging */ + /* Tasks */ + /** - * A request from the client to the server, to enable or adjust logging. + * The status of a task. * - * @category logging/setLevel + * @category `tasks` */ - export interface SetLevelRequest extends JSONRPCRequest { - method: "logging/setLevel"; + export type TaskStatus = + | "working" // The request is currently being processed + | "input_required" // The task is waiting for input (e.g., elicitation or sampling) + | "completed" // The request completed successfully and results are available + | "failed" // The associated request did not complete successfully. For tool calls specifically, this includes cases where the tool call result has `isError` set to true. + | "cancelled"; // The request was cancelled before completion + + /** + * Metadata for augmenting a request with task execution. + * Include this in the `task` field of the request parameters. + * + * @category `tasks` + */ + export interface TaskMetadata { + /** + * Requested duration in milliseconds to retain task from creation. + */ + ttl?: number; + } + + /** + * Metadata for associating messages with a task. + * Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`. + * + * @category `tasks` + */ + export interface RelatedTaskMetadata { + /** + * The task identifier this message is associated with. + */ + taskId: string; + } + + /** + * Data associated with a task. + * + * @category `tasks` + */ + export interface Task { + /** + * The task identifier. + */ + taskId: string; + + /** + * Current task state. + */ + status: TaskStatus; + + /** + * Optional human-readable message describing the current task state. + * This can provide context for any status, including: + * - Reasons for "cancelled" status + * - Summaries for "completed" status + * - Diagnostic information for "failed" status (e.g., error details, what went wrong) + */ + statusMessage?: string; + + /** + * ISO 8601 timestamp when the task was created. + */ + createdAt: string; + + /** + * ISO 8601 timestamp when the task was last updated. + */ + lastUpdatedAt: string; + + /** + * Actual retention duration from creation in milliseconds, null for unlimited. + */ + ttl: number | null; + + /** + * Suggested polling interval in milliseconds. + */ + pollInterval?: number; + } + + /** + * The result returned for a task-augmented request. + * + * @category `tasks` + */ + export interface CreateTaskResult extends Result { + task: Task; + } + + /** + * A successful response for a task-augmented request. + * + * @category `tasks` + */ + export interface CreateTaskResultResponse extends JSONRPCResultResponse { + result: CreateTaskResult; + } + + /** + * A request to retrieve the state of a task. + * + * @category `tasks/get` + */ + export interface GetTaskRequest extends JSONRPCRequest { + method: "tasks/get"; params: { /** - * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/message. + * The task identifier to query. */ - level: LoggingLevel; + taskId: string; }; } /** - * JSONRPCNotification of a log message passed from server to client. If no logging/setLevel request has been sent from the client, the server MAY decide which messages to send automatically. + * The result returned for a {@link GetTaskRequest | tasks/get} request. * - * @category notifications/message + * @category `tasks/get` */ - export interface LoggingMessageNotification extends JSONRPCNotification { - method: "notifications/message"; + export type GetTaskResult = Result & Task; + + /** + * A successful response for a {@link GetTaskRequest | tasks/get} request. + * + * @category `tasks/get` + */ + export interface GetTaskResultResponse extends JSONRPCResultResponse { + result: GetTaskResult; + } + + /** + * A request to retrieve the result of a completed task. + * + * @category `tasks/result` + */ + export interface GetTaskPayloadRequest extends JSONRPCRequest { + method: "tasks/result"; params: { /** - * The severity of this log message. - */ - level: LoggingLevel; - /** - * An optional name of the logger issuing this message. + * The task identifier to retrieve results for. */ - logger?: string; + taskId: string; + }; + } + + /** + * The result returned for a {@link GetTaskPayloadRequest | tasks/result} request. + * The structure matches the result type of the original request. + * For example, a {@link CallToolRequest | tools/call} task would return the {@link CallToolResult} structure. + * + * @category `tasks/result` + */ + export interface GetTaskPayloadResult extends Result { + [key: string]: unknown; + } + + /** + * A successful response for a {@link GetTaskPayloadRequest | tasks/result} request. + * + * @category `tasks/result` + */ + export interface GetTaskPayloadResultResponse extends JSONRPCResultResponse { + result: GetTaskPayloadResult; + } + + /** + * A request to cancel a task. + * + * @category `tasks/cancel` + */ + export interface CancelTaskRequest extends JSONRPCRequest { + method: "tasks/cancel"; + params: { /** - * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + * The task identifier to cancel. */ - data: unknown; + taskId: string; }; } + /** + * The result returned for a {@link CancelTaskRequest | tasks/cancel} request. + * + * @category `tasks/cancel` + */ + export type CancelTaskResult = Result & Task; + + /** + * A successful response for a {@link CancelTaskRequest | tasks/cancel} request. + * + * @category `tasks/cancel` + */ + export interface CancelTaskResultResponse extends JSONRPCResultResponse { + result: CancelTaskResult; + } + + /** + * A request to retrieve a list of tasks. + * + * @category `tasks/list` + */ + export interface ListTasksRequest extends PaginatedRequest { + method: "tasks/list"; + } + + /** + * The result returned for a {@link ListTasksRequest | tasks/list} request. + * + * @category `tasks/list` + */ + export interface ListTasksResult extends PaginatedResult { + tasks: Task[]; + } + + /** + * A successful response for a {@link ListTasksRequest | tasks/list} request. + * + * @category `tasks/list` + */ + export interface ListTasksResultResponse extends JSONRPCResultResponse { + result: ListTasksResult; + } + + /** + * Parameters for a `notifications/tasks/status` notification. + * + * @category `notifications/tasks/status` + */ + export type TaskStatusNotificationParams = NotificationParams & Task; + + /** + * An optional notification from the receiver to the requestor, informing them that a task's status has changed. Receivers are not required to send these notifications. + * + * @category `notifications/tasks/status` + */ + export interface TaskStatusNotification extends JSONRPCNotification { + method: "notifications/tasks/status"; + params: TaskStatusNotificationParams; + } + + /* Logging */ + + /** + * Parameters for a `logging/setLevel` request. + * + * @example Set log level to "info" + * {@includeCode ./examples/SetLevelRequestParams/set-log-level-to-info.json} + * + * @category `logging/setLevel` + */ + export interface SetLevelRequestParams extends RequestParams { + /** + * The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as {@link LoggingMessageNotification | notifications/message}. + */ + level: LoggingLevel; + } + + /** + * A request from the client to the server, to enable or adjust logging. + * + * @example Set logging level request + * {@includeCode ./examples/SetLevelRequest/set-logging-level-request.json} + * + * @category `logging/setLevel` + */ + export interface SetLevelRequest extends JSONRPCRequest { + method: "logging/setLevel"; + params: SetLevelRequestParams; + } + + /** + * A successful response from the server for a {@link SetLevelRequest | logging/setLevel} request. + * + * @example Set logging level result response + * {@includeCode ./examples/SetLevelResultResponse/set-logging-level-result-response.json} + * + * @category `logging/setLevel` + */ + export interface SetLevelResultResponse extends JSONRPCResultResponse { + result: EmptyResult; + } + + /** + * Parameters for a `notifications/message` notification. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotificationParams/log-database-connection-failed.json} + * + * @category `notifications/message` + */ + export interface LoggingMessageNotificationParams extends NotificationParams { + /** + * The severity of this log message. + */ + level: LoggingLevel; + /** + * An optional name of the logger issuing this message. + */ + logger?: string; + /** + * The data to be logged, such as a string message or an object. Any JSON serializable type is allowed here. + */ + data: unknown; + } + + /** + * JSONRPCNotification of a log message passed from server to client. If no `logging/setLevel` request has been sent from the client, the server MAY decide which messages to send automatically. + * + * @example Log database connection failed + * {@includeCode ./examples/LoggingMessageNotification/log-database-connection-failed.json} + * + * @category `notifications/message` + */ + export interface LoggingMessageNotification extends JSONRPCNotification { + method: "notifications/message"; + params: LoggingMessageNotificationParams; + } + /** * The severity of a log message. * * These map to syslog message severities, as specified in RFC-5424: * https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 + * + * @category Common Types */ export type LoggingLevel = | "debug" @@ -1078,75 +2111,177 @@ export namespace MCP {/* JSON-RPC types */ | "emergency"; /* Sampling */ + /** + * Parameters for a `sampling/createMessage` request. + * + * @example Basic request + * {@includeCode ./examples/CreateMessageRequestParams/basic-request.json} + * + * @example Request with tools + * {@includeCode ./examples/CreateMessageRequestParams/request-with-tools.json} + * + * @example Follow-up request with tool results + * {@includeCode ./examples/CreateMessageRequestParams/follow-up-with-tool-results.json} + * + * @category `sampling/createMessage` + */ + export interface CreateMessageRequestParams extends TaskAugmentedRequestParams { + messages: SamplingMessage[]; + /** + * The server's preferences for which model to select. The client MAY ignore these preferences. + */ + modelPreferences?: ModelPreferences; + /** + * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + */ + systemPrompt?: string; + /** + * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + * The client MAY ignore this request. + * + * Default is `"none"`. Values `"thisServer"` and `"allServers"` are soft-deprecated. Servers SHOULD only use these values if the client + * declares {@link ClientCapabilities.sampling.context}. These values may be removed in future spec releases. + */ + includeContext?: "none" | "thisServer" | "allServers"; + /** + * @TJS-type number + */ + temperature?: number; + /** + * The requested maximum number of tokens to sample (to prevent runaway completions). + * + * The client MAY choose to sample fewer tokens than the requested maximum. + */ + maxTokens: number; + stopSequences?: string[]; + /** + * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + */ + metadata?: object; + /** + * Tools that the model may use during generation. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + */ + tools?: Tool[]; + /** + * Controls how the model uses tools. + * The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + * Default is `{ mode: "auto" }`. + */ + toolChoice?: ToolChoice; + } + + /** + * Controls tool selection behavior for sampling requests. + * + * @category `sampling/createMessage` + */ + export interface ToolChoice { + /** + * Controls the tool use ability of the model: + * - `"auto"`: Model decides whether to use tools (default) + * - `"required"`: Model MUST use at least one tool before completing + * - `"none"`: Model MUST NOT use any tools + */ + mode?: "auto" | "required" | "none"; + } + /** * A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. * - * @category sampling/createMessage + * @example Sampling request + * {@includeCode ./examples/CreateMessageRequest/sampling-request.json} + * + * @category `sampling/createMessage` */ export interface CreateMessageRequest extends JSONRPCRequest { method: "sampling/createMessage"; - params: { - messages: SamplingMessage[]; - /** - * The server's preferences for which model to select. The client MAY ignore these preferences. - */ - modelPreferences?: ModelPreferences; - /** - * An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - */ - systemPrompt?: string; - /** - * A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. The client MAY ignore this request. - */ - includeContext?: "none" | "thisServer" | "allServers"; - /** - * @TJS-type number - */ - temperature?: number; - /** - * The requested maximum number of tokens to sample (to prevent runaway completions). - * - * The client MAY choose to sample fewer tokens than the requested maximum. - */ - maxTokens: number; - stopSequences?: string[]; - /** - * Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - */ - metadata?: object; - }; + params: CreateMessageRequestParams; } /** - * The client's response to a sampling/create_message request from the server. The client should inform the user before returning the sampled message, to allow them to inspect the response (human in the loop) and decide whether to allow the server to see it. + * The result returned by the client for a {@link CreateMessageRequest | sampling/createMessage} request. + * The client should inform the user before returning the sampled message, to allow them + * to inspect the response (human in the loop) and decide whether to allow the server to see it. + * + * @example Text response + * {@includeCode ./examples/CreateMessageResult/text-response.json} + * + * @example Tool use response + * {@includeCode ./examples/CreateMessageResult/tool-use-response.json} * - * @category sampling/createMessage + * @example Final response after tool use + * {@includeCode ./examples/CreateMessageResult/final-response.json} + * + * @category `sampling/createMessage` */ export interface CreateMessageResult extends Result, SamplingMessage { /** * The name of the model that generated the message. */ model: string; + /** * The reason why sampling stopped, if known. + * + * Standard values: + * - `"endTurn"`: Natural end of the assistant's turn + * - `"stopSequence"`: A stop sequence was encountered + * - `"maxTokens"`: Maximum token limit was reached + * - `"toolUse"`: The model wants to use one or more tools + * + * This field is an open string to allow for provider-specific stop reasons. */ - stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; + stopReason?: "endTurn" | "stopSequence" | "maxTokens" | "toolUse" | string; + } + + /** + * A successful response from the client for a {@link CreateMessageRequest | sampling/createMessage} request. + * + * @example Sampling result response + * {@includeCode ./examples/CreateMessageResultResponse/sampling-result-response.json} + * + * @category `sampling/createMessage` + */ + export interface CreateMessageResultResponse extends JSONRPCResultResponse { + result: CreateMessageResult; } /** * Describes a message issued to or received from an LLM API. + * + * @example Single content block + * {@includeCode ./examples/SamplingMessage/single-content-block.json} + * + * @example Multiple content blocks + * {@includeCode ./examples/SamplingMessage/multiple-content-blocks.json} + * + * @category `sampling/createMessage` */ export interface SamplingMessage { role: Role; - content: TextContent | ImageContent | AudioContent; + content: SamplingMessageContentBlock | SamplingMessageContentBlock[]; + _meta?: MetaObject; } + /** + * @category `sampling/createMessage` + */ + export type SamplingMessageContentBlock = + | TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent; + /** * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed + * + * @category Common Types */ export interface Annotations { /** - * Describes who the intended customer of this object or data is. + * Describes who the intended audience of this object or data is. * * It can include multiple entries to indicate content useful for multiple audiences (e.g., `["user", "assistant"]`). */ @@ -1176,6 +2311,9 @@ export namespace MCP {/* JSON-RPC types */ lastModified?: string; } + /** + * @category Content + */ export type ContentBlock = | TextContent | ImageContent @@ -1185,6 +2323,11 @@ export namespace MCP {/* JSON-RPC types */ /** * Text provided to or from an LLM. + * + * @example Text content + * {@includeCode ./examples/TextContent/text-content.json} + * + * @category Content */ export interface TextContent { type: "text"; @@ -1199,14 +2342,16 @@ export namespace MCP {/* JSON-RPC types */ */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * An image provided to or from an LLM. + * + * @example `image/png` content with annotations + * {@includeCode ./examples/ImageContent/image-png-content-with-annotations.json} + * + * @category Content */ export interface ImageContent { type: "image"; @@ -1228,14 +2373,16 @@ export namespace MCP {/* JSON-RPC types */ */ annotations?: Annotations; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * Audio provided to or from an LLM. + * + * @example `audio/wav` content + * {@includeCode ./examples/AudioContent/audio-wav-content.json} + * + * @category Content */ export interface AudioContent { type: "audio"; @@ -1248,19 +2395,99 @@ export namespace MCP {/* JSON-RPC types */ data: string; /** - * The MIME type of the audio. Different providers may support different audio types. + * The MIME type of the audio. Different providers may support different audio types. + */ + mimeType: string; + + /** + * Optional annotations for the client. + */ + annotations?: Annotations; + + _meta?: MetaObject; + } + + /** + * A request from the assistant to call a tool. + * + * @example `get_weather` tool use + * {@includeCode ./examples/ToolUseContent/get-weather-tool-use.json} + * + * @category `sampling/createMessage` + */ + export interface ToolUseContent { + type: "tool_use"; + + /** + * A unique identifier for this tool use. + * + * This ID is used to match tool results to their corresponding tool uses. + */ + id: string; + + /** + * The name of the tool to call. + */ + name: string; + + /** + * The arguments to pass to the tool, conforming to the tool's input schema. + */ + input: { [key: string]: unknown }; + + /** + * Optional metadata about the tool use. Clients SHOULD preserve this field when + * including tool uses in subsequent sampling requests to enable caching optimizations. + */ + _meta?: MetaObject; + } + + /** + * The result of a tool use, provided by the user back to the assistant. + * + * @example `get_weather` tool result + * {@includeCode ./examples/ToolResultContent/get-weather-tool-result.json} + * + * @category `sampling/createMessage` + */ + export interface ToolResultContent { + type: "tool_result"; + + /** + * The ID of the tool use this result corresponds to. + * + * This MUST match the ID from a previous {@link ToolUseContent}. + */ + toolUseId: string; + + /** + * The unstructured result content of the tool use. + * + * This has the same format as {@link CallToolResult.content} and can include text, images, + * audio, resource links, and embedded resources. + */ + content: ContentBlock[]; + + /** + * An optional structured result object. + * + * If the tool defined an {@link Tool.outputSchema}, this SHOULD conform to that schema. */ - mimeType: string; + structuredContent?: { [key: string]: unknown }; /** - * Optional annotations for the client. + * Whether the tool use resulted in an error. + * + * If true, the content typically describes the error that occurred. + * Default: false */ - annotations?: Annotations; + isError?: boolean; /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. + * Optional metadata about the tool result. Clients SHOULD preserve this field when + * including tool results in subsequent sampling requests to enable caching optimizations. */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** @@ -1275,6 +2502,11 @@ export namespace MCP {/* JSON-RPC types */ * These preferences are always advisory. The client MAY ignore them. It is also * up to the client to decide how to interpret these preferences and how to * balance them against other considerations. + * + * @example With hints and priorities + * {@includeCode ./examples/ModelPreferences/with-hints-and-priorities.json} + * + * @category `sampling/createMessage` */ export interface ModelPreferences { /** @@ -1327,6 +2559,8 @@ export namespace MCP {/* JSON-RPC types */ * * Keys not declared here are currently left unspecified by the spec and are up * to the client to interpret. + * + * @category `sampling/createMessage` */ export interface ModelHint { /** @@ -1345,44 +2579,66 @@ export namespace MCP {/* JSON-RPC types */ /* Autocomplete */ /** - * A request from the client to the server, to ask for completion options. + * Parameters for a `completion/complete` request. + * + * @category `completion/complete` + * + * @example Prompt argument completion + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion.json} * - * @category completion/complete + * @example Prompt argument completion with context + * {@includeCode ./examples/CompleteRequestParams/prompt-argument-completion-with-context.json} */ - export interface CompleteRequest extends JSONRPCRequest { - method: "completion/complete"; - params: { - ref: PromptReference | ResourceTemplateReference; + export interface CompleteRequestParams extends RequestParams { + ref: PromptReference | ResourceTemplateReference; + /** + * The argument's information + */ + argument: { /** - * The argument's information + * The name of the argument */ - argument: { - /** - * The name of the argument - */ - name: string; - /** - * The value of the argument to use for completion matching. - */ - value: string; - }; + name: string; + /** + * The value of the argument to use for completion matching. + */ + value: string; + }; + /** + * Additional, optional context for completions + */ + context?: { /** - * Additional, optional context for completions + * Previously-resolved variables in a URI template or prompt. */ - context?: { - /** - * Previously-resolved variables in a URI template or prompt. - */ - arguments?: { [key: string]: string }; - }; + arguments?: { [key: string]: string }; }; } /** - * The server's response to a completion/complete request + * A request from the client to the server, to ask for completion options. + * + * @example Completion request + * {@includeCode ./examples/CompleteRequest/completion-request.json} + * + * @category `completion/complete` + */ + export interface CompleteRequest extends JSONRPCRequest { + method: "completion/complete"; + params: CompleteRequestParams; + } + + /** + * The result returned by the server for a {@link CompleteRequest | completion/complete} request. + * + * @category `completion/complete` * - * @category completion/complete + * @example Single completion value + * {@includeCode ./examples/CompleteResult/single-completion-value.json} + * + * @example Multiple completion values with more available + * {@includeCode ./examples/CompleteResult/multiple-completion-values-with-more-available.json} */ export interface CompleteResult extends Result { completion: { @@ -1401,8 +2657,22 @@ export namespace MCP {/* JSON-RPC types */ }; } + /** + * A successful response from the server for a {@link CompleteRequest | completion/complete} request. + * + * @example Completion result response + * {@includeCode ./examples/CompleteResultResponse/completion-result-response.json} + * + * @category `completion/complete` + */ + export interface CompleteResultResponse extends JSONRPCResultResponse { + result: CompleteResult; + } + /** * A reference to a resource or resource template definition. + * + * @category `completion/complete` */ export interface ResourceTemplateReference { type: "ref/resource"; @@ -1416,6 +2686,8 @@ export namespace MCP {/* JSON-RPC types */ /** * Identifies a prompt. + * + * @category `completion/complete` */ export interface PromptReference extends BaseMetadata { type: "ref/prompt"; @@ -1431,29 +2703,56 @@ export namespace MCP {/* JSON-RPC types */ * This request is typically used when the server needs to understand the file system * structure or access specific locations that the client has permission to read from. * - * @category roots/list + * @example List roots request + * {@includeCode ./examples/ListRootsRequest/list-roots-request.json} + * + * @category `roots/list` */ export interface ListRootsRequest extends JSONRPCRequest { method: "roots/list"; + params?: RequestParams; } /** - * The client's response to a roots/list request from the server. - * This result contains an array of Root objects, each representing a root directory + * The result returned by the client for a {@link ListRootsRequest | roots/list} request. + * This result contains an array of {@link Root} objects, each representing a root directory * or file that the server can operate on. * - * @category roots/list + * @example Single root directory + * {@includeCode ./examples/ListRootsResult/single-root-directory.json} + * + * @example Multiple root directories + * {@includeCode ./examples/ListRootsResult/multiple-root-directories.json} + * + * @category `roots/list` */ export interface ListRootsResult extends Result { roots: Root[]; } + /** + * A successful response from the client for a {@link ListRootsRequest | roots/list} request. + * + * @example List roots result response + * {@includeCode ./examples/ListRootsResultResponse/list-roots-result-response.json} + * + * @category `roots/list` + */ + export interface ListRootsResultResponse extends JSONRPCResultResponse { + result: ListRootsResult; + } + /** * Represents a root directory or file that the server can operate on. + * + * @example Project directory root + * {@includeCode ./examples/Root/project-directory.json} + * + * @category `roots/list` */ export interface Root { /** - * The URI identifying the root. This *must* start with file:// for now. + * The URI identifying the root. This *must* start with `file://` for now. * This restriction may be relaxed in future versions of the protocol to allow * other URI schemes. * @@ -1467,52 +2766,120 @@ export namespace MCP {/* JSON-RPC types */ */ name?: string; - /** - * See [General fields: `_meta`](/specification/draft/basic/index#meta) for notes on `_meta` usage. - */ - _meta?: { [key: string]: unknown }; + _meta?: MetaObject; } /** * A notification from the client to the server, informing it that the list of roots has changed. * This notification should be sent whenever the client adds, removes, or modifies any root. - * The server should then request an updated list of roots using the ListRootsRequest. + * The server should then request an updated list of roots using the {@link ListRootsRequest}. * - * @category notifications/roots/list_changed + * @example Roots list changed + * {@includeCode ./examples/RootsListChangedNotification/roots-list-changed.json} + * + * @category `notifications/roots/list_changed` */ export interface RootsListChangedNotification extends JSONRPCNotification { method: "notifications/roots/list_changed"; + params?: NotificationParams; + } + + /** + * The parameters for a request to elicit non-sensitive information from the user via a form in the client. + * + * @example Elicit single field + * {@includeCode ./examples/ElicitRequestFormParams/elicit-single-field.json} + * + * @example Elicit multiple fields + * {@includeCode ./examples/ElicitRequestFormParams/elicit-multiple-fields.json} + * + * @category `elicitation/create` + */ + export interface ElicitRequestFormParams extends TaskAugmentedRequestParams { + /** + * The elicitation mode. + */ + mode?: "form"; + + /** + * The message to present to the user describing what information is being requested. + */ + message: string; + + /** + * A restricted subset of JSON Schema. + * Only top-level properties are allowed, without nesting. + */ + requestedSchema: { + $schema?: string; + type: "object"; + properties: { + [key: string]: PrimitiveSchemaDefinition; + }; + required?: string[]; + }; + } + + /** + * The parameters for a request to elicit information from the user via a URL in the client. + * + * @example Elicit sensitive data + * {@includeCode ./examples/ElicitRequestURLParams/elicit-sensitive-data.json} + * + * @category `elicitation/create` + */ + export interface ElicitRequestURLParams extends TaskAugmentedRequestParams { + /** + * The elicitation mode. + */ + mode: "url"; + + /** + * The message to present to the user explaining why the interaction is needed. + */ + message: string; + + /** + * The ID of the elicitation, which must be unique within the context of the server. + * The client MUST treat this ID as an opaque value. + */ + elicitationId: string; + + /** + * The URL that the user should navigate to. + * + * @format uri + */ + url: string; } + /** + * The parameters for a request to elicit additional information from the user via the client. + * + * @category `elicitation/create` + */ + export type ElicitRequestParams = + | ElicitRequestFormParams + | ElicitRequestURLParams; + /** * A request from the server to elicit additional information from the user via the client. * - * @category elicitation/create + * @example Elicitation request + * {@includeCode ./examples/ElicitRequest/elicitation-request.json} + * + * @category `elicitation/create` */ export interface ElicitRequest extends JSONRPCRequest { method: "elicitation/create"; - params: { - /** - * The message to present to the user. - */ - message: string; - /** - * A restricted subset of JSON Schema. - * Only top-level properties are allowed, without nesting. - */ - requestedSchema: { - type: "object"; - properties: { - [key: string]: PrimitiveSchemaDefinition; - }; - required?: string[]; - }; - }; + params: ElicitRequestParams; } /** * Restricted schema definitions that only allow primitive types * without nested objects or arrays. + * + * @category `elicitation/create` */ export type PrimitiveSchemaDefinition = | StringSchema @@ -1520,6 +2887,12 @@ export namespace MCP {/* JSON-RPC types */ | BooleanSchema | EnumSchema; + /** + * @example Email input schema + * {@includeCode ./examples/StringSchema/email-input-schema.json} + * + * @category `elicitation/create` + */ export interface StringSchema { type: "string"; title?: string; @@ -1530,6 +2903,12 @@ export namespace MCP {/* JSON-RPC types */ default?: string; } + /** + * @example Number input schema + * {@includeCode ./examples/NumberSchema/number-input-schema.json} + * + * @category `elicitation/create` + */ export interface NumberSchema { type: "number" | "integer"; title?: string; @@ -1539,6 +2918,12 @@ export namespace MCP {/* JSON-RPC types */ default?: number; } + /** + * @example Boolean input schema + * {@includeCode ./examples/BooleanSchema/boolean-input-schema.json} + * + * @category `elicitation/create` + */ export interface BooleanSchema { type: "boolean"; title?: string; @@ -1546,34 +2931,266 @@ export namespace MCP {/* JSON-RPC types */ default?: boolean; } - export interface EnumSchema { + /** + * Schema for single-selection enumeration without display titles for options. + * + * @example Color select schema + * {@includeCode ./examples/UntitledSingleSelectEnumSchema/color-select-schema.json} + * + * @category `elicitation/create` + */ + export interface UntitledSingleSelectEnumSchema { + type: "string"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum values to choose from. + */ + enum: string[]; + /** + * Optional default value. + */ + default?: string; + } + + /** + * Schema for single-selection enumeration with display titles for each option. + * + * @example Titled color select schema + * {@includeCode ./examples/TitledSingleSelectEnumSchema/titled-color-select-schema.json} + * + * @category `elicitation/create` + */ + export interface TitledSingleSelectEnumSchema { + type: "string"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Array of enum options with values and display labels. + */ + oneOf: Array<{ + /** + * The enum value. + */ + const: string; + /** + * Display label for this option. + */ + title: string; + }>; + /** + * Optional default value. + */ + default?: string; + } + + /** + * @category `elicitation/create` + */ + // Combined single selection enumeration + export type SingleSelectEnumSchema = + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema; + + /** + * Schema for multiple-selection enumeration without display titles for options. + * + * @example Color multi-select schema + * {@includeCode ./examples/UntitledMultiSelectEnumSchema/color-multi-select-schema.json} + * + * @category `elicitation/create` + */ + export interface UntitledMultiSelectEnumSchema { + type: "array"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for the array items. + */ + items: { + type: "string"; + /** + * Array of enum values to choose from. + */ + enum: string[]; + }; + /** + * Optional default value. + */ + default?: string[]; + } + + /** + * Schema for multiple-selection enumeration with display titles for each option. + * + * @example Titled color multi-select schema + * {@includeCode ./examples/TitledMultiSelectEnumSchema/titled-color-multi-select-schema.json} + * + * @category `elicitation/create` + */ + export interface TitledMultiSelectEnumSchema { + type: "array"; + /** + * Optional title for the enum field. + */ + title?: string; + /** + * Optional description for the enum field. + */ + description?: string; + /** + * Minimum number of items to select. + */ + minItems?: number; + /** + * Maximum number of items to select. + */ + maxItems?: number; + /** + * Schema for array items with enum options and display labels. + */ + items: { + /** + * Array of enum options with values and display labels. + */ + anyOf: Array<{ + /** + * The constant enum value. + */ + const: string; + /** + * Display title for this option. + */ + title: string; + }>; + }; + /** + * Optional default value. + */ + default?: string[]; + } + + /** + * @category `elicitation/create` + */ + // Combined multiple selection enumeration + export type MultiSelectEnumSchema = + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema; + + /** + * Use {@link TitledSingleSelectEnumSchema} instead. + * This interface will be removed in a future version. + * + * @category `elicitation/create` + */ + export interface LegacyTitledEnumSchema { type: "string"; title?: string; description?: string; enum: string[]; - enumNames?: string[]; // Display names for enum values + /** + * (Legacy) Display names for enum values. + * Non-standard according to JSON schema 2020-12. + */ + enumNames?: string[]; default?: string; } /** - * The client's response to an elicitation request. + * @category `elicitation/create` + */ + // Union type for all enum schemas + export type EnumSchema = + | SingleSelectEnumSchema + | MultiSelectEnumSchema + | LegacyTitledEnumSchema; + + /** + * The result returned by the client for an {@link ElicitRequest | elicitation/create} request. + * + * @example Input single field + * {@includeCode ./examples/ElicitResult/input-single-field.json} + * + * @example Input multiple fields + * {@includeCode ./examples/ElicitResult/input-multiple-fields.json} + * + * @example Accept URL mode (no content) + * {@includeCode ./examples/ElicitResult/accept-url-mode-no-content.json} * - * @category elicitation/create + * @category `elicitation/create` */ export interface ElicitResult extends Result { /** * The user action in response to the elicitation. - * - "accept": User submitted the form/confirmed the action - * - "decline": User explicitly decline the action - * - "cancel": User dismissed without making an explicit choice + * - `"accept"`: User submitted the form/confirmed the action + * - `"decline"`: User explicitly declined the action + * - `"cancel"`: User dismissed without making an explicit choice */ action: "accept" | "decline" | "cancel"; /** - * The submitted form data, only present when action is "accept". + * The submitted form data, only present when action is `"accept"` and mode was `"form"`. * Contains values matching the requested schema. + * Omitted for out-of-band mode responses. */ - content?: { [key: string]: string | number | boolean }; + content?: { [key: string]: string | number | boolean | string[] }; + } + + /** + * A successful response from the client for a {@link ElicitRequest | elicitation/create} request. + * + * @example Elicitation result response + * {@includeCode ./examples/ElicitResultResponse/elicitation-result-response.json} + * + * @category `elicitation/create` + */ + export interface ElicitResultResponse extends JSONRPCResultResponse { + result: ElicitResult; + } + + /** + * An optional notification from the server to the client, informing it of a completion of a out-of-band elicitation request. + * + * @example Elicitation complete + * {@includeCode ./examples/ElicitationCompleteNotification/elicitation-complete.json} + * + * @category `notifications/elicitation/complete` + */ + export interface ElicitationCompleteNotification extends JSONRPCNotification { + method: "notifications/elicitation/complete"; + params: { + /** + * The ID of the elicitation that completed. + */ + elicitationId: string; + }; } /* Client messages */ @@ -1591,21 +3208,30 @@ export namespace MCP {/* JSON-RPC types */ | SubscribeRequest | UnsubscribeRequest | CallToolRequest - | ListToolsRequest; + | ListToolsRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; /** @internal */ export type ClientNotification = | CancelledNotification | ProgressNotification | InitializedNotification - | RootsListChangedNotification; + | RootsListChangedNotification + | TaskStatusNotification; /** @internal */ export type ClientResult = | EmptyResult | CreateMessageResult | ListRootsResult - | ElicitResult; + | ElicitResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult; /* Server messages */ /** @internal */ @@ -1613,7 +3239,11 @@ export namespace MCP {/* JSON-RPC types */ | PingRequest | CreateMessageRequest | ListRootsRequest - | ElicitRequest; + | ElicitRequest + | GetTaskRequest + | GetTaskPayloadRequest + | ListTasksRequest + | CancelTaskRequest; /** @internal */ export type ServerNotification = @@ -1623,7 +3253,9 @@ export namespace MCP {/* JSON-RPC types */ | ResourceUpdatedNotification | ResourceListChangedNotification | ToolListChangedNotification - | PromptListChangedNotification; + | PromptListChangedNotification + | ElicitationCompleteNotification + | TaskStatusNotification; /** @internal */ export type ServerResult = @@ -1636,5 +3268,10 @@ export namespace MCP {/* JSON-RPC types */ | ListResourcesResult | ReadResourceResult | CallToolResult - | ListToolsResult; + | CreateTaskResult + | ListToolsResult + | GetTaskResult + | GetTaskPayloadResult + | ListTasksResult + | CancelTaskResult; } diff --git a/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts new file mode 100644 index 00000000000..4569e8f25ac --- /dev/null +++ b/src/vs/workbench/contrib/mcp/common/modelContextProtocolApps.ts @@ -0,0 +1,737 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MCP } from './modelContextProtocol.js'; + +type CallToolResult = MCP.CallToolResult; +type ContentBlock = MCP.ContentBlock; +type Implementation = MCP.Implementation; +type RequestId = MCP.RequestId; +type Tool = MCP.Tool; + +export namespace McpApps { + export type AppRequest = + | MCP.CallToolRequest + | MCP.ReadResourceRequest + | MCP.PingRequest + | (McpUiOpenLinkRequest & MCP.JSONRPCRequest) + | (McpUiUpdateModelContextRequest & MCP.JSONRPCRequest) + | (McpUiMessageRequest & MCP.JSONRPCRequest) + | (McpUiRequestDisplayModeRequest & MCP.JSONRPCRequest) + | (McpApps.McpUiInitializeRequest & MCP.JSONRPCRequest); + + export type AppNotification = + | McpUiInitializedNotification + | McpUiSizeChangedNotification + | MCP.LoggingMessageNotification + | CustomSandboxWheelNotification; + + export type AppMessage = AppRequest | AppNotification; + + export type HostResult = + | MCP.CallToolResult + | MCP.ReadResourceResult + | MCP.EmptyResult + | McpApps.McpUiInitializeResult + | McpUiMessageResult + | McpUiOpenLinkResult + | McpUiRequestDisplayModeResult; + + export type HostNotification = + | McpUiHostContextChangedNotification + | McpUiResourceTeardownRequest + | McpUiToolInputNotification + | McpUiToolInputPartialNotification + | McpUiToolResultNotification + | McpUiToolCancelledNotification + | McpUiSizeChangedNotification; + + export type HostMessage = HostResult | HostNotification; + + + /** Custom notification used for bubbling up sandbox wheel events. */ + export interface CustomSandboxWheelNotification { + method: 'ui/notifications/sandbox-wheel'; + params: { + deltaMode: number; + deltaX: number; + deltaY: number; + deltaZ: number; + }; + } +} + +/* eslint-disable local/code-no-unexternalized-strings */ + + +/** + * Schema updated from the Model Context Protocol Apps repository at + * https://github.com/modelcontextprotocol/ext-apps/blob/main/src/spec.types.ts + * + * ⚠️ Do not edit within `namespace` manually except to update schema versions ⚠️ + */ +export namespace McpApps { + /** + * Current protocol version supported by this SDK. + * + * The SDK automatically handles version negotiation during initialization. + * Apps and hosts don't need to manage protocol versions manually. + */ + export const LATEST_PROTOCOL_VERSION = "2026-01-26"; + + /** + * @description Color theme preference for the host environment. + */ + export type McpUiTheme = "light" | "dark"; + + /** + * @description Display mode for UI presentation. + */ + export type McpUiDisplayMode = "inline" | "fullscreen" | "pip"; + + /** + * @description CSS variable keys available to MCP apps for theming. + */ + export type McpUiStyleVariableKey = + // Background colors + | "--color-background-primary" + | "--color-background-secondary" + | "--color-background-tertiary" + | "--color-background-inverse" + | "--color-background-ghost" + | "--color-background-info" + | "--color-background-danger" + | "--color-background-success" + | "--color-background-warning" + | "--color-background-disabled" + // Text colors + | "--color-text-primary" + | "--color-text-secondary" + | "--color-text-tertiary" + | "--color-text-inverse" + | "--color-text-ghost" + | "--color-text-info" + | "--color-text-danger" + | "--color-text-success" + | "--color-text-warning" + | "--color-text-disabled" + | "--color-text-ghost" + // Border colors + | "--color-border-primary" + | "--color-border-secondary" + | "--color-border-tertiary" + | "--color-border-inverse" + | "--color-border-ghost" + | "--color-border-info" + | "--color-border-danger" + | "--color-border-success" + | "--color-border-warning" + | "--color-border-disabled" + // Ring colors + | "--color-ring-primary" + | "--color-ring-secondary" + | "--color-ring-inverse" + | "--color-ring-info" + | "--color-ring-danger" + | "--color-ring-success" + | "--color-ring-warning" + // Typography - Family + | "--font-sans" + | "--font-mono" + // Typography - Weight + | "--font-weight-normal" + | "--font-weight-medium" + | "--font-weight-semibold" + | "--font-weight-bold" + // Typography - Text Size + | "--font-text-xs-size" + | "--font-text-sm-size" + | "--font-text-md-size" + | "--font-text-lg-size" + // Typography - Heading Size + | "--font-heading-xs-size" + | "--font-heading-sm-size" + | "--font-heading-md-size" + | "--font-heading-lg-size" + | "--font-heading-xl-size" + | "--font-heading-2xl-size" + | "--font-heading-3xl-size" + // Typography - Text Line Height + | "--font-text-xs-line-height" + | "--font-text-sm-line-height" + | "--font-text-md-line-height" + | "--font-text-lg-line-height" + // Typography - Heading Line Height + | "--font-heading-xs-line-height" + | "--font-heading-sm-line-height" + | "--font-heading-md-line-height" + | "--font-heading-lg-line-height" + | "--font-heading-xl-line-height" + | "--font-heading-2xl-line-height" + | "--font-heading-3xl-line-height" + // Border radius + | "--border-radius-xs" + | "--border-radius-sm" + | "--border-radius-md" + | "--border-radius-lg" + | "--border-radius-xl" + | "--border-radius-full" + // Border width + | "--border-width-regular" + // Shadows + | "--shadow-hairline" + | "--shadow-sm" + | "--shadow-md" + | "--shadow-lg"; + + /** + * @description Style variables for theming MCP apps. + * + * Individual style keys are optional - hosts may provide any subset of these values. + * Values are strings containing CSS values (colors, sizes, font stacks, etc.). + * + * Note: This type uses `Record` rather than `Partial>` + * for compatibility with Zod schema generation. Both are functionally equivalent for validation. + */ + export type McpUiStyles = Record; + + /** + * @description Request to open an external URL in the host's default browser. + * @see {@link app.App.sendOpenLink} for the method that sends this request + */ + export interface McpUiOpenLinkRequest { + method: "ui/open-link"; + params: { + /** @description URL to open in the host's browser */ + url: string; + }; + } + + /** + * @description Result from opening a URL. + * @see {@link McpUiOpenLinkRequest} + */ + export interface McpUiOpenLinkResult { + /** @description True if the host failed to open the URL (e.g., due to security policy). */ + isError?: boolean; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; + } + + /** + * @description Request to send a message to the host's chat interface. + * @see {@link app.App.sendMessage} for the method that sends this request + */ + export interface McpUiMessageRequest { + method: "ui/message"; + params: { + /** @description Message role, currently only "user" is supported. */ + role: "user"; + /** @description Message content blocks (text, image, etc.). */ + content: ContentBlock[]; + }; + } + + /** + * @description Result from sending a message. + * @see {@link McpUiMessageRequest} + */ + export interface McpUiMessageResult { + /** @description True if the host rejected or failed to deliver the message. */ + isError?: boolean; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; + } + + /** + * @description Notification that the sandbox proxy iframe is ready to receive content. + * @internal + * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy + */ + export interface McpUiSandboxProxyReadyNotification { + method: "ui/notifications/sandbox-proxy-ready"; + params: {}; + } + + /** + * @description Notification containing HTML resource for the sandbox proxy to load. + * @internal + * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy + */ + export interface McpUiSandboxResourceReadyNotification { + method: "ui/notifications/sandbox-resource-ready"; + params: { + /** @description HTML content to load into the inner iframe. */ + html: string; + /** @description Optional override for the inner iframe's sandbox attribute. */ + sandbox?: string; + /** @description CSP configuration from resource metadata. */ + csp?: McpUiResourceCsp; + /** @description Sandbox permissions from resource metadata. */ + permissions?: McpUiResourcePermissions; + }; + } + + /** + * @description Notification of UI size changes (bidirectional: Guest <-> Host). + * @see {@link app.App.sendSizeChanged} for the method to send this from Guest UI + */ + export interface McpUiSizeChangedNotification { + method: "ui/notifications/size-changed"; + params: { + /** @description New width in pixels. */ + width?: number; + /** @description New height in pixels. */ + height?: number; + }; + } + + /** + * @description Notification containing complete tool arguments (Host -> Guest UI). + */ + export interface McpUiToolInputNotification { + method: "ui/notifications/tool-input"; + params: { + /** @description Complete tool call arguments as key-value pairs. */ + arguments?: Record; + }; + } + + /** + * @description Notification containing partial/streaming tool arguments (Host -> Guest UI). + */ + export interface McpUiToolInputPartialNotification { + method: "ui/notifications/tool-input-partial"; + params: { + /** @description Partial tool call arguments (incomplete, may change). */ + arguments?: Record; + }; + } + + /** + * @description Notification containing tool execution result (Host -> Guest UI). + */ + export interface McpUiToolResultNotification { + method: "ui/notifications/tool-result"; + /** @description Standard MCP tool execution result. */ + params: CallToolResult; + } + + /** + * @description Notification that tool execution was cancelled (Host -> Guest UI). + * Host MUST send this if tool execution was cancelled for any reason (user action, + * sampling error, classifier intervention, etc.). + */ + export interface McpUiToolCancelledNotification { + method: "ui/notifications/tool-cancelled"; + params: { + /** @description Optional reason for the cancellation (e.g., "user action", "timeout"). */ + reason?: string; + }; + } + + /** + * @description CSS blocks that can be injected by apps. + */ + export interface McpUiHostCss { + /** @description CSS for font loading (@font-face rules or @import statements). Apps must apply using applyHostFonts(). */ + fonts?: string; + } + + /** + * @description Style configuration for theming MCP apps. + */ + export interface McpUiHostStyles { + /** @description CSS variables for theming the app. */ + variables?: McpUiStyles; + /** @description CSS blocks that apps can inject. */ + css?: McpUiHostCss; + } + + /** + * @description Rich context about the host environment provided to Guest UIs. + */ + export interface McpUiHostContext { + /** @description Allow additional properties for forward compatibility. */ + [key: string]: unknown; + /** @description Metadata of the tool call that instantiated this App. */ + toolInfo?: { + /** @description JSON-RPC id of the tools/call request. */ + id?: RequestId; + /** @description Tool definition including name, inputSchema, etc. */ + tool: Tool; + }; + /** @description Current color theme preference. */ + theme?: McpUiTheme; + /** @description Style configuration for theming the app. */ + styles?: McpUiHostStyles; + /** @description How the UI is currently displayed. */ + displayMode?: McpUiDisplayMode; + /** @description Display modes the host supports. */ + availableDisplayModes?: string[]; + /** + * @description Container dimensions. Represents the dimensions of the iframe or other + * container holding the app. Specify either width or maxWidth, and either height or maxHeight. + */ + containerDimensions?: ( + | { + /** @description Fixed container height in pixels. */ + height: number; + } + | { + /** @description Maximum container height in pixels. */ + maxHeight?: number | undefined; + } + ) & + ( + | { + /** @description Fixed container width in pixels. */ + width: number; + } + | { + /** @description Maximum container width in pixels. */ + maxWidth?: number | undefined; + } + ); + /** @description User's language and region preference in BCP 47 format. */ + locale?: string; + /** @description User's timezone in IANA format. */ + timeZone?: string; + /** @description Host application identifier. */ + userAgent?: string; + /** @description Platform type for responsive design decisions. */ + platform?: "web" | "desktop" | "mobile"; + /** @description Device input capabilities. */ + deviceCapabilities?: { + /** @description Whether the device supports touch input. */ + touch?: boolean; + /** @description Whether the device supports hover interactions. */ + hover?: boolean; + }; + /** @description Mobile safe area boundaries in pixels. */ + safeAreaInsets?: { + /** @description Top safe area inset in pixels. */ + top: number; + /** @description Right safe area inset in pixels. */ + right: number; + /** @description Bottom safe area inset in pixels. */ + bottom: number; + /** @description Left safe area inset in pixels. */ + left: number; + }; + } + + /** + * @description Notification that host context has changed (Host -> Guest UI). + * @see {@link McpUiHostContext} for the full context structure + */ + export interface McpUiHostContextChangedNotification { + method: "ui/notifications/host-context-changed"; + /** @description Partial context update containing only changed fields. */ + params: McpUiHostContext; + } + + /** + * @description Request to update the agent's context without requiring a follow-up action (Guest UI -> Host). + * + * Unlike `notifications/message` which is for debugging/logging, this request is intended + * to update the Host's model context. Each request overwrites the previous context sent by the Guest UI. + * Unlike messages, context updates do not trigger follow-ups. + * + * The host will typically defer sending the context to the model until the next user message + * (including `ui/message`), and will only send the last update received. + * + * @see {@link app.App.updateModelContext} for the method that sends this request + */ + export interface McpUiUpdateModelContextRequest { + method: "ui/update-model-context"; + params: { + /** @description Context content blocks (text, image, etc.). */ + content?: ContentBlock[]; + /** @description Structured content for machine-readable context data. */ + structuredContent?: Record; + }; + } + + /** + * @description Request for graceful shutdown of the Guest UI (Host -> Guest UI). + * @see {@link app-bridge.AppBridge.teardownResource} for the host method that sends this + */ + export interface McpUiResourceTeardownRequest { + method: "ui/resource-teardown"; + params: {}; + } + + /** + * @description Result from graceful shutdown request. + * @see {@link McpUiResourceTeardownRequest} + */ + export interface McpUiResourceTeardownResult { + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + */ + [key: string]: unknown; + } + + export interface McpUiSupportedContentBlockModalities { + /** @description Host supports text content blocks. */ + text?: {}; + /** @description Host supports image content blocks. */ + image?: {}; + /** @description Host supports audio content blocks. */ + audio?: {}; + /** @description Host supports resource content blocks. */ + resource?: {}; + /** @description Host supports resource link content blocks. */ + resourceLink?: {}; + /** @description Host supports structured content. */ + structuredContent?: {}; + } + + /** + * @description Capabilities supported by the host application. + * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities + */ + export interface McpUiHostCapabilities { + /** @description Experimental features (structure TBD). */ + experimental?: {}; + /** @description Host supports opening external URLs. */ + openLinks?: {}; + /** @description Host can proxy tool calls to the MCP server. */ + serverTools?: { + /** @description Host supports tools/list_changed notifications. */ + listChanged?: boolean; + }; + /** @description Host can proxy resource reads to the MCP server. */ + serverResources?: { + /** @description Host supports resources/list_changed notifications. */ + listChanged?: boolean; + }; + /** @description Host accepts log messages. */ + logging?: {}; + /** @description Sandbox configuration applied by the host. */ + sandbox?: { + /** @description Permissions granted by the host (camera, microphone, geolocation). */ + permissions?: McpUiResourcePermissions; + /** @description CSP domains approved by the host. */ + csp?: McpUiResourceCsp; + }; + /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ + updateModelContext?: McpUiSupportedContentBlockModalities; + /** @description Host supports receiving content messages (ui/message) from the View. */ + message?: McpUiSupportedContentBlockModalities; + } + + /** + * @description Capabilities provided by the View (App). + * @see {@link McpUiInitializeRequest} for the initialization request that includes these capabilities + */ + export interface McpUiAppCapabilities { + /** @description Experimental features (structure TBD). */ + experimental?: {}; + /** @description App exposes MCP-style tools that the host can call. */ + tools?: { + /** @description App supports tools/list_changed notifications. */ + listChanged?: boolean; + }; + /** + * @description Display modes the app supports. See Display Modes section of the spec for details. + * @example ["inline", "fullscreen"] + */ + availableDisplayModes?: McpUiDisplayMode[]; + } + + /** + * @description Initialization request sent from Guest UI to Host. + * @see {@link app.App.connect} for the method that sends this request + */ + export interface McpUiInitializeRequest { + method: "ui/initialize"; + params: { + /** @description App identification (name and version). */ + appInfo: Implementation; + /** @description Features and capabilities this app provides. */ + appCapabilities: McpUiAppCapabilities; + /** @description Protocol version this app supports. */ + protocolVersion: string; + }; + } + + /** + * @description Initialization result returned from Host to Guest UI. + * @see {@link McpUiInitializeRequest} + */ + export interface McpUiInitializeResult { + /** @description Negotiated protocol version string (e.g., "2025-11-21"). */ + protocolVersion: string; + /** @description Host application identification and version. */ + hostInfo: Implementation; + /** @description Features and capabilities provided by the host. */ + hostCapabilities: McpUiHostCapabilities; + /** @description Rich context about the host environment. */ + hostContext: McpUiHostContext; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; + } + + /** + * @description Notification that Guest UI has completed initialization (Guest UI -> Host). + * @see {@link app.App.connect} for the method that sends this notification + */ + export interface McpUiInitializedNotification { + method: "ui/notifications/initialized"; + params?: {}; + } + + /** + * @description Content Security Policy configuration for UI resources. + */ + export interface McpUiResourceCsp { + /** @description Origins for network requests (fetch/XHR/WebSocket). */ + connectDomains?: string[]; + /** @description Origins for static resources (scripts, images, styles, fonts). */ + resourceDomains?: string[]; + /** @description Origins for nested iframes (frame-src directive). */ + frameDomains?: string[]; + /** @description Allowed base URIs for the document (base-uri directive). */ + baseUriDomains?: string[]; + } + + /** + * @description Sandbox permissions requested by the UI resource. + * Hosts MAY honor these by setting appropriate iframe `allow` attributes. + * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. + */ + export interface McpUiResourcePermissions { + /** @description Request camera access (Permission Policy `camera` feature). */ + camera?: {}; + /** @description Request microphone access (Permission Policy `microphone` feature). */ + microphone?: {}; + /** @description Request geolocation access (Permission Policy `geolocation` feature). */ + geolocation?: {}; + /** @description Request clipboard write access (Permission Policy `clipboard-write` feature). */ + clipboardWrite?: {}; + } + + /** + * @description UI Resource metadata for security and rendering configuration. + */ + export interface McpUiResourceMeta { + /** @description Content Security Policy configuration. */ + csp?: McpUiResourceCsp; + /** @description Sandbox permissions requested by the UI. */ + permissions?: McpUiResourcePermissions; + /** @description Dedicated origin for widget sandbox. */ + domain?: string; + /** @description Visual boundary preference - true if UI prefers a visible border. */ + prefersBorder?: boolean; + } + + /** + * @description Request to change the display mode of the UI. + * The host will respond with the actual display mode that was set, + * which may differ from the requested mode if not supported. + * @see {@link app.App.requestDisplayMode} for the method that sends this request + */ + export interface McpUiRequestDisplayModeRequest { + method: "ui/request-display-mode"; + params: { + /** @description The display mode being requested. */ + mode: McpUiDisplayMode; + }; + } + + /** + * @description Result from requesting a display mode change. + * @see {@link McpUiRequestDisplayModeRequest} + */ + export interface McpUiRequestDisplayModeResult { + /** @description The display mode that was actually set. May differ from requested if not supported. */ + mode: McpUiDisplayMode; + /** + * Index signature required for MCP SDK `Protocol` class compatibility. + * Note: The schema intentionally omits this to enforce strict validation. + */ + [key: string]: unknown; + } + + /** + * @description Tool visibility scope - who can access the tool. + */ + export type McpUiToolVisibility = "model" | "app"; + + /** + * @description UI-related metadata for tools. + */ + export interface McpUiToolMeta { + /** + * URI of the UI resource to display for this tool, if any. + * This is converted to `_meta["ui/resourceUri"]`. + * + * @example "ui://weather/widget.html" + */ + resourceUri?: string; + /** + * @description Who can access this tool. Default: ["model", "app"] + * - "model": Tool visible to and callable by the agent + * - "app": Tool callable by the app from this server only + */ + visibility?: McpUiToolVisibility[]; + } + + /** + * Method string constants for MCP Apps protocol messages. + * + * These constants provide a type-safe way to check message methods without + * accessing internal Zod schema properties. External libraries should use + * these constants instead of accessing `schema.shape.method._def.values[0]`. + * + * @example + * ```typescript + * import { SANDBOX_PROXY_READY_METHOD } from '@modelcontextprotocol/ext-apps'; + * + * if (event.data.method === SANDBOX_PROXY_READY_METHOD) { + * // Handle sandbox proxy ready notification + * } + * ``` + */ + export const OPEN_LINK_METHOD: McpUiOpenLinkRequest["method"] = "ui/open-link"; + export const MESSAGE_METHOD: McpUiMessageRequest["method"] = "ui/message"; + export const SANDBOX_PROXY_READY_METHOD: McpUiSandboxProxyReadyNotification["method"] = + "ui/notifications/sandbox-proxy-ready"; + export const SANDBOX_RESOURCE_READY_METHOD: McpUiSandboxResourceReadyNotification["method"] = + "ui/notifications/sandbox-resource-ready"; + export const SIZE_CHANGED_METHOD: McpUiSizeChangedNotification["method"] = + "ui/notifications/size-changed"; + export const TOOL_INPUT_METHOD: McpUiToolInputNotification["method"] = + "ui/notifications/tool-input"; + export const TOOL_INPUT_PARTIAL_METHOD: McpUiToolInputPartialNotification["method"] = + "ui/notifications/tool-input-partial"; + export const TOOL_RESULT_METHOD: McpUiToolResultNotification["method"] = + "ui/notifications/tool-result"; + export const TOOL_CANCELLED_METHOD: McpUiToolCancelledNotification["method"] = + "ui/notifications/tool-cancelled"; + export const HOST_CONTEXT_CHANGED_METHOD: McpUiHostContextChangedNotification["method"] = + "ui/notifications/host-context-changed"; + export const RESOURCE_TEARDOWN_METHOD: McpUiResourceTeardownRequest["method"] = + "ui/resource-teardown"; + export const INITIALIZE_METHOD: McpUiInitializeRequest["method"] = + "ui/initialize"; + export const INITIALIZED_METHOD: McpUiInitializedNotification["method"] = + "ui/notifications/initialized"; + export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method"] = + "ui/request-display-mode"; + export const UPDATE_MODEL_CONTEXT_METHOD: McpUiUpdateModelContextRequest["method"] = + "ui/update-model-context"; +} diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts index 9520d48fe8c..7cdb2b661d0 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpIcons.test.ts @@ -36,8 +36,8 @@ suite('MCP Icons', () => { const result = parseAndValidateMcpIcon({ icons: [ { src: 'ftp://example.com/ignored.png', mimeType: 'image/png' }, - { src: 'data:image/png;base64,AAA', mimeType: 'image/png', sizes: '64x64 16x16' }, - { src: 'https://example.com/icon.png', mimeType: 'image/png', sizes: '128x128' } + { src: 'data:image/png;base64,AAA', mimeType: 'image/png', sizes: ['64x64', '16x16'] }, + { src: 'https://example.com/icon.png', mimeType: 'image/png', sizes: ['128x128'] } ] }, launch, logger); @@ -55,8 +55,8 @@ suite('MCP Icons', () => { const icons = { icons: [ - { src: 'https://example.com/icon.png', mimeType: 'image/png', sizes: '64x64' }, - { src: 'https://other.com/icon.png', mimeType: 'image/png', sizes: '64x64' } + { src: 'https://example.com/icon.png', mimeType: 'image/png', sizes: ['64x64'] }, + { src: 'https://other.com/icon.png', mimeType: 'image/png', sizes: ['64x64'] } ] }; @@ -74,7 +74,7 @@ suite('MCP Icons', () => { const icons = { icons: [ - { src: 'file:///tmp/icon.png', mimeType: 'image/png', sizes: '32x32' } + { src: 'file:///tmp/icon.png', mimeType: 'image/png', sizes: ['32x32'] } ] }; @@ -100,9 +100,9 @@ suite('MCP Icons', () => { const launch = createHttpLaunch('https://example.com'); const parsed = parseAndValidateMcpIcon({ icons: [ - { src: 'https://example.com/dark.png', mimeType: 'image/png', sizes: '16x16 48x48', theme: 'dark' }, - { src: 'https://example.com/any.png', mimeType: 'image/png', sizes: '24x24' }, - { src: 'https://example.com/light.png', mimeType: 'image/png', sizes: '64x64', theme: 'light' } + { src: 'https://example.com/dark.png', mimeType: 'image/png', sizes: ['16x16', '48x48'], theme: 'dark' }, + { src: 'https://example.com/any.png', mimeType: 'image/png', sizes: ['24x24'] }, + { src: 'https://example.com/light.png', mimeType: 'image/png', sizes: ['64x64'], theme: 'light' } ] }, launch, logger); const icons = McpIcons.fromParsed(parsed); @@ -118,8 +118,8 @@ suite('MCP Icons', () => { const launch = createHttpLaunch('https://example.com'); const parsed = parseAndValidateMcpIcon({ icons: [ - { src: 'https://example.com/dark.png', mimeType: 'image/png', sizes: '16x16', theme: 'dark' }, - { src: 'https://example.com/any.png', mimeType: 'image/png', sizes: '64x64' } + { src: 'https://example.com/dark.png', mimeType: 'image/png', sizes: ['16x16'], theme: 'dark' }, + { src: 'https://example.com/any.png', mimeType: 'image/png', sizes: ['64x64'] } ] }, launch, logger); const icons = McpIcons.fromParsed(parsed); @@ -135,7 +135,7 @@ suite('MCP Icons', () => { const launch = createHttpLaunch('https://example.com'); const parsed = parseAndValidateMcpIcon({ icons: [ - { src: 'https://example.com/light.png', mimeType: 'image/png', sizes: '32x32', theme: 'light' } + { src: 'https://example.com/light.png', mimeType: 'image/png', sizes: ['32x32'], theme: 'light' } ] }, launch, logger); const icons = McpIcons.fromParsed(parsed); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts index 14d419314c2..d0c7e955235 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistry.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { timeout } from '../../../../../base/common/async.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; import { ISettableObservable, observableValue } from '../../../../../base/common/observable.js'; import { upcast } from '../../../../../base/common/types.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; @@ -28,6 +29,7 @@ import { TestLoggerService, TestStorageService } from '../../../../test/common/w import { McpRegistry } from '../../common/mcpRegistry.js'; import { IMcpHostDelegate, IMcpMessageTransport } from '../../common/mcpRegistryTypes.js'; import { McpServerConnection } from '../../common/mcpServerConnection.js'; +import { McpTaskManager } from '../../common/mcpTaskManager.js'; import { LazyCollectionState, McpCollectionDefinition, McpServerDefinition, McpServerLaunch, McpServerTransportStdio, McpServerTransportType, McpServerTrust, McpStartServerInteraction } from '../../common/mcpTypes.js'; import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; @@ -145,6 +147,7 @@ suite('Workbench - MCP - Registry', () => { let configurationService: TestConfigurationService; let logger: ILogger; let trustNonceBearer: { trustedAtNonce: string | undefined }; + let taskManager: McpTaskManager; setup(() => { testConfigResolverService = new TestConfigurationResolverService(); @@ -166,6 +169,7 @@ suite('Workbench - MCP - Registry', () => { ); logger = new NullLogger(); + taskManager = store.add(new McpTaskManager()); const instaService = store.add(new TestInstantiationService(services)); registry = store.add(instaService.createInstance(TestMcpRegistry)); @@ -267,7 +271,7 @@ suite('Workbench - MCP - Registry', () => { testCollection.serverDefinitions.set([definition], undefined); store.add(registry.registerCollection(testCollection)); - const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection; + const connection = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer, taskManager }) as McpServerConnection; assert.ok(connection); assert.strictEqual(connection.definition, definition); @@ -275,7 +279,7 @@ suite('Workbench - MCP - Registry', () => { assert.strictEqual((connection.launchDefinition as unknown as { env: { PATH: string } }).env.PATH, 'interactiveValue0'); connection.dispose(); - const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection; + const connection2 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer, taskManager }) as McpServerConnection; assert.ok(connection2); assert.strictEqual((connection2.launchDefinition as unknown as { env: { PATH: string } }).env.PATH, 'interactiveValue0'); @@ -283,7 +287,7 @@ suite('Workbench - MCP - Registry', () => { registry.clearSavedInputs(StorageScope.WORKSPACE); - const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer }) as McpServerConnection; + const connection3 = await registry.resolveConnection({ collectionRef: testCollection, definitionRef: definition, logger, trustNonceBearer, taskManager }) as McpServerConnection; assert.ok(connection3); assert.strictEqual((connection3.launchDefinition as unknown as { env: { PATH: string } }).env.PATH, 'interactiveValue4'); @@ -322,6 +326,7 @@ suite('Workbench - MCP - Registry', () => { definitionRef: definition, logger, trustNonceBearer, + taskManager, }) as McpServerConnection; assert.ok(connection); @@ -430,6 +435,174 @@ suite('Workbench - MCP - Registry', () => { }); }); + suite('Duplicate Collection Prevention', () => { + test('prevents duplicate non-lazy collections with same ID', () => { + const collection1 = { + ...testCollection, + id: 'duplicate-test', + label: 'Collection 1', + }; + const collection2 = { + ...testCollection, + id: 'duplicate-test', + label: 'Collection 2', + }; + + store.add(registry.registerCollection(collection1)); + const disposable2 = registry.registerCollection(collection2); + + // Second registration should return Disposable.None and not add duplicate + assert.strictEqual(disposable2, Disposable.None); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], collection1); + assert.strictEqual(registry.collections.get()[0].label, 'Collection 1'); + }); + + test('allows lazy collection to be replaced by non-lazy with same ID', () => { + const lazyCollection = { + ...testCollection, + id: 'replaceable-test', + label: 'Lazy Collection', + lazy: { + isCached: false, + load: () => Promise.resolve(), + } + }; + const nonLazyCollection = { + ...testCollection, + id: 'replaceable-test', + label: 'Non-Lazy Collection', + }; + + store.add(registry.registerCollection(lazyCollection)); + const disposable2 = store.add(registry.registerCollection(nonLazyCollection)); + + // Should replace lazy with non-lazy + assert.notStrictEqual(disposable2, Disposable.None); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], nonLazyCollection); + assert.strictEqual(registry.collections.get()[0].label, 'Non-Lazy Collection'); + assert.strictEqual(registry.collections.get()[0].lazy, undefined); + }); + + test('prevents lazy collection from duplicating existing non-lazy collection', () => { + const nonLazyCollection = { + ...testCollection, + id: 'protected-test', + label: 'Non-Lazy Collection', + }; + const lazyCollection = { + ...testCollection, + id: 'protected-test', + label: 'Lazy Collection', + lazy: { + isCached: false, + load: () => Promise.resolve(), + } + }; + + store.add(registry.registerCollection(nonLazyCollection)); + const disposable2 = registry.registerCollection(lazyCollection); + + // Lazy collection should not replace or duplicate non-lazy + assert.strictEqual(disposable2, Disposable.None); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], nonLazyCollection); + assert.strictEqual(registry.collections.get()[0].label, 'Non-Lazy Collection'); + }); + + test('allows different collection IDs to coexist', () => { + const collection1 = { + ...testCollection, + id: 'collection-1', + label: 'Collection 1', + }; + const collection2 = { + ...testCollection, + id: 'collection-2', + label: 'Collection 2', + }; + + store.add(registry.registerCollection(collection1)); + store.add(registry.registerCollection(collection2)); + + // Both should be registered since they have different IDs + assert.strictEqual(registry.collections.get().length, 2); + assert.ok(registry.collections.get().some(c => c.id === 'collection-1')); + assert.ok(registry.collections.get().some(c => c.id === 'collection-2')); + }); + + test('disposal of duplicate-preventing registration does not affect original', () => { + const collection1 = { + ...testCollection, + id: 'disposal-test', + label: 'Original Collection', + }; + const collection2 = { + ...testCollection, + id: 'disposal-test', + label: 'Duplicate Attempt', + }; + + const disposable1 = store.add(registry.registerCollection(collection1)); + const disposable2 = registry.registerCollection(collection2); + + assert.strictEqual(disposable2, Disposable.None); + + // Disposing the Disposable.None should do nothing + disposable2.dispose(); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], collection1); + + // Disposing the original should remove it + disposable1.dispose(); + assert.strictEqual(registry.collections.get().length, 0); + }); + + test('simulates extension host restart scenario with when clause', async () => { + // Simulates the bug: ExtensionMcpDiscovery registers lazy collection, + // then MainThreadMcp tries to register non-lazy version on ext host restart + + // Step 1: ExtensionMcpDiscovery registers cached lazy collection + const lazyCollection = { + ...testCollection, + id: 'ext-restart-test', + label: 'Cached Lazy Collection', + lazy: { + isCached: true, + load: () => Promise.resolve(), + } + }; + store.add(registry.registerCollection(lazyCollection)); + assert.strictEqual(registry.collections.get().length, 1); + + // Step 2: Extension activates, MainThreadMcp.$upsertMcpCollection called + // This replaces lazy with non-lazy (normal flow) + const nonLazyFromExtension = { + ...testCollection, + id: 'ext-restart-test', + label: 'Extension-Provided Collection', + }; + store.add(registry.registerCollection(nonLazyFromExtension)); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0].lazy, undefined); + + // Step 3: Extension host restarts, MainThreadMcp disposed + // ExtensionMcpDiscovery's context listener fires again and tries to re-register + // This should NOT create a duplicate + const duplicateAttempt = { + ...testCollection, + id: 'ext-restart-test', + label: 'Should Not Duplicate', + }; + const disposable = registry.registerCollection(duplicateAttempt); + + assert.strictEqual(disposable, Disposable.None); + assert.strictEqual(registry.collections.get().length, 1); + assert.strictEqual(registry.collections.get()[0], nonLazyFromExtension); + }); + }); + suite('Trust Flow', () => { /** * Helper to create a test MCP collection with a specific trust behavior @@ -488,6 +661,7 @@ suite('Workbench - MCP - Registry', () => { definitionRef: definition, logger, trustNonceBearer, + taskManager, }); assert.ok(connection, 'Connection should be created for trusted collection'); @@ -504,6 +678,7 @@ suite('Workbench - MCP - Registry', () => { definitionRef: definition, logger, trustNonceBearer, + taskManager, }); assert.ok(connection, 'Connection should be created when nonce matches'); @@ -520,7 +695,7 @@ suite('Workbench - MCP - Registry', () => { collectionRef: collection, definitionRef: definition, logger, - trustNonceBearer, + trustNonceBearer, taskManager, }); assert.ok(connection, 'Connection should be created when user trusts'); @@ -537,7 +712,7 @@ suite('Workbench - MCP - Registry', () => { collectionRef: collection, definitionRef: definition, logger, - trustNonceBearer, + trustNonceBearer, taskManager, }); assert.strictEqual(connection, undefined, 'Connection should not be created when user rejects'); @@ -554,6 +729,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, autoTrustChanges: true, + taskManager, }); assert.ok(connection, 'Connection should be created with autoTrustChanges'); @@ -572,6 +748,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, promptType: 'never', + taskManager, }); assert.strictEqual(connection, undefined, 'Connection should not be created with promptType "never"'); @@ -588,6 +765,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, promptType: 'only-new', + taskManager, }); assert.strictEqual(connection, undefined, 'Connection should not be created for previously untrusted server'); @@ -605,6 +783,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, promptType: 'all-untrusted', + taskManager, }); assert.ok(connection, 'Connection should be created when user trusts previously untrusted server'); @@ -640,6 +819,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, interaction, + taskManager, }), registry.resolveConnection({ collectionRef: collection, @@ -647,6 +827,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer: trustNonceBearer2, interaction, + taskManager, }) ]); @@ -687,6 +868,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, interaction, + taskManager, }), registry.resolveConnection({ collectionRef: collection, @@ -694,6 +876,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer: trustNonceBearer2, interaction, + taskManager, }) ]); @@ -729,6 +912,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer, interaction, + taskManager, }), registry.resolveConnection({ collectionRef: collection, @@ -736,6 +920,7 @@ suite('Workbench - MCP - Registry', () => { logger, trustNonceBearer: trustNonceBearer2, interaction, + taskManager, }) ]); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts index 8b9d8e89c7d..2648adedeb4 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpRegistryTypes.ts @@ -239,6 +239,7 @@ export class TestMcpRegistry implements IMcpRegistry { definition.launch, new NullLogger(), false, + options.taskManager, this._instantiationService, )); } diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpSamplingLog.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpSamplingLog.test.ts index 59248dc5d5a..4886dbbcc08 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpSamplingLog.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpSamplingLog.test.ts @@ -12,6 +12,7 @@ import { import { TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { ISamplingStoredData, McpSamplingLog } from '../../common/mcpSamplingLog.js'; import { IMcpServer } from '../../common/mcpTypes.js'; +import { asArray } from '../../../../../base/common/arrays.js'; suite('MCP - Sampling Log', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); @@ -216,8 +217,8 @@ suite('MCP - Sampling Log', () => { // Verify all requests are stored correctly assert.strictEqual(data.lastReqs.length, 3); assert.strictEqual(data.lastReqs[0].request.length, 2); // Mixed content request has 2 messages - assert.strictEqual(data.lastReqs[1].request[0].content.type, 'image'); - assert.strictEqual(data.lastReqs[2].request[0].content.type, 'text'); + assert.strictEqual(asArray(data.lastReqs[1].request[0].content)[0].type, 'image'); + assert.strictEqual(asArray(data.lastReqs[2].request[0].content)[0].type, 'text'); }); test('handles multiple servers', async () => { diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts index a52e935c3ac..33114bc70d4 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerConnection.test.ts @@ -22,6 +22,7 @@ import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpSe import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; import { Event } from '../../../../../base/common/event.js'; +import { McpTaskManager } from '../../common/mcpTaskManager.js'; class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { private readonly _transport: TestMcpMessageTransport; @@ -139,6 +140,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -168,6 +170,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -188,6 +191,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -215,6 +219,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -240,6 +245,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -275,6 +281,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); // Start the connection @@ -309,6 +316,7 @@ suite('Workbench - MCP - ServerConnection', () => { dispose: () => { } } as Partial as ILogger, false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -339,6 +347,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); @@ -378,6 +387,7 @@ suite('Workbench - MCP - ServerConnection', () => { serverDefinition.launch, new NullLogger(), false, + store.add(new McpTaskManager()), ); store.add(connection); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts index 6e76897229f..b051bac13ef 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpServerRequestHandler.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import * as sinon from 'sinon'; import { upcast } from '../../../../../base/common/types.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; @@ -13,13 +14,15 @@ import { IProductService } from '../../../../../platform/product/common/productS import { IStorageService } from '../../../../../platform/storage/common/storage.js'; import { TestLoggerService, TestProductService, TestStorageService } from '../../../../test/common/workbenchTestServices.js'; import { IMcpHostDelegate } from '../../common/mcpRegistryTypes.js'; -import { McpServerRequestHandler } from '../../common/mcpServerRequestHandler.js'; +import { McpServerRequestHandler, McpTask } from '../../common/mcpServerRequestHandler.js'; import { McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../common/mcpTypes.js'; import { MCP } from '../../common/modelContextProtocol.js'; import { TestMcpMessageTransport } from './mcpRegistryTypes.js'; import { IOutputService } from '../../../../services/output/common/output.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { McpTaskManager } from '../../common/mcpTaskManager.js'; +import { upcastPartial } from '../../../../../base/test/common/mock.js'; class TestMcpHostDelegate extends Disposable implements IMcpHostDelegate { private readonly _transport: TestMcpMessageTransport; @@ -84,7 +87,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => { .createLogger('mcpServerTest', { hidden: true, name: 'MCP Test' })); // Start the handler creation - const handlerPromise = McpServerRequestHandler.create(instantiationService, { logger, launch: transport }, cts.token); + const handlerPromise = McpServerRequestHandler.create(instantiationService, { logger, launch: transport, taskManager: store.add(new McpTaskManager()) }, cts.token); handler = await handlerPromise; store.add(handler); @@ -217,7 +220,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => { const sentMessages = transport.getSentMessages(); const pingResponse = sentMessages.find(m => 'id' in m && m.id === pingRequest.id && 'result' in m - ) as MCP.JSONRPCResponse; + ) as MCP.JSONRPCResultResponse; assert.ok(pingResponse, 'No ping response was sent'); assert.deepStrictEqual(pingResponse.result, {}); @@ -243,7 +246,7 @@ suite('Workbench - MCP - ServerRequestHandler', () => { const sentMessages = transport.getSentMessages(); const rootsResponse = sentMessages.find(m => 'id' in m && m.id === rootsRequest.id && 'result' in m - ) as MCP.JSONRPCResponse; + ) as MCP.JSONRPCResultResponse; assert.ok(rootsResponse, 'No roots/list response was sent'); assert.strictEqual((rootsResponse.result as MCP.ListRootsResult).roots.length, 2); @@ -379,3 +382,332 @@ suite('Workbench - MCP - ServerRequestHandler', () => { } }); }); + +suite.skip('Workbench - MCP - McpTask', () => { // TODO@connor4312 https://github.com/microsoft/vscode/issues/280126 + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let clock: sinon.SinonFakeTimers; + + setup(() => { + clock = sinon.useFakeTimers(); + }); + + teardown(() => { + clock.restore(); + }); + + function createTask(overrides: Partial = {}): MCP.Task { + return { + taskId: 'task1', + status: 'working', + createdAt: new Date().toISOString(), + lastUpdatedAt: new Date().toISOString(), + ttl: null, + ...overrides + }; + } + + test('should resolve when task completes', async () => { + const mockHandler = upcastPartial({ + getTask: sinon.stub().resolves(createTask({ status: 'completed' })), + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + const task = store.add(new McpTask(createTask())); + task.setHandler(mockHandler); + + // Advance time to trigger polling + await clock.tickAsync(2000); + + // Update to completed state + task.onDidUpdateState(createTask({ status: 'completed' })); + + const result = await task.result; + assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] }); + assert.ok((mockHandler.getTaskResult as sinon.SinonStub).calledWith({ taskId: 'task1' })); + }); + + test('should poll for task updates', async () => { + const getTaskStub = sinon.stub(); + getTaskStub.onCall(0).resolves(createTask({ status: 'working' })); + getTaskStub.onCall(1).resolves(createTask({ status: 'working' })); + getTaskStub.onCall(2).resolves(createTask({ status: 'completed' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler); + + // First poll + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub.callCount, 1); + + // Second poll + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub.callCount, 2); + + // Third poll - completes + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub.callCount, 3); + + const result = await task.result; + assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] }); + }); + + test('should use default poll interval if not specified', async () => { + const getTaskStub = sinon.stub(); + getTaskStub.resolves(createTask({ status: 'working' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + }); + + const task = store.add(new McpTask(createTask())); + task.setHandler(mockHandler); + + // Default poll interval is 2000ms + await clock.tickAsync(2000); + assert.strictEqual(getTaskStub.callCount, 1); + + await clock.tickAsync(2000); + assert.strictEqual(getTaskStub.callCount, 2); + + task.dispose(); + }); + + test('should reject when task fails', async () => { + const mockHandler = upcastPartial({ + getTask: sinon.stub().resolves(createTask({ + status: 'failed', + statusMessage: 'Something went wrong' + })) + }); + + const task = store.add(new McpTask(createTask())); + task.setHandler(mockHandler); + + // Update to failed state + task.onDidUpdateState(createTask({ + status: 'failed', + statusMessage: 'Something went wrong' + })); + + await assert.rejects( + task.result, + (error: Error) => { + assert.ok(error.message.includes('Task task1 failed')); + assert.ok(error.message.includes('Something went wrong')); + return true; + } + ); + }); + + test('should cancel when task is cancelled', async () => { + const task = store.add(new McpTask(createTask())); + + // Update to cancelled state + task.onDidUpdateState(createTask({ status: 'cancelled' })); + + await assert.rejects( + task.result, + (error: Error) => { + assert.strictEqual(error.name, 'Canceled'); + return true; + } + ); + }); + + test('should cancel when cancellation token is triggered', async () => { + const cts = store.add(new CancellationTokenSource()); + const task = store.add(new McpTask(createTask(), cts.token)); + + // Cancel the token + cts.cancel(); + + await assert.rejects( + task.result, + (error: Error) => { + assert.strictEqual(error.name, 'Canceled'); + return true; + } + ); + }); + + test('should handle TTL expiration', async () => { + const now = Date.now(); + clock.setSystemTime(now); + + const task = store.add(new McpTask(createTask({ ttl: 5000 }))); + + // Advance time past TTL + await clock.tickAsync(6000); + + await assert.rejects( + task.result, + (error: Error) => { + assert.strictEqual(error.name, 'Canceled'); + return true; + } + ); + }); + + test('should stop polling when in terminal state', async () => { + const getTaskStub = sinon.stub(); + getTaskStub.resolves(createTask({ status: 'completed' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler); + + // Update to completed state immediately + task.onDidUpdateState(createTask({ status: 'completed' })); + + await task.result; + + // Advance time - should not poll anymore + const initialCallCount = getTaskStub.callCount; + await clock.tickAsync(5000); + assert.strictEqual(getTaskStub.callCount, initialCallCount); + }); + + test('should handle handler reconnection', async () => { + const getTaskStub1 = sinon.stub(); + getTaskStub1.resolves(createTask({ status: 'working' })); + + const mockHandler1 = upcastPartial({ + getTask: getTaskStub1, + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler1); + + // First poll with handler1 + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub1.callCount, 1); + + // Switch to a new handler + const getTaskStub2 = sinon.stub(); + getTaskStub2.resolves(createTask({ status: 'completed' })); + + const mockHandler2 = upcastPartial({ + getTask: getTaskStub2, + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + task.setHandler(mockHandler2); + + // Second poll with handler2 + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub1.callCount, 1); // No more calls to old handler + assert.strictEqual(getTaskStub2.callCount, 1); // New handler is called + + const result = await task.result; + assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] }); + }); + + test('should not poll when handler is undefined', async () => { + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + + // Advance time - should not crash + await clock.tickAsync(5000); + + // Now set a handler and it should start polling + const getTaskStub = sinon.stub(); + getTaskStub.resolves(createTask({ status: 'completed' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + task.setHandler(mockHandler); + await clock.tickAsync(1000); + assert.strictEqual(getTaskStub.callCount, 1); + + task.dispose(); + }); + + test('should handle input_required state', async () => { + const getTaskStub = sinon.stub(); + // getTask call returns completed (triggered by input_required handling) + getTaskStub.resolves(createTask({ status: 'completed' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + getTaskResult: sinon.stub().resolves({ content: [{ type: 'text', text: 'result' }] }) + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler); + + // Update to input_required - this triggers a getTask call + task.onDidUpdateState(createTask({ status: 'input_required' })); + + // Allow the promise to settle + await clock.tickAsync(0); + + // Verify getTask was called + assert.strictEqual(getTaskStub.callCount, 1); + + // Once getTask resolves with completed, should fetch result + const result = await task.result; + assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result' }] }); + }); + + test('should handle getTask returning cancelled during polling', async () => { + const getTaskStub = sinon.stub(); + getTaskStub.resolves(createTask({ status: 'cancelled' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler); + + // Advance time to trigger polling + await clock.tickAsync(1000); + + await assert.rejects( + task.result, + (error: Error) => { + assert.strictEqual(error.name, 'Canceled'); + return true; + } + ); + }); + + test('should return correct task id', () => { + const task = store.add(new McpTask(createTask({ taskId: 'my-task-id' }))); + assert.strictEqual(task.id, 'my-task-id'); + }); + + test('should dispose cleanly', async () => { + const getTaskStub = sinon.stub(); + getTaskStub.resolves(createTask({ status: 'working' })); + + const mockHandler = upcastPartial({ + getTask: getTaskStub, + }); + + const task = store.add(new McpTask(createTask({ pollInterval: 1000 }))); + task.setHandler(mockHandler); + + // Poll once + await clock.tickAsync(1000); + const callCountBeforeDispose = getTaskStub.callCount; + + // Dispose + task.dispose(); + + // Advance time - should not poll anymore + await clock.tickAsync(5000); + assert.strictEqual(getTaskStub.callCount, callCountBeforeDispose); + }); +}); diff --git a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts index b308d02c3c1..b73a1d48fb5 100644 --- a/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts +++ b/src/vs/workbench/contrib/mcp/test/common/mcpTypes.test.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { McpResourceURI } from '../../common/mcpTypes.js'; +import { McpResourceURI, McpServerDefinition, McpServerTransportType } from '../../common/mcpTypes.js'; import * as assert from 'assert'; +import { URI } from '../../../../../base/common/uri.js'; suite('MCP Types', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -27,4 +28,81 @@ suite('MCP Types', () => { roundTrip('custom-scheme:///my-path'); roundTrip('custom-scheme:///my-path/foo/?with=query¶ms=here'); }); + + suite('McpServerDefinition.equals', () => { + const createBasicDefinition = (overrides?: Partial): McpServerDefinition => ({ + id: 'test-server', + label: 'Test Server', + cacheNonce: 'v1.0.0', + launch: { + type: McpServerTransportType.Stdio, + cwd: undefined, + command: 'test-command', + args: [], + env: {}, + envFile: undefined + }, + ...overrides + }); + + test('returns true for identical definitions', () => { + const def1 = createBasicDefinition(); + const def2 = createBasicDefinition(); + assert.strictEqual(McpServerDefinition.equals(def1, def2), true); + }); + + test('returns false when cacheNonce differs', () => { + const def1 = createBasicDefinition({ cacheNonce: 'v1.0.0' }); + const def2 = createBasicDefinition({ cacheNonce: 'v2.0.0' }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), false); + }); + + test('returns false when id differs', () => { + const def1 = createBasicDefinition({ id: 'server-1' }); + const def2 = createBasicDefinition({ id: 'server-2' }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), false); + }); + + test('returns false when label differs', () => { + const def1 = createBasicDefinition({ label: 'Server A' }); + const def2 = createBasicDefinition({ label: 'Server B' }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), false); + }); + + test('returns false when roots differ', () => { + const def1 = createBasicDefinition({ roots: [URI.file('/path1')] }); + const def2 = createBasicDefinition({ roots: [URI.file('/path2')] }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), false); + }); + + test('returns true when roots are both undefined', () => { + const def1 = createBasicDefinition({ roots: undefined }); + const def2 = createBasicDefinition({ roots: undefined }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), true); + }); + + test('returns false when launch differs', () => { + const def1 = createBasicDefinition({ + launch: { + type: McpServerTransportType.Stdio, + cwd: undefined, + command: 'command1', + args: [], + env: {}, + envFile: undefined + } + }); + const def2 = createBasicDefinition({ + launch: { + type: McpServerTransportType.Stdio, + cwd: undefined, + command: 'command2', + args: [], + env: {}, + envFile: undefined + } + }); + assert.strictEqual(McpServerDefinition.equals(def1, def2), false); + }); + }); }); diff --git a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts index 9bee4963b01..e759a24cec4 100644 --- a/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts +++ b/src/vs/workbench/contrib/mcp/test/node/mcpStdioStateHandler.test.ts @@ -46,7 +46,7 @@ suite('McpStdioStateHandler', () => { process.on('SIGTERM', () => process.stdout.write('SIGTERM received')); `); - child.stdin.write('Hello MCP!'); + await new Promise(r => child.stdin.write('Hello MCP!', () => r())); handler.stop(); const result = await output; assert.strictEqual(result.trim(), 'Data received: Hello MCP!'); @@ -59,7 +59,9 @@ suite('McpStdioStateHandler', () => { process.stdin.on('end', () => process.stdout.write('stdin ended\\n')); process.stdin.resume(); process.on('SIGTERM', () => { - process.stdout.write('SIGTERM received', () => process.exit(0)); + process.stdout.write('SIGTERM received', () => { + process.stdout.end(() => process.exit(0)); + }); }); `); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts index 634cf4e3792..54432f41a4a 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/mergeEditorInputModel.ts @@ -8,7 +8,7 @@ import { BugIndicatingError, onUnexpectedError } from '../../../../base/common/e import { Event } from '../../../../base/common/event.js'; import { DisposableStore, IDisposable, IReference } from '../../../../base/common/lifecycle.js'; import { derived, IObservable, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; +import { basename } from '../../../../base/common/resources.js'; import Severity from '../../../../base/common/severity.js'; import { URI } from '../../../../base/common/uri.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -288,17 +288,6 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa public async createInputModel(args: MergeEditorArgs): Promise { const store = new DisposableStore(); - let resultTextFileModel = undefined as ITextFileEditorModel | undefined; - const modelListener = store.add(new DisposableStore()); - const handleDidCreate = (model: ITextFileEditorModel) => { - if (isEqual(args.result, model.resource)) { - modelListener.clear(); - resultTextFileModel = model; - } - }; - modelListener.add(this.textFileService.files.onDidCreate(handleDidCreate)); - this.textFileService.files.models.forEach(handleDidCreate); - let [ base, result, @@ -329,6 +318,9 @@ export class WorkspaceMergeEditorModeFactory implements IMergeEditorInputModelFa store.add(base); store.add(result); + const resultTextFileModel = this.textFileService.files.models.find(m => + m.resource.toString() === result.object.textEditorModel.uri.toString() + ); if (!resultTextFileModel) { throw new BugIndicatingError(); } diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts index c779a89e7f9..d77374dc0fe 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.contribution.ts @@ -5,7 +5,6 @@ import { localize } from '../../../../nls.js'; import { registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; @@ -24,16 +23,6 @@ registerAction2(GoToPreviousChangeAction); registerAction2(CollapseAllAction); registerAction2(ExpandAllAction); -Registry.as(Extensions.Configuration) - .registerConfiguration({ - properties: { - 'multiDiffEditor.experimental.enabled': { - type: 'boolean', - default: true, - description: 'Enable experimental multi diff editor.', - }, - } - }); registerSingleton(IMultiDiffSourceResolverService, MultiDiffSourceResolverService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts index 4c17e6f6750..5747d3131b0 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditor.ts @@ -5,11 +5,14 @@ import * as DOM from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { MultiDiffEditorWidget } from '../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.js'; import { IResourceLabel, IWorkbenchUIElementFactory } from '../../../../editor/browser/widget/multiDiffEditor/workbenchUIElementFactory.js'; import { ITextResourceConfigurationService } from '../../../../editor/common/services/textResourceConfiguration.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { InstantiationService } from '../../../../platform/instantiation/common/instantiationService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; @@ -29,12 +32,15 @@ import { IDiffEditor } from '../../../../editor/common/editorCommon.js'; import { Range } from '../../../../editor/common/core/range.js'; import { MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; +import { autorun, derived, observableValue } from '../../../../base/common/observable.js'; +import { FloatingEditorToolbarWidget } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js'; export class MultiDiffEditor extends AbstractEditorWithViewState { static readonly ID = 'multiDiffEditor'; private _multiDiffEditorWidget: MultiDiffEditorWidget | undefined = undefined; private _viewModel: MultiDiffEditorViewModel | undefined; + private _contentOverlay: MultiDiffEditorContentMenuOverlay | undefined; public get viewModel(): MultiDiffEditorViewModel | undefined { return this._viewModel; @@ -49,7 +55,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState { this._onDidChangeControl.fire(); })); + + this._contentOverlay = this._register(new MultiDiffEditorContentMenuOverlay( + this._multiDiffEditorWidget.getRootElement(), + this._multiDiffEditorWidget.getContextKeyService(), + this._multiDiffEditorWidget.getScopedInstantiationService() + )); } override async setInput(input: MultiDiffEditorInput, options: IMultiDiffEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { await super.setInput(input, options, context, token); this._viewModel = await input.getViewModel(); + this._contentOverlay?.updateResource(input.resource); this._multiDiffEditorWidget!.setViewModel(this._viewModel); const viewState = this.loadEditorViewState(input, context); @@ -106,6 +119,7 @@ export class MultiDiffEditor extends AbstractEditorWithViewState { await super.clearInput(); + this._contentOverlay?.updateResource(undefined); this._multiDiffEditorWidget!.setViewModel(undefined); } @@ -163,6 +177,50 @@ export class MultiDiffEditor extends AbstractEditorWithViewState(this, undefined); + + constructor( + root: HTMLElement, + contextKeyService: IContextKeyService, + instantiationService: IInstantiationService + ) { + super(); + + // Widget + const widget = instantiationService.createInstance( + FloatingEditorToolbarWidget, + MenuId.MultiDiffEditorContent, + contextKeyService, + this.resourceObs); + widget.element.classList.add('multi-diff-root-floating-menu'); + this._register(widget); + + // Derived to show/hide + const showToolbarObs = derived(reader => { + const resource = this.resourceObs.read(reader); + const hasActions = widget.hasActions.read(reader); + + return resource !== undefined && hasActions; + }); + + this._register(autorun(reader => { + const showToolbar = showToolbarObs.read(reader); + if (!showToolbar) { + return; + } + + root.appendChild(widget.element); + reader.store.add(toDisposable(() => { + widget.element.remove(); + })); + })); + } + + public updateResource(resource: URI | undefined): void { + this.resourceObs.set(resource, undefined); + } +} class WorkbenchUIElementFactory implements IWorkbenchUIElementFactory { constructor( diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts index dfd645ce241..5494fe02d06 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.ts @@ -13,7 +13,7 @@ import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrow import { registerNotebookContribution } from '../../notebookEditorExtensions.js'; import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { Event } from '../../../../../../base/common/event.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../../chat/common/participants/chatAgents.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; import { autorun } from '../../../../../../base/common/observable.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticsActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticsActions.ts index 0241529aaa2..78193183da7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticsActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/cellDiagnosticsActions.ts @@ -16,9 +16,7 @@ import { INotebookCellActionContext, NotebookCellAction, findTargetCellEditor } import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { NOTEBOOK_CELL_EDITOR_FOCUSED, NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_HAS_ERROR_DIAGNOSTICS } from '../../../common/notebookContextKeys.js'; import { InlineChatController } from '../../../../inlineChat/browser/inlineChatController.js'; -import { showChatView } from '../../../../chat/browser/chat.js'; -import { IViewsService } from '../../../../../services/views/common/viewsService.js'; -import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js'; +import { IChatWidgetService } from '../../../../chat/browser/chat.js'; export const OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID = 'notebook.cell.openFailureActions'; export const FIX_CELL_ERROR_COMMAND_ID = 'notebook.cell.chat.fixError'; @@ -111,9 +109,8 @@ registerAction2(class extends NotebookCellAction { if (context.cell instanceof CodeCellViewModel) { const error = context.cell.executionErrorDiagnostic.get(); if (error?.message) { - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); - const chatWidget = await showChatView(viewsService, layoutService); + const widgetService = accessor.get(IChatWidgetService); + const chatWidget = await widgetService.revealWidget(); const message = error.name ? `${error.name}: ${error.message}` : error.message; // TODO: can we add special prompt instructions? e.g. use "%pip install" chatWidget?.acceptInput('@workspace /explain ' + message,); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts index f10ea92275c..195b4b23646 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellDiagnostics/diagnosticCellStatusBarContrib.ts @@ -15,7 +15,7 @@ import { registerNotebookContribution } from '../../notebookEditorExtensions.js' import { CodeCellViewModel } from '../../viewModel/codeCellViewModel.js'; import { INotebookCellStatusBarItem, CellStatusbarAlignment } from '../../../common/notebookCommon.js'; import { ICellExecutionError } from '../../../common/notebookExecutionStateService.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../../chat/common/participants/chatAgents.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; export class DiagnosticCellStatusBarContrib extends Disposable implements INotebookEditorContribution { @@ -58,8 +58,9 @@ class DiagnosticCellStatusBarItem extends Disposable { let item: INotebookCellStatusBarItem | undefined; if (error?.location && this.hasNotebookAgent()) { - const keybinding = this.keybindingService.lookupKeybinding(OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID)?.getLabel(); - const tooltip = localize('notebook.cell.status.diagnostic', "Quick Actions {0}", `(${keybinding})`); + const tooltip = this.keybindingService.appendKeybinding( + localize('notebook.cell.status.diagnostic', "Quick Actions"), + OPEN_CELL_FAILURE_ACTIONS_COMMAND_ID); item = { text: `$(sparkle)`, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts index 4434091f38d..2707009bb29 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/statusBarProviders.ts @@ -137,12 +137,10 @@ class CellStatusBarLanguageDetectionProvider implements INotebookCellStatusBarIt const items: INotebookCellStatusBarItem[] = []; if (cached.guess && currentLanguageId !== cached.guess) { const detectedName = this._languageService.getLanguageName(cached.guess) || cached.guess; - let tooltip = localize('notebook.cell.status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName); - const keybinding = this._keybindingService.lookupKeybinding(DETECT_CELL_LANGUAGE); - const label = keybinding?.getLabel(); - if (label) { - tooltip += ` (${label})`; - } + const tooltip = this._keybindingService.appendKeybinding( + localize('notebook.cell.status.autoDetectLanguage', "Accept Detected Language: {0}", detectedName), + DETECT_CELL_LANGUAGE + ); items.push({ text: '$(lightbulb-autofix)', command: DETECT_CELL_LANGUAGE, diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts index c67c5f4ad7a..99a9f731620 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chat/notebookChatUtils.ts @@ -7,7 +7,7 @@ import { normalizeDriveLetter } from '../../../../../../base/common/labels.js'; import { basenameOrAuthority } from '../../../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { localize } from '../../../../../../nls.js'; -import { INotebookOutputVariableEntry } from '../../../../chat/common/chatVariableEntries.js'; +import { INotebookOutputVariableEntry } from '../../../../chat/common/attachments/chatVariableEntries.js'; import { CellUri } from '../../../common/notebookCommon.js'; import { ICellOutputViewModel, INotebookEditor } from '../../notebookBrowser.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookBreakpoints.ts b/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookBreakpoints.ts index 29b503e2639..a049be3d2c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookBreakpoints.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/debug/notebookBreakpoints.ts @@ -16,6 +16,7 @@ import { CellUri, NotebookCellsChangeType } from '../../../common/notebookCommon import { INotebookService } from '../../../common/notebookService.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; +import { hasKey } from '../../../../../../base/common/types.js'; class NotebookBreakpoints extends Disposable implements IWorkbenchContribution { constructor( @@ -58,7 +59,7 @@ class NotebookBreakpoints extends Disposable implements IWorkbenchContribution { })); this._register(this._debugService.getModel().onDidChangeBreakpoints(e => { - const newCellBp = e?.added?.find(bp => 'uri' in bp && bp.uri.scheme === Schemas.vscodeNotebookCell) as IBreakpoint | undefined; + const newCellBp = e?.added?.find(bp => hasKey(bp, { uri: true }) && bp.uri.scheme === Schemas.vscodeNotebookCell) as IBreakpoint | undefined; if (newCellBp) { const parsed = CellUri.parse(newCellBp.uri); if (!parsed) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts index 0870ac79d1b..cc43314944a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/editorHint/emptyCellEditorHint.ts @@ -7,7 +7,7 @@ import { Schemas } from '../../../../../../base/common/network.js'; import { ICodeEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../../../editor/browser/editorExtensions.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../../chat/common/participants/chatAgents.js'; import { EmptyTextEditorHintContribution } from '../../../../codeEditor/browser/emptyTextEditorHint/emptyTextEditorHint.js'; import { IInlineChatSessionService } from '../../../../inlineChat/browser/inlineChatSessionService.js'; import { getNotebookEditorFromEditorPane } from '../../notebookBrowser.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index 072ba66abf4..ae53643d6d7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -18,6 +18,7 @@ import { CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewM import { NotebookViewModel } from '../../viewModel/notebookViewModelImpl.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { CellKind, INotebookFindOptions, NotebookCellsChangeType } from '../../../common/notebookCommon.js'; +import { hasKey } from '../../../../../../base/common/types.js'; export class CellFindMatchModel implements CellFindMatchWithIndex { readonly cell: ICellViewModel; @@ -239,14 +240,14 @@ export class FindModel extends Disposable { // let currCell; if (!this._findMatchesStarts) { this.set(this._findMatches, true); - if ('index' in option) { + if (hasKey(option, { index: true })) { this._currentMatch = option.index; } } else { // const currIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); // currCell = this._findMatches[currIndex.index].cell; const totalVal = this._findMatchesStarts.getTotalSum(); - if ('index' in option) { + if (hasKey(option, { index: true })) { this._currentMatch = option.index; } else if (this._currentMatch === -1) { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts index 237326159ff..48532bfcff7 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/multicursor/notebookMulticursor.ts @@ -24,7 +24,7 @@ import { CommandExecutor, CursorsController } from '../../../../../../editor/com import { DeleteOperations } from '../../../../../../editor/common/cursor/cursorDeleteOperations.js'; import { CursorConfiguration, ICursorSimpleModel } from '../../../../../../editor/common/cursorCommon.js'; import { CursorChangeReason } from '../../../../../../editor/common/cursorEvents.js'; -import { CompositionTypePayload, Handler, ReplacePreviousCharPayload } from '../../../../../../editor/common/editorCommon.js'; +import { CompositionTypePayload, Handler, ITriggerEditorOperationEvent, ReplacePreviousCharPayload } from '../../../../../../editor/common/editorCommon.js'; import { ILanguageConfigurationService } from '../../../../../../editor/common/languages/languageConfigurationRegistry.js'; import { IModelDeltaDecoration, ITextModel, PositionAffinity } from '../../../../../../editor/common/model.js'; import { indentOfLine } from '../../../../../../editor/common/model/textModel.js'; @@ -352,7 +352,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo }; } - private async handleEditorOperationEvent(e: any) { + private handleEditorOperationEvent(e: ITriggerEditorOperationEvent) { this.trackedCells.forEach(cell => { if (cell.cellViewModel.handle === this.anchorCell?.[0].handle) { return; @@ -367,7 +367,7 @@ export class NotebookMultiCursorController extends Disposable implements INotebo }); } - private executeEditorOperation(controller: CursorsController, eventsCollector: ViewModelEventsCollector, e: any) { + private executeEditorOperation(controller: CursorsController, eventsCollector: ViewModelEventsCollector, e: ITriggerEditorOperationEvent) { switch (e.handlerId) { case Handler.CompositionStart: controller.startComposition(eventsCollector); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index c74b6d6062a..d812034b995 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -33,7 +33,7 @@ import { INotebookCellOutlineDataSource, NotebookCellOutlineDataSource } from '. import { CellKind, NotebookCellsChangeType, NotebookSetting } from '../../../common/notebookCommon.js'; import { IEditorService, SIDE_GROUP } from '../../../../../services/editor/common/editorService.js'; import { LifecyclePhase } from '../../../../../services/lifecycle/common/lifecycle.js'; -import { IBreadcrumbsDataSource, IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from '../../../../../services/outline/browser/outline.js'; +import { IBreadcrumbsDataSource, IBreadcrumbsOutlineElement, IOutline, IOutlineComparator, IOutlineCreator, IOutlineListConfig, IOutlineService, IQuickPickDataSource, IQuickPickOutlineElement, OutlineChangeEvent, OutlineConfigCollapseItemsValues, OutlineConfigKeys, OutlineTarget } from '../../../../../services/outline/browser/outline.js'; import { OutlineEntry } from '../../viewModel/OutlineEntry.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { IModelDeltaDecoration } from '../../../../../../editor/common/model.js'; @@ -466,12 +466,12 @@ export class NotebookBreadcrumbsProvider implements IBreadcrumbsDataSource[] { + const result: IBreadcrumbsOutlineElement[] = []; let candidate = this.outlineDataSourceRef?.object?.activeElement; while (candidate) { if (this.showCodeCells || candidate.cell.cellKind !== CellKind.Code) { - result.unshift(candidate); + result.unshift({ element: candidate, label: candidate.label }); } candidate = candidate.parent; } @@ -595,7 +595,7 @@ export class NotebookCellOutline implements IOutline { delegate, renderers, comparator, - options + options, }; } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 099b3639901..dae83ea4e8c 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -9,7 +9,7 @@ import { Action2, MenuId, registerAction2 } from '../../../../../platform/action import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { INotebookOutputActionContext, NOTEBOOK_ACTIONS_CATEGORY } from './coreActions.js'; -import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_HAS_OUTPUTS } from '../../common/notebookContextKeys.js'; +import { NOTEBOOK_CELL_HAS_HIDDEN_OUTPUTS, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../common/notebookContextKeys.js'; import * as icons from '../notebookIcons.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { copyCellOutput } from '../viewModel/cellOutputTextHelper.js'; @@ -19,6 +19,9 @@ import { CellKind, CellUri } from '../../common/notebookCommon.js'; import { CodeCellViewModel } from '../viewModel/codeCellViewModel.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js'; +import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; +import { IFileService } from '../../../../../platform/files/common/files.js'; +import { URI } from '../../../../../base/common/uri.js'; export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy'; @@ -64,43 +67,17 @@ registerAction2(class CopyCellOutputAction extends Action2 { }); } - private getNoteboookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { - if (outputContext && 'notebookEditor' in outputContext) { - return outputContext.notebookEditor; - } - return getNotebookEditorFromEditorPane(editorService.activeEditorPane); - } - async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { - const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); + const editorService = accessor.get(IEditorService); + const clipboardService = accessor.get(IClipboardService); + const logService = accessor.get(ILogService); + const notebookEditor = getNotebookEditorFromContext(editorService, outputContext); if (!notebookEditor) { return; } - let outputViewModel: ICellOutputViewModel | undefined; - if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { - outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); - } else if (outputContext && 'outputViewModel' in outputContext) { - outputViewModel = outputContext.outputViewModel; - } - - if (!outputViewModel) { - // not able to find the output from the provided context, use the active cell - const activeCell = notebookEditor.getActiveCell(); - if (!activeCell) { - return; - } - - if (activeCell.focusedOutputId !== undefined) { - outputViewModel = activeCell.outputsViewModels.find(output => { - return output.model.outputId === activeCell.focusedOutputId; - }); - } else { - outputViewModel = activeCell.outputsViewModels.find(output => output.pickedMimeType?.isTrusted); - } - } - + const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor); if (!outputViewModel) { return; } @@ -112,9 +89,6 @@ registerAction2(class CopyCellOutputAction extends Action2 { await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions); notebookEditor.copyOutputImage(outputViewModel); } else { - const clipboardService = accessor.get(IClipboardService); - const logService = accessor.get(ILogService); - copyCellOutput(mimeType, outputViewModel, clipboardService, logService); } } @@ -136,6 +110,41 @@ export function getOutputViewModelFromId(outputId: string, notebookEditor: INote return undefined; } +function getNotebookEditorFromContext(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { + if (outputContext && 'notebookEditor' in outputContext) { + return outputContext.notebookEditor; + } + return getNotebookEditorFromEditorPane(editorService.activeEditorPane); +} + +function getOutputViewModelFromContext(outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined, notebookEditor: INotebookEditor): ICellOutputViewModel | undefined { + let outputViewModel: ICellOutputViewModel | undefined; + + if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { + outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); + } else if (outputContext && 'outputViewModel' in outputContext) { + outputViewModel = outputContext.outputViewModel; + } + + if (!outputViewModel) { + // not able to find the output from the provided context, use the active cell + const activeCell = notebookEditor.getActiveCell(); + if (!activeCell) { + return undefined; + } + + if (activeCell.focusedOutputId !== undefined) { + outputViewModel = activeCell.outputsViewModels.find(output => { + return output.model.outputId === activeCell.focusedOutputId; + }); + } else { + outputViewModel = activeCell.outputsViewModels.find(output => output.pickedMimeType?.isTrusted); + } + } + + return outputViewModel; +} + export const OPEN_OUTPUT_COMMAND_ID = 'notebook.cellOutput.openInTextEditor'; registerAction2(class OpenCellOutputInEditorAction extends Action2 { @@ -149,29 +158,17 @@ registerAction2(class OpenCellOutputInEditorAction extends Action2 { }); } - private getNoteboookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { - if (outputContext && 'notebookEditor' in outputContext) { - return outputContext.notebookEditor; - } - return getNotebookEditorFromEditorPane(editorService.activeEditorPane); - } - async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { - const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); + const editorService = accessor.get(IEditorService); const notebookModelService = accessor.get(INotebookEditorModelResolverService); + const openerService = accessor.get(IOpenerService); + const notebookEditor = getNotebookEditorFromContext(editorService, outputContext); if (!notebookEditor) { return; } - let outputViewModel: ICellOutputViewModel | undefined; - if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { - outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); - } else if (outputContext && 'outputViewModel' in outputContext) { - outputViewModel = outputContext.outputViewModel; - } - - const openerService = accessor.get(IOpenerService); + const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor); if (outputViewModel?.model.outputId && notebookEditor.textModel?.uri) { // reserve notebook document reference since the active notebook editor might not be pinned so it can be replaced by the output editor @@ -182,6 +179,93 @@ registerAction2(class OpenCellOutputInEditorAction extends Action2 { } }); +export const SAVE_OUTPUT_IMAGE_COMMAND_ID = 'notebook.cellOutput.saveImage'; + +registerAction2(class SaveCellOutputImageAction extends Action2 { + constructor() { + super({ + id: SAVE_OUTPUT_IMAGE_COMMAND_ID, + title: localize('notebookActions.saveOutputImage', "Save Image"), + menu: { + id: MenuId.NotebookOutputToolbar, + when: ContextKeyExpr.regex(NOTEBOOK_CELL_OUTPUT_MIMETYPE.key, /^image\//) + }, + f1: false, + category: NOTEBOOK_ACTIONS_CATEGORY, + icon: icons.saveIcon, + }); + } + + async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { + const editorService = accessor.get(IEditorService); + const fileDialogService = accessor.get(IFileDialogService); + const fileService = accessor.get(IFileService); + const logService = accessor.get(ILogService); + + const notebookEditor = getNotebookEditorFromContext(editorService, outputContext); + if (!notebookEditor) { + return; + } + + const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor); + if (!outputViewModel) { + return; + } + + const mimeType = outputViewModel.pickedMimeType?.mimeType; + + // Only handle image mime types + if (!mimeType?.startsWith('image/')) { + return; + } + + const outputItem = outputViewModel.model.outputs.find(output => output.mime === mimeType); + if (!outputItem) { + logService.error('Could not find output item with mime type', mimeType); + return; + } + + // Determine file extension based on mime type + const mimeToExt: { [key: string]: string } = { + 'image/png': 'png', + 'image/jpeg': 'jpg', + 'image/jpg': 'jpg', + 'image/gif': 'gif', + 'image/svg+xml': 'svg', + 'image/webp': 'webp', + 'image/bmp': 'bmp', + 'image/tiff': 'tiff' + }; + + const extension = mimeToExt[mimeType] || 'png'; + const defaultFileName = `image.${extension}`; + + const defaultUri = notebookEditor.textModel?.uri + ? URI.joinPath(URI.file(notebookEditor.textModel.uri.fsPath), '..', defaultFileName) + : undefined; + + const uri = await fileDialogService.showSaveDialog({ + defaultUri, + filters: [{ + name: localize('imageFiles', "Image Files"), + extensions: [extension] + }] + }); + + if (!uri) { + return; // User cancelled + } + + try { + const imageData = outputItem.data; + await fileService.writeFile(uri, imageData); + logService.info('Saved image output to', uri.toString()); + } catch (error) { + logService.error('Failed to save image output', error); + } + } +}); + export const OPEN_OUTPUT_IN_OUTPUT_PREVIEW_COMMAND_ID = 'notebook.cellOutput.openInOutputPreview'; registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 { @@ -198,25 +282,16 @@ registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 }); } - private getNotebookEditor(editorService: IEditorService, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): INotebookEditor | undefined { - if (outputContext && 'notebookEditor' in outputContext) { - return outputContext.notebookEditor; - } - return getNotebookEditorFromEditorPane(editorService.activeEditorPane); - } - async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { - const notebookEditor = this.getNotebookEditor(accessor.get(IEditorService), outputContext); + const editorService = accessor.get(IEditorService); + const openerService = accessor.get(IOpenerService); + + const notebookEditor = getNotebookEditorFromContext(editorService, outputContext); if (!notebookEditor) { return; } - let outputViewModel: ICellOutputViewModel | undefined; - if (outputContext && 'outputId' in outputContext && typeof outputContext.outputId === 'string') { - outputViewModel = getOutputViewModelFromId(outputContext.outputId, notebookEditor); - } else if (outputContext && 'outputViewModel' in outputContext) { - outputViewModel = outputContext.outputViewModel; - } + const outputViewModel = getOutputViewModelFromContext(outputContext, notebookEditor); if (!outputViewModel) { return; @@ -256,7 +331,6 @@ registerAction2(class OpenCellOutputInNotebookOutputEditorAction extends Action2 outputIndex, ); - const openerService = accessor.get(IOpenerService); openerService.open(outputURI, { openToSide: true }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts index 55310ea8efb..12f5e4d2e88 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/cellChatActions.ts @@ -22,7 +22,7 @@ import { NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED } from '../../../comm import { Iterable } from '../../../../../../base/common/iterator.js'; import { ICodeEditor } from '../../../../../../editor/browser/editorBrowser.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { ChatContextKeys } from '../../../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../../../chat/common/actions/chatContextKeys.js'; import { InlineChatController } from '../../../../inlineChat/browser/inlineChatController.js'; import { EditorAction2 } from '../../../../../../editor/browser/editorExtensions.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts index 2d05e10fe9e..251cf6b7521 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/chat/notebook.chat.contribution.ts @@ -21,15 +21,13 @@ import { ServicesAccessor } from '../../../../../../platform/instantiation/commo import { IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js'; import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../../common/contributions.js'; import { IEditorService } from '../../../../../services/editor/common/editorService.js'; -import { IWorkbenchLayoutService } from '../../../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../../../services/views/common/viewsService.js'; -import { IChatWidget, IChatWidgetService, showChatView } from '../../../../chat/browser/chat.js'; -import { IChatContextPicker, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../../../chat/browser/chatContextPickService.js'; -import { ChatDynamicVariableModel } from '../../../../chat/browser/contrib/chatDynamicVariables.js'; -import { computeCompletionRanges } from '../../../../chat/browser/contrib/chatInputCompletions.js'; -import { IChatAgentService } from '../../../../chat/common/chatAgents.js'; -import { ChatContextKeys } from '../../../../chat/common/chatContextKeys.js'; -import { chatVariableLeader } from '../../../../chat/common/chatParserTypes.js'; +import { IChatWidget, IChatWidgetService } from '../../../../chat/browser/chat.js'; +import { IChatContextPicker, IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService } from '../../../../chat/browser/attachments/chatContextPickService.js'; +import { ChatDynamicVariableModel } from '../../../../chat/browser/attachments/chatDynamicVariables.js'; +import { computeCompletionRanges } from '../../../../chat/browser/widget/input/editor/chatInputCompletions.js'; +import { IChatAgentService } from '../../../../chat/common/participants/chatAgents.js'; +import { ChatContextKeys } from '../../../../chat/common/actions/chatContextKeys.js'; +import { chatVariableLeader } from '../../../../chat/common/requestParser/chatParserTypes.js'; import { ChatAgentLocation } from '../../../../chat/common/constants.js'; import { NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT, NOTEBOOK_CELL_OUTPUT_MIMETYPE } from '../../../common/notebookContextKeys.js'; import { INotebookKernelService } from '../../../common/notebookKernelService.js'; @@ -312,7 +310,7 @@ class KernelVariableContextPicker implements IChatContextPickerItem { } -registerAction2(class CopyCellOutputAction extends Action2 { +registerAction2(class AddCellOutputToChatAction extends Action2 { constructor() { super({ id: 'notebook.cellOutput.addToChat', @@ -338,8 +336,6 @@ registerAction2(class CopyCellOutputAction extends Action2 { async run(accessor: ServicesAccessor, outputContext: INotebookOutputActionContext | { outputViewModel: ICellOutputViewModel } | undefined): Promise { const notebookEditor = this.getNoteboookEditor(accessor.get(IEditorService), outputContext); - const viewService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); if (!notebookEditor) { return; @@ -375,15 +371,8 @@ registerAction2(class CopyCellOutputAction extends Action2 { const mimeType = outputViewModel.pickedMimeType?.mimeType; const chatWidgetService = accessor.get(IChatWidgetService); - let widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - const widgets = chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat); - if (widgets.length === 0) { - return; - } - widget = widgets[0]; - } - if (mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) { + const widget = await chatWidgetService.revealWidget(); + if (widget && mimeType && NOTEBOOK_CELL_OUTPUT_MIME_TYPE_LIST_FOR_CHAT_CONST.includes(mimeType)) { const entry = createNotebookOutputVariableEntry(outputViewModel, mimeType, notebookEditor); if (!entry) { @@ -391,7 +380,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { } widget.attachmentModel.addContext(entry); - (await showChatView(viewService, layoutService))?.focusInput(); + (await chatWidgetService.revealWidget())?.focusInput(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts index 399b121e1e0..7bc043c489f 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/coreActions.ts @@ -306,8 +306,7 @@ function sendEntryTelemetry(accessor: ServicesAccessor, id: string, context?: an } function isCellToolbarContext(context?: unknown): context is INotebookCellToolbarActionContext { - // eslint-disable-next-line local/code-no-any-casts - return !!context && !!(context as INotebookActionContext).notebookEditor && (context as any).$mid === MarshalledId.NotebookCellActionContext; + return !!context && !!(context as INotebookActionContext).notebookEditor && (context as INotebookActionContext & { $mid: MarshalledId }).$mid === MarshalledId.NotebookCellActionContext; } function isMultiCellArgs(arg: unknown): arg is IMultiCellArgs { diff --git a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts index e1bc6e74511..167161bc10e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/layoutActions.ts @@ -117,6 +117,7 @@ registerAction2(class ToggleLineNumberFromEditorTitle extends Action2 { super({ id: 'notebook.toggleLineNumbersFromEditorTitle', title: localize2('notebook.toggleLineNumbers', 'Toggle Notebook Line Numbers'), + shortTitle: localize2('notebook.toggleLineNumbers.short', 'Line Numbers'), precondition: NOTEBOOK_EDITOR_FOCUSED, menu: [ { @@ -129,7 +130,7 @@ registerAction2(class ToggleLineNumberFromEditorTitle extends Action2 { f1: true, toggled: { condition: ContextKeyExpr.notEquals('config.notebook.lineNumbers', 'off'), - title: localize('notebook.showLineNumbers', "Notebook Line Numbers"), + title: localize('notebook.showLineNumbers', "Line Numbers"), } }); } @@ -164,11 +165,17 @@ registerAction2(class ToggleBreadcrumbFromEditorTitle extends Action2 { super({ id: 'breadcrumbs.toggleFromEditorTitle', title: localize2('notebook.toggleBreadcrumb', 'Toggle Breadcrumbs'), + shortTitle: localize2('notebook.toggleBreadcrumb.short', 'Breadcrumbs'), + toggled: { + condition: ContextKeyExpr.equals('config.breadcrumbs.enabled', true), + title: localize('cmd.toggle2', "Breadcrumbs") + }, menu: [{ id: MenuId.NotebookEditorLayoutConfigure, group: 'notebookLayoutDetails', order: 2 }], + category: NOTEBOOK_ACTIONS_CATEGORY, f1: false }); } @@ -251,13 +258,14 @@ registerAction2(class ToggleNotebookStickyScroll extends Action2 { id: 'notebook.action.toggleNotebookStickyScroll', title: { ...localize2('toggleStickyScroll', "Toggle Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Sticky Scroll"), }, + shortTitle: localize2('toggleStickyScroll.short', "Sticky Scroll"), category: Categories.View, toggled: { condition: ContextKeyExpr.equals('config.notebook.stickyScroll.enabled', true), - title: localize('notebookStickyScroll', "Toggle Notebook Sticky Scroll"), - mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Toggle Notebook Sticky Scroll"), + title: localize('notebookStickyScroll', "Sticky Scroll"), + mnemonicTitle: localize({ key: 'mitoggleNotebookStickyScroll', comment: ['&& denotes a mnemonic'] }, "&&Sticky Scroll"), }, menu: [ { id: MenuId.CommandPalette }, diff --git a/src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.ts index bdc52d491cb..0573cec726e 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/notebookIndentationActions.ts @@ -124,8 +124,7 @@ function changeNotebookIndentation(accessor: ServicesAccessor, insertSpaces: boo })); // store the initial values of the configuration - // eslint-disable-next-line local/code-no-any-casts - const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as any; + const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as Record; const initialInsertSpaces = initialConfig['editor.insertSpaces']; // remove the initial values from the configuration delete initialConfig['editor.indentSize']; @@ -196,8 +195,7 @@ function convertNotebookIndentation(accessor: ServicesAccessor, tabsToSpaces: bo })).then(() => { // store the initial values of the configuration - // eslint-disable-next-line local/code-no-any-casts - const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as any; + const initialConfig = configurationService.getValue(NotebookSetting.cellEditorOptionsCustomizations) as Record; const initialIndentSize = initialConfig['editor.indentSize']; const initialTabSize = initialConfig['editor.tabSize']; // remove the initial values from the configuration diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index cd153dbe702..f9e3ac054d0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -745,7 +745,7 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase const modifiedMedataRaw = Object.assign({}, this.modified.metadata); const originalCellMetadata = this.original.metadata; for (const key of cellMetadataKeys) { - if (key in originalCellMetadata) { + if (Object.hasOwn(originalCellMetadata, key)) { modifiedMedataRaw[key] = originalCellMetadata[key]; } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts index 9666f98016a..c6fffa8a87a 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookDeletedCellDecorator.ts @@ -21,6 +21,7 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { ServiceCollection } from '../../../../../../platform/instantiation/common/serviceCollection.js'; import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js'; import { overviewRulerDeletedForeground } from '../../../../scm/common/quickDiff.js'; +import { IActionViewItemProvider } from '../../../../../../base/browser/ui/actionbar/actionbar.js'; const ttPolicy = createTrustedTypesPolicy('notebookRenderer', { createHTML: value => value }); @@ -35,7 +36,7 @@ export class NotebookDeletedCellDecorator extends Disposable implements INoteboo private readonly deletedCellInfos = new Map(); constructor( private readonly _notebookEditor: INotebookEditor, - private readonly toolbar: { menuId: MenuId; className: string; telemetrySource?: string; argFactory: (deletedCellIndex: number) => any } | undefined, + private readonly toolbar: { menuId: MenuId; className: string; telemetrySource?: string; argFactory: (deletedCellIndex: number) => any; actionViewItemProvider?: IActionViewItemProvider } | undefined, @ILanguageService private readonly languageService: ILanguageService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { @@ -178,7 +179,7 @@ export class NotebookDeletedCellWidget extends Disposable { constructor( private readonly _notebookEditor: INotebookEditor, - private readonly _toolbarOptions: { menuId: MenuId; className: string; telemetrySource?: string; argFactory: (deletedCellIndex: number) => any } | undefined, + private readonly _toolbarOptions: { menuId: MenuId; className: string; telemetrySource?: string; argFactory: (deletedCellIndex: number) => any; actionViewItemProvider?: IActionViewItemProvider } | undefined, private readonly code: string, private readonly language: string, container: HTMLElement, @@ -232,8 +233,8 @@ export class NotebookDeletedCellWidget extends Disposable { renderShortTitle: true, arg: this._toolbarOptions.argFactory(this._originalIndex), }, + actionViewItemProvider: this._toolbarOptions.actionViewItemProvider }); - this._store.add(toolbarWidget); toolbar.style.position = 'absolute'; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts index 8007f857b45..91655a29370 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/inlineDiff/notebookOriginalModelRefFactory.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AsyncReferenceCollection, IReference, ReferenceCollection } from '../../../../../../base/common/lifecycle.js'; -import { IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; +import { IModifiedFileEntry } from '../../../../chat/common/editing/chatEditingService.js'; import { INotebookService } from '../../../common/notebookService.js'; import { bufferToStream, VSBuffer } from '../../../../../../base/common/buffer.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css index 97830695b83..36fc8b4bee6 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebookCellChat.css @@ -173,6 +173,7 @@ background-color: var(--vscode-textPreformat-background); padding: 1px 3px; border-radius: 4px; + border: 1px solid var(--vscode-textPreformat-border); } .monaco-workbench .notebookOverlay .cell-chat-part .inline-chat .markdownMessage .message .interactive-result-code-block { diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index fbe016aa70d..a63b68b7d38 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -316,7 +316,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri })); // register comment decoration - this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {}); + this._register(this.codeEditorService.registerDecorationType('comment-controller', COMMENTEDITOR_DECORATION_KEY, {})); } // Add or remove the cell undo redo comparison key based on the user setting @@ -992,9 +992,9 @@ configurationRegistry.registerConfiguration({ type: 'string', enum: ['hidden', 'visible', 'visibleAfterExecute'], enumDescriptions: [ - nls.localize('notebook.showCellStatusbar.hidden.description', "The cell Status bar is always hidden."), - nls.localize('notebook.showCellStatusbar.visible.description', "The cell Status bar is always visible."), - nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell Status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], + nls.localize('notebook.showCellStatusbar.hidden.description', "The cell status bar is always hidden."), + nls.localize('notebook.showCellStatusbar.visible.description', "The cell status bar is always visible."), + nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], default: 'visible', tags: ['notebookLayout'] }, diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index d2433fb11d0..740d8945559 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -539,6 +539,7 @@ export interface INotebookEditor { readonly textModel?: NotebookTextModel; readonly isVisible: boolean; readonly isReadOnly: boolean; + readonly isReplHistory: boolean; readonly notebookOptions: NotebookOptions; readonly isDisposed: boolean; readonly activeKernel: INotebookKernel | undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellLayoutManager.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellLayoutManager.ts index e4286800ff4..579e49dd829 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookCellLayoutManager.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellLayoutManager.ts @@ -41,7 +41,9 @@ export class NotebookCellLayoutManager extends Disposable { } if (this._pendingLayouts?.has(cell)) { - this._pendingLayouts?.get(cell)!.dispose(); + const oldPendingLayout = this._pendingLayouts.get(cell)!; + oldPendingLayout.dispose(); + this._layoutDisposables.delete(oldPendingLayout); } const deferred = new DeferredPromise(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 189741c25c7..5b21ceec1d3 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -29,7 +29,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Color, RGBA } from '../../../../base/common/color.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { combinedDisposable, Disposable, DisposableStore, dispose } from '../../../../base/common/lifecycle.js'; +import { combinedDisposable, Disposable, DisposableStore, dispose, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { setTimeout0 } from '../../../../base/common/platform.js'; import { extname, isEqual } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; @@ -1516,16 +1516,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD })); let hasPendingChangeContentHeight = false; + const renderScrollHeightDisposable = this._localStore.add(new MutableDisposable()); this._localStore.add(this._list.onDidChangeContentHeight(() => { if (hasPendingChangeContentHeight) { return; } hasPendingChangeContentHeight = true; - this._localStore.add(DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), () => { + renderScrollHeightDisposable.value = DOM.scheduleAtNextAnimationFrame(DOM.getWindow(this.getDomNode()), () => { hasPendingChangeContentHeight = false; this._updateScrollHeight(); - }, 100)); + }, 100); })); this._localStore.add(this._list.onDidRemoveOutputs(outputs => { @@ -2130,7 +2131,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return false; } - let container: any = activeSelection.commonAncestorContainer; + let container: Node | null = activeSelection.commonAncestorContainer; if (!this._body.contains(container)) { return false; @@ -2993,7 +2994,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } //#region --- webview IPC ---- - postMessage(message: any) { + postMessage(message: unknown) { if (this._webview?.isResolved()) { this._webview.postKernelMessage(message); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts index d454bfda04a..28ef2d416ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookIcons.ts @@ -33,6 +33,7 @@ export const toggleWhitespace = registerIcon('notebook-diff-cell-toggle-whitespa export const renderOutputIcon = registerIcon('notebook-render-output', Codicon.preview, localize('renderOutputIcon', 'Icon to render output in diff editor.')); export const mimetypeIcon = registerIcon('notebook-mimetype', Codicon.code, localize('mimetypeIcon', 'Icon for a mime type in notebook editors.')); export const copyIcon = registerIcon('notebook-copy', Codicon.copy, localize('copyIcon', 'Icon to copy content to clipboard')); +export const saveIcon = registerIcon('notebook-save', Codicon.save, localize('saveIcon', 'Icon to save content to disk')); export const previousChangeIcon = registerIcon('notebook-diff-editor-previous-change', Codicon.arrowUp, localize('previousChangeIcon', 'Icon for the previous change action in the diff editor.')); export const nextChangeIcon = registerIcon('notebook-diff-editor-next-change', Codicon.arrowDown, localize('nextChangeIcon', 'Icon for the next change action in the diff editor.')); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts index d57424d08f0..c8d6c88b853 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts @@ -272,8 +272,7 @@ export class NotebookEditorWidgetService implements INotebookEditorService { removeNotebookEditor(editor: INotebookEditor): void { const notebookUri = editor.getViewModel()?.notebookDocument.uri; - if (this._notebookEditors.has(editor.getId())) { - this._notebookEditors.delete(editor.getId()); + if (this._notebookEditors.delete(editor.getId())) { this._onNotebookEditorsRemove.fire(editor); } if (this._mostRecentRepl.get() === notebookUri?.toString()) { diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 4fbeb3f99b7..00c0d710b54 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -31,7 +31,7 @@ import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, NotebookExtensionDescription, INotebookStaticPreloadInfo, NotebookData } from '../../common/notebookCommon.js'; import { NotebookEditorInput } from '../../common/notebookEditorInput.js'; import { INotebookEditorModelResolverService } from '../../common/notebookEditorModelResolverService.js'; -import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo as NotebookStaticPreloadInfo } from '../../common/notebookOutputRenderer.js'; +import { NotebookOutputRendererInfo, NotebookStaticPreloadInfo } from '../../common/notebookOutputRenderer.js'; import { NotebookEditorDescriptor, NotebookProviderInfo } from '../../common/notebookProvider.js'; import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from '../../common/notebookService.js'; import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, EditorInputFactoryObject, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction, type MergeEditorInputFactoryFunction } from '../../../../services/editor/common/editorResolverService.js'; diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts index 8086431811b..9ef0ba1dc82 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookWorkerServiceImpl.ts @@ -6,7 +6,8 @@ import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; import { IWebWorkerClient, Proxied } from '../../../../../base/common/worker/webWorker.js'; -import { createWebWorker } from '../../../../../base/browser/webWorkerFactory.js'; +import { WebWorkerDescriptor } from '../../../../../platform/webWorker/browser/webWorkerDescriptor.js'; +import { IWebWorkerService } from '../../../../../platform/webWorker/browser/webWorkerService.js'; import { NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; import { CellUri, IMainCellDto, INotebookDiffResult, NotebookCellsChangeType, NotebookRawContentEventDto } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; @@ -26,10 +27,11 @@ export class NotebookEditorWorkerServiceImpl extends Disposable implements INote constructor( @INotebookService notebookService: INotebookService, @IModelService modelService: IModelService, + @IWebWorkerService webWorkerService: IWebWorkerService, ) { super(); - this._workerManager = this._register(new WorkerManager(notebookService, modelService)); + this._workerManager = this._register(new WorkerManager(notebookService, modelService, webWorkerService)); } canComputeDiff(original: URI, modified: URI): boolean { throw new Error('Method not implemented.'); @@ -55,6 +57,7 @@ class WorkerManager extends Disposable { constructor( private readonly _notebookService: INotebookService, private readonly _modelService: IModelService, + private readonly _webWorkerService: IWebWorkerService, ) { super(); this._editorWorkerClient = null; @@ -64,7 +67,7 @@ class WorkerManager extends Disposable { withWorker(): Promise { // this._lastWorkerUsedTime = (new Date()).getTime(); if (!this._editorWorkerClient) { - this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService); + this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, this._modelService, this._webWorkerService); this._register(this._editorWorkerClient); } return Promise.resolve(this._editorWorkerClient); @@ -240,7 +243,11 @@ class NotebookWorkerClient extends Disposable { private _modelManager: NotebookEditorModelManager | null; - constructor(private readonly _notebookService: INotebookService, private readonly _modelService: IModelService) { + constructor( + private readonly _notebookService: INotebookService, + private readonly _modelService: IModelService, + private readonly _webWorkerService: IWebWorkerService, + ) { super(); this._worker = null; this._modelManager = null; @@ -273,9 +280,11 @@ class NotebookWorkerClient extends Disposable { private _getOrCreateWorker(): IWebWorkerClient { if (!this._worker) { try { - this._worker = this._register(createWebWorker( - FileAccess.asBrowserUri('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.js'), - 'NotebookEditorWorker' + this._worker = this._register(this._webWorkerService.createWorkerClient( + new WebWorkerDescriptor({ + esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/contrib/notebook/common/services/notebookWebWorkerMain.js'), + label: 'NotebookEditorWorker' + }) )); } catch (err) { throw (err); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts index 1ecb319844c..ebc8c15a436 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellComments.ts @@ -147,10 +147,9 @@ export class CellComments extends CellContentPart { } private _applyTheme() { - const theme = this.themeService.getColorTheme(); const fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; for (const { widget } of this._commentThreadWidgets.values()) { - widget.applyTheme(theme, fontInfo); + widget.applyTheme(fontInfo); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts index 58ae569e6e4..a2350b6824a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellEditorOptions.ts @@ -198,6 +198,7 @@ registerAction2(class ToggleLineNumberAction extends Action2 { super({ id: 'notebook.toggleLineNumbers', title: localize2('notebook.toggleLineNumbers', 'Toggle Notebook Line Numbers'), + shortTitle: localize2('notebook.toggleLineNumbers.short', 'Line Numbers'), precondition: NOTEBOOK_EDITOR_FOCUSED, menu: [ { @@ -210,7 +211,7 @@ registerAction2(class ToggleLineNumberAction extends Action2 { f1: true, toggled: { condition: ContextKeyExpr.notEquals('config.notebook.lineNumbers', 'off'), - title: localize('notebook.showLineNumbers', "Notebook Line Numbers"), + title: localize('notebook.showLineNumbers', "Line Numbers"), } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts index 6b4ba9c20bd..b8e9d472870 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellExecution.ts @@ -16,6 +16,7 @@ import { INotebookExecutionStateService } from '../../../common/notebookExecutio import { executingStateIcon } from '../../notebookIcons.js'; import { renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js'; import { CellViewModelStateChangeEvent } from '../../notebookViewEvents.js'; +import { hasKey } from '../../../../../../base/common/types.js'; const UPDATE_EXECUTION_ORDER_GRACE_PERIOD = 200; @@ -38,7 +39,7 @@ export class CellExecutionPart extends CellContentPart { // Add a method to watch for cell execution state changes this._register(this._notebookExecutionStateService.onDidChangeExecution(e => { - if (this.currentCell && 'affectsCell' in e && e.affectsCell(this.currentCell.uri)) { + if (this.currentCell && hasKey(e, { affectsCell: true }) && e.affectsCell(this.currentCell.uri)) { this._updatePosition(); } })); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index 765aa0fe09d..e828b4061fb 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -48,6 +48,8 @@ export class CodeCell extends Disposable { private _collapsedExecutionIcon: CollapsedCodeCellExecutionIcon; private _cellEditorOptions: CellEditorOptions; private _useNewApproachForEditorLayout = true; + private _pointerDownInEditor = false; + private _pointerDraggingInEditor = false; private readonly _cellLayout: CodeCellLayout; private readonly _debug: (output: string) => void; constructor( @@ -342,6 +344,10 @@ export class CodeCell extends Disposable { if (this._useNewApproachForEditorLayout) { this._register(this.templateData.editor.onDidScrollChange(e => { + // Option 4: Gate scroll-driven reactions during active drag-selection + if (this._pointerDownInEditor || this._pointerDraggingInEditor) { + return; + } if (this._cellLayout.editorVisibility === 'Invisible' || !this.templateData.editor.hasTextFocus()) { return; } @@ -379,6 +385,11 @@ export class CodeCell extends Disposable { return; } + // Option 3: Avoid relayouts during active pointer drag to prevent stuck selection mode + if ((this._pointerDownInEditor || this._pointerDraggingInEditor) && this._useNewApproachForEditorLayout) { + return; + } + const selections = this.templateData.editor.getSelections(); if (selections?.length) { @@ -417,13 +428,70 @@ export class CodeCell extends Disposable { } private registerMouseListener() { + // Pointer-state handling in notebook cell editors has a couple of easy-to-regress edge cases: + // 1) Holding the left mouse button while wheel/trackpad scrolling should scroll as usual. + // We therefore only treat the interaction as an "active drag selection" after actual pointer movement. + // 2) "Stuck selection mode" can occur if we miss the corresponding mouseup (e.g. releasing outside the window, + // focus loss, or ESC cancelling Monaco selection/drag). When this happens, leaving any of our drag/pointer + // flags set will incorrectly gate scroll/layout syncing and make the editor feel stuck. + // To avoid that, we reset state on multiple cancellation paths and also self-heal on mousemove. + const resetPointerState = () => { + this._pointerDownInEditor = false; + this._pointerDraggingInEditor = false; + this._cellLayout.setPointerDown(false); + }; + this._register(this.templateData.editor.onMouseDown(e => { // prevent default on right mouse click, otherwise it will trigger unexpected focus changes // the catch is, it means we don't allow customization of right button mouse down handlers other than the built in ones. if (e.event.rightButton) { e.event.preventDefault(); } + + if (this._useNewApproachForEditorLayout) { + // Track pointer-down and pointer-drag separately. + // Holding the left button while wheel/trackpad scrolling should behave like normal scrolling. + if (e.event.leftButton) { + this._pointerDownInEditor = true; + this._pointerDraggingInEditor = false; + this._cellLayout.setPointerDown(false); + } + } })); + + if (this._useNewApproachForEditorLayout) { + this._register(this.templateData.editor.onMouseMove(e => { + if (!this._pointerDownInEditor) { + return; + } + + // Self-heal: if we missed a mouseup (e.g. focus loss), clear the drag state as soon as we can observe it. + if (!e.event.leftButton) { + resetPointerState(); + return; + } + + if (!this._pointerDraggingInEditor) { + // Only consider it a drag-selection once the pointer actually moves with the left button down. + this._pointerDraggingInEditor = true; + this._cellLayout.setPointerDown(true); + } + })); + } + + if (this._useNewApproachForEditorLayout) { + // Ensure we reset pointer-down even if mouseup lands outside the editor + const win = DOM.getWindow(this.notebookEditor.getDomNode()); + this._register(DOM.addDisposableListener(win, 'mouseup', resetPointerState)); + this._register(DOM.addDisposableListener(win, 'pointerup', resetPointerState)); + this._register(DOM.addDisposableListener(win, 'pointercancel', resetPointerState)); + this._register(DOM.addDisposableListener(win, 'blur', resetPointerState)); + this._register(DOM.addDisposableListener(win, 'keydown', e => { + if (e.key === 'Escape' && (this._pointerDownInEditor || this._pointerDraggingInEditor)) { + resetPointerState(); + } + })); + } } private shouldPreserveEditor() { @@ -675,6 +743,8 @@ export class CodeCellLayout { public _previousScrollBottom?: number; public _lastChangedEditorScrolltop?: number; private _initialized: boolean = false; + private _pointerDown: boolean = false; + private _establishedContentHeight?: number; constructor( private readonly _enabled: boolean, private readonly notebookEditor: IActiveNotebookEditorDelegate, @@ -684,6 +754,10 @@ export class CodeCellLayout { private readonly _initialEditorDimension: IDimension ) { } + + public setPointerDown(isDown: boolean) { + this._pointerDown = isDown; + } /** * Dynamically lays out the code cell's Monaco editor to simulate a "sticky" run/exec area while * constraining the visible editor height to the notebook viewport. It adjusts two things: @@ -693,6 +767,22 @@ export class CodeCellLayout { * crop content when the cell is partially visible (top or bottom clipped) or when content is * taller than the viewport. * + * Additional invariants: + * - Content height stability: once the layout has been initialized, scroll-driven re-layouts can + * observe transient Monaco content heights that reflect the current clipped layout (rather than + * the full input height). To keep the notebook list layout stable (avoiding overlapping cells + * while navigating/scrolling), we store the actual content height in `_establishedContentHeight` + * and reuse it for scroll-driven relayouts. This prevents the editor from shrinking back to its + * initial height after content has been added (e.g., pasting text) or when Monaco reports a + * transient smaller content height while the cell is clipped. + * + * We refresh `_establishedContentHeight` when the editor's content size changes + * (`onDidContentSizeChange`) and also when width/layout changes can affect wrapping-driven height + * (`viewCellLayoutChange`/`nbLayoutChange`). + * - Pointer-drag gating: while the user is holding the mouse button down in the editor (drag + * selection or potential drag selection), we avoid programmatic `editor.setScrollTop(...)` updates + * to prevent selection/scroll feedback loops and "stuck selection" behavior. + * * --------------------------------------------------------------------------- * SECTION 1. OVERALL NOTEBOOK VIEW (EACH CELL HAS AN 18px GAP ABOVE IT) * Legend: @@ -783,8 +873,52 @@ export class CodeCellLayout { const elementTop = this.notebookEditor.getAbsoluteTopOfElement(this.viewCell); const elementBottom = this.notebookEditor.getAbsoluteBottomOfElement(this.viewCell); const elementHeight = this.notebookEditor.getHeightOfElement(this.viewCell); - const gotContentHeight = editor.getContentHeight(); - const editorContentHeight = Math.max((gotContentHeight === -1 ? editor.getLayoutInfo().height : gotContentHeight), gotContentHeight === -1 ? this._initialEditorDimension.height : gotContentHeight); // || this.calculatedEditorHeight || 0; + let editorContentHeight: number; + const isInit = !this._initialized && reason === 'init'; + if (isInit) { + // CONTENT HEIGHT SELECTION (INIT) + // ------------------------------- + // Editors are pooled and may be re-attached to different cells as the user scrolls. + // At the moment a pooled editor is first attached to a new cell, Monaco can still + // report the previous cell's `getContentHeight()` (for example a tall multi-line + // cell) even though the new cell only contains a single line. If we trusted that + // stale value here, the very first layout of the new cell would render with an + // oversized editor and visually overlap the next cell. + // + // To avoid this, the initial layout ignores `getContentHeight()` entirely and uses + // the notebook's own notion of the editor height for this cell + // (`_initialEditorDimension.height`). This value is derived from the cell model + // (line count + padding) and is stable across editor reuse. Once the model has + // been resolved and Monaco reports a real content height, subsequent layout + // reasons (`onDidContentSizeChange`, `viewCellLayoutChange`, `nbLayoutChange`) + // will refresh `_establishedContentHeight` in the normal way. + editorContentHeight = this._initialEditorDimension.height; + this._establishedContentHeight = editorContentHeight; + } else { + // CONTENT HEIGHT SELECTION (NON-INIT) + // ----------------------------------- + // For all non-init reasons, we rely on Monaco's `getContentHeight()` together with + // `_establishedContentHeight` to keep the notebook list layout stable while + // scrolling and resizing: + // - `onDidContentSizeChange` / `viewCellLayoutChange` / `nbLayoutChange` update + // `_establishedContentHeight` to the latest full content height. + // - `nbDidScroll` reuses `_establishedContentHeight` so that transient, smaller + // values reported while the editor itself is clipped do not shrink the row + // height (which would otherwise cause overlapping cells). + const gotContentHeight = editor.getContentHeight(); + // If we've already calculated the editor content height once before and the contents haven't changed, use that. + const fallbackEditorContentHeight = gotContentHeight === -1 ? Math.max(editor.getLayoutInfo().height, this._initialEditorDimension.height) : gotContentHeight; + const shouldRefreshContentHeight = !this._initialized || reason === 'onDidContentSizeChange' || reason === 'viewCellLayoutChange' || reason === 'nbLayoutChange'; + if (shouldRefreshContentHeight) { + // Update the established content height when content changes, during initialization, + // or when width/layout changes can affect wrapping-driven height. + editorContentHeight = fallbackEditorContentHeight; + this._establishedContentHeight = editorContentHeight; + } else { + // Reuse the previously established content height to avoid transient Monaco content height changes during scroll + editorContentHeight = this._establishedContentHeight ?? fallbackEditorContentHeight; + } + } const editorBottom = elementTop + this.viewCell.layoutInfo.outputContainerOffset; const scrollBottom = this.notebookEditor.scrollBottom; // When loading, scrollBottom -scrollTop === 0; @@ -830,7 +964,7 @@ export class CodeCellLayout { } } - this._logService.debug(`${reason} (${this._editorVisibility})`); + this._logService.debug(`${reason} (${this._editorVisibility}, ${this._initialized})`); this._logService.debug(`=> Editor Top = ${top}px (editHeight = ${editorHeight}, editContentHeight: ${editorContentHeight})`); this._logService.debug(`=> eleTop = ${elementTop}, eleBottom = ${elementBottom}, eleHeight = ${elementHeight}`); this._logService.debug(`=> scrollTop = ${scrollTop}, top = ${top}`); @@ -845,7 +979,8 @@ export class CodeCellLayout { width: this._initialized ? editorWidth : this._initialEditorDimension.width, height }, true); - if (editorScrollTop >= 0) { + // Option 3: Avoid programmatic scrollTop changes while user is actively dragging selection + if (!this._pointerDown && editorScrollTop >= 0) { this._lastChangedEditorScrolltop = editorScrollTop; editor.setScrollTop(editorScrollTop); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index f50518f22a6..f0ad0a8c91b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -119,8 +119,7 @@ async function webviewPreloads(ctx: PreloadContext) { const acquireVsCodeApi = globalThis.acquireVsCodeApi; const vscode = acquireVsCodeApi(); - // eslint-disable-next-line local/code-no-any-casts - delete (globalThis as any).acquireVsCodeApi; + delete (globalThis as { acquireVsCodeApi: unknown }).acquireVsCodeApi; const tokenizationStyle = new CSSStyleSheet(); tokenizationStyle.replaceSync(ctx.style.tokenizationCss); @@ -697,24 +696,21 @@ async function webviewPreloads(ctx: PreloadContext) { function focusFirstFocusableOrContainerInOutput(cellOrOutputId: string, alternateId?: string) { const cellOutputContainer = window.document.getElementById(cellOrOutputId) ?? - (alternateId ? window.document.getElementById(alternateId) : undefined); - if (cellOutputContainer) { + (!!alternateId ? window.document.getElementById(alternateId) : undefined); + if (!!cellOutputContainer) { if (cellOutputContainer.contains(window.document.activeElement)) { return; } - const id = cellOutputContainer.id; let focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null; if (!focusableElement) { focusableElement = cellOutputContainer; focusableElement.tabIndex = -1; - postNotebookMessage('outputInputFocus', { inputFocused: false, id }); - } else { - const inputFocused = hasActiveEditableElement(focusableElement, focusableElement.ownerDocument); - postNotebookMessage('outputInputFocus', { inputFocused, id }); } - lastFocusedOutput = cellOutputContainer; - postNotebookMessage('outputFocus', { id: cellOutputContainer.id }); + if (lastFocusedOutput?.id !== cellOutputContainer.id) { + lastFocusedOutput = cellOutputContainer; + postNotebookMessage('outputFocus', { id: cellOutputContainer.id }); + } focusableElement.focus(); } } @@ -1459,8 +1455,7 @@ async function webviewPreloads(ctx: PreloadContext) { document.designMode = 'On'; while (find && matches.length < 500) { - // eslint-disable-next-line local/code-no-any-casts - find = (window as any).find(query, /* caseSensitive*/ !!options.caseSensitive, + find = (window as unknown as { find: (query: string, caseSensitive: boolean, backwards: boolean, wrapAround: boolean, wholeWord: boolean, searchInFrames: boolean, includeMarkup: boolean) => boolean }).find(query, /* caseSensitive*/ !!options.caseSensitive, /* backwards*/ false, /* wrapAround*/ false, /* wholeWord */ !!options.wholeWord, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions.ts index ae027df4af5..5fa05c95ca7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEditorOptions.ts @@ -84,7 +84,7 @@ export class BaseCellEditorOptions extends Disposable implements IBaseCellEditor private _computeEditorOptions() { const editorOptions = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: this.language })); const editorOptionsOverrideRaw = this.notebookOptions.getDisplayOptions().editorOptionsCustomizations; - const editorOptionsOverride: Record = {}; + const editorOptionsOverride: Record = {}; if (editorOptionsOverrideRaw) { for (const key in editorOptionsOverrideRaw) { if (key.indexOf('editor.') === 0) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index 3edf25692ae..eb12485f97e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -326,7 +326,6 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM override dispose() { super.dispose(); - // eslint-disable-next-line local/code-no-any-casts - (this.foldingDelegate as any) = null; + (this.foldingDelegate as unknown) = null; } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index ca7a0dabfe6..25755ec15f7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -731,9 +731,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD this._decorationIdToCellMap.delete(id); } - if (this._overviewRulerDecorations.has(id)) { - this._overviewRulerDecorations.delete(id); - } + this._overviewRulerDecorations.delete(id); }); const result: string[] = []; diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts index 8a0d6a442a3..93331ab07cb 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorStickyScroll.ts @@ -167,8 +167,7 @@ export class NotebookStickyScroll extends Disposable { // Forward wheel events to the notebook editor to enable scrolling when hovering over sticky scroll this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.WHEEL, (event: WheelEvent) => { - // eslint-disable-next-line local/code-no-any-casts - this.notebookCellList.triggerScrollFromMouseWheelEvent(event as any as IMouseWheelEvent); + this.notebookCellList.triggerScrollFromMouseWheelEvent(event as unknown as IMouseWheelEvent); })); } @@ -189,7 +188,7 @@ export class NotebookStickyScroll extends Disposable { this._contextMenuService.showContextMenu({ menuId: MenuId.NotebookStickyScrollContext, getAnchor: () => event, - menuActionOptions: { shouldForwardArgs: true, arg: args }, + menuActionOptions: { shouldForwardArgs: true, arg: args, renderShortTitle: true }, }); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts index 91c2ebf389f..c01fa7525ae 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookEditorToolbar.ts @@ -284,6 +284,7 @@ export class NotebookEditorWorkbenchToolbar extends Disposable { this.contextMenuService.showContextMenu({ menuId: MenuId.NotebookToolbarContext, getAnchor: () => event, + menuActionOptions: { renderShortTitle: true } }); })); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts index 683914cab1d..34ab350f242 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookHorizontalTracker.ts @@ -72,8 +72,7 @@ export class NotebookHorizontalTracker extends Disposable { stopPropagation: () => { } }; - // eslint-disable-next-line local/code-no-any-casts - (hoveringOnEditor[1] as CodeEditorWidget).delegateScrollFromMouseWheelEvent(evt as any); + (hoveringOnEditor[1] as CodeEditorWidget).delegateScrollFromMouseWheelEvent(evt as unknown as IMouseWheelEvent); })); } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index fb9acedd244..3867369b0f5 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -421,8 +421,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { */ private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined { const metaData = notebookTextModel.metadata; - // eslint-disable-next-line local/code-no-any-casts - let suggestedKernelLanguage: string | undefined = (metaData as any)?.metadata?.language_info?.name; + const language_info = (metaData?.metadata as Record)?.language_info as Record | undefined; + let suggestedKernelLanguage: string | undefined = language_info?.name; // TODO how do we suggest multi language notebooks? if (!suggestedKernelLanguage) { const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts index b0ade6276a4..652fc736628 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookViewZones.ts @@ -205,7 +205,7 @@ export class NotebookViewZones extends Disposable { } } -function safeInvoke1Arg(func: Function, arg1: any): void { +function safeInvoke1Arg(func: Function, arg1: unknown): void { try { func(arg1); } catch (e) { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts index 8dccbae91da..87249b03323 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellOutputTextModel.ts @@ -18,7 +18,7 @@ export class NotebookCellOutputTextModel extends Disposable implements ICellOutp return this._rawOutput.outputs || []; } - get metadata(): Record | undefined { + get metadata(): Record | undefined { return this._rawOutput.metadata; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index e712afb30a5..fd9b856ab82 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -572,13 +572,12 @@ export function sortObjectPropertiesRecursively(obj: any): any { } if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { return ( - // eslint-disable-next-line local/code-no-any-casts Object.keys(obj) .sort() .reduce>((sortedObj, prop) => { sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); return sortedObj; - }, {}) as any + }, {}) ); } return obj; diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index ceb9a5bee0e..9a95995f5a1 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -10,7 +10,7 @@ import { Disposable, dispose, IDisposable } from '../../../../../base/common/lif import { Schemas } from '../../../../../base/common/network.js'; import { filter } from '../../../../../base/common/objects.js'; import { isEqual } from '../../../../../base/common/resources.js'; -import { isDefined } from '../../../../../base/common/types.js'; +import { hasKey, isDefined } from '../../../../../base/common/types.js'; import { URI } from '../../../../../base/common/uri.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; @@ -436,11 +436,11 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } private _getCellIndexWithOutputIdHandleFromEdits(outputId: string, rawEdits: ICellEditOperation[]) { - const edit = rawEdits.find(e => 'outputs' in e && e.outputs.some(o => o.outputId === outputId)); + const edit = rawEdits.find(e => hasKey(e, { outputs: true }) && e.outputs.some(o => o.outputId === outputId)); if (edit) { - if ('index' in edit) { + if (hasKey(edit, { index: true })) { return edit.index; - } else if ('handle' in edit) { + } else if (hasKey(edit, { handle: true })) { const cellIndex = this._getCellIndexByHandle(edit.handle); this._assertIndex(cellIndex); return cellIndex; @@ -621,10 +621,10 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return false; } - if (('index' in edit) && !this.newCellsFromLastEdit.has(this.cells[edit.index].handle)) { + if (hasKey(edit, { index: true }) && !this.newCellsFromLastEdit.has(this.cells[edit.index].handle)) { return false; } - if ('handle' in edit && !this.newCellsFromLastEdit.has(edit.handle)) { + if (hasKey(edit, { handle: true }) && !this.newCellsFromLastEdit.has(edit.handle)) { return false; } } @@ -675,12 +675,12 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _doApplyEdits(rawEdits: ICellEditOperation[], synchronous: boolean, computeUndoRedo: boolean, beginSelectionState: ISelectionState | undefined, undoRedoGroup: UndoRedoGroup | undefined): void { const editsWithDetails = rawEdits.map((edit, index) => { let cellIndex: number = -1; - if ('index' in edit) { + if (hasKey(edit, { index: true })) { cellIndex = edit.index; - } else if ('handle' in edit) { + } else if (hasKey(edit, { handle: true })) { cellIndex = this._getCellIndexByHandle(edit.handle); this._assertIndex(cellIndex); - } else if ('outputId' in edit) { + } else if (hasKey(edit, { outputId: true })) { cellIndex = this._getCellIndexWithOutputIdHandle(edit.outputId); if (this._indexIsInvalid(cellIndex)) { // The referenced output may have been created in this batch of edits @@ -1110,8 +1110,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel let k: keyof NullablePartialNotebookCellMetadata; for (k in metadata) { const value = metadata[k] ?? undefined; - // eslint-disable-next-line local/code-no-any-casts - newMetadata[k] = value as any; + newMetadata[k] = value; } return this._changeCellMetadata(cell, newMetadata, computeUndoRedo, beginSelectionState, undoRedoGroup); @@ -1152,8 +1151,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel let k: keyof NotebookCellInternalMetadata; for (k in internalMetadata) { const value = internalMetadata[k] ?? undefined; - // eslint-disable-next-line local/code-no-any-casts - newInternalMetadata[k] = value as any; + (newInternalMetadata[k] as unknown) = value; } cell.internalMetadata = newInternalMetadata; diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 53ad06b5e3c..5ce068885bb 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import { assertType } from '../../../../base/common/types.js'; +import { assertType, hasKey } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IWriteFileOptions, IFileStatWithMetadata, FileOperationError, FileOperationResult } from '../../../../platform/files/common/files.js'; @@ -111,7 +111,7 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE } get hasErrorState(): boolean { - if (this._workingCopy && 'hasState' in this._workingCopy) { + if (this._workingCopy && hasKey(this._workingCopy, { hasState: true })) { return this._workingCopy.hasState(StoredFileWorkingCopyState.ERROR); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index 3863545f528..8fbf6dcb14e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -8,7 +8,7 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; -import { INotebookRendererInfo, ContributedNotebookRendererEntrypoint, NotebookRendererMatch, RendererMessagingSpec, NotebookRendererEntrypoint, INotebookStaticPreloadInfo as INotebookStaticPreloadInfo } from './notebookCommon.js'; +import { INotebookRendererInfo, ContributedNotebookRendererEntrypoint, NotebookRendererMatch, RendererMessagingSpec, NotebookRendererEntrypoint, INotebookStaticPreloadInfo } from './notebookCommon.js'; class DependencyList { private readonly value: ReadonlySet; diff --git a/src/vs/workbench/contrib/notebook/common/notebookRange.ts b/src/vs/workbench/contrib/notebook/common/notebookRange.ts index 75a7a105757..1487d1869b0 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookRange.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookRange.ts @@ -19,12 +19,12 @@ export interface ICellRange { } -export function isICellRange(candidate: any): candidate is ICellRange { +export function isICellRange(candidate: unknown): candidate is ICellRange { if (!candidate || typeof candidate !== 'object') { return false; } - return typeof (candidate).start === 'number' - && typeof (candidate).end === 'number'; + return typeof (candidate as ICellRange).start === 'number' + && typeof (candidate as ICellRange).end === 'number'; } export function cellIndexesToRanges(indexes: number[]) { diff --git a/src/vs/workbench/contrib/notebook/test/browser/NotebookEditorWidgetService.test.ts b/src/vs/workbench/contrib/notebook/test/browser/NotebookEditorWidgetService.test.ts index 3ed3d21b187..02ba46f12a6 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/NotebookEditorWidgetService.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/NotebookEditorWidgetService.test.ts @@ -34,8 +34,7 @@ class TestNotebookEditorWidgetService extends NotebookEditorWidgetService { protected override createWidget(): NotebookEditorWidget { return new class extends mock() { override onWillHide = () => { }; - // eslint-disable-next-line local/code-no-any-casts - override getDomNode = () => { return { remove: () => { } } as any; }; + override getDomNode = () => { return { remove: () => { } } as HTMLElement; }; override dispose = () => { }; }; } @@ -87,9 +86,8 @@ suite('NotebookEditorWidgetService', () => { override groups = [editorGroup1, editorGroup2]; override getPart(group: IEditorGroup | GroupIdentifier): IEditorPart; override getPart(container: unknown): IEditorPart; - override getPart(container: unknown): import('../../../../services/editor/common/editorGroupsService.js').IEditorPart { - // eslint-disable-next-line local/code-no-any-casts - return { windowId: 0 } as any; + override getPart(container: unknown): IEditorPart { + return { windowId: 0 } as IEditorPart; } }); instantiationService.stub(IEditorService, new class extends mock() { diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts index b7a87cdbc51..836da5ad28d 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookCellDiagnostics.test.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/ import { TestConfigurationService } from '../../../../../../platform/configuration/test/common/testConfigurationService.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { IMarkerData, IMarkerService } from '../../../../../../platform/markers/common/markers.js'; -import { IChatAgent, IChatAgentData, IChatAgentService } from '../../../../chat/common/chatAgents.js'; +import { IChatAgent, IChatAgentData, IChatAgentService } from '../../../../chat/common/participants/chatAgents.js'; import { CellDiagnostics } from '../../../browser/contrib/cellDiagnostics/cellDiagnosticEditorContrib.js'; import { CodeCellViewModel } from '../../../browser/viewModel/codeCellViewModel.js'; import { CellKind, NotebookSetting } from '../../../common/notebookCommon.js'; diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 179479848ae..0471a9044a0 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -69,8 +69,7 @@ suite('Notebook Outline', function () { }; - // eslint-disable-next-line local/code-no-any-casts - const testOutlineEntryFactory = instantiationService.createInstance(NotebookOutlineEntryFactory) as any; + const testOutlineEntryFactory = instantiationService.createInstance(NotebookOutlineEntryFactory) as NotebookOutlineEntryFactory; testOutlineEntryFactory.cacheSymbols = async () => { symbolsCached = true; }; instantiationService.stub(INotebookOutlineEntryFactory, testOutlineEntryFactory); @@ -110,42 +109,67 @@ suite('Notebook Outline', function () { test('Notebook falsely detects "empty cells"', async function () { await withNotebookOutline([ [' 的时代 ', 'md', CellKind.Markup] - ], OutlineTarget.OutlinePane, outline => { + ], OutlineTarget.OutlinePane, (outline, notebookEditor) => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '的时代'); + assert.deepStrictEqual(outline.entries[0].label, '的时代', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in outline label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '的时代', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in quickpick entry label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); }); await withNotebookOutline([ [' ', 'md', CellKind.Markup] - ], OutlineTarget.OutlinePane, outline => { + ], OutlineTarget.OutlinePane, (outline, notebookEditor) => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'empty cell'); + assert.deepStrictEqual(outline.entries[0].label, 'empty cell', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up as an empty cell in outline label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'empty cell', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up as an empty cell in quickpick entry label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); }); await withNotebookOutline([ ['+++++[]{}--)(0 ', 'md', CellKind.Markup] - ], OutlineTarget.OutlinePane, outline => { + ], OutlineTarget.OutlinePane, (outline, notebookEditor) => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0'); + assert.deepStrictEqual(outline.entries[0].label, '+++++[]{}--)(0', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in outline label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in quickpick entry label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); }); await withNotebookOutline([ ['+++++[]{}--)(0 Hello **&^ ', 'md', CellKind.Markup] - ], OutlineTarget.OutlinePane, outline => { + ], OutlineTarget.OutlinePane, (outline, notebookEditor) => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0 Hello **&^'); + assert.deepStrictEqual(outline.entries[0].label, '+++++[]{}--)(0 Hello **&^', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in outline label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0 Hello **&^', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in quickpick entry label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); }); await withNotebookOutline([ ['!@#$\n Überschrïft', 'md', CellKind.Markup] - ], OutlineTarget.OutlinePane, outline => { + ], OutlineTarget.OutlinePane, (outline, notebookEditor) => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); - assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$'); + assert.deepStrictEqual(outline.entries[0].label, '!@#$', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in outline label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); + assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$', + `cell content: ${notebookEditor.cellAt(0).model.getValue()} did not show up correctly in quickpick entry label. \n Cell text buffer line 1: ${outline.entries[0].cell.textBuffer.getLineContent(1)}` + ); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts index a7f35bacea3..758d9299c3f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts @@ -644,14 +644,14 @@ suite('Notebook Outline View Providers', function () { // Validate assert.equal(results.length, 3); - assert.equal(results[0].label, 'fakeRoot'); - assert.equal(results[0].level, -1); + assert.equal(results[0].element.label, 'fakeRoot'); + assert.equal(results[0].element.level, -1); - assert.equal(results[1].label, 'h1'); - assert.equal(results[1].level, 1); + assert.equal(results[1].element.label, 'h1'); + assert.equal(results[1].element.level, 1); - assert.equal(results[2].label, '# code cell 2'); - assert.equal(results[2].level, 7); + assert.equal(results[2].element.label, '# code cell 2'); + assert.equal(results[2].element.level, 7); }); test('Breadcrumbs 1: Code Cells Off ', async function () { @@ -695,11 +695,11 @@ suite('Notebook Outline View Providers', function () { // Validate assert.equal(results.length, 2); - assert.equal(results[0].label, 'fakeRoot'); - assert.equal(results[0].level, -1); + assert.equal(results[0].element.label, 'fakeRoot'); + assert.equal(results[0].element.level, -1); - assert.equal(results[1].label, 'h1'); - assert.equal(results[1].level, 1); + assert.equal(results[1].element.label, 'h1'); + assert.equal(results[1].element.level, 1); }); // #endregion diff --git a/src/vs/workbench/contrib/notebook/test/browser/diff/editorHeightCalculator.test.ts b/src/vs/workbench/contrib/notebook/test/browser/diff/editorHeightCalculator.test.ts index 93b4c7662ac..d7b23c71cdb 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/diff/editorHeightCalculator.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/diff/editorHeightCalculator.test.ts @@ -23,8 +23,7 @@ import { HeightOfHiddenLinesRegionInDiffEditor } from '../../../browser/diff/dif suite('NotebookDiff EditorHeightCalculator', () => { ['Hide Unchanged Regions', 'Show Unchanged Regions'].forEach(suiteTitle => { suite(suiteTitle, () => { - // eslint-disable-next-line local/code-no-any-casts - const fontInfo: FontInfo = { lineHeight: 18, fontSize: 18 } as any; + const fontInfo: FontInfo = { lineHeight: 18, fontSize: 18 } as FontInfo; let disposables: DisposableStore; let textModelResolver: ITextModelService; let editorWorkerService: IEditorWorkerService; @@ -56,11 +55,10 @@ suite('NotebookDiff EditorHeightCalculator', () => { override async createModelReference(resource: URI): Promise> { return { dispose: () => { }, - // eslint-disable-next-line local/code-no-any-casts object: { textEditorModel: resource === original ? originalModel : modifiedModel, getLanguageId: () => 'javascript', - } as any + } as IResolvedTextEditorModel }; } }; diff --git a/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiff.test.ts index f99212e5a43..e9c25750a93 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/diff/notebookDiff.test.ts @@ -649,10 +649,8 @@ suite('NotebookDiff', () => { assert.strictEqual(diffViewModel.items.length, 2); assert.strictEqual(diffViewModel.items[0].type, 'placeholder'); diffViewModel.items[0].showHiddenCells(); - // eslint-disable-next-line local/code-no-any-casts - assert.strictEqual((diffViewModel.items[0] as unknown as SideBySideDiffElementViewModel).original!.textModel.equal((diffViewModel.items[0] as any).modified!.textModel), true); - // eslint-disable-next-line local/code-no-any-casts - assert.strictEqual((diffViewModel.items[1] as any).original!.textModel.equal((diffViewModel.items[1] as any).modified!.textModel), false); + assert.strictEqual((diffViewModel.items[0] as unknown as SideBySideDiffElementViewModel).original!.textModel.equal((diffViewModel.items[0] as unknown as SideBySideDiffElementViewModel).modified!.textModel), true); + assert.strictEqual((diffViewModel.items[1] as unknown as SideBySideDiffElementViewModel).original!.textModel.equal((diffViewModel.items[1] as unknown as SideBySideDiffElementViewModel).modified!.textModel), false); await verifyChangeEventIsNotFired(diffViewModel); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookCellLayoutManager.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookCellLayoutManager.test.ts index 93521070365..18d98f23e36 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookCellLayoutManager.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookCellLayoutManager.test.ts @@ -6,6 +6,11 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ICellViewModel } from '../../browser/notebookBrowser.js'; import { NotebookCellLayoutManager } from '../../browser/notebookCellLayoutManager.js'; +import { INotebookCellList } from '../../browser/view/notebookRenderingCommon.js'; +import { INotebookLoggingService } from '../../common/notebookLoggingService.js'; +import { NotebookEditorWidget } from '../../browser/notebookEditorWidget.js'; +import { NotebookViewModel } from '../../browser/viewModel/notebookViewModelImpl.js'; +import { ICellRange } from '../../common/notebookRange.js'; suite('NotebookCellLayoutManager', () => { @@ -15,7 +20,7 @@ suite('NotebookCellLayoutManager', () => { return { handle: 'cell1' } as unknown as ICellViewModel; }; - class MockList { + class MockList implements Pick { private _height = new Map(); getViewIndex(cell: ICellViewModel) { return this.cells.indexOf(cell) < 0 ? undefined : this.cells.indexOf(cell); } elementHeight(cell: ICellViewModel) { return this._height.get(cell) ?? 100; } @@ -24,13 +29,23 @@ suite('NotebookCellLayoutManager', () => { getViewIndexCalled = false; cells: ICellViewModel[] = []; } - class MockLoggingService { debug() { } } - class MockNotebookWidget { - viewModel = { hasCell: (cell: ICellViewModel) => true, getCellIndex: () => 0 }; + class MockLoggingService implements INotebookLoggingService { + readonly _serviceBrand: undefined; + debug() { } + info() { } + warn() { } + error() { } + trace() { } + } + class MockNotebookWidget implements Pick { + viewModel: NotebookViewModel | undefined = { + hasCell: (cell: ICellViewModel) => true, + getCellIndex: () => 0 + } as unknown as NotebookViewModel; hasEditorFocus() { return true; } getAbsoluteTopOfElement() { return 0; } getLength() { return 1; } - visibleRanges = [{ start: 0 }]; + visibleRanges: ICellRange[] = [{ start: 0, end: 0 }]; getDomNode(): HTMLElement { return { style: { @@ -47,8 +62,7 @@ suite('NotebookCellLayoutManager', () => { list.cells.push(cell); list.cells.push(cell2); const widget = new MockNotebookWidget(); - // eslint-disable-next-line local/code-no-any-casts - const mgr = store.add(new NotebookCellLayoutManager(widget as any, list as any, new MockLoggingService() as any)); + const mgr = store.add(new NotebookCellLayoutManager(widget as unknown as NotebookEditorWidget, list as unknown as INotebookCellList, new MockLoggingService())); mgr.layoutNotebookCell(cell, 200); mgr.layoutNotebookCell(cell2, 200); assert.strictEqual(list.elementHeight(cell), 200); @@ -63,8 +77,7 @@ suite('NotebookCellLayoutManager', () => { list.cells.push(cell); list.cells.push(cell2); const widget = new MockNotebookWidget(); - // eslint-disable-next-line local/code-no-any-casts - const mgr = store.add(new NotebookCellLayoutManager(widget as any, list as any, new MockLoggingService() as any)); + const mgr = store.add(new NotebookCellLayoutManager(widget as unknown as NotebookEditorWidget, list as unknown as INotebookCellList, new MockLoggingService())); const promise = mgr.layoutNotebookCell(cell, 200); mgr.layoutNotebookCell(cell2, 200); @@ -82,8 +95,7 @@ suite('NotebookCellLayoutManager', () => { const cell = mockCellViewModel(); const list = new MockList(); const widget = new MockNotebookWidget(); - // eslint-disable-next-line local/code-no-any-casts - const mgr = store.add(new NotebookCellLayoutManager(widget as any, list as any, new MockLoggingService() as any)); + const mgr = store.add(new NotebookCellLayoutManager(widget as unknown as NotebookEditorWidget, list as unknown as INotebookCellList, new MockLoggingService())); await mgr.layoutNotebookCell(cell, 200); assert.strictEqual(list.elementHeight(cell), 100); }); @@ -93,8 +105,7 @@ suite('NotebookCellLayoutManager', () => { const list = new MockList(); list.cells.push(cell); const widget = new MockNotebookWidget(); - // eslint-disable-next-line local/code-no-any-casts - const mgr = store.add(new NotebookCellLayoutManager(widget as any, list as any, new MockLoggingService() as any)); + const mgr = store.add(new NotebookCellLayoutManager(widget as unknown as NotebookEditorWidget, list as unknown as INotebookCellList, new MockLoggingService())); await mgr.layoutNotebookCell(cell, 100); assert.strictEqual(list.elementHeight(cell), 100); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 302b59ba647..9274063cef1 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -281,8 +281,6 @@ suite('NotebookFileWorkingCopyModel', function () { return Promise.resolve({ name: 'savedFile' } as IFileStatWithMetadata); } }; - // eslint-disable-next-line local/code-no-any-casts - (serializer as any).test = 'yes'; let resolveSerializer: (serializer: INotebookSerializer) => void = () => { }; const serializerPromise = new Promise(resolve => { @@ -305,8 +303,7 @@ suite('NotebookFileWorkingCopyModel', function () { resolveSerializer(serializer); await model.getNotebookSerializer(); - // eslint-disable-next-line local/code-no-any-casts - const result = await model.save?.({} as any, {} as any); + const result = await model.save?.({} as IFileStatWithMetadata, {} as CancellationToken); assert.strictEqual(result!.name, 'savedFile'); }); @@ -342,7 +339,7 @@ function mockNotebookService(notebook: NotebookTextModel, notebookSerializer: Pr override async createNotebookTextDocumentSnapshot(uri: URI, context: SnapshotContext, token: CancellationToken): Promise { const info = await this.withNotebookDataProvider(notebook.viewType); const serializer = info.serializer; - const outputSizeLimit = configurationService.getValue(NotebookSetting.outputBackupSizeLimit) ?? 1024; + const outputSizeLimit = configurationService.getValue(NotebookSetting.outputBackupSizeLimit) ?? 1024; const data: NotebookData = notebook.createSnapshot({ context: context, outputSizeLimit: outputSizeLimit, transientOptions: serializer.options }); const bytes = await serializer.notebookToData(data); diff --git a/src/vs/workbench/contrib/notebook/test/browser/view/cellPart.test.ts b/src/vs/workbench/contrib/notebook/test/browser/view/cellPart.test.ts index b7c5f2d5f85..36cfcb94107 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/view/cellPart.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/view/cellPart.test.ts @@ -401,4 +401,545 @@ suite('CellPart', () => { ); } }); + + test('CodeCellLayout reuses content height after init', () => { + const LINE_HEIGHT = 21; + const STATUSBAR_HEIGHT = 22; + const CELL_TOP_MARGIN = 6; + const CELL_OUTLINE_WIDTH = 1; + const VIEWPORT_HEIGHT = 1000; + const ELEMENT_TOP = 100; + const ELEMENT_HEIGHT = 1200; + const OUTPUT_CONTAINER_OFFSET = 300; + const EDITOR_HEIGHT = 800; + + let contentHeight = 800; + const stubEditor = { + layoutCalls: [] as { width: number; height: number }[], + _lastScrollTopSet: -1, + getLayoutInfo: () => ({ width: 600, height: EDITOR_HEIGHT }), + getContentHeight: () => contentHeight, + layout: (dim: { width: number; height: number }) => { + stubEditor.layoutCalls.push(dim); + }, + setScrollTop: (v: number) => { + stubEditor._lastScrollTopSet = v; + }, + hasModel: () => true, + }; + const editorPart = { style: { top: '' } }; + const template: Partial = { + editor: stubEditor as unknown as ICodeEditor, + editorPart: editorPart as unknown as HTMLElement, + }; + const viewCell: Partial = { + isInputCollapsed: false, + layoutInfo: { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: EDITOR_HEIGHT, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + editorWidth: 600, + } as unknown as CodeCellLayoutInfo, + }; + const notebookEditor = { + scrollTop: 0, + get scrollBottom() { + return VIEWPORT_HEIGHT; + }, + setScrollTop: (v: number) => { + /* no-op */ + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const layout = new CodeCellLayout( + true, + notebookEditor as unknown as IActiveNotebookEditorDelegate, + viewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: EDITOR_HEIGHT } + ); + + layout.layoutEditor('init'); + assert.strictEqual(layout.editorVisibility, 'Full'); + assert.strictEqual(stubEditor.layoutCalls.at(-1)?.height, 800); + + // Simulate Monaco reporting a transient smaller content height on scroll. + contentHeight = 200; + layout.layoutEditor('nbDidScroll'); + assert.strictEqual(layout.editorVisibility, 'Full'); + assert.strictEqual( + stubEditor.layoutCalls.at(-1)?.height, + 800, + 'nbDidScroll should reuse the established content height' + ); + + layout.layoutEditor('onDidContentSizeChange'); + assert.strictEqual(layout.editorVisibility, 'Full'); + assert.strictEqual( + stubEditor.layoutCalls.at(-1)?.height, + 200, + 'onDidContentSizeChange should refresh the content height' + ); + }); + + test('CodeCellLayout refreshes content height on viewCellLayoutChange', () => { + const LINE_HEIGHT = 21; + const CELL_TOP_MARGIN = 6; + const CELL_OUTLINE_WIDTH = 1; + const STATUSBAR_HEIGHT = 22; + const VIEWPORT_HEIGHT = 1000; + const ELEMENT_TOP = 100; + const ELEMENT_HEIGHT = 1200; + const INITIAL_CONTENT_HEIGHT = 37; + const OUTPUT_CONTAINER_OFFSET = 300; + const UPDATED_CONTENT_HEIGHT = 200; + + let contentHeight = INITIAL_CONTENT_HEIGHT; + const stubEditor = { + layoutCalls: [] as { width: number; height: number }[], + _lastScrollTopSet: -1, + getLayoutInfo: () => ({ width: 600, height: INITIAL_CONTENT_HEIGHT }), + getContentHeight: () => contentHeight, + layout: (dim: { width: number; height: number }) => { + stubEditor.layoutCalls.push(dim); + }, + setScrollTop: (v: number) => { + stubEditor._lastScrollTopSet = v; + }, + hasModel: () => true, + }; + const editorPart = { style: { top: '' } }; + const template: Partial = { + editor: stubEditor as unknown as ICodeEditor, + editorPart: editorPart as unknown as HTMLElement, + }; + const viewCell: Partial = { + isInputCollapsed: false, + layoutInfo: { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: INITIAL_CONTENT_HEIGHT, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + editorWidth: 600, + } as unknown as CodeCellLayoutInfo, + }; + const notebookEditor = { + scrollTop: 0, + get scrollBottom() { + return VIEWPORT_HEIGHT; + }, + setScrollTop: (v: number) => { + /* no-op */ + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const layout = new CodeCellLayout( + true, + notebookEditor as unknown as IActiveNotebookEditorDelegate, + viewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: INITIAL_CONTENT_HEIGHT } + ); + + layout.layoutEditor('init'); + assert.strictEqual(stubEditor.layoutCalls.at(-1)?.height, INITIAL_CONTENT_HEIGHT); + + // Simulate wrapping-driven height increase after width/layout settles. + contentHeight = UPDATED_CONTENT_HEIGHT; + layout.layoutEditor('viewCellLayoutChange'); + assert.strictEqual( + stubEditor.layoutCalls.at(-1)?.height, + UPDATED_CONTENT_HEIGHT, + 'viewCellLayoutChange should refresh the content height' + ); + + // Ensure subsequent scrolls still reuse the established (larger) height. + contentHeight = 50; + layout.layoutEditor('nbDidScroll'); + assert.strictEqual( + stubEditor.layoutCalls.at(-1)?.height, + UPDATED_CONTENT_HEIGHT, + 'nbDidScroll should reuse the refreshed content height' + ); + }); + + test('CodeCellLayout maintains content height after paste when scrolling', () => { + /** + * Regression test for https://github.com/microsoft/vscode/issues/284524 + * + * Scenario: Cell starts with 1 line (37px), user pastes text (grows to 679px), + * then scrolls. During scroll, Monaco may report a transient smaller height (39px) + * due to the clipped layout. The fix uses _establishedContentHeight to maintain + * the actual content height (679px) instead of using the transient or initial values. + */ + const LINE_HEIGHT = 21; + const CELL_TOP_MARGIN = 6; + const CELL_OUTLINE_WIDTH = 1; + const STATUSBAR_HEIGHT = 22; + const VIEWPORT_HEIGHT = 1000; + const ELEMENT_TOP = 100; + const ELEMENT_HEIGHT = 1200; + const INITIAL_CONTENT_HEIGHT = 37; // 1 line + const INITIAL_EDITOR_HEIGHT = INITIAL_CONTENT_HEIGHT; + const OUTPUT_CONTAINER_OFFSET = 300; + const PASTED_CONTENT_HEIGHT = 679; + + let contentHeight = INITIAL_CONTENT_HEIGHT; + const stubEditor = { + layoutCalls: [] as { width: number; height: number }[], + _lastScrollTopSet: -1, + getLayoutInfo: () => ({ width: 600, height: INITIAL_EDITOR_HEIGHT }), + getContentHeight: () => contentHeight, + layout: (dim: { width: number; height: number }) => { + stubEditor.layoutCalls.push(dim); + }, + setScrollTop: (v: number) => { + stubEditor._lastScrollTopSet = v; + }, + hasModel: () => true, + }; + const editorPart = { style: { top: '' } }; + const template: Partial = { + editor: stubEditor as unknown as ICodeEditor, + editorPart: editorPart as unknown as HTMLElement, + }; + const layoutInfo = { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: INITIAL_EDITOR_HEIGHT, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + editorWidth: 600, + }; + const viewCell: Partial = { + isInputCollapsed: false, + layoutInfo: layoutInfo as unknown as CodeCellLayoutInfo, + }; + const notebookEditor = { + scrollTop: 0, + get scrollBottom() { + return notebookEditor.scrollTop + VIEWPORT_HEIGHT; + }, + setScrollTop: (v: number) => { + notebookEditor.scrollTop = v; + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const layout = new CodeCellLayout( + true, + notebookEditor as unknown as IActiveNotebookEditorDelegate, + viewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: INITIAL_EDITOR_HEIGHT } + ); + + // Initial layout + layout.layoutEditor('init'); + + // Simulate pasting content - content grows to 679px + contentHeight = PASTED_CONTENT_HEIGHT; + layoutInfo.editorHeight = PASTED_CONTENT_HEIGHT; + layout.layoutEditor('onDidContentSizeChange'); + + // Now scroll and Monaco reports transient smaller height (39px) + // The fix should use the established 679px, not the transient 39px or initial 37px + contentHeight = 39; + notebookEditor.scrollTop = 200; + layout.layoutEditor('nbDidScroll'); + + const finalHeight = stubEditor.layoutCalls.at(-1)?.height; + + // Verify the layout doesn't use the transient 39px value from Monaco + assert.notStrictEqual( + finalHeight, + 39, + 'Should not use Monaco\'s transient value (39px)' + ); + + // Verify the layout doesn't shrink back to the initial 37px value + assert.notStrictEqual( + finalHeight, + 37, + 'Should not use initial content height (37px)' + ); + + // The layout should be based on the established 679px content height + // The exact height will be calculated based on viewport, scroll position, etc. + // but should be significantly larger than 39px or 37px + assert.ok( + finalHeight && finalHeight > 100, + `Layout height (${finalHeight}px) should be calculated from established 679px content, not transient 39px or initial 37px` + ); + }); + + test('CodeCellLayout does not programmatically scroll editor while pointer down', () => { + const LINE_HEIGHT = 21; + const CELL_TOP_MARGIN = 6; + const CELL_OUTLINE_WIDTH = 1; + const STATUSBAR_HEIGHT = 22; + const VIEWPORT_HEIGHT = 220; + const ELEMENT_TOP = 100; + const EDITOR_CONTENT_HEIGHT = 500; + const EDITOR_HEIGHT = EDITOR_CONTENT_HEIGHT; + const OUTPUT_CONTAINER_OFFSET = 600; + const ELEMENT_HEIGHT = 900; + const scrollTop = ELEMENT_TOP + CELL_TOP_MARGIN + 20; + const scrollBottom = scrollTop + VIEWPORT_HEIGHT; + + const stubEditor = { + _lastScrollTopSet: -1, + getLayoutInfo: () => ({ width: 600, height: EDITOR_HEIGHT }), + getContentHeight: () => EDITOR_CONTENT_HEIGHT, + layout: () => { + /* no-op */ + }, + setScrollTop: (v: number) => { + stubEditor._lastScrollTopSet = v; + }, + hasModel: () => true, + }; + const editorPart = { style: { top: '' } }; + const template: Partial = { + editor: stubEditor as unknown as ICodeEditor, + editorPart: editorPart as unknown as HTMLElement, + }; + const viewCell: Partial = { + isInputCollapsed: false, + layoutInfo: { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: EDITOR_HEIGHT, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + } as unknown as CodeCellLayoutInfo, + }; + const notebookEditor = { + scrollTop, + get scrollBottom() { + return scrollBottom; + }, + setScrollTop: (v: number) => { + /* no-op */ + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const layout = new CodeCellLayout( + true, + notebookEditor as unknown as IActiveNotebookEditorDelegate, + viewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: EDITOR_HEIGHT } + ); + + layout.layoutEditor('init'); + stubEditor._lastScrollTopSet = -1; + + layout.setPointerDown(true); + layout.layoutEditor('nbDidScroll'); + assert.strictEqual(layout.editorVisibility, 'Full (Small Viewport)'); + assert.strictEqual( + stubEditor._lastScrollTopSet, + -1, + 'Expected no programmatic editor.setScrollTop while pointer is down' + ); + + layout.setPointerDown(false); + layout.layoutEditor('nbDidScroll'); + assert.strictEqual(layout.editorVisibility, 'Full (Small Viewport)'); + assert.notStrictEqual( + stubEditor._lastScrollTopSet, + -1, + 'Expected editor.setScrollTop to resume once pointer is released' + ); + }); + + test('CodeCellLayout init ignores stale pooled editor content height', () => { + /** + * Regression guard for fast-scroll overlap when editors are pooled. + * + * A Monaco editor instance can be reused between cells. If we trusted the pooled + * editor's `getContentHeight()` during the first layout of a new cell, a short + * cell might inherit a previous tall cell's content height and render with an + * oversized editor, visually overlapping the next cell. The layout should instead + * seed its initial content height from the cell's own initial editor dimension. + */ + const LINE_HEIGHT = 21; + const CELL_TOP_MARGIN = 6; + const CELL_OUTLINE_WIDTH = 1; + const STATUSBAR_HEIGHT = 22; + const VIEWPORT_HEIGHT = 400; + const ELEMENT_TOP = 100; + const ELEMENT_HEIGHT = 500; + const OUTPUT_CONTAINER_OFFSET = 200; + + let pooledContentHeight = 200; // tall previous cell + const pooledEditor = { + layoutCalls: [] as { width: number; height: number }[], + _lastScrollTopSet: -1, + getLayoutInfo: () => ({ width: 600, height: pooledContentHeight }), + getContentHeight: () => pooledContentHeight, + layout: (dim: { width: number; height: number }) => { + pooledEditor.layoutCalls.push(dim); + }, + setScrollTop: (v: number) => { + pooledEditor._lastScrollTopSet = v; + }, + hasModel: () => true, + }; + const editorPart = { style: { top: '' } }; + const template: Partial = { + editor: pooledEditor as unknown as ICodeEditor, + editorPart: editorPart as unknown as HTMLElement, + }; + + // First, layout a tall cell to establish a large content height on the pooled editor. + const tallViewCell: Partial = { + isInputCollapsed: false, + layoutInfo: { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: 200, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + editorWidth: 600, + } as unknown as CodeCellLayoutInfo, + }; + const tallNotebookEditor = { + scrollTop: 0, + get scrollBottom() { + return VIEWPORT_HEIGHT; + }, + setScrollTop: (_v: number) => { + /* no-op for this test */ + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const tallLayout = new CodeCellLayout( + true, + tallNotebookEditor as unknown as IActiveNotebookEditorDelegate, + tallViewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: 200 } + ); + + tallLayout.layoutEditor('init'); + assert.strictEqual( + pooledEditor.layoutCalls.at(-1)?.height, + 200, + 'Expected tall cell to lay out using its own height' + ); + + // Now reuse the same editor for a short cell while leaving the pooled content height large. + pooledContentHeight = 200; // simulate stale value from previous cell + const shortViewCell: Partial = { + isInputCollapsed: false, + layoutInfo: { + statusBarHeight: STATUSBAR_HEIGHT, + topMargin: CELL_TOP_MARGIN, + outlineWidth: CELL_OUTLINE_WIDTH, + editorHeight: 37, + outputContainerOffset: OUTPUT_CONTAINER_OFFSET, + editorWidth: 600, + } as unknown as CodeCellLayoutInfo, + }; + const shortNotebookEditor = { + scrollTop: 0, + get scrollBottom() { + return VIEWPORT_HEIGHT; + }, + setScrollTop: (_v: number) => { + /* no-op for this test */ + }, + getLayoutInfo: () => ({ + fontInfo: { lineHeight: LINE_HEIGHT }, + height: VIEWPORT_HEIGHT, + stickyHeight: 0, + }), + getAbsoluteTopOfElement: () => ELEMENT_TOP, + getAbsoluteBottomOfElement: () => ELEMENT_TOP + OUTPUT_CONTAINER_OFFSET, + getHeightOfElement: () => ELEMENT_HEIGHT, + notebookOptions: { + getLayoutConfiguration: () => ({ editorTopPadding: 6 }), + }, + }; + + const shortLayout = new CodeCellLayout( + true, + shortNotebookEditor as unknown as IActiveNotebookEditorDelegate, + shortViewCell as CodeCellViewModel, + template as CodeCellRenderTemplate, + { debug: () => { } }, + { width: 600, height: 37 } + ); + + shortLayout.layoutEditor('init'); + assert.strictEqual( + pooledEditor.layoutCalls.at(-1)?.height, + 37, + 'Init layout for a short cell should use the cell\'s initial height, not the pooled editor\'s stale content height' + ); + }); }); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 407aef31dbe..daa2516d6ab 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -29,7 +29,6 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { Disposable, dispose, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ILoggerService, LogLevel, LogLevelToLocalizedString, LogLevelToString } from '../../../../platform/log/common/log.js'; -import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; @@ -43,6 +42,7 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs. import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { hasKey } from '../../../../base/common/types.js'; +import { IDefaultLogLevelsService } from '../../../services/log/common/defaultLogLevels.js'; const IMPORTED_LOG_ID_PREFIX = 'importedLog.'; diff --git a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts index 9fe87b7af5e..89591f8ed93 100644 --- a/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts +++ b/src/vs/workbench/contrib/output/browser/outputLinkProvider.ts @@ -12,7 +12,8 @@ import { OUTPUT_MODE_ID, LOG_MODE_ID } from '../../../services/output/common/out import { OutputLinkComputer } from '../common/outputLinkComputer.js'; import { IDisposable, dispose, Disposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { createWebWorker } from '../../../../base/browser/webWorkerFactory.js'; +import { WebWorkerDescriptor } from '../../../../platform/webWorker/browser/webWorkerDescriptor.js'; +import { IWebWorkerService } from '../../../../platform/webWorker/browser/webWorkerService.js'; import { IWebWorkerClient } from '../../../../base/common/worker/webWorker.js'; import { WorkerTextModelSyncClient } from '../../../../editor/common/services/textModelSync/textModelSync.impl.js'; import { FileAccess } from '../../../../base/common/network.js'; @@ -29,6 +30,7 @@ export class OutputLinkProvider extends Disposable { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IModelService private readonly modelService: IModelService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, + @IWebWorkerService private readonly webWorkerService: IWebWorkerService, ) { super(); @@ -70,7 +72,7 @@ export class OutputLinkProvider extends Disposable { this.disposeWorkerScheduler.schedule(); if (!this.worker) { - this.worker = new OutputLinkWorkerClient(this.contextService, this.modelService); + this.worker = new OutputLinkWorkerClient(this.contextService, this.modelService, this.webWorkerService); } return this.worker; @@ -96,11 +98,14 @@ class OutputLinkWorkerClient extends Disposable { constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IModelService modelService: IModelService, + @IWebWorkerService webWorkerService: IWebWorkerService, ) { super(); - this._workerClient = this._register(createWebWorker( - FileAccess.asBrowserUri('vs/workbench/contrib/output/common/outputLinkComputerMain.js'), - 'OutputLinkDetectionWorker' + this._workerClient = this._register(webWorkerService.createWorkerClient( + new WebWorkerDescriptor({ + esmModuleLocation: FileAccess.asBrowserUri('vs/workbench/contrib/output/common/outputLinkComputerMain.js'), + label: 'OutputLinkDetectionWorker' + }) )); this._workerTextModelSyncClient = this._register(WorkerTextModelSyncClient.create(this._workerClient, modelService)); this._initializeBarrier = this._ensureWorkspaceFolders(); diff --git a/src/vs/workbench/contrib/output/browser/outputServices.ts b/src/vs/workbench/contrib/output/browser/outputServices.ts index cb0a48affe3..0093c7efe79 100644 --- a/src/vs/workbench/contrib/output/browser/outputServices.ts +++ b/src/vs/workbench/contrib/output/browser/outputServices.ts @@ -21,7 +21,6 @@ import { IViewsService } from '../../../services/views/common/viewsService.js'; import { OutputViewPane } from './outputView.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDefaultLogLevelsService } from '../../logs/common/defaultLogLevels.js'; import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { localize } from '../../../../nls.js'; @@ -30,6 +29,7 @@ import { VSBuffer } from '../../../../base/common/buffer.js'; import { telemetryLogId } from '../../../../platform/telemetry/common/telemetryUtils.js'; import { toLocalISOString } from '../../../../base/common/date.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IDefaultLogLevelsService } from '../../../services/log/common/defaultLogLevels.js'; const OUTPUT_ACTIVE_CHANNEL_KEY = 'output.activechannel'; @@ -132,15 +132,91 @@ class OutputViewFilters extends Disposable implements IOutputViewFilters { filterHistory: string[]; private _filterText = ''; + private _includePatterns: string[] = []; + private _excludePatterns: string[] = []; get text(): string { return this._filterText; } set text(filterText: string) { if (this._filterText !== filterText) { this._filterText = filterText; + const { includePatterns, excludePatterns } = this.parseText(filterText); + this._includePatterns = includePatterns; + this._excludePatterns = excludePatterns; this._onDidChange.fire(); } } + private parseText(filterText: string): { includePatterns: string[]; excludePatterns: string[] } { + const includePatterns: string[] = []; + const excludePatterns: string[] = []; + + // Parse patterns respecting quoted strings + const patterns = this.splitByCommaRespectingQuotes(filterText); + + for (const pattern of patterns) { + const trimmed = pattern.trim(); + if (trimmed.length === 0) { + continue; + } + + if (trimmed.startsWith('!')) { + // Negative filter - remove the ! prefix + const negativePattern = trimmed.substring(1).trim(); + if (negativePattern.length > 0) { + excludePatterns.push(negativePattern); + } + } else { + includePatterns.push(trimmed); + } + } + + return { includePatterns, excludePatterns }; + } + + get includePatterns(): string[] { + return this._includePatterns; + } + + get excludePatterns(): string[] { + return this._excludePatterns; + } + + private splitByCommaRespectingQuotes(text: string): string[] { + const patterns: string[] = []; + let current = ''; + let inQuotes = false; + let quoteChar = ''; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + + if (!inQuotes && (char === '"')) { + // Start of quoted string + inQuotes = true; + quoteChar = char; + current += char; + } else if (inQuotes && char === quoteChar) { + // End of quoted string + inQuotes = false; + current += char; + } else if (!inQuotes && char === ',') { + // Comma outside quotes - split here + if (current.length > 0) { + patterns.push(current); + } + current = ''; + } else { + current += char; + } + } + + // Add the last pattern + if (current.length > 0) { + patterns.push(current); + } + + return patterns; + } private readonly _trace: IContextKey; get trace(): boolean { @@ -299,7 +375,6 @@ export class OutputService extends Disposable implements IOutputService, ITextMo })); this._register(this.loggerService.onDidChangeLogLevel(() => { - this.resetLogLevelFilters(); this.setLevelContext(); this.setLevelIsDefaultContext(); })); @@ -520,18 +595,6 @@ export class OutputService extends Disposable implements IOutputService, ITextMo return this.instantiationService.createInstance(OutputChannel, channelData, this.outputLocation, this.outputFolderCreationPromise); } - private resetLogLevelFilters(): void { - const descriptor = this.activeChannel?.outputChannelDescriptor; - const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; - if (channelLogLevel !== undefined) { - this.filters.error = channelLogLevel <= LogLevel.Error; - this.filters.warning = channelLogLevel <= LogLevel.Warning; - this.filters.info = channelLogLevel <= LogLevel.Info; - this.filters.debug = channelLogLevel <= LogLevel.Debug; - this.filters.trace = channelLogLevel <= LogLevel.Trace; - } - } - private setLevelContext(): void { const descriptor = this.activeChannel?.outputChannelDescriptor; const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; @@ -542,7 +605,7 @@ export class OutputService extends Disposable implements IOutputService, ITextMo const descriptor = this.activeChannel?.outputChannelDescriptor; const channelLogLevel = descriptor ? this.getLogLevel(descriptor) : undefined; if (channelLogLevel !== undefined) { - const channelDefaultLogLevel = await this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId); + const channelDefaultLogLevel = this.defaultLogLevelsService.getDefaultLogLevel(descriptor?.extensionId); this.activeOutputChannelLevelIsDefaultContext.set(channelDefaultLogLevel === channelLogLevel); } else { this.activeOutputChannelLevelIsDefaultContext.set(false); diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 9cb7c8962ee..c3b2b8c245b 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -92,7 +92,7 @@ export class OutputViewPane extends FilterViewPane { super({ ...options, filterOptions: { - placeholder: localize('outputView.filter.placeholder', "Filter"), + placeholder: localize('outputView.filter.placeholder', "Filter (e.g. text, !excludeText, text1,text2)"), focusContextKey: OUTPUT_FILTER_FOCUS_CONTEXT.key, text: viewState.filter || '', history: [] @@ -268,6 +268,7 @@ export class OutputEditor extends AbstractTextResourceEditor { options.padding = undefined; options.readOnly = true; options.domReadOnly = true; + options.roundedSelection = false; options.unicodeHighlight = { nonBasicASCII: false, invisibleCharacters: false, @@ -443,6 +444,38 @@ export class FilterController extends Disposable implements IEditorContribution } } + private shouldShowLine(model: ITextModel, range: Range, positive: string[], negative: string[]): { show: boolean; matches: IModelDeltaDecoration[] } { + const matches: IModelDeltaDecoration[] = []; + + // Check negative filters first - if any match, hide the line + if (negative.length > 0) { + for (const pattern of negative) { + const negativeMatches = model.findMatches(pattern, range, false, false, null, false); + if (negativeMatches.length > 0) { + return { show: false, matches: [] }; + } + } + } + + // If there are positive filters, at least one must match + if (positive.length > 0) { + let hasPositiveMatch = false; + for (const pattern of positive) { + const positiveMatches = model.findMatches(pattern, range, false, false, null, false); + if (positiveMatches.length > 0) { + hasPositiveMatch = true; + for (const match of positiveMatches) { + matches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); + } + } + } + return { show: hasPositiveMatch, matches }; + } + + // No positive filters means show everything (that passed negative filters) + return { show: true, matches }; + } + private compute(model: ITextModel, fromLineNumber: number): { findMatches: IModelDeltaDecoration[]; hiddenAreas: Range[]; categories: Map } { const filters = this.outputService.filters; const activeChannel = this.outputService.getActiveChannel(); @@ -472,12 +505,10 @@ export class FilterController extends Disposable implements IEditorContribution hiddenAreas.push(entry.range); continue; } - if (filters.text) { - const matches = model.findMatches(filters.text, entry.range, false, false, null, false); - if (matches.length) { - for (const match of matches) { - findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); - } + if (filters.includePatterns.length > 0 || filters.excludePatterns.length > 0) { + const result = this.shouldShowLine(model, entry.range, filters.includePatterns, filters.excludePatterns); + if (result.show) { + findMatches.push(...result.matches); } else { hiddenAreas.push(entry.range); } @@ -486,18 +517,16 @@ export class FilterController extends Disposable implements IEditorContribution return { findMatches, hiddenAreas, categories }; } - if (!filters.text) { + if (filters.includePatterns.length === 0 && filters.excludePatterns.length === 0) { return { findMatches, hiddenAreas, categories }; } const lineCount = model.getLineCount(); for (let lineNumber = fromLineNumber; lineNumber <= lineCount; lineNumber++) { const lineRange = new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)); - const matches = model.findMatches(filters.text, lineRange, false, false, null, false); - if (matches.length) { - for (const match of matches) { - findMatches.push({ range: match.range, options: FindDecorations._FIND_MATCH_DECORATION }); - } + const result = this.shouldShowLine(model, lineRange, filters.includePatterns, filters.excludePatterns); + if (result.show) { + findMatches.push(...result.matches); } else { hiddenAreas.push(lineRange); } diff --git a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index 80676ac2b56..b2a4b030a6e 100644 --- a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts @@ -7,6 +7,7 @@ import { inputLatency } from '../../../../base/browser/performance.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -16,6 +17,7 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib private readonly _scheduler: RunOnceScheduler; constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, @IEditorService private readonly _editorService: IEditorService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { @@ -31,8 +33,9 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib }, 60000)); - // Only log 1% of users selected randomly to reduce the volume of data - if (Math.random() <= 0.01) { + // Only log 1% of users selected randomly to reduce the volume of data, always report if GPU + // acceleration is enabled as it's opt-in + if (Math.random() <= 0.01 || this._configurationService.getValue('editor.experimentalGpuAcceleration') === 'on') { this._setupListener(); } @@ -64,16 +67,20 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib render: InputLatencyStatisticFragment; total: InputLatencyStatisticFragment; sampleCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of samples measured.' }; + gpuAcceleration: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether GPU acceleration was enabled at the time the event was reported.' }; }; - type PerformanceInputLatencyEvent = inputLatency.IInputLatencyMeasurements; + type PerformanceInputLatencyEvent = inputLatency.IInputLatencyMeasurements & { + gpuAcceleration: boolean; + }; this._telemetryService.publicLog2('performance.inputLatency', { keydown: measurements.keydown, input: measurements.input, render: measurements.render, total: measurements.total, - sampleCount: measurements.sampleCount + sampleCount: measurements.sampleCount, + gpuAcceleration: this._configurationService.getValue('editor.experimentalGpuAcceleration') === 'on' }); } } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 076761078d5..759f22b3926 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -23,7 +23,7 @@ import { KeybindingsEditorModel, KEYBINDING_ENTRY_TEMPLATE_ID } from '../../../s import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService, IUserFriendlyKeybinding } from '../../../../platform/keybinding/common/keybinding.js'; import { DefineKeybindingWidget, KeybindingsSearchWidget } from './keybindingWidgets.js'; -import { CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, CONTEXT_WHEN_FOCUS } from '../common/preferences.js'; +import { CONTEXT_KEYBINDING_FOCUS, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, CONTEXT_WHEN_FOCUS } from '../common/preferences.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IKeybindingEditingService } from '../../../services/keybinding/common/keybindingEditing.js'; import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js'; @@ -108,6 +108,7 @@ export class KeybindingsEditor extends EditorPane imp private keybindingsEditorContextKey: IContextKey; private keybindingFocusContextKey: IContextKey; private searchFocusContextKey: IContextKey; + private searchHasValueContextKey: IContextKey; private readonly sortByPrecedenceAction: Action; private readonly recordKeysAction: Action; @@ -138,6 +139,7 @@ export class KeybindingsEditor extends EditorPane imp this.keybindingsEditorContextKey = CONTEXT_KEYBINDINGS_EDITOR.bindTo(this.contextKeyService); this.searchFocusContextKey = CONTEXT_KEYBINDINGS_SEARCH_FOCUS.bindTo(this.contextKeyService); this.keybindingFocusContextKey = CONTEXT_KEYBINDING_FOCUS.bindTo(this.contextKeyService); + this.searchHasValueContextKey = CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE.bindTo(this.contextKeyService); this.searchHistoryDelayer = new Delayer(500); this.recordKeysAction = this._register(new Action(KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, localize('recordKeysLabel', "Record Keys"), ThemeIcon.asClassName(keybindingsRecordKeysIcon))); @@ -321,6 +323,7 @@ export class KeybindingsEditor extends EditorPane imp clearSearchResults(): void { this.searchWidget.clear(); + this.searchHasValueContextKey.set(false); } showSimilarKeybindings(keybindingEntry: IKeybindingItemEntry): void { @@ -375,7 +378,9 @@ export class KeybindingsEditor extends EditorPane imp }) })); this._register(this.searchWidget.onDidChange(searchValue => { - clearInputAction.enabled = !!searchValue; + const hasValue = !!searchValue; + clearInputAction.enabled = hasValue; + this.searchHasValueContextKey.set(hasValue); this.delayedFiltering.trigger(() => this.filterKeybindings()); this.updateSearchOptions(); })); @@ -821,7 +826,7 @@ export class KeybindingsEditor extends EditorPane imp }; } - private onKeybindingEditingError(error: any): void { + private onKeybindingEditingError(error: unknown): void { this.notificationService.error(typeof error === 'string' ? error : localize('error', "Error '{0}' while editing the keybinding. Please open 'keybindings.json' file and check for errors.", `${error}`)); } } @@ -881,23 +886,21 @@ class ActionsColumnRenderer implements ITableRenderer{ class: ThemeIcon.asClassName(keybindingsEditIcon), enabled: true, id: 'editKeybinding', - tooltip: keybinding ? localize('editKeybindingLabelWithKey', "Change Keybinding {0}", `(${keybinding.getLabel()})`) : localize('editKeybindingLabel', "Change Keybinding"), + tooltip: this.keybindingsService.appendKeybinding(localize('editKeybindingLabel', "Change Keybinding"), KEYBINDINGS_EDITOR_COMMAND_DEFINE), run: () => this.keybindingsEditor.defineKeybinding(keybindingItemEntry, false) }; } private createAddAction(keybindingItemEntry: IKeybindingItemEntry): IAction { - const keybinding = this.keybindingsService.lookupKeybinding(KEYBINDINGS_EDITOR_COMMAND_DEFINE); return { class: ThemeIcon.asClassName(keybindingsAddIcon), enabled: true, id: 'addKeybinding', - tooltip: keybinding ? localize('addKeybindingLabelWithKey', "Add Keybinding {0}", `(${keybinding.getLabel()})`) : localize('addKeybindingLabel', "Add Keybinding"), + tooltip: this.keybindingsService.appendKeybinding(localize('addKeybindingLabel', "Add Keybinding"), KEYBINDINGS_EDITOR_COMMAND_DEFINE), run: () => this.keybindingsEditor.defineKeybinding(keybindingItemEntry, false) }; } diff --git a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css index 7fbef9032db..10786988808 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css +++ b/src/vs/workbench/contrib/preferences/browser/media/keybindingsEditor.css @@ -232,6 +232,7 @@ .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table.focused .monaco-list-row.selected .monaco-table-tr .monaco-table-td .monaco-keybinding-key { color: var(--vscode-list-activeSelectionForeground); + border-color: var(--vscode-widget-shadow) !important; } .keybindings-editor > .keybindings-body > .keybindings-table-container .monaco-table .monaco-list-row:hover:not(.selected):not(.focused) .monaco-table-tr .monaco-table-td .monaco-keybinding-key { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index ed0beac4ec2..9bd44961577 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -609,6 +609,7 @@ background-color: var(--vscode-textPreformat-background); padding: 1px 3px; border-radius: 4px; + border: 1px solid var(--vscode-textPreformat-border); } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown .monaco-tokenized-source { diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css index cebb3bdd089..946683f7f4c 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsWidgets.css @@ -38,6 +38,7 @@ .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-item-bool .setting-list-object-value { width: 100%; cursor: pointer; + flex: 1; } .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-object-widget .setting-list-object-key { @@ -180,7 +181,6 @@ .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .monaco-text-button { width: initial; white-space: nowrap; - padding: 4px 14px; } .settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-item-control.setting-list-hide-add-button .setting-list-new-row { @@ -385,3 +385,7 @@ .codicon-settings-edit:hover { cursor: pointer; } + +.settings-editor > .settings-body .settings-tree-container .setting-item.setting-item-list .setting-list-row.invalid-key .setting-list-object-key { + color: var(--vscode-list-errorForeground); +} diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index ccf0c627bc1..dc4ed6e1f88 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -43,7 +43,7 @@ import { PreferencesEditorInput, SettingsEditor2Input } from '../../../services/ import { SettingsEditorModel } from '../../../services/preferences/common/preferencesModels.js'; import { CURRENT_PROFILE_CONTEXT, IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; import { ExplorerFolderContext, ExplorerRootContext } from '../../files/common/files.js'; -import { CONTEXT_AI_SETTING_RESULTS_AVAILABLE, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ACCEPT_WHEN, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REJECT_WHEN, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH } from '../common/preferences.js'; +import { CONTEXT_AI_SETTING_RESULTS_AVAILABLE, CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE, CONTEXT_KEYBINDING_FOCUS, CONTEXT_SETTINGS_EDITOR, CONTEXT_SETTINGS_JSON_EDITOR, CONTEXT_SETTINGS_ROW_FOCUS, CONTEXT_SETTINGS_SEARCH_FOCUS, CONTEXT_TOC_ROW_FOCUS, CONTEXT_WHEN_FOCUS, KEYBINDINGS_EDITOR_COMMAND_ACCEPT_WHEN, KEYBINDINGS_EDITOR_COMMAND_ADD, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_HISTORY, KEYBINDINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, KEYBINDINGS_EDITOR_COMMAND_COPY, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND, KEYBINDINGS_EDITOR_COMMAND_COPY_COMMAND_TITLE, KEYBINDINGS_EDITOR_COMMAND_DEFINE, KEYBINDINGS_EDITOR_COMMAND_DEFINE_WHEN, KEYBINDINGS_EDITOR_COMMAND_FOCUS_KEYBINDINGS, KEYBINDINGS_EDITOR_COMMAND_RECORD_SEARCH_KEYS, KEYBINDINGS_EDITOR_COMMAND_REJECT_WHEN, KEYBINDINGS_EDITOR_COMMAND_REMOVE, KEYBINDINGS_EDITOR_COMMAND_RESET, KEYBINDINGS_EDITOR_COMMAND_SEARCH, KEYBINDINGS_EDITOR_COMMAND_SHOW_SIMILAR, KEYBINDINGS_EDITOR_COMMAND_SORTBY_PRECEDENCE, KEYBINDINGS_EDITOR_SHOW_DEFAULT_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_EXTENSION_KEYBINDINGS, KEYBINDINGS_EDITOR_SHOW_USER_KEYBINDINGS, REQUIRE_TRUSTED_WORKSPACE_SETTING_TAG, SETTINGS_EDITOR_COMMAND_CLEAR_SEARCH_RESULTS, SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU, SETTINGS_EDITOR_COMMAND_TOGGLE_AI_SEARCH } from '../common/preferences.js'; import { PreferencesContribution } from '../common/preferencesContribution.js'; import { KeybindingsEditor } from './keybindingsEditor.js'; import { ConfigureLanguageBasedSettingsAction } from './preferencesActions.js'; @@ -165,11 +165,11 @@ interface IOpenSettingsActionOptions { focusSearch?: boolean; } -function sanitizeBoolean(arg: any): boolean | undefined { +function sanitizeBoolean(arg: unknown): boolean | undefined { return isBoolean(arg) ? arg : undefined; } -function sanitizeString(arg: any): string | undefined { +function sanitizeString(arg: unknown): string | undefined { return isString(arg) ? arg : undefined; } @@ -652,7 +652,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor, args: any): void { + run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); preferencesEditor?.focusSettings(); } @@ -672,7 +672,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor, args: any): void { + run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); preferencesEditor?.focusSettings(); } @@ -965,7 +965,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon title: nls.localize('clear', "Clear Search Results"), keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), + when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS, CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE), primary: KeyCode.Escape, } }); @@ -1011,7 +1011,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS, CONTEXT_WHEN_FOCUS.toNegated()), primary: KeyCode.Enter, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.defineKeybinding(editorPane.activeKeybindingEntry!, false); @@ -1024,7 +1024,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyA), - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.defineKeybinding(editorPane.activeKeybindingEntry!, true); @@ -1037,7 +1037,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyE), - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor && editorPane.activeKeybindingEntry!.keybindingItem.keybinding) { editorPane.defineWhenExpression(editorPane.activeKeybindingEntry!); @@ -1053,7 +1053,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon mac: { primary: KeyMod.CtrlCmd | KeyCode.Backspace }, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.removeKeybinding(editorPane.activeKeybindingEntry!); @@ -1066,7 +1066,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: 0, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.resetKeybinding(editorPane.activeKeybindingEntry!); @@ -1079,7 +1079,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), primary: KeyMod.CtrlCmd | KeyCode.KeyF, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.focusSearch(); @@ -1093,7 +1093,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), primary: KeyMod.Alt | KeyCode.KeyK, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyK }, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.recordSearchKeys(); @@ -1107,7 +1107,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR), primary: KeyMod.Alt | KeyCode.KeyP, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyP }, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.toggleSortByPrecedence(); @@ -1120,7 +1120,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: 0, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.showSimilarKeybindings(editorPane.activeKeybindingEntry!); @@ -1133,7 +1133,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS, CONTEXT_WHEN_FOCUS.negate()), primary: KeyMod.CtrlCmd | KeyCode.KeyC, - handler: async (accessor, args: any) => { + handler: async (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { await editorPane.copyKeybinding(editorPane.activeKeybindingEntry!); @@ -1146,7 +1146,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: 0, - handler: async (accessor, args: any) => { + handler: async (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { await editorPane.copyKeybindingCommand(editorPane.activeKeybindingEntry!); @@ -1159,7 +1159,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDING_FOCUS), primary: 0, - handler: async (accessor, args: any) => { + handler: async (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { await editorPane.copyKeybindingCommandTitle(editorPane.activeKeybindingEntry!); @@ -1172,7 +1172,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_KEYBINDINGS_SEARCH_FOCUS), primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - handler: (accessor, args: any) => { + handler: (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.focusKeybindings(); @@ -1185,7 +1185,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_WHEN_FOCUS, SuggestContext.Visible.toNegated()), primary: KeyCode.Escape, - handler: async (accessor, args: any) => { + handler: async (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.rejectWhenExpression(editorPane.activeKeybindingEntry!); @@ -1198,7 +1198,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon weight: KeybindingWeight.WorkbenchContrib, when: ContextKeyExpr.and(CONTEXT_KEYBINDINGS_EDITOR, CONTEXT_WHEN_FOCUS, SuggestContext.Visible.toNegated()), primary: KeyCode.Enter, - handler: async (accessor, args: any) => { + handler: async (accessor, args: unknown) => { const editorPane = accessor.get(IEditorService).activeEditorPane; if (editorPane instanceof KeybindingsEditor) { editorPane.acceptWhenExpression(editorPane.activeKeybindingEntry!); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts index 9bea259deaa..ce67c88172a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesIcons.ts @@ -7,7 +7,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -export const settingsScopeDropDownIcon = registerIcon('settings-folder-dropdown', Codicon.triangleDown, localize('settingsScopeDropDownIcon', 'Icon for the folder dropdown button in the split JSON Settings editor.')); +export const settingsScopeDropDownIcon = registerIcon('settings-folder-dropdown', Codicon.chevronDown, localize('settingsScopeDropDownIcon', 'Icon for the folder dropdown button in the split JSON Settings editor.')); export const settingsMoreActionIcon = registerIcon('settings-more-action', Codicon.gear, localize('settingsMoreActionIcon', 'Icon for the \'more actions\' action in the Settings UI.')); export const keybindingsRecordKeysIcon = registerIcon('keybindings-record-keys', Codicon.recordKeys, localize('keybindingsRecordKeysIcon', 'Icon for the \'record keys\' action in the keybinding UI.')); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts index d52dbd34301..7fa5fd339a9 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesRenderers.ts @@ -51,7 +51,7 @@ import { McpCommandIds } from '../../mcp/common/mcpCommandIds.js'; export interface IPreferencesRenderer extends IDisposable { render(): void; - updatePreference(key: string, value: any, source: ISetting): void; + updatePreference(key: string, value: unknown, source: ISetting): void; focusPreference(setting: ISetting): void; clearFocus(setting: ISetting): void; editPreference(setting: ISetting): boolean; @@ -87,7 +87,7 @@ export class UserSettingsRenderer extends Disposable implements IPreferencesRend this.mcpSettingsRenderer.render(); } - updatePreference(key: string, value: any, source: IIndexedSetting): void { + updatePreference(key: string, value: unknown, source: IIndexedSetting): void { const overrideIdentifiers = source.overrideOf ? overrideIdentifiersFromKey(source.overrideOf.key) : null; const resource = this.preferencesModel.uri; this.configurationService.updateValue(key, value, { overrideIdentifiers, resource }, this.preferencesModel.configurationTarget) @@ -181,8 +181,8 @@ class EditSettingRenderer extends Disposable { associatedPreferencesModel!: IPreferencesEditorModel; private toggleEditPreferencesForMouseMoveDelayer: Delayer; - private readonly _onUpdateSetting: Emitter<{ key: string; value: any; source: IIndexedSetting }> = this._register(new Emitter<{ key: string; value: any; source: IIndexedSetting }>()); - readonly onUpdateSetting: Event<{ key: string; value: any; source: IIndexedSetting }> = this._onUpdateSetting.event; + private readonly _onUpdateSetting: Emitter<{ key: string; value: unknown; source: IIndexedSetting }> = this._register(new Emitter<{ key: string; value: unknown; source: IIndexedSetting }>()); + readonly onUpdateSetting: Event<{ key: string; value: unknown; source: IIndexedSetting }> = this._onUpdateSetting.event; constructor(private editor: ICodeEditor, private primarySettingsModel: ISettingsEditorModel, private settingHighlighter: SettingHighlighter, @@ -447,7 +447,7 @@ class EditSettingRenderer extends Disposable { return []; } - private updateSetting(key: string, value: any, source: IIndexedSetting): void { + private updateSetting(key: string, value: unknown, source: IIndexedSetting): void { this._onUpdateSetting.fire({ key, value, source }); } } diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 4a38b9bb227..3721b873139 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -6,21 +6,18 @@ import { distinct } from '../../../../base/common/arrays.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; -import { IMatch, matchesContiguousSubString, matchesSubString, matchesWords } from '../../../../base/common/filters.js'; +import { IMatch, matchesBaseContiguousSubString, matchesContiguousSubString, matchesSubString, matchesWords } from '../../../../base/common/filters.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import * as strings from '../../../../base/common/strings.js'; import { TfIdfCalculator, TfIdfDocument } from '../../../../base/common/tfIdf.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; -import { IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IGroupFilter, ISearchResult, ISetting, ISettingMatch, ISettingMatcher, ISettingsEditorModel, ISettingsGroup, SettingKeyMatchTypes, SettingMatchType } from '../../../services/preferences/common/preferences.js'; import { nullRange } from '../../../services/preferences/common/preferencesModels.js'; -import { EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME, EMBEDDINGS_SEARCH_PROVIDER_NAME, IAiSearchProvider, IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration, LLM_RANKED_SEARCH_PROVIDER_NAME, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME } from '../common/preferences.js'; +import { EMBEDDINGS_SEARCH_PROVIDER_NAME, IAiSearchProvider, IPreferencesSearchService, IRemoteSearchProvider, ISearchProvider, IWorkbenchSettingsConfiguration, LLM_RANKED_SEARCH_PROVIDER_NAME, STRING_MATCH_SEARCH_PROVIDER_NAME, TF_IDF_SEARCH_PROVIDER_NAME } from '../common/preferences.js'; export interface IEndpointDetails { urlBase?: string; @@ -30,27 +27,14 @@ export interface IEndpointDetails { export class PreferencesSearchService extends Disposable implements IPreferencesSearchService { declare readonly _serviceBrand: undefined; - // @ts-expect-error disable remote search for now, ref https://github.com/microsoft/vscode/issues/172411 - private _installedExtensions: Promise; private _remoteSearchProvider: IRemoteSearchProvider | undefined; private _aiSearchProvider: IAiSearchProvider | undefined; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService ) { super(); - - // This request goes to the shared process but results won't change during a window's lifetime, so cache the results. - this._installedExtensions = this.extensionManagementService.getInstalled(ExtensionType.User).then(exts => { - // Filter to enabled extensions that have settings - return exts - .filter(ext => this.extensionEnablementService.isEnabled(ext)) - .filter(ext => ext.manifest && ext.manifest.contributes && ext.manifest.contributes.configuration) - .filter(ext => !!ext.identifier.uuid); - }); } getLocalSearchProvider(filter: string): LocalSearchProvider { @@ -261,7 +245,7 @@ export class SettingMatches { for (const word of queryWords) { // Search the description lines. for (let lineIndex = 0; lineIndex < setting.description.length; lineIndex++) { - const descriptionMatches = matchesContiguousSubString(word, setting.description[lineIndex]); + const descriptionMatches = matchesBaseContiguousSubString(word, setting.description[lineIndex]); if (descriptionMatches?.length) { descriptionMatchingWords.set(word, descriptionMatches.map(match => this.toDescriptionRange(setting, match, lineIndex))); } @@ -409,8 +393,7 @@ class EmbeddingsSearchProvider implements IRemoteSearchProvider { private _filter: string = ''; constructor( - private readonly _aiSettingsSearchService: IAiSettingsSearchService, - private readonly _excludeSelectionStep: boolean + private readonly _aiSettingsSearchService: IAiSettingsSearchService ) { this._recordProvider = new SettingsRecordProvider(); } @@ -425,7 +408,7 @@ class EmbeddingsSearchProvider implements IRemoteSearchProvider { } this._recordProvider.updateModel(preferencesModel); - this._aiSettingsSearchService.startSearch(this._filter, this._excludeSelectionStep, token); + this._aiSettingsSearchService.startSearch(this._filter, token); return { filterMatches: await this.getEmbeddingsItems(token), @@ -441,7 +424,7 @@ class EmbeddingsSearchProvider implements IRemoteSearchProvider { return []; } - const providerName = this._excludeSelectionStep ? EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME : EMBEDDINGS_SEARCH_PROVIDER_NAME; + const providerName = EMBEDDINGS_SEARCH_PROVIDER_NAME; for (const settingKey of settings) { if (filterMatches.length === EmbeddingsSearchProvider.EMBEDDINGS_SETTINGS_SEARCH_MAX_PICKS) { break; @@ -589,7 +572,7 @@ class AiSearchProvider implements IAiSearchProvider { constructor( @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { - this._embeddingsSearchProvider = new EmbeddingsSearchProvider(this.aiSettingsSearchService, false); + this._embeddingsSearchProvider = new EmbeddingsSearchProvider(this.aiSettingsSearchService); this._recordProvider = new SettingsRecordProvider(); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 086580f3db6..db818b43764 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -55,7 +55,7 @@ import { IChatEntitlementService } from '../../../services/chat/common/chatEntit import { APPLICATION_SCOPES, IWorkbenchConfigurationService } from '../../../services/configuration/common/configuration.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; +import { ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING, IOpenSettingsOptions, IPreferencesService, ISearchResult, ISetting, ISettingsEditorModel, ISettingsEditorOptions, ISettingsGroup, SettingMatchType, SettingValueType, validateSettingsEditorOptions } from '../../../services/preferences/common/preferences.js'; import { SettingsEditor2Input } from '../../../services/preferences/common/preferencesEditorInput.js'; import { nullRange, Settings2EditorModel } from '../../../services/preferences/common/preferencesModels.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; @@ -204,7 +204,7 @@ export class SettingsEditor2 extends EditorPane { private settingFastUpdateDelayer: Delayer; private settingSlowUpdateDelayer: Delayer; - private pendingSettingUpdate: { key: string; value: any; languageFilter: string | undefined } | null = null; + private pendingSettingUpdate: { key: string; value: unknown; languageFilter: string | undefined } | null = null; private readonly viewState: ISettingsEditorViewState; private readonly _searchResultModel = this._register(new MutableDisposable()); @@ -299,6 +299,9 @@ export class SettingsEditor2 extends EditorPane { || e.affectedKeys.has(WorkbenchSettingsEditorSettings.EnableNaturalLanguageSearch)) { this.updateAiSearchToggleVisibility(); } + if (e.affectsConfiguration(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING)) { + this.onConfigUpdate(undefined, true, true); + } if (e.source !== ConfigurationTarget.DEFAULT) { this.onConfigUpdate(e.affectedKeys); } @@ -352,6 +355,9 @@ export class SettingsEditor2 extends EditorPane { } private canShowAdvancedSettings(): boolean { + if (this.configurationService.getValue(ALWAYS_SHOW_ADVANCED_SETTINGS_SETTING) ?? false) { + return true; + } return this.viewState.tagFilters?.has(ADVANCED_SETTING_TAG) ?? false; } @@ -360,7 +366,8 @@ export class SettingsEditor2 extends EditorPane { * Returns true if: * - The setting is not tagged as advanced, OR * - The setting matches an ID filter (@id:settingKey), OR - * - The setting key appears in the search query + * - The setting key appears in the search query, OR + * - The @hasPolicy filter is active (policy settings should always be shown when filtering by policy) */ private shouldShowSetting(setting: ISetting): boolean { if (!setting.tags?.includes(ADVANCED_SETTING_TAG)) { @@ -372,6 +379,9 @@ export class SettingsEditor2 extends EditorPane { if (this.viewState.query?.toLowerCase().includes(setting.key.toLowerCase())) { return true; } + if (this.viewState.tagFilters?.has(POLICY_SETTING_TAG)) { + return true; + } return false; } @@ -740,7 +750,7 @@ export class SettingsEditor2 extends EditorPane { return `@${EXTENSION_SETTING_TAG}${extensionId} `; }).sort(); return installedExtensionsTags.filter(extFilter => !query.includes(extFilter)); - } else if (queryParts[queryParts.length - 1].startsWith('@')) { + } else if (query === '' || queryParts[queryParts.length - 1].startsWith('@')) { return SettingsEditor2.SUGGESTIONS.filter(tag => !query.includes(tag)).map(tag => tag.endsWith(':') ? tag : tag + ' '); } return []; @@ -1049,8 +1059,46 @@ export class SettingsEditor2 extends EditorPane { this.settingsTree.scrollTop = 0; } } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { - this.settingsTree.reveal(element, 0); - this.settingsTree.setFocus([element]); + let targetElement = element; + // Searches equvalent old Object currently living in the Tree nodes. + if (!this.settingsTree.hasElement(targetElement)) { + if (element instanceof SettingsTreeGroupElement) { + const targetId = element.id; + + const findInViewNodes = (nodes: any[]): SettingsTreeGroupElement | undefined => { + for (const node of nodes) { + if (node.element instanceof SettingsTreeGroupElement && node.element.id === targetId) { + return node.element; + } + if (node.children && node.children.length > 0) { + const found = findInViewNodes(node.children); + if (found) { + return found; + } + } + } + return undefined; + }; + + try { + const rootNode = this.settingsTree.getNode(null); + if (rootNode && rootNode.children) { + const foundOldElement = findInViewNodes(rootNode.children); + if (foundOldElement) { + // Now we don't reveal the New Object, reveal the Old Object" + targetElement = foundOldElement; + } + } + } catch (err) { + // Tree might be in an invalid state, ignore + } + } + } + + if (this.settingsTree.hasElement(targetElement)) { + this.settingsTree.reveal(targetElement, 0); + this.settingsTree.setFocus([targetElement]); + } } })); @@ -1180,7 +1228,7 @@ export class SettingsEditor2 extends EditorPane { })); } - private onDidChangeSetting(key: string, value: any, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void { + private onDidChangeSetting(key: string, value: unknown, type: SettingValueType | SettingValueType[], manualReset: boolean, scope: ConfigurationScope | undefined): void { const parsedQuery = parseQuery(this.searchWidget.getValue()); const languageFilter = parsedQuery.languageFilter; if (manualReset || (this.pendingSettingUpdate && this.pendingSettingUpdate.key !== key)) { @@ -1248,7 +1296,7 @@ export class SettingsEditor2 extends EditorPane { } private getAncestors(element: SettingsTreeElement): SettingsTreeElement[] { - const ancestors: any[] = []; + const ancestors: SettingsTreeElement[] = []; while (element.parent) { if (element.parent.id !== 'root') { @@ -1261,7 +1309,7 @@ export class SettingsEditor2 extends EditorPane { return ancestors.reverse(); } - private updateChangedSetting(key: string, value: any, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise { + private updateChangedSetting(key: string, value: unknown, manualReset: boolean, languageFilter: string | undefined, scope: ConfigurationScope | undefined): Promise { // ConfigurationService displays the error if this fails. // Force a render afterwards because onDidConfigurationUpdate doesn't fire if the update doesn't result in an effective setting value change. const settingsTarget = this.settingsTargetsWidget.settingsTarget; @@ -1416,7 +1464,7 @@ export class SettingsEditor2 extends EditorPane { this.settingsOrderByTocIndex = this.createSettingsOrderByTocIndex(resolvedSettingsRoot); } - private async onConfigUpdate(keys?: ReadonlySet, forceRefresh = false, schemaChange = false): Promise { + private async onConfigUpdate(keys?: ReadonlySet, forceRefresh = false, triggerSearch = false): Promise { if (keys && this.settingsTreeModel) { return this.updateElementsByKey(keys); } @@ -1549,9 +1597,7 @@ export class SettingsEditor2 extends EditorPane { resolvedSettingsRoot.children!.push(await createTocTreeForExtensionSettings(this.extensionService, extensionSettingsGroups, filter)); - const commonlyUsedDataToUse = getCommonlyUsedData(toggleData); - const commonlyUsed = resolveSettingsTree(commonlyUsedDataToUse, groups, undefined, this.logService); - resolvedSettingsRoot.children!.unshift(commonlyUsed.tree); + resolvedSettingsRoot.children!.unshift(getCommonlyUsedData(groups, toggleData?.commonlyUsed)); if (toggleData && setAdditionalGroups) { // Add the additional groups to the model to help with searching. @@ -1571,16 +1617,64 @@ export class SettingsEditor2 extends EditorPane { this.searchResultModel?.updateChildren(); + const firstVisibleElement = this.settingsTree.firstVisibleElement; + let anchorId: string | undefined; + + if (firstVisibleElement instanceof SettingsTreeSettingElement) { + anchorId = firstVisibleElement.setting.key; + } else if (firstVisibleElement instanceof SettingsTreeGroupElement) { + anchorId = firstVisibleElement.id; + } + if (this.settingsTreeModel.value) { this.refreshModels(resolvedSettingsRoot); - if (schemaChange && this.searchResultModel) { + if (triggerSearch && this.searchResultModel) { // If an extension's settings were just loaded and a search is active, retrigger the search so it shows up return await this.onSearchInputChanged(false); } this.refreshTOCTree(); this.renderTree(undefined, forceRefresh); + + if (anchorId) { + const newModel = this.settingsTreeModel.value; + let newElement: SettingsTreeElement | undefined; + + // eslint-disable-next-line no-restricted-syntax + const settings = newModel.getElementsByName(anchorId); + if (settings && settings.length > 0) { + newElement = settings[0]; + } else { + const findGroup = (roots: SettingsTreeGroupElement[]): SettingsTreeGroupElement | undefined => { + for (const g of roots) { + if (g.id === anchorId) { + return g; + } + if (g.children) { + for (const child of g.children) { + if (child instanceof SettingsTreeGroupElement) { + const found = findGroup([child]); + if (found) { + return found; + } + } + } + } + } + return undefined; + }; + newElement = findGroup([newModel.root]); + } + + if (newElement) { + try { + this.settingsTree.reveal(newElement, 0); + } catch (e) { + // Ignore the error + } + } + } } else { this.settingsTreeModel.value = this.instantiationService.createInstance(SettingsTreeModel, this.viewState, this.workspaceTrustManagementService.isWorkspaceTrusted()); this.refreshModels(resolvedSettingsRoot); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index 8b3463d3c33..47874f09ade 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -10,9 +10,11 @@ import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js' import { SimpleIconLabel } from '../../../../base/browser/ui/iconLabel/simpleIconLabel.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Emitter } from '../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IMarkdownString, MarkdownString, createMarkdownLink } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { URI } from '../../../../base/common/uri.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -65,6 +67,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { private readonly indicatorsContainerElement: HTMLElement; private readonly previewIndicator: SettingIndicator; + private readonly advancedIndicator: SettingIndicator; private readonly workspaceTrustIndicator: SettingIndicator; private readonly scopeOverridesIndicator: SettingIndicator; private readonly syncIgnoredIndicator: SettingIndicator; @@ -89,7 +92,8 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.indicatorsContainerElement.style.display = 'inline'; this.previewIndicator = this.createPreviewIndicator(); - this.isolatedIndicators = [this.previewIndicator]; + this.advancedIndicator = this.createAdvancedIndicator(); + this.isolatedIndicators = [this.previewIndicator, this.advancedIndicator]; this.workspaceTrustIndicator = this.createWorkspaceTrustIndicator(); this.scopeOverridesIndicator = this.createScopeOverridesIndicator(); @@ -223,6 +227,28 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { }; } + private createAdvancedIndicator(): SettingIndicator { + const disposables = new DisposableStore(); + const advancedIndicator = $('span.setting-indicator.setting-item-preview'); + const advancedLabel = disposables.add(new SimpleIconLabel(advancedIndicator)); + advancedLabel.text = localize('advancedLabel', "Advanced"); + + const showHover = (focus: boolean) => { + return this.hoverService.showInstantHover({ + ...this.defaultHoverOptions, + content: ADVANCED_INDICATOR_DESCRIPTION, + target: advancedIndicator + }, focus); + }; + this.addHoverDisposables(disposables, advancedIndicator, showHover); + + return { + element: advancedIndicator, + label: advancedLabel, + disposables + }; + } + private render() { this.indicatorsContainerElement.innerText = ''; this.indicatorsContainerElement.style.display = 'none'; @@ -319,15 +345,12 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { updatePreviewIndicator(element: SettingsTreeSettingElement) { const isPreviewSetting = element.tags?.has('preview'); const isExperimentalSetting = element.tags?.has('experimental'); - const isAdvancedSetting = element.tags?.has('advanced'); - this.previewIndicator.element.style.display = (isPreviewSetting || isExperimentalSetting || isAdvancedSetting) ? 'inline' : 'none'; + this.previewIndicator.element.style.display = (isPreviewSetting || isExperimentalSetting) ? 'inline' : 'none'; this.previewIndicator.label.text = isPreviewSetting ? localize('previewLabel', "Preview") : - isExperimentalSetting ? - localize('experimentalLabel', "Experimental") : - localize('advancedLabel', "Advanced"); + localize('experimentalLabel', "Experimental"); - const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : isExperimentalSetting ? EXPERIMENTAL_INDICATOR_DESCRIPTION : ADVANCED_INDICATOR_DESCRIPTION; + const content = isPreviewSetting ? PREVIEW_INDICATOR_DESCRIPTION : EXPERIMENTAL_INDICATOR_DESCRIPTION; const showHover = (focus: boolean) => { return this.hoverService.showInstantHover({ ...this.defaultHoverOptions, @@ -340,6 +363,12 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.render(); } + updateAdvancedIndicator(element: SettingsTreeSettingElement) { + const isAdvancedSetting = element.tags?.has('advanced'); + this.advancedIndicator.element.style.display = isAdvancedSetting ? 'inline' : 'none'; + this.render(); + } + private getInlineScopeDisplayText(completeScope: string): string { const [scope, language] = completeScope.split(':'); const localizedScope = scope === 'user' ? @@ -455,7 +484,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { contentMarkdownString = prefaceText; for (const scope of element.overriddenScopeList) { const scopeDisplayText = this.getInlineScopeDisplayText(scope); - contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(scope)} "${getAccessibleScopeDisplayText(scope, this.languageService)}")`; + contentMarkdownString += '\n- ' + createMarkdownLink(scopeDisplayText, SettingScopeLink.create(scope).toString(), getAccessibleScopeDisplayText(scope, this.languageService)); } } if (element.overriddenDefaultsLanguageList.length) { @@ -466,7 +495,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { contentMarkdownString += prefaceText; for (const language of element.overriddenDefaultsLanguageList) { const scopeDisplayText = this.languageService.getLanguageName(language); - contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(`default:${language}`)} "${scopeDisplayText}")`; + contentMarkdownString += '\n- ' + createMarkdownLink(scopeDisplayText ?? language, SettingScopeLink.create(`default:${language}`).toString()); } } const content: IMarkdownString = { @@ -478,7 +507,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { ...this.defaultHoverOptions, content, linkHandler: (url: string) => { - const [scope, language] = decodeURIComponent(url).split(':'); + const [scope, language] = SettingScopeLink.parse(url).split(':'); onDidClickOverrideElement.fire({ settingKey: element.setting.key, scope: scope as ScopeString, @@ -573,12 +602,14 @@ function getAccessibleScopeDisplayMidSentenceText(completeScope: string, languag export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, configurationService: IWorkbenchConfigurationService, userDataProfilesService: IUserDataProfilesService, languageService: ILanguageService): string { const ariaLabelSections: string[] = []; - // Add preview or experimental or advanced indicator text + // Add preview or experimental indicator text if (element.tags?.has('preview')) { ariaLabelSections.push(localize('previewLabel', "Preview")); } else if (element.tags?.has('experimental')) { ariaLabelSections.push(localize('experimentalLabel', "Experimental")); - } else if (element.tags?.has('advanced')) { + } + + if (element.tags?.has('advanced')) { ariaLabelSections.push(localize('advancedLabel', "Advanced")); } @@ -635,3 +666,21 @@ export function getIndicatorsLabelAriaLabel(element: SettingsTreeSettingElement, const ariaLabel = ariaLabelSections.join('. '); return ariaLabel; } + +/** + * Internal links used to open a specific scope in the settings editor + */ +namespace SettingScopeLink { + export function create(scope: string): URI { + return URI.from({ + scheme: Schemas.internal, + path: '/', + query: encodeURIComponent(scope) + }); + } + + export function parse(link: string): string { + const uri = URI.parse(link); + return decodeURIComponent(uri.query); + } +} diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 5be14514660..a2e37c2a0c0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -5,7 +5,7 @@ import { isWeb, isWindows } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; -import { ExtensionToggleData } from '../common/preferences.js'; +import { ISetting, ISettingsGroup } from '../../../services/preferences/common/preferences.js'; export interface ITOCFilter { include?: { @@ -33,6 +33,7 @@ const defaultCommonlyUsedSettings: string[] = [ 'files.autoSave', 'editor.defaultFormatter', 'editor.fontFamily', + 'chat.agent.maxRequests', 'editor.wordWrap', 'files.exclude', 'workbench.colorTheme', @@ -41,11 +42,26 @@ const defaultCommonlyUsedSettings: string[] = [ 'editor.formatOnPaste' ]; -export function getCommonlyUsedData(toggleData: ExtensionToggleData | undefined): ITOCEntry { +export function getCommonlyUsedData(settingGroups: ISettingsGroup[], commonlyUsed: string[] = defaultCommonlyUsedSettings): ITOCEntry { + const allSettings = new Map(); + for (const group of settingGroups) { + for (const section of group.sections) { + for (const s of section.settings) { + allSettings.set(s.key, s); + } + } + } + const settings: ISetting[] = []; + for (const id of commonlyUsed) { + const setting = allSettings.get(id); + if (setting) { + settings.push(setting); + } + } return { id: 'commonlyUsed', label: localize('commonlyUsed', "Commonly Used"), - settings: toggleData?.commonlyUsed ?? defaultCommonlyUsedSettings + settings }; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 8500e0c1706..8381b17a6b3 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -776,7 +776,7 @@ const SETTINGS_EXTENSION_TOGGLE_TEMPLATE_ID = 'settings.extensionToggle.template export interface ISettingChangeEvent { key: string; - value: any; // undefined => reset/unconfigure + value: unknown; // undefined => reset/unconfigure type: SettingValueType | SettingValueType[]; manualReset: boolean; scope: ConfigurationScope | undefined; @@ -890,9 +890,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre abstract renderTemplate(container: HTMLElement): any; - abstract renderElement(element: ITreeNode, index: number, templateData: any): void; + abstract renderElement(element: ITreeNode, index: number, templateData: unknown): void; - protected renderCommonTemplate(tree: any, _container: HTMLElement, typeClass: string): ISettingItemTemplate { + protected renderCommonTemplate(tree: unknown, _container: HTMLElement, typeClass: string): ISettingItemTemplate { _container.classList.add('setting-item'); _container.classList.add('setting-item-' + typeClass); @@ -963,11 +963,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } protected renderSettingToolbar(container: HTMLElement): ToolBar { - const toggleMenuKeybinding = this._keybindingService.lookupKeybinding(SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); - let toggleMenuTitle = localize('settingsContextMenuTitle', "More Actions... "); - if (toggleMenuKeybinding) { - toggleMenuTitle += ` (${toggleMenuKeybinding && toggleMenuKeybinding.getLabel()})`; - } + const toggleMenuTitle = this._keybindingService.appendKeybinding( + localize('settingsContextMenuTitle', "More Actions... "), + SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU); const toolbar = new ToolBar(container, this._contextMenuService, { toggleMenuTitle, @@ -1018,7 +1016,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre } })); - const onChange = (value: any) => this._onDidChangeSetting.fire({ + const onChange = (value: unknown) => this._onDidChangeSetting.fire({ key: element.setting.key, value, type: template.context!.valueType, @@ -1041,6 +1039,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings); template.indicatorsLabel.updateDefaultOverrideIndicator(element); template.indicatorsLabel.updatePreviewIndicator(element); + template.indicatorsLabel.updateAdvancedIndicator(element); template.elementDisposables.add(this.onDidChangeIgnoredSettings(() => { template.indicatorsLabel.updateSyncIgnored(element, this.ignoredSettings); })); @@ -1088,7 +1087,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre return renderedMarkdown.element; } - protected abstract renderValue(dataElement: SettingsTreeSettingElement, template: ISettingItemTemplate, onChange: (value: any) => void): void; + protected abstract renderValue(dataElement: SettingsTreeSettingElement, template: ISettingItemTemplate, onChange: (value: unknown) => void): void; disposeTemplate(template: IDisposableTemplate): void { template.toDispose.dispose(); @@ -1518,7 +1517,7 @@ class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITr protected renderValue(dataElement: SettingsTreeSettingElement, template: ISettingObjectItemTemplate, onChange: (value: Record | undefined) => void): void { const items = getObjectDisplayValue(dataElement); - const { key, objectProperties, objectPatternProperties, objectAdditionalProperties } = dataElement.setting; + const { key, objectProperties, objectPatternProperties, objectAdditionalProperties, propertyNames } = dataElement.setting; template.objectDropdownWidget!.setValue(items, { settingKey: key, @@ -1529,7 +1528,8 @@ class SettingObjectRenderer extends AbstractSettingObjectRenderer implements ITr ) : true, keySuggester: createObjectKeySuggester(dataElement), - valueSuggester: createObjectValueSuggester(dataElement) + valueSuggester: createObjectValueSuggester(dataElement), + propertyNames }); template.context = dataElement; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts index af28df144b9..f85311aa7f6 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTreeModels.ts @@ -492,7 +492,23 @@ export class SettingsTreeSettingElement extends SettingsTreeElement { if (!idFilters || !idFilters.size) { return true; } - return idFilters.has(this.setting.key); + + // Check for exact match first + if (idFilters.has(this.setting.key)) { + return true; + } + + // Check for wildcard patterns (ending with .*) + for (const filter of idFilters) { + if (filter.endsWith('*')) { + const prefix = filter.slice(0, -1); // Remove '*' suffix + if (this.setting.key.startsWith(prefix)) { + return true; + } + } + } + + return false; } matchesAllLanguages(languageFilter?: string): boolean { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 0222b7dd360..1322fca29ed 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -30,6 +30,8 @@ import { defaultButtonStyles, getInputBoxStyle, getSelectBoxStyles } from '../.. import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { hasNativeContextMenu } from '../../../../platform/window/common/window.js'; import { SettingValueType } from '../../../services/preferences/common/preferences.js'; +import { validatePropertyName } from '../../../services/preferences/common/preferencesValidation.js'; +import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from '../common/settingsEditorColorRegistry.js'; import './media/settingsWidgets.css'; import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from './preferencesIcons.js'; @@ -908,6 +910,7 @@ interface IObjectSetValueOptions { isReadOnly?: boolean; keySuggester?: IObjectKeySuggester; valueSuggester?: IObjectValueSuggester; + propertyNames?: IJSONSchema; } interface IObjectRenderEditWidgetOptions { @@ -924,6 +927,7 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget undefined; private valueSuggester: IObjectValueSuggester = () => undefined; + private propertyNames: IJSONSchema | undefined; constructor( container: HTMLElement, @@ -940,6 +944,7 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget('settingsTocRowF export const CONTEXT_SETTINGS_ROW_FOCUS = new RawContextKey('settingRowFocus', false); export const CONTEXT_KEYBINDINGS_EDITOR = new RawContextKey('inKeybindings', false); export const CONTEXT_KEYBINDINGS_SEARCH_FOCUS = new RawContextKey('inKeybindingsSearch', false); +export const CONTEXT_KEYBINDINGS_SEARCH_HAS_VALUE = new RawContextKey('keybindingsSearchHasValue', false); export const CONTEXT_KEYBINDING_FOCUS = new RawContextKey('keybindingFocus', false); export const CONTEXT_WHEN_FOCUS = new RawContextKey('whenFocus', false); export const CONTEXT_AI_SETTING_RESULTS_AVAILABLE = new RawContextKey('aiSettingResultsAvailable', false); @@ -117,7 +118,6 @@ export const EXTENSION_FETCH_TIMEOUT_MS = 1000; export const STRING_MATCH_SEARCH_PROVIDER_NAME = 'local'; export const TF_IDF_SEARCH_PROVIDER_NAME = 'tfIdf'; export const FILTER_MODEL_SEARCH_PROVIDER_NAME = 'filterModel'; -export const EMBEDDINGS_ONLY_SEARCH_PROVIDER_NAME = 'embeddingsOnly'; export const EMBEDDINGS_SEARCH_PROVIDER_NAME = 'embeddingsFull'; export const LLM_RANKED_SEARCH_PROVIDER_NAME = 'llmRanked'; diff --git a/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css b/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css index 1f176a45dff..8e82e95f333 100644 --- a/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css +++ b/src/vs/workbench/contrib/processExplorer/browser/media/processExplorer.css @@ -57,20 +57,14 @@ .mac:not(.fullscreen) .process-explorer .monaco-list:focus::before { /* Rounded corners to make focus outline appear properly (unless fullscreen) */ - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; -} - -.mac:not(.fullscreen).macos-bigsur-or-newer .process-explorer .monaco-list:focus::before { - /* macOS Big Sur increased rounded corners size */ border-bottom-right-radius: 10px; border-bottom-left-radius: 10px; } -.mac:not(.fullscreen).macos-tahoe-or-newer .process-explorer .monaco-list:focus::before { - /* macOS Tahoe increased rounded corners size even more */ - border-bottom-right-radius: 12px; - border-bottom-left-radius: 12px; +.mac.macos-tahoe:not(.fullscreen) .process-explorer .monaco-list:focus::before { + /* macOS Tahoe increased rounded corners size */ + border-bottom-right-radius: 16px; + border-bottom-left-radius: 16px; } .process-explorer .monaco-list-row:first-of-type { diff --git a/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts b/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts index 934fe32b29f..b54812a080a 100644 --- a/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts +++ b/src/vs/workbench/contrib/processExplorer/browser/processExplorerControl.ts @@ -170,8 +170,7 @@ class ProcessHeaderTreeRenderer implements ITreeRenderer this.scmService.repositories); + () => Iterable.filter(this.scmService.repositories, r => r.provider.isHidden !== true)); this._activeRepositoryHistoryItemRefName = derived(reader => { const activeRepository = this.scmViewService.activeRepository.read(reader); @@ -164,7 +164,7 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe } // Source control provider status bar entry - if (this.scmService.repositoryCount > 1) { + if (this.scmViewService.repositories.length > 1) { const icon = getSCMRepositoryIcon(activeRepository, activeRepository.repository); const repositoryStatusbarEntry: IStatusbarEntry = { name: localize('status.scm.provider', "Source Control Provider"), diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index f70be633c6c..20c78c396f1 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -136,15 +136,12 @@ height: 22px; } -.scm-view .monaco-list-row .history, -.scm-view .monaco-list-row .history-item-group, .scm-view .monaco-list-row .resource-group { display: flex; height: 100%; align-items: center; } -.scm-view .monaco-list-row .history-item-group .monaco-icon-label, .scm-view .monaco-list-row .history-item .monaco-icon-label { flex-grow: 1; align-items: center; @@ -272,7 +269,6 @@ margin-left: 4px; } -.scm-view .monaco-list-row .history > .name, .scm-view .monaco-list-row .resource-group > .name { flex: 1; overflow: hidden; @@ -382,7 +378,11 @@ } .scm-view .scm-editor-container .monaco-editor { - border-radius: 2px; + border-radius: 4px; +} + +.scm-view .scm-editor-container .monaco-editor .overflow-guard { + border-radius: 4px; } .scm-view .scm-editor { @@ -393,7 +393,7 @@ box-sizing: border-box; border: 1px solid var(--vscode-input-border, transparent); background-color: var(--vscode-input-background); - border-radius: 2px; + border-radius: 4px; } .scm-view .button-container { @@ -589,58 +589,82 @@ white-space: nowrap; } +.scm-repositories-view .scm-artifact .timestamp-container { + flex-shrink: 0; + margin-left: 2px; + margin-right: 4px; + opacity: 0.5; +} + +.scm-repositories-view .scm-artifact .timestamp-container.duplicate { + height: 22px; + min-width: 6px; + border-left: 1px solid currentColor; + opacity: 0.25; + + .timestamp { + display: none; + } +} + +.scm-repositories-view .monaco-list .monaco-list-row:hover .scm-artifact .timestamp-container.duplicate { + border-left: 0; + opacity: 0.5; + + .timestamp { + display: block; + } +} + /* History item hover */ -.monaco-hover.history-item-hover p:first-child { +.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:first-child > p { margin-top: 4px; } -.monaco-hover.history-item-hover p:last-child { +.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:last-child p { margin-bottom: 2px !important; } -.monaco-hover.history-item-hover p:last-child span:not(.codicon) { +.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown:last-child p span:not(.codicon) { padding: 2px 0; } -.monaco-hover.history-item-hover hr { +.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown hr { margin-top: 4px; margin-bottom: 4px; } -.monaco-hover.history-item-hover hr + p { +.monaco-hover.history-item-hover .history-item-hover-container > .rendered-markdown > p { margin: 4px 0; } -.monaco-hover.history-item-hover span:not(.codicon) { +.monaco-hover.history-item-hover .history-item-hover-container div:nth-of-type(3):nth-last-of-type(2) > p { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.monaco-hover.history-item-hover .history-item-hover-container span:not(.codicon) { margin-bottom: 0 !important; } -.monaco-hover.history-item-hover p > span > span.codicon.codicon-git-branch { +.monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-git-branch { font-size: 12px; margin-bottom: 2px !important; } -.monaco-hover.history-item-hover p > span > span.codicon.codicon-tag, -.monaco-hover.history-item-hover p > span > span.codicon.codicon-target { +.monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-tag, +.monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-target { font-size: 14px; margin-bottom: 2px !important; } -.monaco-hover.history-item-hover p > span > span.codicon.codicon-cloud { +.monaco-hover.history-item-hover .history-item-hover-container p > span > span.codicon.codicon-cloud { font-size: 14px; margin-bottom: 1px !important; } -.monaco-hover.history-item-hover .hover-row.status-bar .action { - display: flex; - align-items: center; -} - -.monaco-hover.history-item-hover .hover-row.status-bar .action .codicon { - color: inherit; -} - /* Graph */ .pane-header .scm-graph-view-badge-container { @@ -689,6 +713,7 @@ max-width: 100px; overflow: hidden; text-overflow: ellipsis; + padding-right: 2px; } .scm-history-view .monaco-list-row > .monaco-tl-row > .monaco-tl-twistie.force-no-twistie { diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts index 59c51ee0c20..b1eef6b6908 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffModel.ts @@ -26,7 +26,7 @@ import { LineRangeMapping } from '../../../../editor/common/diff/rangeMapping.js import { IDiffEditorModel } from '../../../../editor/common/editorCommon.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; -import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/chatEditingService.js'; +import { IChatEditingService, ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { autorun } from '../../../../base/common/observable.js'; @@ -106,7 +106,7 @@ export class QuickDiffModel extends Disposable { private _quickDiffsPromise?: Promise; private _diffDelayer = this._register(new ThrottledDelayer(200)); - private readonly _onDidChange = new Emitter<{ changes: QuickDiffChange[]; diff: ISplice[] }>(); + private readonly _onDidChange = this._register(new Emitter<{ changes: QuickDiffChange[]; diff: ISplice[] }>()); readonly onDidChange: Event<{ changes: QuickDiffChange[]; diff: ISplice[] }> = this._onDidChange.event; private _allChanges: QuickDiffChange[] = []; @@ -183,6 +183,8 @@ export class QuickDiffModel extends Disposable { .filter(change => change.providerId === quickDiff.id); return { + providerId: quickDiff.id, + providerKind: quickDiff.kind, original: quickDiff.originalResource, modified: this._model.resource, changes: changes.map(change => change.change), @@ -419,66 +421,48 @@ export class QuickDiffModel extends Disposable { } findNextClosestChange(lineNumber: number, inclusive = true, providerId?: string): number { - const visibleQuickDiffIds = this.quickDiffs - .filter(quickDiff => (!providerId || quickDiff.id === providerId) && - this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) - .map(quickDiff => quickDiff.id); - - if (!inclusive) { - // Next visible change - let nextChangeIndex = this.changes - .findIndex(change => visibleQuickDiffIds.includes(change.providerId) && - change.change.modifiedStartLineNumber > lineNumber); - - if (nextChangeIndex !== -1) { - return nextChangeIndex; - } - - // First visible change - nextChangeIndex = this.changes - .findIndex(change => visibleQuickDiffIds.includes(change.providerId)); - - return nextChangeIndex !== -1 ? nextChangeIndex : 0; - } - - const primaryQuickDiffId = this.quickDiffs - .find(quickDiff => quickDiff.kind === 'primary')?.id; + const visibleQuickDiffIds = new Set(this.quickDiffs + .filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) + .map(quickDiff => quickDiff.id)); - const primaryInclusiveChangeIndex = this.changes - .findIndex(change => change.providerId === primaryQuickDiffId && - change.change.modifiedStartLineNumber <= lineNumber && - getModifiedEndLineNumber(change.change) >= lineNumber); + for (let i = 0; i < this.changes.length; i++) { + if (providerId && this.changes[i].providerId !== providerId) { + continue; + } - if (primaryInclusiveChangeIndex !== -1) { - return primaryInclusiveChangeIndex; - } + // Skip quick diffs that are not visible + if (!visibleQuickDiffIds.has(this.changes[i].providerId)) { + continue; + } - // Next visible change - let nextChangeIndex = this.changes - .findIndex(change => visibleQuickDiffIds.includes(change.providerId) && - change.change.modifiedStartLineNumber <= lineNumber && - getModifiedEndLineNumber(change.change) >= lineNumber); + const change = this.changes[i].change; - if (nextChangeIndex !== -1) { - return nextChangeIndex; + if (inclusive) { + if (getModifiedEndLineNumber(change) >= lineNumber) { + return i; + } + } else { + if (change.modifiedStartLineNumber > lineNumber) { + return i; + } + } } - // First visible change - nextChangeIndex = this.changes - .findIndex(change => visibleQuickDiffIds.includes(change.providerId)); - - return nextChangeIndex !== -1 ? nextChangeIndex : 0; + return 0; } findPreviousClosestChange(lineNumber: number, inclusive = true, providerId?: string): number { + const visibleQuickDiffIds = new Set(this.quickDiffs + .filter(quickDiff => this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) + .map(quickDiff => quickDiff.id)); + for (let i = this.changes.length - 1; i >= 0; i--) { if (providerId && this.changes[i].providerId !== providerId) { continue; } // Skip quick diffs that are not visible - const quickDiff = this.quickDiffs.find(quickDiff => quickDiff.id === this.changes[i].providerId); - if (!quickDiff || !this.quickDiffService.isQuickDiffProviderVisible(quickDiff.id)) { + if (!visibleQuickDiffIds.has(this.changes[i].providerId)) { continue; } diff --git a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts index 51bf45c1fad..15ab45055b3 100644 --- a/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts +++ b/src/vs/workbench/contrib/scm/browser/quickDiffWidget.ts @@ -47,7 +47,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { gotoNextLocation, gotoPreviousLocation } from '../../../../platform/theme/common/iconRegistry.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Color } from '../../../../base/common/color.js'; -import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { getOuterEditor } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { quickDiffDecorationCount } from './quickDiffDecorator.js'; import { hasNativeContextMenu } from '../../../../platform/window/common/window.js'; @@ -130,8 +130,7 @@ class QuickDiffWidgetEditorAction extends Action { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService ) { - const keybinding = keybindingService.lookupKeybinding(action.id); - const label = action.label + (keybinding ? ` (${keybinding.getLabel()})` : ''); + const label = keybindingService.appendKeybinding(action.label, action.id); super(action.id, label, cssClass); @@ -358,9 +357,11 @@ class QuickDiffWidget extends PeekViewWidget { super._fillHead(container, true); // Render an empty picker which will be populated later + const action = new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event)); + this._disposables.add(action); + this.dropdownContainer = dom.prepend(this._titleElement!, dom.$('.dropdown')); - this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, - new QuickDiffPickerBaseAction((event?: IQuickDiffSelectItem) => this.switchQuickDiff(event))); + this.dropdown = this.instantiationService.createInstance(QuickDiffPickerViewItem, action); this.dropdown.render(this.dropdownContainer); } @@ -462,9 +463,18 @@ class QuickDiffWidget extends PeekViewWidget { return this.diffEditor.hasTextFocus(); } + toggleFocus(): void { + if (this.diffEditor.hasTextFocus()) { + this.editor.focus(); + } else { + this.diffEditor.focus(); + } + } + override dispose() { - super.dispose(); + this.dropdown?.dispose(); this.menu?.dispose(); + super.dispose(); } } @@ -544,6 +554,12 @@ export class QuickDiffEditorController extends Disposable implements IEditorCont this.widget?.showChange(this.widget.index, false); } + toggleFocus(): void { + if (this.widget) { + this.widget.toggleFocus(); + } + } + next(lineNumber?: number): void { if (!this.assertWidget()) { return; @@ -937,6 +953,26 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'togglePeekWidgetFocus', + weight: KeybindingWeight.EditorContrib, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.F2), + when: isQuickDiffVisible, + handler: (accessor: ServicesAccessor) => { + const outerEditor = getOuterEditorFromDiffEditor(accessor); + if (!outerEditor) { + return; + } + + const controller = QuickDiffEditorController.get(outerEditor); + if (!controller) { + return; + } + + controller.toggleFocus(); + } +}); + function setPositionAndSelection(change: IChange, editor: ICodeEditor, accessibilityService: IAccessibilityService, codeEditorService: ICodeEditorService) { const position = new Position(change.modifiedStartLineNumber, 1); editor.setPosition(position); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 606bda048d0..c38dc40be06 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -29,6 +29,7 @@ import { RepositoryPicker, SCMViewService } from './scmViewService.js'; import { SCMRepositoriesViewPane } from './scmRepositoriesViewPane.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { Context as SuggestContext } from '../../../../editor/contrib/suggest/browser/suggest.js'; +import { InlineCompletionContextKeys } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionContextKeys.js'; import { MANAGE_TRUST_COMMAND_ID, WorkspaceTrustContext } from '../../workspace/common/workspace.js'; import { IQuickDiffService } from '../common/quickDiff.js'; import { QuickDiffService } from '../common/quickDiffService.js'; @@ -41,12 +42,12 @@ import { SCMHistoryViewPane } from './scmHistoryViewPane.js'; import { QuickDiffModelService, IQuickDiffModelService } from './quickDiffModel.js'; import { QuickDiffEditorController } from './quickDiffWidget.js'; import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { RemoteNameContext } from '../../../common/contextkeys.js'; +import { RemoteNameContext, ResourceContextKey } from '../../../common/contextkeys.js'; import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js'; import { SCMAccessibilityHelp } from './scmAccessibilityHelp.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; import { SCMHistoryItemContextContribution } from './scmHistoryChatContext.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; import product from '../../../../platform/product/common/product.js'; @@ -459,10 +460,28 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'scm.clearValidation', + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and( + ContextKeyExpr.has('scmRepository'), + ContextKeys.SCMInputHasValidationMessage), + primary: KeyCode.Escape, + handler: async (accessor) => { + const scmViewService = accessor.get(ISCMViewService); + scmViewService.activeRepository.get()?.repository.input.clearValidation(); + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: 'scm.clearInput', weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(ContextKeyExpr.has('scmRepository'), SuggestContext.Visible.toNegated(), EditorContextKeys.hasNonEmptySelection.toNegated()), + when: ContextKeyExpr.and( + ContextKeyExpr.has('scmRepository'), + SuggestContext.Visible.toNegated(), + InlineCompletionContextKeys.inlineSuggestionVisible.toNegated(), + ContextKeys.SCMInputHasValidationMessage.toNegated(), + EditorContextKeys.hasNonEmptySelection.toNegated()), primary: KeyCode.Escape, handler: async (accessor) => { const scmService = accessor.get(ISCMService); @@ -684,6 +703,7 @@ registerAction2(class extends Action2 { ChatContextKeys.Setup.hidden.negate(), ChatContextKeys.Setup.disabled.negate(), ChatContextKeys.Setup.installed.negate(), + ContextKeyExpr.in(ResourceContextKey.Resource.key, 'git.mergeChanges'), ContextKeyExpr.equals('git.activeResourceHasMergeConflicts', true) ) } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index 8f8c3af5935..b7443247c73 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -9,8 +9,12 @@ import { badgeBackground, chartsBlue, chartsPurple, foreground } from '../../../ import { asCssVariable, ColorIdentifier, registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js'; import { rot } from '../../../../base/common/numbers.js'; -import { svgElem } from '../../../../base/browser/dom.js'; +import { $, svgElem } from '../../../../base/browser/dom.js'; import { PANEL_BACKGROUND } from '../../../common/theme.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { IMarkdownString, isEmptyMarkdownString, isMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; import { findLastIdx } from '../../../../base/common/arraysFind.js'; export const SWIMLANE_HEIGHT = 22; @@ -47,10 +51,16 @@ export const colorRegistry: ColorIdentifier[] = [ ]; function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { - for (const ref of historyItem.references ?? []) { - const colorIdentifier = colorMap.get(ref.id); - if (colorIdentifier !== undefined) { - return colorIdentifier; + if (historyItem.id === SCMIncomingHistoryItemId) { + return historyItemRemoteRefColor; + } else if (historyItem.id === SCMOutgoingHistoryItemId) { + return historyItemRefColor; + } else { + for (const ref of historyItem.references ?? []) { + const colorIdentifier = colorMap.get(ref.id); + if (colorIdentifier !== undefined) { + return colorIdentifier; + } } } @@ -379,7 +389,42 @@ export function toISCMHistoryItemViewModelArray( } satisfies ISCMHistoryItemViewModel); } - // Inject incoming/outgoing changes nodes if ahead/behind and there is a merge base + // Add incoming/outgoing changes history item view models. While working + // with the view models is a little bit more complex, we are doing this + // after creating the view models so that we can use the swimlane colors + // to add the incoming/outgoing changes history items view models to the + // correct swimlanes. + addIncomingOutgoingChangesHistoryItems( + viewModels, + currentHistoryItemRef, + currentHistoryItemRemoteRef, + addIncomingChanges, + addOutgoingChanges, + mergeBase + ); + + return viewModels; +} + +export function getHistoryItemIndex(historyItemViewModel: ISCMHistoryItemViewModel): number { + const historyItem = historyItemViewModel.historyItem; + const inputSwimlanes = historyItemViewModel.inputSwimlanes; + + // Find the history item in the input swimlanes + const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); + + // Circle index - use the input swimlane index if present, otherwise add it to the end + return inputIndex !== -1 ? inputIndex : inputSwimlanes.length; +} + +function addIncomingOutgoingChangesHistoryItems( + viewModels: ISCMHistoryItemViewModel[], + currentHistoryItemRef?: ISCMHistoryItemRef, + currentHistoryItemRemoteRef?: ISCMHistoryItemRef, + addIncomingChanges?: boolean, + addOutgoingChanges?: boolean, + mergeBase?: string +): void { if (currentHistoryItemRef?.revision !== currentHistoryItemRemoteRef?.revision && mergeBase) { // Incoming changes node if (addIncomingChanges && currentHistoryItemRemoteRef && currentHistoryItemRemoteRef.revision !== mergeBase) { @@ -388,108 +433,100 @@ export function toISCMHistoryItemViewModelArray( const afterHistoryItemIndex = viewModels.findIndex(vm => vm.historyItem.id === mergeBase); if (beforeHistoryItemIndex !== -1 && afterHistoryItemIndex !== -1) { - // Update the before node so that the incoming and outgoing swimlanes - // point to the `incoming-changes` node instead of the merge base - viewModels[beforeHistoryItemIndex] = { - ...viewModels[beforeHistoryItemIndex], - inputSwimlanes: viewModels[beforeHistoryItemIndex].inputSwimlanes - .map(node => { - return node.id === mergeBase && node.color === historyItemRemoteRefColor - ? { ...node, id: SCMIncomingHistoryItemId } - : node; - }), - outputSwimlanes: viewModels[beforeHistoryItemIndex].outputSwimlanes - .map(node => { - return node.id === mergeBase && node.color === historyItemRemoteRefColor - ? { ...node, id: SCMIncomingHistoryItemId } - : node; - }) - }; - - // Create incoming changes node - const inputSwimlanes = viewModels[beforeHistoryItemIndex].outputSwimlanes.map(i => deepClone(i)); - const outputSwimlanes = viewModels[afterHistoryItemIndex].inputSwimlanes.map(i => deepClone(i)); - const displayIdLength = viewModels[0].historyItem.displayId?.length ?? 0; - - const incomingChangesHistoryItem = { - id: SCMIncomingHistoryItemId, - displayId: '0'.repeat(displayIdLength), - parentIds: [mergeBase], - author: currentHistoryItemRemoteRef?.name, - subject: localize('incomingChanges', 'Incoming Changes'), - message: '' - } satisfies ISCMHistoryItem; - - // Insert incoming changes node - viewModels.splice(afterHistoryItemIndex, 0, { - historyItem: incomingChangesHistoryItem, - kind: 'incoming-changes', - inputSwimlanes, - outputSwimlanes - }); + // There is a known edge case in which the incoming changes have already + // been merged. For this scenario, we will not be showing the incoming + // changes history item. https://github.com/microsoft/vscode/issues/276064 + const incomingChangeMerged = viewModels[beforeHistoryItemIndex].historyItem.parentIds.length === 2 && + viewModels[beforeHistoryItemIndex].historyItem.parentIds.includes(mergeBase); + + if (!incomingChangeMerged) { + // Update the before node so that the incoming and outgoing swimlanes + // point to the `incoming-changes` node instead of the merge base + viewModels[beforeHistoryItemIndex] = { + ...viewModels[beforeHistoryItemIndex], + inputSwimlanes: viewModels[beforeHistoryItemIndex].inputSwimlanes + .map(node => { + return node.id === mergeBase && node.color === historyItemRemoteRefColor + ? { ...node, id: SCMIncomingHistoryItemId } + : node; + }), + outputSwimlanes: viewModels[beforeHistoryItemIndex].outputSwimlanes + .map(node => { + return node.id === mergeBase && node.color === historyItemRemoteRefColor + ? { ...node, id: SCMIncomingHistoryItemId } + : node; + }) + }; + + // Create incoming changes node + const inputSwimlanes = viewModels[beforeHistoryItemIndex].outputSwimlanes.map(i => deepClone(i)); + const outputSwimlanes = viewModels[afterHistoryItemIndex].inputSwimlanes.map(i => deepClone(i)); + const displayIdLength = viewModels[0].historyItem.displayId?.length ?? 0; + + const incomingChangesHistoryItem = { + id: SCMIncomingHistoryItemId, + displayId: '0'.repeat(displayIdLength), + parentIds: [mergeBase], + author: currentHistoryItemRemoteRef?.name, + subject: localize('incomingChanges', 'Incoming Changes'), + message: '' + } satisfies ISCMHistoryItem; + + // Insert incoming changes node + viewModels.splice(afterHistoryItemIndex, 0, { + historyItem: incomingChangesHistoryItem, + kind: 'incoming-changes', + inputSwimlanes, + outputSwimlanes + }); + } } } // Outgoing changes node if (addOutgoingChanges && currentHistoryItemRef?.revision && currentHistoryItemRef.revision !== mergeBase) { - // Find the before/after indices using the merge base (might not be present if the current history item is not loaded yet) - let beforeHistoryItemIndex = findLastIdx(viewModels, vm => vm.outputSwimlanes.some(node => node.id === currentHistoryItemRef.revision)); - const afterHistoryItemIndex = viewModels.findIndex(vm => vm.historyItem.id === currentHistoryItemRef.revision); - - if (afterHistoryItemIndex !== -1) { - if (beforeHistoryItemIndex === -1 && afterHistoryItemIndex > 0) { - beforeHistoryItemIndex = afterHistoryItemIndex - 1; - } - - // Update the after node to point to the `outgoing-changes` node - viewModels[afterHistoryItemIndex].inputSwimlanes.push({ - id: currentHistoryItemRef.revision, - color: historyItemRefColor - }); - - const inputSwimlanes = beforeHistoryItemIndex !== -1 - ? viewModels[beforeHistoryItemIndex].outputSwimlanes - .map(node => { - return addIncomingChanges && node.id === mergeBase && node.color === historyItemRemoteRefColor - ? { ...node, id: SCMIncomingHistoryItemId } - : node; - }) - : []; - const outputSwimlanes = viewModels[afterHistoryItemIndex].inputSwimlanes.slice(0); - const displayIdLength = viewModels[0].historyItem.displayId?.length ?? 0; + // Find the index of the current history item view model (might not be present if the current history item is not loaded yet) + const currentHistoryItemRefIndex = viewModels.findIndex(vm => vm.kind === 'HEAD' && vm.historyItem.id === currentHistoryItemRef.revision); + if (currentHistoryItemRefIndex !== -1) { + // Create outgoing changes node const outgoingChangesHistoryItem = { id: SCMOutgoingHistoryItemId, - displayId: '0'.repeat(displayIdLength), - parentIds: [mergeBase], + displayId: viewModels[0].historyItem.displayId + ? '0'.repeat(viewModels[0].historyItem.displayId.length) + : undefined, + parentIds: [currentHistoryItemRef.revision], author: currentHistoryItemRef?.name, subject: localize('outgoingChanges', 'Outgoing Changes'), message: '' } satisfies ISCMHistoryItem; + // Copy the input swimlanes from the current history item ref + const inputSwimlanes = viewModels[currentHistoryItemRefIndex].inputSwimlanes.slice(0); + + // Copy the input swimlanes and add the current history item ref + const outputSwimlanes = inputSwimlanes.slice(0).concat({ + id: currentHistoryItemRef.revision, + color: historyItemRefColor + } satisfies ISCMHistoryItemGraphNode); + // Insert outgoing changes node - viewModels.splice(afterHistoryItemIndex, 0, { + viewModels.splice(currentHistoryItemRefIndex, 0, { historyItem: outgoingChangesHistoryItem, kind: 'outgoing-changes', inputSwimlanes, outputSwimlanes }); + + // Update the input swimlane for the current history item + // ref so that it connects with the outgoing changes node + viewModels[currentHistoryItemRefIndex + 1].inputSwimlanes.push({ + id: currentHistoryItemRef.revision, + color: historyItemRefColor + } satisfies ISCMHistoryItemGraphNode); } } } - - return viewModels; -} - -export function getHistoryItemIndex(historyItemViewModel: ISCMHistoryItemViewModel): number { - const historyItem = historyItemViewModel.historyItem; - const inputSwimlanes = historyItemViewModel.inputSwimlanes; - - // Find the history item in the input swimlanes - const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); - - // Circle index - use the input swimlane index if present, otherwise add it to the end - return inputIndex !== -1 ? inputIndex : inputSwimlanes.length; } export function compareHistoryItemRefs( @@ -519,3 +556,52 @@ export function compareHistoryItemRefs( return ref1Order - ref2Order; } + +export function toHistoryItemHoverContent(markdownRendererService: IMarkdownRendererService, historyItem: ISCMHistoryItem, includeReferences: boolean): { content: string | IMarkdownString | HTMLElement; disposables: IDisposable } { + const disposables = new DisposableStore(); + + if (historyItem.tooltip === undefined) { + return { content: historyItem.message, disposables }; + } + + if (isMarkdownString(historyItem.tooltip)) { + return { content: historyItem.tooltip, disposables }; + } + + // References as "injected" into the hover here since the extension does + // not know that color used in the graph to render the history item at which + // the reference is pointing to. They are being added before the last element + // of the array which is assumed to contain the hover commands. + const tooltipSections = historyItem.tooltip.slice(); + + if (includeReferences && historyItem.references?.length) { + const markdownString = new MarkdownString('', { supportHtml: true, supportThemeIcons: true }); + + for (const reference of historyItem.references) { + const labelIconId = ThemeIcon.isThemeIcon(reference.icon) ? reference.icon.id : ''; + + const labelBackgroundColor = reference.color ? asCssVariable(reference.color) : asCssVariable(historyItemHoverDefaultLabelBackground); + const labelForegroundColor = reference.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground); + markdownString.appendMarkdown(` $(${labelIconId}) `); + markdownString.appendText(reference.name); + markdownString.appendMarkdown('  '); + } + + markdownString.appendMarkdown(`\n\n---\n\n`); + tooltipSections.splice(tooltipSections.length - 1, 0, markdownString); + } + + // Render tooltip content + const hoverContainer = $('.history-item-hover-container'); + for (const markdownString of tooltipSections) { + if (isEmptyMarkdownString(markdownString)) { + continue; + } + + const renderedContent = markdownRendererService.render(markdownString); + hoverContainer.appendChild(renderedContent.element); + disposables.add(renderedContent); + } + + return { content: hoverContainer, disposables }; +} diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts index dde9e7d5f6b..3f9548e3448 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryChatContext.ts @@ -20,12 +20,10 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IChatWidget, showChatView } from '../../chat/browser/chat.js'; -import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, picksWithPromiseFn } from '../../chat/browser/chatContextPickService.js'; -import { ChatContextKeys } from '../../chat/common/chatContextKeys.js'; -import { ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemVariableEntry } from '../../chat/common/chatVariableEntries.js'; +import { IChatWidget, IChatWidgetService } from '../../chat/browser/chat.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, picksWithPromiseFn } from '../../chat/browser/attachments/chatContextPickService.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { ISCMHistoryItemChangeVariableEntry, ISCMHistoryItemVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; import { ScmHistoryItemResolver } from '../../multiDiffEditor/browser/scmMultiDiffSourceResolver.js'; import { ISCMHistoryItem, ISCMHistoryItemChange } from '../common/history.js'; import { ISCMProvider, ISCMService, ISCMViewService } from '../common/scm.js'; @@ -269,14 +267,12 @@ registerAction2(class extends Action2 { } override async run(accessor: ServicesAccessor, provider: ISCMProvider, historyItem: ISCMHistoryItem): Promise { - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); - const widget = await showChatView(viewsService, layoutService); + const chatWidgetService = accessor.get(IChatWidgetService); + const widget = await chatWidgetService.revealWidget(); if (!provider || !historyItem || !widget) { return; } - await widget.waitForReady(); widget.attachmentModel.addContext(SCMHistoryItemContext.asAttachment(provider, historyItem)); } }); @@ -297,9 +293,8 @@ registerAction2(class extends Action2 { } override async run(accessor: ServicesAccessor, provider: ISCMProvider, historyItem: ISCMHistoryItem): Promise { - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); - const widget = await showChatView(viewsService, layoutService); + const chatWidgetService = accessor.get(IChatWidgetService); + const widget = await chatWidgetService.revealWidget(); if (!provider || !historyItem || !widget) { return; } @@ -325,9 +320,8 @@ registerAction2(class extends Action2 { } override async run(accessor: ServicesAccessor, historyItem: ISCMHistoryItem, historyItemChange: ISCMHistoryItemChange): Promise { - const viewsService = accessor.get(IViewsService); - const layoutService = accessor.get(IWorkbenchLayoutService); - const widget = await showChatView(viewsService, layoutService); + const chatWidgetService = accessor.get(IChatWidgetService); + const widget = await chatWidgetService.revealWidget(); if (!historyItem || !historyItemChange.modifiedUri || !widget) { return; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 7d6719bfcd4..ab1900fc1db 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -5,8 +5,7 @@ import './media/scm.css'; import { $, append, h, reset } from '../../../../base/browser/dom.js'; -import { IHoverOptions, IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; -import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; +import { HoverStyle, IDelayedHoverOptions, IHoverLifecycleOptions } from '../../../../base/browser/ui/hover/hover.js'; import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { IIdentityProvider, IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; import { LabelFuzzyScore } from '../../../../base/browser/ui/tree/abstractTree.js'; @@ -19,7 +18,7 @@ import { localize } from '../../../../nls.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 { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; @@ -28,13 +27,12 @@ import { asCssVariable, ColorIdentifier, foreground } from '../../../../platform import { IFileIconTheme, IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverLabelForeground, historyItemHoverDefaultLabelBackground, getHistoryItemIndex } from './scmHistory.js'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverLabelForeground, historyItemHoverDefaultLabelBackground, getHistoryItemIndex, toHistoryItemHoverContent } from './scmHistory.js'; import { getHistoryItemEditorTitle, getProviderKey, isSCMHistoryItemChangeNode, isSCMHistoryItemChangeViewModelTreeElement, isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemChangeViewModelTreeElement, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement, SCMIncomingHistoryItemId, SCMOutgoingHistoryItemId } from '../common/history.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService, ViewMode } from '../common/scm.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; -import { IWorkbenchLayoutService, Position } from '../../../services/layout/browser/layoutService.js'; import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js'; import { Action2, IMenuService, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { Sequencer, Throttler } from '../../../../base/common/async.js'; @@ -53,7 +51,6 @@ import { Iterable } from '../../../../base/common/iterator.js'; import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { compare } from '../../../../base/common/strings.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { groupBy as groupBy2 } from '../../../../base/common/collections.js'; @@ -76,6 +73,8 @@ import { ElementsDragAndDropData, ListViewTargetSector } from '../../../../base/ import { CodeDataTransfers } from '../../../../platform/dnd/browser/dnd.js'; import { SCMHistoryItemTransferData } from './scmHistoryChatContext.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; @@ -296,13 +295,11 @@ registerAction2(class extends Action2 { menu: [ { id: MenuId.SCMHistoryItemContext, - when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), group: 'inline', order: 1 }, { id: MenuId.SCMHistoryItemContext, - when: ContextKeyExpr.equals('config.multiDiffEditor.experimental.enabled', true), group: '0_view', order: 1 } @@ -312,38 +309,54 @@ registerAction2(class extends Action2 { override async run(accessor: ServicesAccessor, provider: ISCMProvider, ...historyItems: ISCMHistoryItem[]) { const commandService = accessor.get(ICommandService); - - if (!provider || historyItems.length === 0) { - return; - } - - const historyItem = historyItems[0]; - const historyItemLast = historyItems[historyItems.length - 1]; const historyProvider = provider.historyProvider.get(); const historyItemRef = historyProvider?.historyItemRef.get(); const historyItemRemoteRef = historyProvider?.historyItemRemoteRef.get(); - if (historyItems.length > 1) { - const ancestor = await historyProvider?.resolveHistoryItemRefsCommonAncestor([historyItem.id, historyItemLast.id]); - if (!ancestor || (ancestor !== historyItem.id && ancestor !== historyItemLast.id)) { - return; - } + if (!provider || !historyProvider || !historyItemRef || historyItems.length === 0) { + return; } - let title: string, historyItemId: string, historyItemParentId: string | undefined; + const historyItem = historyItems[0]; + let title: string | undefined, historyItemId: string | undefined, historyItemParentId: string | undefined; - if (historyItem.id === SCMIncomingHistoryItemId) { - title = `${historyItem.subject} - ${historyItemRef?.name} \u2194 ${historyItemRemoteRef?.name}`; - historyItemId = historyProvider!.historyItemRemoteRef.get()!.id; - historyItemParentId = historyItem.parentIds[0]; - } else if (historyItem.id === SCMOutgoingHistoryItemId) { - title = `${historyItem.subject} - ${historyItemRemoteRef?.name} \u2194 ${historyItemRef?.name}`; - historyItemId = historyProvider!.historyItemRef.get()!.id; - historyItemParentId = historyItem.parentIds[0]; + if (historyItemRemoteRef && (historyItem.id === SCMIncomingHistoryItemId || historyItem.id === SCMOutgoingHistoryItemId)) { + // Incoming/Outgoing changes history item + const mergeBase = await historyProvider.resolveHistoryItemRefsCommonAncestor([ + historyItemRef.name, + historyItemRemoteRef.name + ]); + + if (mergeBase && historyItem.id === SCMIncomingHistoryItemId) { + // Incoming changes history item + title = `${historyItem.subject} - ${historyItemRef.name} \u2194 ${historyItemRemoteRef.name}`; + historyItemId = historyItemRemoteRef.id; + historyItemParentId = mergeBase; + } else if (mergeBase && historyItem.id === SCMOutgoingHistoryItemId) { + // Outgoing changes history item + title = `${historyItem.subject} - ${historyItemRemoteRef.name} \u2194 ${historyItemRef.name}`; + historyItemId = historyItemRef.id; + historyItemParentId = mergeBase; + } } else { title = getHistoryItemEditorTitle(historyItem); historyItemId = historyItem.id; - historyItemParentId = historyItem.parentIds.length > 0 ? historyItem.parentIds[0] : undefined; + + if (historyItem.parentIds.length > 0) { + // History item right above the incoming changes history item + if (historyItem.parentIds[0] === SCMIncomingHistoryItemId && historyItemRemoteRef) { + historyItemParentId = await historyProvider.resolveHistoryItemRefsCommonAncestor([ + historyItemRef.name, + historyItemRemoteRef.name + ]); + } else { + historyItemParentId = historyItem.parentIds[0]; + } + } + } + + if (!title || !historyItemId || !historyItemParentId) { + return; } const multiDiffSourceUri = ScmHistoryItemResolver.getMultiDiffSourceUri(provider, historyItemId, historyItemParentId, ''); @@ -431,13 +444,14 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer; constructor( - private readonly hoverDelegate: IHoverDelegate, + private readonly _viewContainerLocation: ViewContainerLocation | null, @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IContextMenuService private readonly _contextMenuService: IContextMenuService, @IHoverService private readonly _hoverService: IHoverService, @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService, @IMenuService private readonly _menuService: IMenuService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { @@ -445,10 +459,6 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer { const labelConfig = this._badgesConfig.read(reader); - templateData.labelContainer.textContent = ''; + templateData.labelContainer.replaceChildren(); const references = historyItem.references ? historyItem.references.slice(0) : []; @@ -577,6 +586,45 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer; + hoverLifecycleOptions: IHoverLifecycleOptions | undefined; + } { + // Source Control Graph view in the panel + if (this._viewContainerLocation === ViewContainerLocation.Panel) { + return { + hoverOptions: { + additionalClasses: ['history-item-hover'], + appearance: { + compact: true + }, + position: { + hoverPosition: HoverPosition.RIGHT + }, + style: HoverStyle.Mouse + }, + hoverLifecycleOptions: undefined + }; + } + + return { + hoverOptions: { + additionalClasses: ['history-item-hover'], + appearance: { + compact: true, + showPointer: true + }, + position: { + hoverPosition: HoverPosition.RIGHT + }, + style: HoverStyle.Pointer + }, + hoverLifecycleOptions: { + groupId: 'scm-history-item' + } + }; + } + private _processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { if (!filterData) { return [undefined, undefined]; @@ -722,10 +770,6 @@ class HistoryItemLoadMoreRenderer implements ICompressibleTreeRenderer this.getHoverOptions(), configurationService, hoverService); - } - - private getHoverOptions(): Partial { - const sideBarPosition = this.layoutService.getSideBarPosition(); - - let hoverPosition: HoverPosition; - if (this._viewContainerLocation === ViewContainerLocation.Sidebar) { - hoverPosition = sideBarPosition === Position.LEFT ? HoverPosition.RIGHT : HoverPosition.LEFT; - } else if (this._viewContainerLocation === ViewContainerLocation.AuxiliaryBar) { - hoverPosition = sideBarPosition === Position.LEFT ? HoverPosition.LEFT : HoverPosition.RIGHT; - } else { - hoverPosition = HoverPosition.RIGHT; - } - - return { additionalClasses: ['history-item-hover'], position: { hoverPosition, forcePosition: true } }; - } -} - class SCMHistoryViewPaneActionRunner extends ActionRunner { constructor(@IProgressService private readonly _progressService: IProgressService) { super(); @@ -923,7 +938,7 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource 0 ? historyItem.parentIds[0] : undefined; + + if (historyItem.parentIds.length > 0) { + // History item right above the incoming changes history item + if (historyItem.parentIds[0] === SCMIncomingHistoryItemId) { + const historyItemRef = historyProvider?.historyItemRef.get(); + const historyItemRemoteRef = historyProvider?.historyItemRemoteRef.get(); + + if (!historyProvider || !historyItemRef || !historyItemRemoteRef) { + return []; + } + + historyItemParentId = await historyProvider.resolveHistoryItemRefsCommonAncestor([ + historyItemRef.name, + historyItemRemoteRef.name]); + } else { + historyItemParentId = historyItem.parentIds[0]; + } + } } const historyItemChanges = await historyProvider?.provideHistoryItemChanges(historyItemId, historyItemParentId) ?? []; @@ -1661,17 +1696,22 @@ export class SCMHistoryViewPane extends ViewPane { element.badge.textContent = 'Outdated'; container.appendChild(element.root); - this._register(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), element.root, { - markdown: { - value: localize('scmGraphViewOutdated', "Please refresh the graph using the refresh action ($(refresh))."), - supportThemeIcons: true - }, - markdownNotSupportedFallback: undefined - })); - this._register(autorun(reader => { const outdated = this._repositoryOutdated.read(reader); element.root.style.display = outdated ? '' : 'none'; + + if (outdated) { + reader.store.add(this.hoverService.setupDelayedHover(element.root, { + appearance: { + compact: true, + showPointer: true + }, + content: new MarkdownString(localize('scmGraphViewOutdated', "Please refresh the graph using the refresh action ($(refresh))."), { supportThemeIcons: true }), + position: { + hoverPosition: HoverPosition.BELOW + } + })); + } })); } @@ -1946,9 +1986,6 @@ export class SCMHistoryViewPane extends ViewPane { private _createTree(container: HTMLElement): void { this._treeIdentityProvider = new SCMHistoryTreeIdentityProvider(); - const historyItemHoverDelegate = this.instantiationService.createInstance(HistoryItemHoverDelegate, this.viewDescriptorService.getViewLocationById(this.id)); - this._register(historyItemHoverDelegate); - const resourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(resourceLabels); @@ -1964,7 +2001,7 @@ export class SCMHistoryViewPane extends ViewPane { new ListDelegate(), new SCMHistoryTreeCompressionDelegate(), [ - this.instantiationService.createInstance(HistoryItemRenderer, historyItemHoverDelegate), + this.instantiationService.createInstance(HistoryItemRenderer, this.viewDescriptorService.getViewLocationById(this.id)), this.instantiationService.createInstance(HistoryItemChangeRenderer, () => this._treeViewModel.viewMode.get(), resourceLabels), this.instantiationService.createInstance(HistoryItemLoadMoreRenderer, this._repositoryIsLoadingMore, () => this._loadMore()), ], @@ -1977,7 +2014,12 @@ export class SCMHistoryViewPane extends ViewPane { dnd: new SCMHistoryTreeDragAndDrop(), keyboardNavigationLabelProvider: new SCMHistoryTreeKeyboardNavigationLabelProvider(), horizontalScrolling: false, - multipleSelectionSupport: false + multipleSelectionSupport: false, + twistieAdditionalCssClass: (e: unknown) => { + return isSCMHistoryItemViewModelTreeElement(e) || isSCMHistoryItemLoadMoreTreeElement(e) + ? 'force-no-twistie' + : undefined; + } } ) as WorkbenchCompressibleAsyncDataTree; this._register(this._tree); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts index d168d958d50..d5c6b8459e5 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoriesViewPane.ts @@ -9,7 +9,7 @@ import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPan import { append, $ } from '../../../../base/browser/dom.js'; import { IListVirtualDelegate, IIdentityProvider } from '../../../../base/browser/ui/list/list.js'; import { IAsyncDataSource, ITreeEvent, ITreeContextMenuEvent, ITreeNode, ITreeElementRenderDetails } from '../../../../base/browser/ui/tree/tree.js'; -import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; +import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -42,8 +42,11 @@ import { URI } from '../../../../base/common/uri.js'; import { basename } from '../../../../base/common/resources.js'; import { ICompressibleTreeRenderer } from '../../../../base/browser/ui/tree/objectTree.js'; import { ICompressedTreeNode } from '../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; -import { ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js'; +import { IAsyncDataTreeViewState, ITreeCompressionDelegate } from '../../../../base/browser/ui/tree/asyncDataTree.js'; import { Codicon } from '../../../../base/common/codicons.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IActionViewItemProvider } from '../../../../base/browser/ui/actionbar/actionbar.js'; +import { fromNow } from '../../../../base/common/date.js'; type TreeElement = ISCMRepository | SCMArtifactGroupTreeElement | SCMArtifactTreeElement | IResourceNode; @@ -80,6 +83,7 @@ class ArtifactGroupRenderer implements ICompressibleTreeRenderer, FuzzyScore>, index: number, templateData: ArtifactTemplate): void { @@ -170,6 +180,7 @@ class ArtifactRenderer implements ICompressibleTreeRenderer(inputOrElement); - for (const artifact of artifacts) { - artifactsTree.add(URI.from({ - scheme: 'scm-artifact', path: artifact.name - }), { - repository, - group: inputOrElement.artifactGroup, - artifact, - type: 'artifact' - }); + if (inputOrElement.artifactGroup.supportsFolders) { + // Resource tree for artifacts + const artifactsTree = new ResourceTree(inputOrElement); + for (let index = 0; index < artifacts.length; index++) { + const artifact = artifacts[index]; + const artifactUri = URI.from({ scheme: 'scm-artifact', path: artifact.name }); + const artifactDirectory = artifact.id.lastIndexOf('/') > 0 + ? artifact.id.substring(0, artifact.id.lastIndexOf('/')) + : artifact.id; + + const prevArtifact = index > 0 ? artifacts[index - 1] : undefined; + const prevArtifactDirectory = prevArtifact && prevArtifact.id.lastIndexOf('/') > 0 + ? prevArtifact.id.substring(0, prevArtifact.id.lastIndexOf('/')) + : prevArtifact?.id; + + const hideTimestamp = index > 0 && + artifact.timestamp !== undefined && + prevArtifact?.timestamp !== undefined && + artifactDirectory === prevArtifactDirectory && + fromNow(prevArtifact.timestamp) === fromNow(artifact.timestamp); + + artifactsTree.add(artifactUri, { + repository, + group: inputOrElement.artifactGroup, + artifact, + hideTimestamp, + type: 'artifact' + }); + } + + return Iterable.map(artifactsTree.root.children, node => node.element ?? node); } - return Iterable.map(artifactsTree.root.children, node => node.element ?? node); + // Flat list of artifacts + return artifacts.map((artifact, index, artifacts) => ({ + repository, + group: inputOrElement.artifactGroup, + artifact, + hideTimestamp: index > 0 && + artifact.timestamp !== undefined && + artifacts[index - 1].timestamp !== undefined && + fromNow(artifacts[index - 1].timestamp!) === fromNow(artifact.timestamp), + type: 'artifact' + } satisfies SCMArtifactTreeElement)); } else if (isSCMArtifactNode(inputOrElement)) { return Iterable.map(inputOrElement.children, node => node.element && node.childrenCount === 0 ? node.element : node); - } else if (isSCMArtifactTreeElement(inputOrElement)) { } + } return []; } @@ -388,19 +446,25 @@ export class SCMRepositoriesViewPane extends ViewPane { @ISCMViewService private readonly scmViewService: ISCMViewService, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, + @ICommandService private readonly commandService: ICommandService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, - @IHoverService hoverService: IHoverService + @IHoverService hoverService: IHoverService, + @IStorageService private readonly storageService: IStorageService ) { super({ ...options, titleMenuId: MenuId.SCMSourceControlTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService); this.visibleCountObs = observableConfigValue('scm.repositories.visible', 10, this.configurationService); this.providerCountBadgeObs = observableConfigValue<'hidden' | 'auto' | 'visible'>('scm.providerCountBadge', 'hidden', this.configurationService); + this.storageService.onWillSaveState(() => { + this.storeTreeViewState(); + }, this, this._store); + this._register(this.updateChildrenThrottler); } @@ -416,7 +480,8 @@ export class SCMRepositoriesViewPane extends ViewPane { treeContainer.classList.toggle('auto-provider-counts', providerCountBadge === 'auto'); })); - this.createTree(treeContainer); + const viewState = this.loadTreeViewState(); + this.createTree(treeContainer, viewState); this.onDidChangeBodyVisibility(async visible => { if (!visible) { @@ -426,7 +491,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this.treeOperationSequencer.queue(async () => { // Initial rendering - await this.tree.setInput(this.scmViewService); + await this.tree.setInput(this.scmViewService, viewState); // scm.repositories.visible setting this.visibilityDisposables.add(autorun(reader => { @@ -461,6 +526,17 @@ export class SCMRepositoriesViewPane extends ViewPane { for (const repository of this.scmService.repositories) { this.onDidAddRepository(repository); } + + // Expand repository if there is only one + this.visibilityDisposables.add(autorun(async reader => { + const explorerEnabledConfig = this.scmViewService.explorerEnabledConfig.read(reader); + const didFinishLoadingRepositories = this.scmViewService.didFinishLoadingRepositories.read(reader); + + if (viewState === undefined && explorerEnabledConfig && didFinishLoadingRepositories && this.scmViewService.repositories.length === 1) { + await this.treeOperationSequencer.queue(() => + this.tree.expand(this.scmViewService.repositories[0])); + } + })); }); }, this, this._store); } @@ -475,7 +551,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this.tree.domFocus(); } - private createTree(container: HTMLElement): void { + private createTree(container: HTMLElement, viewState?: IAsyncDataTreeViewState): void { this.treeIdentityProvider = new RepositoryTreeIdentityProvider(); this.treeDataSource = this.instantiationService.createInstance(RepositoryTreeDataSource); this._register(this.treeDataSource); @@ -488,8 +564,8 @@ export class SCMRepositoriesViewPane extends ViewPane { new RepositoriesTreeCompressionDelegate(), [ this.instantiationService.createInstance(RepositoryRenderer, MenuId.SCMSourceControlInline, getActionViewItemProvider(this.instantiationService)), - this.instantiationService.createInstance(ArtifactGroupRenderer), - this.instantiationService.createInstance(ArtifactRenderer) + this.instantiationService.createInstance(ArtifactGroupRenderer, getActionViewItemProvider(this.instantiationService)), + this.instantiationService.createInstance(ArtifactRenderer, getActionViewItemProvider(this.instantiationService)) ], this.treeDataSource, { @@ -504,7 +580,10 @@ export class SCMRepositoriesViewPane extends ViewPane { } // Explorer mode - if (isSCMArtifactNode(e)) { + if (viewState?.expanded && (isSCMRepository(e) || isSCMArtifactGroupTreeElement(e) || isSCMArtifactTreeElement(e))) { + // Only expand repositories/artifact groups/artifacts that were expanded before + return viewState.expanded.indexOf(this.treeIdentityProvider.getId(e)) === -1; + } else if (isSCMArtifactNode(e)) { // Only expand artifact folders as they are compressed by default return !(e.childrenCount === 1 && Iterable.first(e.children)?.element === undefined); } else { @@ -541,6 +620,7 @@ export class SCMRepositoriesViewPane extends ViewPane { this.tree.updateOptions({ multipleSelectionSupport: selectionMode === 'multiple' }); })); + this._register(this.tree.onDidOpen(this.onTreeDidOpen, this)); this._register(this.tree.onDidChangeSelection(this.onTreeSelectionChange, this)); this._register(this.tree.onDidChangeFocus(this.onTreeDidChangeFocus, this)); this._register(this.tree.onDidFocus(this.onDidTreeFocus, this)); @@ -585,6 +665,14 @@ export class SCMRepositoriesViewPane extends ViewPane { this.repositoryDisposables.deleteAndDispose(repository); } + private onTreeDidOpen(e: IOpenEvent): void { + if (!e.element || !isSCMArtifactTreeElement(e.element) || !e.element.artifact.command) { + return; + } + + this.commandService.executeCommand(e.element.artifact.command.id, e.element.repository.provider, e.element.artifact); + } + private onTreeContextMenu(e: ITreeContextMenuEvent): void { if (!e.element) { return; @@ -773,6 +861,26 @@ export class SCMRepositoriesViewPane extends ViewPane { }); } + private loadTreeViewState(): IAsyncDataTreeViewState | undefined { + const storageViewState = this.storageService.get('scm.repositoriesViewState', StorageScope.WORKSPACE); + if (!storageViewState) { + return undefined; + } + + try { + const treeViewState = JSON.parse(storageViewState); + return treeViewState; + } catch { + return undefined; + } + } + + private storeTreeViewState(): void { + if (this.tree) { + this.storageService.store('scm.repositoriesViewState', JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } + } + override dispose(): void { this.visibilityDisposables.dispose(); super.dispose(); diff --git a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts index 2eb4daa3b53..fb552ee0e24 100644 --- a/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts +++ b/src/vs/workbench/contrib/scm/browser/scmRepositoryRenderer.ts @@ -89,18 +89,12 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer provider.classList.toggle('active', e)); @@ -202,7 +196,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer('scmCurrentHistoryItemRefInFilter', false), RepositoryCount: new RawContextKey('scmRepositoryCount', 0), RepositoryVisibilityCount: new RawContextKey('scmRepositoryVisibleCount', 0), + SCMInputHasValidationMessage: new RawContextKey('scmInputHasValidationMessage', false), RepositoryVisibility(repository: ISCMRepository) { return new RawContextKey(`scmRepositoryVisible:${repository.provider.id}`, false); } @@ -1065,6 +1054,10 @@ class RepositoryVisibilityActionController { } private onDidAddRepository(repository: ISCMRepository): void { + if (repository.provider.isHidden) { + return; + } + const action = registerAction2(class extends RepositoryVisibilityAction { constructor() { super(repository); @@ -1120,15 +1113,17 @@ class RepositoryVisibilityActionController { } class SetListViewModeAction extends ViewAction { - constructor() { + constructor( + id = 'workbench.scm.action.setListViewMode', + menu: Partial = {}) { super({ - id: 'workbench.scm.action.setListViewMode', + id, title: localize('setListViewMode', "View as List"), viewId: VIEW_PANE_ID, f1: false, icon: Codicon.listTree, toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.List), - menu: { id: Menus.ViewSort, group: '1_viewmode' } + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } }); } @@ -1137,17 +1132,33 @@ class SetListViewModeAction extends ViewAction { } } -class SetTreeViewModeAction extends ViewAction { +class SetListViewModeNavigationAction extends SetListViewModeAction { constructor() { + super( + 'workbench.scm.action.setListViewModeNavigation', + { + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree)), + group: 'navigation', + isHiddenByDefault: true, + order: -1000 + }); + } +} + +class SetTreeViewModeAction extends ViewAction { + constructor( + id = 'workbench.scm.action.setTreeViewMode', + menu: Partial = {}) { super( { - id: 'workbench.scm.action.setTreeViewMode', + id, title: localize('setTreeViewMode', "View as Tree"), viewId: VIEW_PANE_ID, f1: false, icon: Codicon.listFlat, toggled: ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree), - menu: { id: Menus.ViewSort, group: '1_viewmode' } + menu: { id: Menus.ViewSort, group: '1_viewmode', ...menu } }); } @@ -1156,8 +1167,24 @@ class SetTreeViewModeAction extends ViewAction { } } +class SetTreeViewModeNavigationAction extends SetTreeViewModeAction { + constructor() { + super( + 'workbench.scm.action.setTreeViewModeNavigation', + { + id: MenuId.SCMTitle, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_PANE_ID), ContextKeys.RepositoryCount.notEqualsTo(0), ContextKeys.SCMViewMode.isEqualTo(ViewMode.List)), + group: 'navigation', + isHiddenByDefault: true, + order: -1000 + }); + } +} + registerAction2(SetListViewModeAction); registerAction2(SetTreeViewModeAction); +registerAction2(SetListViewModeNavigationAction); +registerAction2(SetTreeViewModeNavigationAction); abstract class RepositorySortAction extends Action2 { constructor(private sortKey: ISCMRepositorySortKey, title: string) { @@ -1217,13 +1244,17 @@ abstract class RepositorySelectionModeAction extends Action2 { menu: [ { id: Menus.Repositories, - when: ContextKeyExpr.greater(ContextKeys.RepositoryCount.key, 1), + when: ContextKeyExpr.and( + ContextKeyExpr.has('scm.providerCount'), + ContextKeyExpr.greater('scm.providerCount', 1)), group: '2_selectionMode', order }, { id: MenuId.SCMSourceControlTitle, - when: ContextKeyExpr.greater(ContextKeys.RepositoryCount.key, 1), + when: ContextKeyExpr.and( + ContextKeyExpr.has('scm.providerCount'), + ContextKeyExpr.greater('scm.providerCount', 1)), group: '2_selectionMode', order }, @@ -1338,6 +1369,31 @@ class ExpandAllRepositoriesAction extends ViewAction { registerAction2(CollapseAllRepositoriesAction); registerAction2(ExpandAllRepositoriesAction); +class CollapseAllAction extends ViewAction { + constructor() { + super({ + id: `workbench.scm.action.collapseAll`, + title: localize('scmCollapseAll', "Collapse All"), + viewId: VIEW_PANE_ID, + f1: false, + icon: Codicon.collapseAll, + menu: { + id: MenuId.SCMResourceGroupContext, + group: '9_collapse', + when: ContextKeys.SCMViewMode.isEqualTo(ViewMode.Tree), + } + }); + } + + async runInView(_accessor: ServicesAccessor, view: SCMViewPane, context?: ISCMResourceGroup): Promise { + if (context) { + view.collapseAllResources(context); + } + } +} + +registerAction2(CollapseAllAction); + const enum SCMInputWidgetCommandId { CancelAction = 'scm.input.cancelAction', SetupAction = 'scm.input.triggerSetup' @@ -1666,6 +1722,7 @@ class SCMInputWidget { private model: { readonly input: ISCMInput; readonly textModel: ITextModel } | undefined; private repositoryIdContextKey: IContextKey; + private validationMessageContextKey: IContextKey; private readonly repositoryDisposables = new DisposableStore(); private validation: IInputValidation | undefined; @@ -1747,6 +1804,7 @@ class SCMInputWidget { this.repositoryDisposables.add(input.onDidChangeFocus(() => this.focus())); this.repositoryDisposables.add(input.onDidChangeValidationMessage((e) => this.setValidation(e, { focus: true, timeout: true }))); this.repositoryDisposables.add(input.onDidChangeValidateInput((e) => triggerValidation())); + this.repositoryDisposables.add(input.onDidClearValidation(() => this.clearValidation())); // Keep API in sync with model and validate this.repositoryDisposables.add(textModel.onDidChangeContent(() => { @@ -1876,6 +1934,7 @@ class SCMInputWidget { this.contextKeyService = contextKeyService.createScoped(this.element); this.repositoryIdContextKey = this.contextKeyService.createKey('scmRepository', undefined); + this.validationMessageContextKey = ContextKeys.SCMInputHasValidationMessage.bindTo(this.contextKeyService); this.inputEditorOptions = new SCMInputWidgetEditorOptions(overflowWidgetsDomNode, this.configurationService); this.disposables.add(this.inputEditorOptions.onDidChange(this.onDidChangeEditorOptions, this)); @@ -2039,6 +2098,7 @@ class SCMInputWidget { return; } + this.validationMessageContextKey.set(true); const disposables = new DisposableStore(); this.validationContextView = this.contextViewService.showContextView({ @@ -2116,6 +2176,7 @@ class SCMInputWidget { this.validationContextView?.close(); this.validationContextView = undefined; this.validationHasFocus = false; + this.validationMessageContextKey.set(false); } dispose(): void { @@ -2407,15 +2468,17 @@ export class SCMViewPane extends ViewPane { overrideStyles: this.getLocationBasedColors().listOverrideStyles, compressionEnabled: compressionEnabled.get(), collapseByDefault: (e: unknown) => { - // Repository, Resource Group, Resource Folder (Tree) - if (isSCMRepository(e) || isSCMResourceGroup(e) || isSCMResourceNode(e)) { - return false; + // Repository, Resource Group, Resource Folder (Tree) are not collapsed by default + return !(isSCMRepository(e) || isSCMResourceGroup(e) || isSCMResourceNode(e)); + }, + accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider), + twistieAdditionalCssClass: (e: unknown) => { + if (isSCMActionButton(e) || isSCMInput(e)) { + return 'force-no-twistie'; } - // History Item Group, History Item, or History Item Change - return (viewState?.expanded ?? []).indexOf(getSCMResourceId(e as TreeElement)) === -1; + return undefined; }, - accessibilityProvider: this.instantiationService.createInstance(SCMAccessibilityProvider) }) as WorkbenchCompressibleAsyncDataTree; this.disposables.add(this.tree); @@ -2816,6 +2879,14 @@ export class SCMViewPane extends ViewPane { } } + collapseAllResources(group: ISCMResourceGroup): void { + for (const { element } of this.tree.getNode(group).children) { + if (!isSCMViewService(element)) { + this.tree.collapse(element, true); + } + } + } + focusPreviousInput(): void { this.treeOperationSequencer.queue(() => this.focusInput(-1)); } @@ -3042,6 +3113,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource r.provider === element.provider); if (!repository) { @@ -3049,6 +3122,8 @@ class SCMTreeDataSource extends Disposable implements IAsyncDataSource; readonly graphShowOutgoingChangesConfig: IObservable; - private didFinishLoading: boolean = false; private didSelectRepository: boolean = false; private previousState: ISCMViewServiceState | undefined; private readonly disposables = new DisposableStore(); @@ -124,20 +123,25 @@ export class SCMViewService implements ISCMViewService { private _repositories: ISCMRepositoryView[] = []; get repositories(): ISCMRepository[] { - return this._repositories.map(r => r.repository); + return this._repositories + .filter(r => r.repository.provider.isHidden !== true) + .map(r => r.repository); } + readonly didFinishLoadingRepositories = observableValue(this, false); + get visibleRepositories(): ISCMRepository[] { // In order to match the legacy behaviour, when the repositories are sorted by discovery time, // the visible repositories are sorted by the selection index instead of the discovery time. if (this._repositoriesSortKey === ISCMRepositorySortKey.DiscoveryTime) { - return this._repositories.filter(r => r.selectionIndex !== -1) + return this._repositories + .filter(r => r.repository.provider.isHidden !== true && r.selectionIndex !== -1) .sort((r1, r2) => r1.selectionIndex - r2.selectionIndex) .map(r => r.repository); } return this._repositories - .filter(r => r.selectionIndex !== -1) + .filter(r => r.repository.provider.isHidden !== true && r.selectionIndex !== -1) .map(r => r.repository); } @@ -190,16 +194,12 @@ export class SCMViewService implements ISCMViewService { const removed = new Set(last.removed); for (const repository of e.added) { - if (removed.has(repository)) { - removed.delete(repository); - } else { + if (!removed.delete(repository)) { added.add(repository); } } for (const repository of e.removed) { - if (added.has(repository)) { - added.delete(repository); - } else { + if (!added.delete(repository)) { removed.add(repository); } } @@ -343,12 +343,12 @@ export class SCMViewService implements ISCMViewService { // or during a profile switch. extensionService.onWillStop(() => { this.onWillSaveState(); - this.didFinishLoading = false; + this.didFinishLoadingRepositories.set(false, undefined); }, this, this.disposables); } private onDidAddRepository(repository: ISCMRepository): void { - if (!this.didFinishLoading) { + if (!this.didFinishLoadingRepositories.get()) { this.eventuallyFinishLoading(); } @@ -358,7 +358,7 @@ export class SCMViewService implements ISCMViewService { let removed: Iterable = Iterable.empty(); - if (this.previousState && !this.didFinishLoading) { + if (this.previousState && !this.didFinishLoadingRepositories.get()) { const index = this.previousState.all.indexOf(getProviderStorageKey(repository.provider)); if (index === -1) { @@ -425,7 +425,7 @@ export class SCMViewService implements ISCMViewService { } private onDidRemoveRepository(repository: ISCMRepository): void { - if (!this.didFinishLoading) { + if (!this.didFinishLoadingRepositories.get()) { this.eventuallyFinishLoading(); } @@ -566,7 +566,8 @@ export class SCMViewService implements ISCMViewService { } private onWillSaveState(): void { - if (!this.didFinishLoading) { // don't remember state, if the workbench didn't really finish loading + if (!this.didFinishLoadingRepositories.get()) { + // Don't remember state, if the workbench didn't really finish loading return; } @@ -583,11 +584,11 @@ export class SCMViewService implements ISCMViewService { } private finishLoading(): void { - if (this.didFinishLoading) { + if (this.didFinishLoadingRepositories.get()) { return; } - this.didFinishLoading = true; + this.didFinishLoadingRepositories.set(true, undefined); } dispose(): void { diff --git a/src/vs/workbench/contrib/scm/common/artifact.ts b/src/vs/workbench/contrib/scm/common/artifact.ts index b70a5220ccc..7a310a13e2e 100644 --- a/src/vs/workbench/contrib/scm/common/artifact.ts +++ b/src/vs/workbench/contrib/scm/common/artifact.ts @@ -7,6 +7,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ISCMRepository } from './scm.js'; +import { Command } from '../../../../editor/common/languages.js'; export interface ISCMArtifactProvider { readonly onDidChangeArtifacts: Event; @@ -18,6 +19,7 @@ export interface ISCMArtifactGroup { readonly id: string; readonly name: string; readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; + readonly supportsFolders?: boolean; } export interface ISCMArtifact { @@ -25,6 +27,8 @@ export interface ISCMArtifact { readonly name: string; readonly description?: string; readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; + readonly timestamp?: number; + readonly command?: Command; } export interface SCMArtifactGroupTreeElement { @@ -37,5 +41,6 @@ export interface SCMArtifactTreeElement { readonly repository: ISCMRepository; readonly group: ISCMArtifactGroup; readonly artifact: ISCMArtifact; + readonly hideTimestamp: boolean; readonly type: 'artifact'; } diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index 64763e49591..921c89b6eee 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -72,7 +72,7 @@ export interface ISCMHistoryItem { readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; readonly references?: ISCMHistoryItemRef[]; - readonly tooltip?: string | IMarkdownString | undefined; + readonly tooltip?: IMarkdownString | Array | undefined; } export interface ISCMHistoryItemGraphNode { diff --git a/src/vs/workbench/contrib/scm/common/quickDiff.ts b/src/vs/workbench/contrib/scm/common/quickDiff.ts index bc994f648cb..543a3b69204 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiff.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiff.ts @@ -66,12 +66,14 @@ export const editorGutterItemGlyphForeground = registerColor('editorGutter.itemG ); export const editorGutterItemBackground = registerColor('editorGutter.itemBackground', { dark: opaque(listInactiveSelectionBackground, editorBackground), light: darken(opaque(listInactiveSelectionBackground, editorBackground), .05), hcDark: Color.white, hcLight: Color.black }, nls.localize('editorGutterItemBackground', 'Editor gutter decoration color for gutter item background. This color should be opaque.')); +type QuickDiffProviderKind = 'primary' | 'secondary' | 'contributed'; + export interface QuickDiffProvider { readonly id: string; readonly label: string; readonly rootUri: URI | undefined; readonly selector?: LanguageSelector; - readonly kind: 'primary' | 'secondary' | 'contributed'; + readonly kind: QuickDiffProviderKind; getOriginalResource(uri: URI): Promise; } @@ -79,7 +81,7 @@ export interface QuickDiff { readonly id: string; readonly label: string; readonly originalResource: URI; - readonly kind: 'primary' | 'secondary' | 'contributed'; + readonly kind: QuickDiffProviderKind; } export interface QuickDiffChange { @@ -91,6 +93,8 @@ export interface QuickDiffChange { } export interface QuickDiffResult { + readonly providerId: string; + readonly providerKind: QuickDiffProviderKind; readonly original: URI; readonly modified: URI; readonly changes: IChange[]; diff --git a/src/vs/workbench/contrib/scm/common/quickDiffService.ts b/src/vs/workbench/contrib/scm/common/quickDiffService.ts index 6759d4e3560..c354555653a 100644 --- a/src/vs/workbench/contrib/scm/common/quickDiffService.ts +++ b/src/vs/workbench/contrib/scm/common/quickDiffService.ts @@ -152,5 +152,6 @@ export class QuickDiffService extends Disposable implements IQuickDiffService { export async function getOriginalResource(quickDiffService: IQuickDiffService, uri: URI, language: string | undefined, isSynchronized: boolean | undefined): Promise { const quickDiffs = await quickDiffService.getQuickDiffs(uri, language, isSynchronized); - return quickDiffs.length > 0 ? quickDiffs[0].originalResource : null; + const primaryQuickDiffs = quickDiffs.find(quickDiff => quickDiff.kind === 'primary'); + return primaryQuickDiffs ? primaryQuickDiffs.originalResource : null; } diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 1804b3d1646..1bf661ed072 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -82,6 +82,7 @@ export interface ISCMProvider extends IDisposable { readonly rootUri?: URI; readonly iconPath?: URI | { light: URI; dark: URI } | ThemeIcon; + readonly isHidden?: boolean; readonly inputBoxTextModel: ITextModel; readonly contextValue: IObservable; readonly count: IObservable; @@ -162,6 +163,9 @@ export interface ISCMInput { showValidationMessage(message: string | IMarkdownString, type: InputValidationType): void; readonly onDidChangeValidationMessage: Event; + clearValidation(): void; + readonly onDidClearValidation: Event; + showNextHistoryValue(): void; showPreviousHistoryValue(): void; } @@ -237,6 +241,7 @@ export interface ISCMViewService { repositories: ISCMRepository[]; readonly onDidChangeRepositories: Event; + readonly didFinishLoadingRepositories: IObservable; visibleRepositories: readonly ISCMRepository[]; readonly onDidChangeVisibleRepositories: Event; diff --git a/src/vs/workbench/contrib/scm/common/scmService.ts b/src/vs/workbench/contrib/scm/common/scmService.ts index bd3c282b499..8b6e1a13559 100644 --- a/src/vs/workbench/contrib/scm/common/scmService.ts +++ b/src/vs/workbench/contrib/scm/common/scmService.ts @@ -86,6 +86,13 @@ class SCMInput extends Disposable implements ISCMInput { private readonly _onDidChangeValidationMessage = new Emitter(); readonly onDidChangeValidationMessage: Event = this._onDidChangeValidationMessage.event; + clearValidation(): void { + this._onDidClearValidation.fire(); + } + + private readonly _onDidClearValidation = new Emitter(); + readonly onDidClearValidation: Event = this._onDidClearValidation.event; + private _validateInput: IInputValidator = () => Promise.resolve(undefined); get validateInput(): IInputValidator { @@ -430,6 +437,10 @@ export class SCMService implements ISCMService { let bestMatchLength = Number.POSITIVE_INFINITY; for (const repository of this.repositories) { + if (repository.provider.isHidden === true) { + continue; + } + const root = repository.provider.rootUri; if (!root) { diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index d322704bd79..6dadccb53f4 100644 --- a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ColorIdentifier } from '../../../../../platform/theme/common/colorUtils.js'; import { colorRegistry, historyItemBaseRefColor, historyItemRefColor, historyItemRemoteRefColor, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; -import { ISCMHistoryItem, ISCMHistoryItemRef } from '../../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef, SCMIncomingHistoryItemId } from '../../common/history.js'; function toSCMHistoryItem(id: string, parentIds: string[], references?: ISCMHistoryItemRef[]): ISCMHistoryItem { return { id, parentIds, subject: '', message: '', references } satisfies ISCMHistoryItem; @@ -593,4 +593,371 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'h'); assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemBaseRefColor); }); + + /** + * * a(b) [origin/main] + * * b(e) + * | * c(d) [main] + * | * d(e) + * |/ + * * e(f) + * * f(g) + */ + test('graph with incoming/outgoing changes (remote ref first)', () => { + const models = [ + toSCMHistoryItem('a', ['b'], [{ id: 'origin/main', name: 'origin/main' }]), + toSCMHistoryItem('b', ['e']), + toSCMHistoryItem('c', ['d'], [{ id: 'main', name: 'main' }]), + toSCMHistoryItem('d', ['e']), + toSCMHistoryItem('e', ['f']), + toSCMHistoryItem('f', ['g']), + ] satisfies ISCMHistoryItem[]; + + const colorMap = new Map([ + ['origin/main', historyItemRemoteRefColor], + ['main', historyItemRefColor] + ]); + + const viewModels = toISCMHistoryItemViewModelArray( + models, + colorMap, + { id: 'main', name: 'main', revision: 'c' }, + { id: 'origin/main', name: 'origin/main', revision: 'a' }, + undefined, + true, + true, + 'e' + ); + + assert.strictEqual(viewModels.length, 8); + + // node a + assert.strictEqual(viewModels[0].kind, 'node'); + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemRemoteRefColor); + + // node b + assert.strictEqual(viewModels[1].kind, 'node'); + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemRemoteRefColor); + + // outgoing changes node + assert.strictEqual(viewModels[2].kind, 'outgoing-changes'); + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, historyItemRefColor); + + // node c + assert.strictEqual(viewModels[3].kind, 'HEAD'); + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'c'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, historyItemRefColor); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, historyItemRefColor); + + // node d + assert.strictEqual(viewModels[4].kind, 'node'); + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, SCMIncomingHistoryItemId); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, historyItemRefColor); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, SCMIncomingHistoryItemId); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemRefColor); + + // incoming changes node + assert.strictEqual(viewModels[5].kind, 'incoming-changes'); + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, SCMIncomingHistoryItemId); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemRefColor); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemRefColor); + + // node e + assert.strictEqual(viewModels[6].kind, 'node'); + assert.strictEqual(viewModels[6].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, historyItemRefColor); + + assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, historyItemRemoteRefColor); + + // node f + assert.strictEqual(viewModels[7].kind, 'node'); + assert.strictEqual(viewModels[7].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[7].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[7].inputSwimlanes[0].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[7].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[7].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[7].outputSwimlanes[0].color, historyItemRemoteRefColor); + }); + + /** + * * a(b) [main] + * * b(e) + * | * c(d) [origin/main] + * | * d(e) + * |/ + * * e(f) + * * f(g) + */ + test('graph with incoming/outgoing changes (local ref first)', () => { + const models = [ + toSCMHistoryItem('a', ['b'], [{ id: 'main', name: 'main' }]), + toSCMHistoryItem('b', ['e']), + toSCMHistoryItem('c', ['d'], [{ id: 'origin/main', name: 'origin/main' }]), + toSCMHistoryItem('d', ['e']), + toSCMHistoryItem('e', ['f']), + toSCMHistoryItem('f', ['g']), + ] satisfies ISCMHistoryItem[]; + + const colorMap = new Map([ + ['origin/main', historyItemRemoteRefColor], + ['main', historyItemRefColor] + ]); + + const viewModels = toISCMHistoryItemViewModelArray( + models, + colorMap, + { id: 'main', name: 'main', revision: 'a' }, + { id: 'origin/main', name: 'origin/main', revision: 'c' }, + undefined, + true, + true, + 'e' + ); + + assert.strictEqual(viewModels.length, 8); + + // outgoing changes node + assert.strictEqual(viewModels[0].kind, 'outgoing-changes'); + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'a'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemRefColor); + + // node a + assert.strictEqual(viewModels[1].kind, 'HEAD'); + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'a'); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemRefColor); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemRefColor); + + // node b + assert.strictEqual(viewModels[2].kind, 'node'); + assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemRefColor); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemRefColor); + + // node c + assert.strictEqual(viewModels[3].kind, 'node'); + assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemRefColor); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, historyItemRemoteRefColor); + + // node d + assert.strictEqual(viewModels[4].kind, 'node'); + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[4].outputSwimlanes[1].id, SCMIncomingHistoryItemId); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemRemoteRefColor); + + // incoming changes node + assert.strictEqual(viewModels[5].kind, 'incoming-changes'); + assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[5].inputSwimlanes[1].id, SCMIncomingHistoryItemId); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemRemoteRefColor); + + // node e + assert.strictEqual(viewModels[6].kind, 'node'); + assert.strictEqual(viewModels[6].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[6].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[6].inputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[6].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[6].inputSwimlanes[1].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[6].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[6].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[6].outputSwimlanes[0].color, historyItemRefColor); + + // node f + assert.strictEqual(viewModels[7].kind, 'node'); + assert.strictEqual(viewModels[7].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[7].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[7].inputSwimlanes[0].color, historyItemRefColor); + + assert.strictEqual(viewModels[7].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[7].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[7].outputSwimlanes[0].color, historyItemRefColor); + }); + + /** + * * a(b) [origin/main] + * * b(c,d) + * |\ + * | * c(e) [main] + * * | d(e) + * |/ + * * e(f) + * * f(g) + */ + test('graph with merged incoming changes', () => { + const models = [ + toSCMHistoryItem('a', ['b'], [{ id: 'origin/main', name: 'origin/main' }]), + toSCMHistoryItem('b', ['c', 'd']), + toSCMHistoryItem('c', ['e'], [{ id: 'main', name: 'main' }]), + toSCMHistoryItem('d', ['e']), + toSCMHistoryItem('e', ['f']), + toSCMHistoryItem('f', ['g']), + ] satisfies ISCMHistoryItem[]; + + const colorMap = new Map([ + ['origin/main', historyItemRemoteRefColor], + ['main', historyItemRefColor] + ]); + + const viewModels = toISCMHistoryItemViewModelArray( + models, + colorMap, + { id: 'main', name: 'main', revision: 'c' }, + { id: 'origin/main', name: 'origin/main', revision: 'a' }, + undefined, + true, + true, + 'c' + ); + + assert.strictEqual(viewModels.length, 6); + + // node a + assert.strictEqual(viewModels[0].kind, 'node'); + assert.strictEqual(viewModels[0].inputSwimlanes.length, 0); + + assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemRemoteRefColor); + + // node b + assert.strictEqual(viewModels[1].kind, 'node'); + assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemRemoteRefColor); + + assert.strictEqual(viewModels[1].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[1].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[1].outputSwimlanes[1].color, colorRegistry[0]); + + // node c + assert.strictEqual(viewModels[2].kind, 'HEAD'); + assert.strictEqual(viewModels[2].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemRemoteRefColor); + assert.strictEqual(viewModels[2].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].inputSwimlanes[1].color, colorRegistry[0]); + + assert.strictEqual(viewModels[2].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[2].outputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[2].outputSwimlanes[1].color, colorRegistry[0]); + + // node d + assert.strictEqual(viewModels[3].kind, 'node'); + assert.strictEqual(viewModels[3].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[3].inputSwimlanes[1].id, 'd'); + assert.strictEqual(viewModels[3].inputSwimlanes[1].color, colorRegistry[0]); + + assert.strictEqual(viewModels[3].outputSwimlanes.length, 2); + assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[3].outputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[3].outputSwimlanes[1].color, colorRegistry[0]); + + // node e + assert.strictEqual(viewModels[4].kind, 'node'); + assert.strictEqual(viewModels[4].inputSwimlanes.length, 2); + assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemRefColor); + assert.strictEqual(viewModels[4].inputSwimlanes[1].id, 'e'); + assert.strictEqual(viewModels[4].inputSwimlanes[1].color, colorRegistry[0]); + + assert.strictEqual(viewModels[4].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemRefColor); + + // node f + assert.strictEqual(viewModels[5].kind, 'node'); + assert.strictEqual(viewModels[5].inputSwimlanes.length, 1); + assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'f'); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemRefColor); + + assert.strictEqual(viewModels[5].outputSwimlanes.length, 1); + assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'g'); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemRefColor); + }); }); diff --git a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts index 7d451a4cf68..8d449a07b3e 100644 --- a/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/anythingQuickAccess.ts @@ -52,6 +52,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri import { stripIcons } from '../../../../base/common/iconLabels.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { ASK_QUICK_QUESTION_ACTION_ID } from '../../chat/browser/actions/chatQuickInputActions.js'; import { IQuickChatService } from '../../chat/browser/chat.js'; @@ -136,6 +137,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider p.helpEntries.some(h => h.commandCenterOrder !== undefined)) .flatMap(provider => provider.helpEntries .filter(h => h.commandCenterOrder !== undefined) diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 814a993312e..e493fe31c92 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -27,6 +27,7 @@ left: 0; width: 16px; height: 100%; + border-radius: var(--vscode-cornerRadius-small); color: inherit; box-sizing: border-box; background-position: center center; diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 350fa0ad053..2383e0c60f5 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -228,6 +228,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { this.inputBox.focus(); } })); + controlsDiv.appendChild(this.useSearchInEditorsBox.domNode); super.renderSubcontrols(controlsDiv); } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 2bd5c7eabdf..5d68510bdae 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -93,7 +93,7 @@ const quickAccessRegistry = Registry.as(QuickAccessExtensi quickAccessRegistry.registerQuickAccessProvider({ ctor: AnythingQuickAccessProvider, prefix: AnythingQuickAccessProvider.PREFIX, - placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.PREFIX, GotoSymbolQuickAccessProvider.PREFIX), + placeholder: nls.localize('anythingQuickAccessPlaceholder', "Search files by name (append {0} to go to line or {1} to go to symbol)", AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX, GotoSymbolQuickAccessProvider.PREFIX), contextKey: defaultQuickAccessContextKeyValue, helpEntries: [{ description: nls.localize('anythingQuickAccess', "Go to File"), diff --git a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts index 1f37b942b83..7a180f06375 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from '../../../../base/browser/dom.js'; -import { ResolvedKeybinding } from '../../../../base/common/keybindings.js'; import * as nls from '../../../../nls.js'; import { WorkbenchCompressibleAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; @@ -20,10 +19,6 @@ export function isSearchViewFocused(viewsService: IViewsService): boolean { return !!(searchView && DOM.isAncestorOfActiveElement(searchView.getContainer())); } -export function appendKeyBindingLabel(label: string, inputKeyBinding: ResolvedKeybinding | undefined): string { - return doAppendKeyBindingLabel(label, inputKeyBinding); -} - export function getSearchView(viewsService: IViewsService): SearchView | undefined { return viewsService.getActiveViewWithId(VIEW_ID) as SearchView; } @@ -68,8 +63,3 @@ function hasDownstreamMatch(elements: RenderableMatch[], focusElement: Renderabl export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); } - -function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string { - return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label; -} - diff --git a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts index ab06c9be1b9..a86e84e2849 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts @@ -14,7 +14,7 @@ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { category, getSearchView } from './searchActionsBase.js'; import { isWindows } from '../../../../base/common/platform.js'; import { searchMatchComparer } from './searchCompare.js'; -import { RenderableMatch, ISearchTreeMatch, isSearchTreeMatch, ISearchTreeFileMatch, ISearchTreeFolderMatch, ISearchTreeFolderMatchWithResource, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchWithResource } from './searchTreeModel/searchTreeCommon.js'; +import { RenderableMatch, ISearchTreeMatch, isSearchTreeMatch, ISearchTreeFileMatch, ISearchTreeFolderMatch, ISearchTreeFolderMatchWithResource, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchWithResource, isTextSearchHeading } from './searchTreeModel/searchTreeCommon.js'; //#region Actions registerAction2(class CopyMatchCommandAction extends Action2 { @@ -94,8 +94,8 @@ registerAction2(class CopyAllCommandAction extends Action2 { } - override async run(accessor: ServicesAccessor): Promise { - await copyAllCommand(accessor); + override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise { + await copyAllCommand(accessor, match); } }); @@ -177,7 +177,7 @@ async function copyMatchCommand(accessor: ServicesAccessor, match: RenderableMat } } -async function copyAllCommand(accessor: ServicesAccessor) { +async function copyAllCommand(accessor: ServicesAccessor, match: RenderableMatch | undefined | null) { const viewsService = accessor.get(IViewsService); const clipboardService = accessor.get(IClipboardService); const labelService = accessor.get(ILabelService); @@ -185,8 +185,13 @@ async function copyAllCommand(accessor: ServicesAccessor) { const searchView = getSearchView(viewsService); if (searchView) { const root = searchView.searchResult; + const isAISearchElement = isAISearchResult(match); + + if (!match) { + match = getSelectedRow(accessor); + } - const text = allFolderMatchesToString(root.folderMatches(), labelService); + const text = allFolderMatchesToString(root.folderMatches(isAISearchElement), labelService); await clipboardService.writeText(text); } } @@ -274,4 +279,28 @@ function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined return searchView?.getControl().getSelection()[0]; } +function isAISearchResult(element: RenderableMatch | undefined | null): boolean { + if (!element) { + return false; + } + + if (isSearchTreeMatch(element)) { + return element.parent().parent().isAIContributed(); + } + + if (isSearchTreeFileMatch(element)) { + return element.parent().isAIContributed(); + } + + if (isSearchTreeFolderMatch(element)) { + return element.isAIContributed(); + } + + if (isTextSearchHeading(element)) { + return element.isAIContributed; + } + + return false; +} + //#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchChatContext.ts b/src/vs/workbench/contrib/search/browser/searchChatContext.ts index ce0f46ef2d0..1a463744b4c 100644 --- a/src/vs/workbench/contrib/search/browser/searchChatContext.ts +++ b/src/vs/workbench/contrib/search/browser/searchChatContext.ts @@ -13,8 +13,8 @@ import { ILabelService } from '../../../../platform/label/common/label.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType, VIEW_ID } from '../../../services/search/common/search.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextValueItem, picksWithPromiseFn } from '../../chat/browser/chatContextPickService.js'; -import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../../chat/common/chatVariableEntries.js'; +import { IChatContextPickerItem, IChatContextPickerPickItem, IChatContextPickService, IChatContextValueItem, picksWithPromiseFn } from '../../chat/browser/attachments/chatContextPickService.js'; +import { IChatRequestVariableEntry, ISymbolVariableEntry } from '../../chat/common/attachments/chatVariableEntries.js'; import { SearchContext } from '../common/constants.js'; import { SearchView } from './searchView.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; @@ -236,7 +236,7 @@ export async function searchFilesAndFolders( configurationService: IConfigurationService, searchService: ISearchService ): Promise<{ folders: URI[]; files: URI[] }> { - const segmentMatchPattern = caseInsensitiveGlobPattern(fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern)); + const segmentMatchPattern = fuzzyMatch ? fuzzyMatchingGlobPattern(pattern) : continousMatchingGlobPattern(pattern); const searchExcludePattern = getExcludes(configurationService.getValue({ resource: workspace })) || {}; const searchOptions: IFileQuery = { @@ -249,6 +249,7 @@ export async function searchFilesAndFolders( cacheKey, excludePattern: searchExcludePattern, sortByScore: true, + ignoreGlobCase: true, }; let searchResult: ISearchComplete | undefined; @@ -284,19 +285,6 @@ function continousMatchingGlobPattern(pattern: string): string { return '*' + pattern + '*'; } -function caseInsensitiveGlobPattern(pattern: string): string { - let caseInsensitiveFilePattern = ''; - for (let i = 0; i < pattern.length; i++) { - const char = pattern[i]; - if (/[a-zA-Z]/.test(char)) { - caseInsensitiveFilePattern += `[${char.toLowerCase()}${char.toUpperCase()}]`; - } else { - caseInsensitiveFilePattern += char; - } - } - return caseInsensitiveFilePattern; -} - // TODO: remove this and have support from the search service function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMatchPattern: string): URI[] { const uniqueFolders = new ResourceSet(); @@ -318,7 +306,7 @@ function getMatchingFoldersFromFiles(resources: URI[], workspace: URI, segmentMa for (const folderResource of uniqueFolders) { const stats = folderResource.path.split('/'); const dirStat = stats[stats.length - 1]; - if (!dirStat || !glob.match(segmentMatchPattern, dirStat)) { + if (!dirStat || !glob.match(segmentMatchPattern, dirStat, { ignoreCase: true })) { continue; } diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/folderMatch.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/folderMatch.ts index 22eead9d1c4..7e0db52044e 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/folderMatch.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/folderMatch.ts @@ -225,9 +225,7 @@ export class FolderMatchImpl extends Disposable implements ISearchTreeFolderMatc doAddFile(fileMatch: ISearchTreeFileMatch): void { this._fileMatches.set(fileMatch.resource, fileMatch); - if (this._unDisposedFileMatches.has(fileMatch.resource)) { - this._unDisposedFileMatches.delete(fileMatch.resource); - } + this._unDisposedFileMatches.delete(fileMatch.resource); } hasOnlyReadOnlyMatches(): boolean { @@ -264,9 +262,7 @@ export class FolderMatchImpl extends Disposable implements ISearchTreeFolderMatc this._folderMatches.set(folderMatch.resource, folderMatch); this._folderMatchesMap.set(folderMatch.resource, folderMatch); - if (this._unDisposedFolderMatches.has(folderMatch.resource)) { - this._unDisposedFolderMatches.delete(folderMatch.resource); - } + this._unDisposedFolderMatches.delete(folderMatch.resource); } private async batchReplace(matches: (ISearchTreeFileMatch | ISearchTreeFolderMatchWithResource)[]): Promise { diff --git a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts index 4a0814891c8..60c3e91e875 100644 --- a/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts +++ b/src/vs/workbench/contrib/search/browser/searchTreeModel/searchResult.ts @@ -13,7 +13,7 @@ import { IProgress, IProgressStep } from '../../../../../platform/progress/commo import { NotebookEditorWidget } from '../../../notebook/browser/notebookEditorWidget.js'; import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js'; import { IAITextQuery, IFileMatch, ISearchComplete, ITextQuery, QueryType } from '../../../../services/search/common/search.js'; -import { arrayContainsElementOrParent, IChangeEvent, ISearchTreeFileMatch, ISearchTreeFolderMatch, IPlainTextSearchHeading, ISearchModel, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchWithResource, isSearchTreeMatch, isTextSearchHeading, ITextSearchHeading, mergeSearchResultEvents, RenderableMatch, SEARCH_RESULT_PREFIX } from './searchTreeCommon.js'; +import { arrayContainsElementOrParent, IChangeEvent, ISearchTreeFileMatch, ISearchTreeFolderMatch, IPlainTextSearchHeading, ISearchModel, ISearchResult, isSearchTreeFileMatch, isSearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, isSearchTreeFolderMatchWithResource, isSearchTreeMatch, isTextSearchHeading, ITextSearchHeading, mergeSearchResultEvents, RenderableMatch, SEARCH_RESULT_PREFIX } from './searchTreeCommon.js'; import { RangeHighlightDecorations } from './rangeDecorations.js'; import { PlainTextSearchHeadingImpl } from './textSearchHeading.js'; @@ -113,13 +113,18 @@ export class SearchResultImpl extends Disposable implements ISearchResult { if (!arrayContainsElementOrParent(currentElement, removedElems)) { if (isTextSearchHeading(currentElement)) { currentElement.hide(); - } else if (!isSearchTreeFolderMatch(currentElement) || isSearchTreeFolderMatchWithResource(currentElement)) { + } else if (!isSearchTreeFolderMatch(currentElement) || isSearchTreeFolderMatchWithResource(currentElement) || isSearchTreeFolderMatchNoRoot(currentElement)) { if (isSearchTreeFileMatch(currentElement)) { currentElement.parent().remove(currentElement); } else if (isSearchTreeMatch(currentElement)) { currentElement.parent().remove(currentElement); } else if (isSearchTreeFolderMatchWithResource(currentElement)) { currentElement.parent().remove(currentElement); + } else if (isSearchTreeFolderMatchNoRoot(currentElement)) { + const parent = currentElement.parent(); + if (isTextSearchHeading(parent)) { + parent.remove(currentElement); + } } removedElems.push(currentElement); } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index c90211bf5f9..fb52bbb7553 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -13,7 +13,8 @@ import { Delayer, RunOnceScheduler, Throttler } from '../../../../base/common/as import * as errors from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { isLinux } from '../../../../base/common/platform.js'; import * as strings from '../../../../base/common/strings.js'; import { URI } from '../../../../base/common/uri.js'; import * as network from '../../../../base/common/network.js'; @@ -56,7 +57,6 @@ import { Memento } from '../../../common/memento.js'; import { IViewDescriptorService } from '../../../common/views.js'; import { NotebookEditor } from '../../notebook/browser/notebookEditor.js'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from './patternInputWidget.js'; -import { appendKeyBindingLabel } from './searchActionsBase.js'; import { IFindInFilesArgs } from './searchActionsFind.js'; import { searchDetailsIcon } from './searchIcons.js'; import { renderSearchMessage } from './searchMessage.js'; @@ -173,6 +173,7 @@ export class SearchView extends ViewPane { private resultsElement!: HTMLElement; private currentSelectedFileMatch: ISearchTreeFileMatch | undefined; + private readonly currentEditorCursorListener = this._register(new MutableDisposable()); private delayedRefresh: Delayer; private changedWhileHidden: boolean; @@ -1043,6 +1044,15 @@ export class SearchView extends ViewPane { this.searchResultHeaderFocused.reset(); this.isEditableItem.reset(); })); + + // Setup cursor position monitoring to clear selected match when cursor moves + this._register(this.editorService.onDidActiveEditorChange(() => { + const editor = getCodeEditor(this.editorService.activeTextEditorControl); + this.currentEditorCursorListener.value = editor?.onDidChangeCursorPosition(() => { + this.currentSelectedFileMatch?.setSelectedMatch(null); + this.currentSelectedFileMatch = undefined; + }); + })); } private onContextMenu(e: ITreeContextMenuEvent): void { @@ -1618,6 +1628,7 @@ export class SearchView extends ViewPane { maxResults: this.searchConfig.maxResults ?? undefined, disregardIgnoreFiles: !useExcludesAndIgnoreFiles || undefined, disregardExcludeSettings: !useExcludesAndIgnoreFiles || undefined, + ignoreGlobCase: !isLinux || undefined, onlyOpenEditors: onlySearchInOpenEditors, excludePattern, includePattern, @@ -1726,6 +1737,20 @@ export class SearchView extends ViewPane { } } + private appendSearchWithAIButton(messageEl: HTMLElement) { + const searchWithAIButtonTooltip = this.keybindingService.appendKeybinding( + nls.localize('triggerAISearch.tooltip', "Search with AI."), + Constants.SearchCommandIds.SearchWithAIActionId + ); + const searchWithAIButtonText = nls.localize('searchWithAIButtonTooltip', "Search with AI"); + const searchWithAIButton = this.messageDisposables.add(new SearchLinkButton( + searchWithAIButtonText, + () => { + this.commandService.executeCommand(Constants.SearchCommandIds.SearchWithAIActionId); + }, this.hoverService, searchWithAIButtonTooltip)); + dom.append(messageEl, searchWithAIButton.element); + } + private async onSearchComplete( progressComplete: () => void, excludePatternText?: string, @@ -1761,29 +1786,6 @@ export class SearchView extends ViewPane { } - if (this.shouldShowAIResults() && !allResults) { - const messageEl = this.clearMessage(); - const noResultsMessage = nls.localize('noResultsFallback', "No results found. "); - dom.append(messageEl, noResultsMessage); - - - const searchWithAIButtonTooltip = appendKeyBindingLabel( - nls.localize('triggerAISearch.tooltip', "Search with AI."), - this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.SearchWithAIActionId) - ); - const searchWithAIButtonText = nls.localize('searchWithAIButtonTooltip', "Search with AI."); - const searchWithAIButton = this.messageDisposables.add(new SearchLinkButton( - searchWithAIButtonText, - () => { - this.commandService.executeCommand(Constants.SearchCommandIds.SearchWithAIActionId); - }, this.hoverService, searchWithAIButtonTooltip)); - dom.append(messageEl, searchWithAIButton.element); - - if (!aiResults) { - return; - } - } - if (!allResults) { const hasExcludes = !!excludePatternText; const hasIncludes = !!includePatternText; @@ -1799,7 +1801,7 @@ export class SearchView extends ViewPane { } else if (hasExcludes) { message = nls.localize('noOpenEditorResultsExcludes', "No results found in open editors excluding '{0}' - ", excludePatternText); } else { - message = nls.localize('noOpenEditorResultsFound', "No results found in open editors. Review your settings for configured exclusions and check your gitignore files - "); + message = nls.localize('noOpenEditorResultsFound', "No results found in open editors. Review your configured exclusions and check your gitignore files - "); } } else { if (hasIncludes && hasExcludes) { @@ -1809,7 +1811,7 @@ export class SearchView extends ViewPane { } else if (hasExcludes) { message = nls.localize('noResultsExcludes', "No results found excluding '{0}' - ", excludePatternText); } else { - message = nls.localize('noResultsFound', "No results found. Review your settings for configured exclusions and check your gitignore files - "); + message = nls.localize('noResultsFound', "No results found. Review your configured exclusions and check your gitignore files - "); } } @@ -1819,6 +1821,11 @@ export class SearchView extends ViewPane { const messageEl = this.clearMessage(); dom.append(messageEl, message); + if (this.shouldShowAIResults()) { + this.appendSearchWithAIButton(messageEl); + dom.append(messageEl, $('span', undefined, ' - ')); + } + if (!completed) { const searchAgainButton = this.messageDisposables.add(new SearchLinkButton( nls.localize('rerunSearch.message', "Search again"), @@ -1832,13 +1839,6 @@ export class SearchView extends ViewPane { dom.append(messageEl, openSettingsButton.element); } - if (completed) { - dom.append(messageEl, $('span', undefined, ' - ')); - - const learnMoreButton = this.messageDisposables.add(new SearchLinkButton(nls.localize('openSettings.learnMore', "Learn More"), this.onLearnMore.bind(this), this.hoverService)); - dom.append(messageEl, learnMoreButton.element); - } - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { this.showSearchWithoutFolderMessage(); } @@ -1992,10 +1992,6 @@ export class SearchView extends ViewPane { this.preferencesService.openUserSettings(options); } - private onLearnMore(): void { - this.openerService.open(URI.parse('https://go.microsoft.com/fwlink/?linkid=853977')); - } - private onSearchAgain(): void { this.inputPatternExcludes.setValue(''); this.inputPatternIncludes.setValue(''); @@ -2044,15 +2040,20 @@ export class SearchView extends ViewPane { dom.append(messageEl, ' - '); - const openInEditorTooltip = appendKeyBindingLabel( + const openInEditorTooltip = this.keybindingService.appendKeybinding( nls.localize('openInEditor.tooltip', "Copy current search results to an editor"), - this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.OpenInEditorCommandId)); + Constants.SearchCommandIds.OpenInEditorCommandId); const openInEditorButton = this.messageDisposables.add(new SearchLinkButton( nls.localize('openInEditor.message', "Open in editor"), () => this.instantiationService.invokeFunction(createEditorFromSearchResult, this.searchResult, this.searchIncludePattern.getValue(), this.searchExcludePattern.getValue(), this.searchIncludePattern.onlySearchInOpenEditors()), this.hoverService, openInEditorTooltip)); dom.append(messageEl, openInEditorButton.element); + if (this.shouldShowAIResults()) { + dom.append(messageEl, ' - '); + this.appendSearchWithAIButton(messageEl); + } + this.reLayout(); } else if (!msgWasHidden) { dom.hide(this.messagesElement); diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index aecf245b582..dc22006c34a 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -26,7 +26,7 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { ISearchConfigurationProperties } from '../../../services/search/common/search.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { ContextScopedReplaceInput } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; -import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from './searchActionsBase.js'; +import { isSearchViewFocused, getSearchView } from './searchActionsBase.js'; import * as Constants from '../common/constants.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { isMacintosh } from '../../../../base/common/platform.js'; @@ -84,11 +84,11 @@ class ReplaceAllAction extends Action { this._searchWidget = searchWidget; } - override run(): Promise { + override run(): Promise { if (this._searchWidget) { return this._searchWidget.triggerReplaceAll(); } - return Promise.resolve(null); + return Promise.resolve(); } } @@ -117,8 +117,7 @@ export class SearchWidget extends Widget { private static readonly REPLACE_ALL_DISABLED_LABEL = nls.localize('search.action.replaceAll.disabled.label', "Replace All (Submit Search to Enable)"); private static readonly REPLACE_ALL_ENABLED_LABEL = (keyBindingService2: IKeybindingService): string => { - const kb = keyBindingService2.lookupKeybinding(ReplaceAllAction.ID); - return appendKeyBindingLabel(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), kb); + return keyBindingService2.appendKeybinding(nls.localize('search.action.replaceAll.enabled.label', "Replace All"), ReplaceAllAction.ID); }; domNode: HTMLElement | undefined; @@ -400,9 +399,9 @@ export class SearchWidget extends Widget { label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search'), validation: (value: string) => this.validateSearchInput(value), placeholder: nls.localize('search.placeHolder', "Search"), - appendCaseSensitiveLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.ToggleCaseSensitiveCommandId)), - appendWholeWordsLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.ToggleWholeWordCommandId)), - appendRegexLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.ToggleRegexCommandId)), + appendCaseSensitiveLabel: this.keybindingService.appendKeybinding('', Constants.SearchCommandIds.ToggleCaseSensitiveCommandId), + appendWholeWordsLabel: this.keybindingService.appendKeybinding('', Constants.SearchCommandIds.ToggleWholeWordCommandId), + appendRegexLabel: this.keybindingService.appendKeybinding('', Constants.SearchCommandIds.ToggleRegexCommandId), history: new Set(history), showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, @@ -465,7 +464,7 @@ export class SearchWidget extends Widget { this.showContextToggle = new Toggle({ isChecked: false, - title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keybindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId)), + title: this.keybindingService.appendKeybinding(nls.localize('showContext', "Toggle Context Lines"), ToggleSearchEditorContextLinesCommandId), icon: searchShowContextIcon, hoverLifecycleOptions, ...defaultToggleStyles @@ -513,7 +512,7 @@ export class SearchWidget extends Widget { this.replaceInput = this._register(new ContextScopedReplaceInput(replaceBox, this.contextViewService, { label: nls.localize('label.Replace', 'Replace: Type replace term and press Enter to preview'), placeholder: nls.localize('search.replace.placeHolder', "Replace"), - appendPreserveCaseLabel: appendKeyBindingLabel('', this.keybindingService.lookupKeybinding(Constants.SearchCommandIds.TogglePreserveCaseId)), + appendPreserveCaseLabel: this.keybindingService.appendKeybinding('', Constants.SearchCommandIds.TogglePreserveCaseId), history: new Set(options.replaceHistory), showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, @@ -548,9 +547,9 @@ export class SearchWidget extends Widget { this._register(this.replaceInput.onPreserveCaseKeyDown((keyboardEvent: IKeyboardEvent) => this.onPreserveCaseKeyDown(keyboardEvent))); } - triggerReplaceAll(): Promise { + triggerReplaceAll(): Promise { this._onReplaceAll.fire(); - return Promise.resolve(null); + return Promise.resolve(); } private onToggleReplaceButton(): void { diff --git a/src/vs/workbench/contrib/search/common/cacheState.ts b/src/vs/workbench/contrib/search/common/cacheState.ts index 9090835bda7..bf7f55ecfcf 100644 --- a/src/vs/workbench/contrib/search/common/cacheState.ts +++ b/src/vs/workbench/contrib/search/common/cacheState.ts @@ -45,7 +45,7 @@ export class FileQueryCacheState { constructor( private cacheQuery: (cacheKey: string) => IFileQuery, - private loadFn: (query: IFileQuery) => Promise, + private loadFn: (query: IFileQuery) => Promise, private disposeFn: (cacheKey: string) => Promise, private previousCacheState: FileQueryCacheState | undefined ) { diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index 6bf4e8c993b..02f039ea7cd 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -38,7 +38,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { CellMatch, NotebookCompatibleFileMatch } from '../../browser/notebookSearch/notebookSearchModel.js'; import { INotebookFileInstanceMatch } from '../../browser/notebookSearch/notebookSearchModelBase.js'; -import { ISearchResult, ISearchTreeFolderMatch, MATCH_PREFIX } from '../../browser/searchTreeModel/searchTreeCommon.js'; +import { ISearchResult, ISearchTreeFolderMatch, isSearchTreeFolderMatchNoRoot, MATCH_PREFIX } from '../../browser/searchTreeModel/searchTreeCommon.js'; import { FolderMatchImpl } from '../../browser/searchTreeModel/folderMatch.js'; import { SearchResultImpl } from '../../browser/searchTreeModel/searchResult.js'; import { MatchImpl } from '../../browser/searchTreeModel/match.js'; @@ -421,6 +421,47 @@ suite('SearchResult', () => { assert.deepStrictEqual([{ elements: expectedArrayResult, removed: true, added: false }], target.args[0]); }); + test('batchRemove should remove FolderMatchNoRoot (Other files) correctly', function () { + const target = sinon.spy(); + const testObject = aSearchResult(); + + testObject.query = { + type: QueryType.Text, + contentPattern: { pattern: 'foo' }, + folderQueries: [{ + folder: createFileUriFromPathFromRoot('/workspace') + }] + }; + + // Add a file inside the workspace folder + addToSearchResult(testObject, [ + aRawMatch('/workspace/file.txt', + new TextSearchMatch('preview 1', lineOneRange)), + ]); + + // Add a file outside of the workspace folder (goes to "Other files") + addToSearchResult(testObject, [ + aRawMatch('/other/outside.txt', + new TextSearchMatch('preview 2', lineOneRange)), + ]); + + // Should have 2 folder matches: workspace root and "Other files" + const folderMatches = testObject.folderMatches(); + assert.strictEqual(folderMatches.length, 2); + + // Find the "Other files" folder match (FolderMatchNoRoot) + const otherFilesMatch = folderMatches.find(fm => isSearchTreeFolderMatchNoRoot(fm)); + assert.ok(otherFilesMatch, 'Should have an Other files folder match'); + assert.strictEqual(otherFilesMatch.allDownstreamFileMatches().length, 1); + + store.add(testObject.onChange(target)); + testObject.batchRemove([otherFilesMatch]); + + assert.ok(target.calledOnce); + // After removal, the Other files folder should be cleared + assert.strictEqual(otherFilesMatch.allDownstreamFileMatches().length, 0); + }); + test('batchReplace should trigger the onChange event correctly', async function () { const replaceSpy = sinon.spy(); instantiationService.stub(IReplaceService, 'replace', (arg: any) => { diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index 29f82cafa74..6add03050d1 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -192,6 +192,16 @@ async function createSnippetFile(scope: string, defaultPath: URI, quickInputServ '\t// \t],', '\t// \t"description": "Log output to console"', '\t// }', + '\t//', + '\t// You can also restrict snippets to specific files using include/exclude patterns:', + '\t// "Test snippet": {', + '\t// \t"scope": "javascript,typescript",', + '\t// \t"prefix": "test",', + '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', + '\t// \t"include": ["**/*.test.ts", "*.spec.ts"],', + '\t// \t"exclude": ["**/temp/*.ts"],', + '\t// \t"description": "Insert test block"', + '\t// }', '}' ].join('\n')); @@ -218,6 +228,15 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS '\t// \t],', '\t// \t"description": "Log output to console"', '\t// }', + '\t//', + '\t// You can also restrict snippets to specific files using include/exclude patterns:', + '\t// "Test snippet": {', + '\t// \t"prefix": "test",', + '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', + '\t// \t"include": ["**/*.test.ts", "*.spec.ts"],', + '\t// \t"exclude": ["**/temp/*.ts"],', + '\t// \t"description": "Insert test block"', + '\t// }', '}' ].join('\n'); await textFileService.write(pick.filepath, contents); diff --git a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index beea5ed367d..fa5f7087803 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts @@ -39,7 +39,8 @@ export class ApplyFileSnippetAction extends SnippetsAction { return; } - const snippets = await snippetService.getSnippets(undefined, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + const resourceUri = editor.getModel().uri; + const snippets = await snippetService.getSnippets(undefined, resourceUri, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); if (snippets.length === 0) { return; } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts index 5d71cf5e8cf..338e0ff196a 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts @@ -127,13 +127,13 @@ export class InsertSnippetAction extends SnippetEditorAction { if (name) { // take selected snippet - snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true }) + snippetService.getSnippets(languageId, undefined, { includeNoPrefixSnippets: true }) .then(snippets => snippets.find(snippet => snippet.name === name)) .then(resolve, reject); } else { // let user pick a snippet - resolve(instaService.invokeFunction(pickSnippet, languageId)); + resolve(instaService.invokeFunction(pickSnippet, languageId, editor.getModel().uri)); } }); diff --git a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts index e9f9a63d98a..393e0c2bab1 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts @@ -23,7 +23,7 @@ export async function getSurroundableSnippets(snippetsService: ISnippetsService, model.tokenization.tokenizeIfCheap(lineNumber); const languageId = model.getLanguageIdAtPosition(lineNumber, column); - const allSnippets = await snippetsService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets }); + const allSnippets = await snippetsService.getSnippets(languageId, model.uri, { includeNoPrefixSnippets: true, includeDisabledSnippets }); return allSnippets.filter(snippet => snippet.usesSelection); } @@ -54,12 +54,13 @@ export class SurroundWithSnippetEditorAction extends SnippetEditorAction { const snippetsService = accessor.get(ISnippetsService); const clipboardService = accessor.get(IClipboardService); - const snippets = await getSurroundableSnippets(snippetsService, editor.getModel(), editor.getPosition(), true); + const model = editor.getModel(); + const snippets = await getSurroundableSnippets(snippetsService, model, editor.getPosition(), true); if (!snippets.length) { return; } - const snippet = await instaService.invokeFunction(pickSnippet, snippets); + const snippet = await instaService.invokeFunction(pickSnippet, snippets, model.uri); if (!snippet) { return; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts index 53802851eca..ede209f332c 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts @@ -88,7 +88,7 @@ class FileTemplateCodeActionProvider implements CodeActionProvider { return undefined; } - const snippets = await this._snippetService.getSnippets(model.getLanguageId(), { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); + const snippets = await this._snippetService.getSnippets(model.getLanguageId(), model.uri, { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); const actions: CodeAction[] = []; for (const snippet of snippets) { if (actions.length >= FileTemplateCodeActionProvider._MAX_CODE_ACTIONS) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 4bd9ab73446..abc6ff72545 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -105,7 +105,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const triggerCharacterLow = context.triggerCharacter?.toLowerCase() ?? ''; const languageId = this._getLanguageIdAtPosition(model, position); const languageConfig = this._languageConfigurationService.getLanguageConfiguration(languageId); - const snippets = new Set(await this._snippets.getSnippets(languageId)); + const snippets = new Set(await this._snippets.getSnippets(languageId, model.uri)); const suggestions: SnippetCompletion[] = []; for (const snippet of snippets) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts index 205fd707021..99839fac08a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts @@ -12,8 +12,9 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Event } from '../../../../base/common/event.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; -export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippets: string | Snippet[]): Promise { +export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippets: string | Snippet[], resourceUri?: URI): Promise { const snippetService = accessor.get(ISnippetsService); const quickInputService = accessor.get(IQuickInputService); @@ -26,7 +27,7 @@ export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippe if (Array.isArray(languageIdOrSnippets)) { snippets = languageIdOrSnippets; } else { - snippets = (await snippetService.getSnippets(languageIdOrSnippets, { includeDisabledSnippets: true, includeNoPrefixSnippets: true })); + snippets = (await snippetService.getSnippets(languageIdOrSnippets, resourceUri, { includeDisabledSnippets: true, includeNoPrefixSnippets: true })); } snippets.sort((a, b) => a.snippetSource - b.snippetSource); diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 226fccbff8e..c8859acb39a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -75,6 +75,20 @@ const snippetSchemaProperties: IJSONSchemaMap = { description: { description: nls.localize('snippetSchema.json.description', 'The snippet description.'), type: ['string', 'array'] + }, + include: { + markdownDescription: nls.localize('snippetSchema.json.include', 'A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to include the snippet for specific files, e.g. `["**/*.test.ts", "*.spec.ts"]` or `"**/*.spec.ts"`. Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. You can exclude matching files via the `exclude` property.'), + type: ['string', 'array'], + items: { + type: 'string' + } + }, + exclude: { + markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude the snippet from specific files, e.g. `["**/*.min.js"]` or `"*.min.js"`. Patterns will match on the absolute path of a file if they contain a path separator and will match on the name of the file otherwise. Exclude patterns take precedence over `include` patterns.'), + type: ['string', 'array'], + items: { + type: 'string' + } } }; diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.ts b/src/vs/workbench/contrib/snippets/browser/snippets.ts index ae096afb2a6..6a7dfdaa5bc 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.ts @@ -5,6 +5,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { SnippetFile, Snippet } from './snippetsFile.js'; +import { URI } from '../../../../base/common/uri.js'; export const ISnippetsService = createDecorator('snippetService'); @@ -27,7 +28,7 @@ export interface ISnippetsService { updateUsageTimestamp(snippet: Snippet): void; - getSnippets(languageId: string | undefined, opt?: ISnippetGetOptions): Promise; + getSnippets(languageId: string | undefined, resourceUri?: URI, opt?: ISnippetGetOptions): Promise; - getSnippetsSync(languageId: string, opt?: ISnippetGetOptions): Snippet[]; + getSnippetsSync(languageId: string, resourceUri?: URI, opt?: ISnippetGetOptions): Snippet[]; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 98829a81bf0..7d843f8f3db 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -16,6 +16,8 @@ import { relativePath } from '../../../../base/common/resources.js'; import { isObject } from '../../../../base/common/types.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { WindowIdleValue, getActiveWindow } from '../../../../base/browser/dom.js'; +import { match as matchGlob } from '../../../../base/common/glob.js'; +import { Schemas } from '../../../../base/common/network.js'; class SnippetBodyInsights { @@ -113,6 +115,8 @@ export class Snippet { readonly source: string, readonly snippetSource: SnippetSource, readonly snippetIdentifier: string, + readonly include?: string[], + readonly exclude?: string[], readonly extensionId?: ExtensionIdentifier, ) { this.prefixLow = prefix.toLowerCase(); @@ -138,6 +142,34 @@ export class Snippet { get usesSelection(): boolean { return this._bodyInsights.value.usesSelectionVariable; } + + isFileIncluded(resourceUri: URI): boolean { + const uriPath = resourceUri.scheme === Schemas.file ? resourceUri.fsPath : resourceUri.path; + const fileName = basename(uriPath); + + const getMatchTarget = (pattern: string): string => { + return pattern.includes('/') ? uriPath : fileName; + }; + + if (this.exclude) { + for (const pattern of this.exclude.filter(Boolean)) { + if (matchGlob(pattern, getMatchTarget(pattern), { ignoreCase: true })) { + return false; + } + } + } + + if (this.include) { + for (const pattern of this.include.filter(Boolean)) { + if (matchGlob(pattern, getMatchTarget(pattern), { ignoreCase: true })) { + return true; + } + } + return false; + } + + return true; + } } @@ -147,9 +179,11 @@ interface JsonSerializedSnippet { scope?: string; prefix: string | string[] | undefined; description: string; + include?: string | string[]; + exclude?: string | string[]; } -function isJsonSerializedSnippet(thing: any): thing is JsonSerializedSnippet { +function isJsonSerializedSnippet(thing: unknown): thing is JsonSerializedSnippet { return isObject(thing) && Boolean((thing).body); } @@ -192,7 +226,7 @@ export class SnippetFile { } private _filepathSelect(selector: string, bucket: Snippet[]): void { - // for `fooLang.json` files all snippets are accepted + // for `fooLang.json` files apply inclusion/exclusion rules only if (selector + '.json' === basename(this.location.path)) { bucket.push(...this.data); } @@ -286,6 +320,24 @@ export class SnippetFile { scopes = []; } + let include: string[] | undefined; + if (snippet.include) { + if (Array.isArray(snippet.include)) { + include = snippet.include; + } else if (typeof snippet.include === 'string') { + include = [snippet.include]; + } + } + + let exclude: string[] | undefined; + if (snippet.exclude) { + if (Array.isArray(snippet.exclude)) { + exclude = snippet.exclude; + } else if (typeof snippet.exclude === 'string') { + exclude = [snippet.exclude]; + } + } + let source: string; if (this._extension) { // extension snippet -> show the name of the extension @@ -314,6 +366,8 @@ export class SnippetFile { source, this.source, this._extension ? `${relativePath(this._extension.extensionLocation, this.location)}/${name}` : `${basename(this.location.path)}/${name}`, + include, + exclude, this._extension?.identifier, )); } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 187615df1de..4c681e481e6 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -265,7 +265,7 @@ export class SnippetsService implements ISnippetsService { return this._files.values(); } - async getSnippets(languageId: string | undefined, opts?: ISnippetGetOptions): Promise { + async getSnippets(languageId: string | undefined, resourceUri?: URI, opts?: ISnippetGetOptions): Promise { await this._joinSnippets(); const result: Snippet[] = []; @@ -289,10 +289,10 @@ export class SnippetsService implements ISnippetsService { } } await Promise.all(promises); - return this._filterAndSortSnippets(result, opts); + return this._filterAndSortSnippets(result, resourceUri, opts); } - getSnippetsSync(languageId: string, opts?: ISnippetGetOptions): Snippet[] { + getSnippetsSync(languageId: string, resourceUri?: URI, opts?: ISnippetGetOptions): Snippet[] { const result: Snippet[] = []; if (this._languageService.isRegisteredLanguageId(languageId)) { for (const file of this._files.values()) { @@ -302,10 +302,10 @@ export class SnippetsService implements ISnippetsService { file.select(languageId, result); } } - return this._filterAndSortSnippets(result, opts); + return this._filterAndSortSnippets(result, resourceUri, opts); } - private _filterAndSortSnippets(snippets: Snippet[], opts?: ISnippetGetOptions): Snippet[] { + private _filterAndSortSnippets(snippets: Snippet[], resourceUri?: URI, opts?: ISnippetGetOptions): Snippet[] { const result: Snippet[] = []; @@ -322,6 +322,10 @@ export class SnippetsService implements ISnippetsService { // isTopLevel requested but mismatching continue; } + if (resourceUri && !snippet.isFileIncluded(resourceUri)) { + // include/exclude settings don't match + continue; + } result.push(snippet); } diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index 43dd710d128..1e6db4d54c1 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -93,7 +93,7 @@ export class TabCompletionController implements IEditorContribution { const model = this._editor.getModel(); model.tokenization.tokenizeIfCheap(selection.positionLineNumber); const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn); - const snippets = this._snippetService.getSnippetsSync(id); + const snippets = this._snippetService.getSnippetsSync(id, model.uri); if (!snippets) { // nothing for this language diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 6dd26435753..14afd71b76a 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -22,14 +22,19 @@ import { CompletionModel } from '../../../../../editor/contrib/suggest/browser/c import { CompletionItem } from '../../../../../editor/contrib/suggest/browser/suggest.js'; import { WordDistance } from '../../../../../editor/contrib/suggest/browser/wordDistance.js'; import { EditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { URI } from '../../../../../base/common/uri.js'; class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; constructor(readonly snippets: Snippet[]) { } - getSnippets() { - return Promise.resolve(this.getSnippetsSync()); + getSnippets(languageId?: string, resourceUri?: URI) { + return Promise.resolve(this.getSnippetsSync(languageId!, resourceUri)); } - getSnippetsSync(): Snippet[] { + getSnippetsSync(languageId?: string, resourceUri?: URI): Snippet[] { + // Filter snippets based on resourceUri if provided + if (resourceUri) { + return this.snippets.filter(snippet => snippet.isFileIncluded(resourceUri)); + } return this.snippets; } getSnippetFiles(): any { @@ -1057,4 +1062,106 @@ suite('SnippetsService', function () { assert.strictEqual(result2.suggestions.length, 1); } }); + + test('getSnippetsSync - include pattern', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts']), + new Snippet(false, ['fooLang'], 'SpecSnippet', 'spec', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.spec.ts']), + new Snippet(false, ['fooLang'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Test file should only get TestSnippet and AllSnippet + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 2); + assert.ok(snippets.some(s => s.name === 'TestSnippet')); + assert.ok(snippets.some(s => s.name === 'AllSnippet')); + + // Spec file should only get SpecSnippet and AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(snippets.length, 2); + assert.ok(snippets.some(s => s.name === 'SpecSnippet')); + assert.ok(snippets.some(s => s.name === 'AllSnippet')); + + // Regular file should only get AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + + // Without URI, all snippets should be returned (backward compatibility) + snippets = snippetService.getSnippetsSync('fooLang'); + assert.strictEqual(snippets.length, 3); + }); + + test('getSnippetsSync - exclude pattern', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'ProdSnippet', 'prod', '', 'snippet', 'test', SnippetSource.User, generateUuid(), undefined, ['**/*.min.js', '**/dist/**']), + new Snippet(false, ['fooLang'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Regular .js file should get both snippets + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.js')); + assert.strictEqual(snippets.length, 2); + + // Minified file should only get AllSnippet (ProdSnippet is excluded) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.min.js')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + + // File in dist folder should only get AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/dist/bundle.js')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + }); + + test('getSnippetsSync - include and exclude patterns together', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts', '**/*.spec.ts'], ['**/*.perf.test.ts']), + ]); + + // Regular test file should get the snippet + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 1); + + // Spec file should get the snippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(snippets.length, 1); + + // Performance test file should NOT get the snippet (excluded) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.perf.test.ts')); + assert.strictEqual(snippets.length, 0); + + // Regular file should NOT get the snippet (not included) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.ts')); + assert.strictEqual(snippets.length, 0); + }); + + test('getSnippetsSync - filename-only patterns (no path separator)', function () { + // Patterns without '/' should match on filename only (like files.associations) + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['*.test.ts']), + new Snippet(false, ['fooLang'], 'ConfigSnippet', 'config', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['config.json']), + ]); + + // *.test.ts should match any file ending in .test.ts regardless of path + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'TestSnippet'); + + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/other/deep/path/bar.test.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'TestSnippet'); + + // config.json should match filename exactly + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/config.json')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'ConfigSnippet'); + + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/deep/nested/path/config.json')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'ConfigSnippet'); + + // myconfig.json should NOT match config.json pattern + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/myconfig.json')); + assert.strictEqual(snippets.length, 0); + }); }); diff --git a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts index c3eddd4f431..12f8e73bc27 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts @@ -720,6 +720,7 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.yeoman.code.ext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.cordova.high" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.cordova.low" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.azure.yaml" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.xamarin.android" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.xamarin.ios" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.android.cpp" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -1276,6 +1277,8 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { tags['workspace.npm'] = nameSet.has('package.json') || nameSet.has('node_modules'); tags['workspace.bower'] = nameSet.has('bower.json') || nameSet.has('bower_components'); + tags['workspace.azure.yaml'] = nameSet.has('azure.yaml') || nameSet.has('azure.yml'); + tags['workspace.java.pom'] = nameSet.has('pom.xml'); tags['workspace.java.gradle'] = nameSet.has('build.gradle') || nameSet.has('settings.gradle') || nameSet.has('build.gradle.kts') || nameSet.has('settings.gradle.kts') || nameSet.has('gradlew') || nameSet.has('gradlew.bat'); diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index b8372a961fe..96ce5ef97fd 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -86,8 +86,8 @@ import { IPreferencesService } from '../../../services/preferences/common/prefer import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { CHAT_OPEN_ACTION_ID } from '../../chat/browser/actions/chatActions.js'; -import { IChatAgentService } from '../../chat/common/chatAgents.js'; -import { IChatService } from '../../chat/common/chatService.js'; +import { IChatAgentService } from '../../chat/common/participants/chatAgents.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; import { configureTaskIcon, isWorkspaceFolder, ITaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, QUICKOPEN_SKIP_CONFIG, TaskQuickPick } from './taskQuickPick.js'; import { IHostService } from '../../../services/host/browser/host.js'; import * as dom from '../../../../base/browser/dom.js'; @@ -1286,16 +1286,14 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } public removeRecentlyUsedTask(taskRecentlyUsedKey: string) { - if (this._getTasksFromStorage('historical').has(taskRecentlyUsedKey)) { - this._getTasksFromStorage('historical').delete(taskRecentlyUsedKey); + if (this._getTasksFromStorage('historical').delete(taskRecentlyUsedKey)) { this._saveRecentlyUsedTasks(); } } public removePersistentTask(key: string) { this._log(nls.localize('taskService.removePersistentTask', 'Removing persistent task {0}', key), true); - if (this._getTasksFromStorage('persistent').has(key)) { - this._getTasksFromStorage('persistent').delete(key); + if (this._getTasksFromStorage('persistent').delete(key)) { this._savePersistentTasks(); } } @@ -2172,22 +2170,34 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (!this._taskSystem) { return; } - const response = await this._taskSystem.terminate(task); - if (response.success) { - try { - // Before restarting, check if the task still exists and get updated version - const updatedTask = await this._findUpdatedTask(task); - if (updatedTask) { - await this.run(updatedTask); - } else { + + // Check if the task is currently running + const isTaskRunning = await this.getActiveTasks().then(tasks => tasks.some(t => t.getMapKey() === task.getMapKey())); + + if (isTaskRunning) { + // Task is running, terminate it first + const response = await this._taskSystem.terminate(task); + if (!response.success) { + this._notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.configurationProperties.name)); + return; + } + } + + // Task is not running or was successfully terminated, now run it + try { + // Before restarting, check if the task still exists and get updated version + const updatedTask = await this._findUpdatedTask(task); + if (updatedTask) { + await this.run(updatedTask); + } else { + const success = await this.run(task); + if (!success || (typeof success.exitCode === 'number' && success.exitCode !== 0)) { // Task no longer exists, show warning this._notificationService.warn(nls.localize('TaskSystem.taskNoLongerExists', 'Task {0} no longer exists or has been modified. Cannot restart.', task.configurationProperties.name)); } - } catch { - // eat the error, we don't care about it here } - } else { - this._notificationService.warn(nls.localize('TaskSystem.restartFailed', 'Failed to terminate and restart task {0}', Types.isString(task) ? task : task.configurationProperties.name)); + } catch { + // eat the error, we don't care about it here } } @@ -2275,6 +2285,17 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } else { return undefined; } + }, + async (taskKey: string) => { + // Look up task by its map key across all workspace tasks + const taskMap = await this._getGroupedTasks(); + const allTasks = taskMap.all(); + for (const task of allTasks) { + if (task.getMapKey() === taskKey) { + return task; + } + } + return undefined; } ); } @@ -2997,7 +3018,12 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer return entries; } private async _showTwoLevelQuickPick(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, type?: string, name?: string) { - return this._instantiationService.createInstance(TaskQuickPick).show(placeHolder, defaultEntry, type, name); + const taskQuickPick = this._instantiationService.createInstance(TaskQuickPick); + try { + return await taskQuickPick.show(placeHolder, defaultEntry, type, name); + } finally { + taskQuickPick.dispose(); + } } private async _showQuickPick(tasks: Promise | Task[], placeHolder: string, defaultEntry?: ITaskQuickPickEntry, group: boolean = false, sort: boolean = false, selectedEntry?: ITaskQuickPickEntry, additionalEntries?: ITaskQuickPickEntry[], name?: string): Promise { @@ -3209,8 +3235,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } - rerun(terminalInstanceId: number): void { - const task = this._taskSystem?.getTaskForTerminal(terminalInstanceId); + async rerun(terminalInstanceId: number): Promise { + const task = await this._taskSystem?.getTaskForTerminal(terminalInstanceId); if (task) { this._restart(task); } else { diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 854bfb93576..080d0432909 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -9,15 +9,19 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; import { ITaskService, IWorkspaceFolderTaskResult } from '../common/taskService.js'; import { RunOnOptions, Task, TaskRunSource, TaskSource, TaskSourceKind, TASKS_CATEGORY, WorkspaceFileTaskSource, IWorkspaceTaskSource } from '../common/tasks.js'; +import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IQuickPickItem, IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Action2 } from '../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { URI } from '../../../../base/common/uri.js'; import { Event } from '../../../../base/common/event.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +const HAS_PROMPTED_FOR_AUTOMATIC_TASKS = 'task.hasPromptedForAutomaticTasks.v2'; const ALLOW_AUTOMATIC_TASKS = 'task.allowAutomaticTasks'; export class RunAutomaticTasks extends Disposable implements IWorkbenchContribution { @@ -26,7 +30,10 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut @ITaskService private readonly _taskService: ITaskService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, - @ILogService private readonly _logService: ILogService) { + @ILogService private readonly _logService: ILogService, + @IStorageService private readonly _storageService: IStorageService, + @INotificationService private readonly _notificationService: INotificationService, + @IOpenerService private readonly _openerService: IOpenerService) { super(); if (this._taskService.isReconnected) { this._tryRunTasks(); @@ -40,7 +47,9 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut if (!this._workspaceTrustManagementService.isWorkspaceTrusted()) { return; } - if (this._hasRunTasks || this._configurationService.getValue(ALLOW_AUTOMATIC_TASKS) === 'off') { + const { value, userValue } = this._configurationService.inspect(ALLOW_AUTOMATIC_TASKS); + // If user explicitly set it to 'off', don't run or prompt + if (this._hasRunTasks || (value === 'off' && userValue !== undefined)) { return; } this._hasRunTasks = true; @@ -77,7 +86,7 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut this._logService.trace(`RunAutomaticTasks: updated taskNames=${JSON.stringify(autoTasks.taskNames)}`); } - this._runWithPermission(this._taskService, this._configurationService, autoTasks.tasks, autoTasks.taskNames); + this._runWithPermission(this._taskService, this._configurationService, this._storageService, this._notificationService, this._openerService, autoTasks.tasks, autoTasks.taskNames, autoTasks.locations); } private _runTasks(taskService: ITaskService, tasks: Array>) { @@ -149,14 +158,59 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut return { tasks, taskNames, locations }; } - private async _runWithPermission(taskService: ITaskService, configurationService: IConfigurationService, tasks: (Task | Promise)[], taskNames: string[]) { + private async _runWithPermission(taskService: ITaskService, configurationService: IConfigurationService, storageService: IStorageService, notificationService: INotificationService, openerService: IOpenerService, tasks: (Task | Promise)[], taskNames: string[], locations: Map) { if (taskNames.length === 0) { return; } - if (configurationService.getValue(ALLOW_AUTOMATIC_TASKS) === 'off') { + if (configurationService.getValue(ALLOW_AUTOMATIC_TASKS) === 'on') { + this._runTasks(taskService, tasks); return; } - this._runTasks(taskService, tasks); + const hasShownPromptForAutomaticTasks = storageService.getBoolean(HAS_PROMPTED_FOR_AUTOMATIC_TASKS, StorageScope.WORKSPACE, false); + if (hasShownPromptForAutomaticTasks) { + return; + } + // We have automatic tasks - prompt to allow. + const allow = await this._showPrompt(notificationService, storageService, openerService, configurationService, taskNames, locations); + if (allow) { + this._runTasks(taskService, tasks); + } + } + + private _showPrompt(notificationService: INotificationService, storageService: IStorageService, openerService: IOpenerService, configurationService: IConfigurationService, taskNames: string[], locations: Map): Promise { + return new Promise(resolve => { + notificationService.prompt(Severity.Info, nls.localize('tasks.run.allowAutomatic', + "This workspace has tasks ({0}) defined ({1}) that run automatically when you open this workspace. Do you allow automatic tasks to run when you open this workspace?", + taskNames.join(', '), + Array.from(locations.keys()).join(', ') + ), + [{ + label: nls.localize('allow', "Allow and Run"), + run: () => { + resolve(true); + configurationService.updateValue(ALLOW_AUTOMATIC_TASKS, 'on', ConfigurationTarget.USER); + } + }, + { + label: nls.localize('disallow', "Disallow"), + run: () => { + resolve(false); + configurationService.updateValue(ALLOW_AUTOMATIC_TASKS, 'off', ConfigurationTarget.USER); + } + }, + { + label: locations.size === 1 ? nls.localize('openTask', "Open File") : nls.localize('openTasks', "Open Files"), + run: async () => { + for (const location of locations) { + await openerService.open(location[1]); + } + resolve(false); + } + }], + { onCancel: () => resolve(false) } + ); + storageService.store(HAS_PROMPTED_FOR_AUTOMATIC_TASKS, true, StorageScope.WORKSPACE, StorageTarget.MACHINE); + }); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 73cd4c58597..19a537dd101 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -31,7 +31,7 @@ import schemaVersion1 from '../common/jsonSchema_v1.js'; import schemaVersion2, { updateProblemMatchers, updateTaskDefinitions } from '../common/jsonSchema_v2.js'; import { AbstractTaskService, ConfigureTaskAction } from './abstractTaskService.js'; import { tasksSchemaId } from '../../../services/configuration/common/configuration.js'; -import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; import { WorkbenchStateContext } from '../../../common/contextkeys.js'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from '../../../../platform/quickinput/common/quickAccess.js'; import { TasksQuickAccessProvider } from './tasksQuickAccess.js'; @@ -542,7 +542,8 @@ configurationRegistry.registerConfiguration({ nls.localize('task.allowAutomaticTasks.off', "Never"), ], description: nls.localize('task.allowAutomaticTasks', "Enable automatic tasks - note that tasks won't run in an untrusted workspace."), - default: 'on', + default: 'off', + scope: ConfigurationScope.APPLICATION, restricted: true }, [TaskSettingId.Reconnection]: { diff --git a/src/vs/workbench/contrib/tasks/browser/taskProblemMonitor.ts b/src/vs/workbench/contrib/tasks/browser/taskProblemMonitor.ts index 020a8adc5c3..8b8486c3491 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskProblemMonitor.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskProblemMonitor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js'; import { AbstractProblemCollector } from '../common/problemCollectors.js'; import { ITerminalInstance } from '../../terminal/browser/terminal.js'; import { URI } from '../../../../base/common/uri.js'; @@ -17,7 +17,7 @@ interface ITerminalMarkerData { export class TaskProblemMonitor extends Disposable { private readonly terminalMarkerMap: Map = new Map(); - private readonly terminalDisposables: Map = new Map(); + private readonly terminalDisposables = new DisposableMap(); constructor() { super(); @@ -34,8 +34,7 @@ export class TaskProblemMonitor extends Disposable { store.add(terminal.onDisposed(() => { this.terminalMarkerMap.delete(terminal.instanceId); - this.terminalDisposables.get(terminal.instanceId)?.dispose(); - this.terminalDisposables.delete(terminal.instanceId); + this.terminalDisposables.deleteAndDispose(terminal.instanceId); })); store.add(problemMatcher.onDidFindErrors((markers: ITaskMarker[]) => { diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index 528341bae74..d42b08000d2 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -5,7 +5,7 @@ import * as nls from '../../../../nls.js'; import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import Severity from '../../../../base/common/severity.js'; import { AbstractProblemCollector, StartStopProblemCollector } from '../common/problemCollectors.js'; import { ITaskGeneralEvent, ITaskProcessEndedEvent, ITaskProcessStartedEvent, TaskEventKind, TaskRunType } from '../common/tasks.js'; @@ -17,13 +17,12 @@ import type { IMarker } from '@xterm/xterm'; import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ITerminalStatus } from '../../terminal/common/terminal.js'; -interface ITerminalData { +interface ITerminalData extends IDisposable { terminal: ITerminalInstance; task: Task; status: ITerminalStatus; problemMatcher: AbstractProblemCollector; taskRunEnded: boolean; - disposeListener?: MutableDisposable; } const TASK_TERMINAL_STATUS_ID = 'task_terminal_status'; @@ -38,7 +37,7 @@ const INFO_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: C const INFO_INACTIVE_TASK_STATUS: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, icon: Codicon.info, severity: Severity.Info, tooltip: nls.localize('taskTerminalStatus.infosInactive', "Task has infos and is waiting...") }; export class TaskTerminalStatus extends Disposable { - private terminalMap: Map = new Map(); + private terminalMap: DisposableMap = this._register(new DisposableMap()); private _marker: IMarker | undefined; constructor(@ITaskService taskService: ITaskService, @IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService) { super(); @@ -50,34 +49,42 @@ export class TaskTerminalStatus extends Disposable { case TaskEventKind.ProcessEnded: this.eventEnd(event); break; } })); - this._register(toDisposable(() => { - for (const terminalData of this.terminalMap.values()) { - terminalData.disposeListener?.dispose(); - } - this.terminalMap.clear(); - })); } addTerminal(task: Task, terminal: ITerminalInstance, problemMatcher: AbstractProblemCollector) { const status: ITerminalStatus = { id: TASK_TERMINAL_STATUS_ID, severity: Severity.Info }; terminal.statusList.add(status); - this._register(problemMatcher.onDidFindFirstMatch(() => { + const store = new DisposableStore(); + store.add(problemMatcher.onDidFindFirstMatch(() => { this._marker = terminal.registerMarker(); if (this._marker) { - this._register(this._marker); + store.add(this._marker); } })); - this._register(problemMatcher.onDidFindErrors(() => { + store.add(problemMatcher.onDidFindErrors(() => { if (this._marker) { terminal.addBufferMarker({ marker: this._marker, hoverMessage: nls.localize('task.watchFirstError', "Beginning of detected errors for this run"), disableCommandStorage: true }); } })); - this._register(problemMatcher.onDidRequestInvalidateLastMarker(() => { + store.add(problemMatcher.onDidRequestInvalidateLastMarker(() => { this._marker?.dispose(); this._marker = undefined; })); - this.terminalMap.set(terminal.instanceId, { terminal, task, status, problemMatcher, taskRunEnded: false }); + store.add(terminal.onDisposed(() => { + this.terminalMap.deleteAndDispose(terminal.instanceId); + })); + + this.terminalMap.set(terminal.instanceId, { + terminal, + task, + status, + problemMatcher, + taskRunEnded: false, + dispose() { + store.dispose(); + }, + }); } private terminalFromEvent(event: { terminalId: number | undefined }): ITerminalData | undefined { @@ -138,16 +145,6 @@ export class TaskTerminalStatus extends Disposable { if (!terminalData) { return; } - if (!terminalData.disposeListener) { - terminalData.disposeListener = this._register(new MutableDisposable()); - terminalData.disposeListener.value = terminalData.terminal.onDisposed(() => { - if (!event.terminalId) { - return; - } - this.terminalMap.delete(event.terminalId); - terminalData.disposeListener?.dispose(); - }); - } terminalData.taskRunEnded = false; terminalData.terminal.statusList.remove(terminalData.status); // We don't want to show an infinite status for a background task that doesn't have a problem matcher. diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c43128578f4..3c13bc0fdad 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -221,6 +221,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { contextKeyService: IContextKeyService, instantiationService: IInstantiationService, taskSystemInfoResolver: ITaskSystemInfoResolver, + private _taskLookup: (taskKey: string) => Promise, ) { super(); @@ -1142,8 +1143,8 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { } else if (task.command.presentation && (task.command.presentation.focus || task.command.presentation.reveal === RevealKind.Always)) { this._terminalService.setActiveInstance(terminal); await this._terminalService.revealTerminal(terminal); - if (task.command.presentation.focus) { - this._terminalService.focusInstance(terminal); + if (task.command.presentation.focus && terminal) { + await this._terminalService.focusInstance(terminal); } } if (this._activeTasks[task.getMapKey()]) { @@ -1944,13 +1945,20 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem { return 'other'; } - public getTaskForTerminal(instanceId: number): Task | undefined { + public async getTaskForTerminal(instanceId: number): Promise { + // First check if there's an active task for this terminal for (const key in this._activeTasks) { const activeTask = this._activeTasks[key]; if (activeTask.terminal?.instanceId === instanceId) { return activeTask.task; } } + // If no active task, check the terminals map for the last task that ran in this terminal + const terminalData = this._terminals[instanceId.toString()]; + if (terminalData?.lastTask) { + // Look up the task using the callback provided by the task service + return await this._taskLookup(terminalData.lastTask); + } return undefined; } diff --git a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts index dbad5612b09..13ef3e3d26a 100644 --- a/src/vs/workbench/contrib/tasks/common/problemCollectors.ts +++ b/src/vs/workbench/contrib/tasks/common/problemCollectors.ts @@ -546,8 +546,7 @@ export class WatchingProblemCollector extends AbstractProblemCollector implement } else { this._onDidRequestInvalidateLastMarker.fire(); } - if (this._activeBackgroundMatchers.has(background.key)) { - this._activeBackgroundMatchers.delete(background.key); + if (this._activeBackgroundMatchers.delete(background.key)) { this.resetCurrentResource(); this._onDidStateChange.fire(IProblemCollectorEvent.create(ProblemCollectorEventKind.BackgroundProcessingEnds)); result = true; diff --git a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts index 4e83d5b40c9..a758a3e4e86 100644 --- a/src/vs/workbench/contrib/tasks/common/problemMatcher.ts +++ b/src/vs/workbench/contrib/tasks/common/problemMatcher.ts @@ -502,6 +502,9 @@ class SingleLineMatcher extends AbstractLineMatcher { const matches = this.pattern.regexp.exec(lines[start]); if (matches) { this.fillProblemData(data, this.pattern, matches); + if (data.kind === ProblemLocationKind.Location && !data.location && !data.line && data.file) { + data.kind = ProblemLocationKind.File; + } const match = this.getMarkerMatch(data); if (match) { return { match: match, continue: false }; @@ -1501,13 +1504,13 @@ class ProblemPatternRegistryImpl implements IProblemPatternRegistry { private fillDefaults(): void { this.add('msCompile', { - regexp: /^(?:\s*\d+>)?(\S.*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\)\s*:\s+((?:fatal +)?error|warning|info)\s+(\w+\d+)\s*:\s*(.*)$/, + regexp: /^\s*(?:\s*\d+>)?(\S.*?)(?:\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\))?\s*:\s+(?:(\S+)\s+)?((?:fatal +)?error|warning|info)\s+(\w+\d+)?\s*:\s*(.*)$/, kind: ProblemLocationKind.Location, file: 1, location: 2, - severity: 3, - code: 4, - message: 5 + severity: 4, + code: 5, + message: 6 }); this.add('gulp-tsc', { regexp: /^([^\s].*)\((\d+|\d+,\d+|\d+,\d+,\d+,\d+)\):\s+(\d+)\s+(.*)$/, diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 10d600380fb..5b5a12c84f5 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -152,7 +152,7 @@ export interface ITaskSystem { revealTask(task: Task): boolean; customExecutionComplete(task: Task, result: number): Promise; isTaskVisible(task: Task): boolean; - getTaskForTerminal(instanceId: number): Task | undefined; + getTaskForTerminal(instanceId: number): Promise; getTerminalsForTasks(tasks: SingleOrMany): URI[] | undefined; getTaskProblems(instanceId: number): Map | undefined; getFirstInstance(task: Task): Task | undefined; diff --git a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts index 32477960bae..bffaa341d92 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts @@ -48,8 +48,8 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; import { IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IChatService } from '../../chat/common/chatService.js'; -import { IChatAgentService } from '../../chat/common/chatAgents.js'; +import { IChatService } from '../../chat/common/chatService/chatService.js'; +import { IChatAgentService } from '../../chat/common/participants/chatAgents.js'; import { IHostService } from '../../../services/host/browser/host.js'; interface IWorkspaceFolderConfigurationResult { diff --git a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts index 0e183c912cb..f6b40bdb344 100644 --- a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts +++ b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts @@ -43,6 +43,10 @@ class TestTerminal extends Disposable implements Partial { override dispose(): void { super.dispose(); } + + private readonly _onDisposed = this._register(new Emitter()); + readonly onDisposed = this._onDisposed.event; + } class TestTask extends CommonTask { diff --git a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts index 0b14df78ffe..2248500652b 100644 --- a/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts +++ b/src/vs/workbench/contrib/tasks/test/common/problemMatcher.test.ts @@ -6,6 +6,7 @@ import * as matchers from '../../common/problemMatcher.js'; import assert from 'assert'; import { ValidationState, IProblemReporter, ValidationStatus } from '../../../../../base/common/parsers.js'; +import { MarkerSeverity } from '../../../../../platform/markers/common/markers.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; class ProblemReporter implements IProblemReporter { @@ -266,3 +267,132 @@ suite('ProblemPatternParser', () => { }); }); }); + +suite('ProblemPatternRegistry - msCompile', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + test('matches lines with leading whitespace', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' /workspace/app.cs(5,10): error CS1001: Sample message'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS1001'); + assert.strictEqual(marker.message, 'Sample message'); + }); + + test('matches lines without diagnostic code', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = '/workspace/app.cs(3,7): warning : Message without code'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, undefined); + assert.strictEqual(marker.message, 'Message without code'); + }); + + test('matches lines without location information', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'Main.cs: warning CS0168: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS0168'); + assert.strictEqual(marker.message, 'The variable \'x\' is declared but never used'); + assert.strictEqual(marker.severity, MarkerSeverity.Warning); + }); + + test('matches lines with build prefixes and fatal errors', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' 1>c:/workspace/app.cs(12): fatal error C1002: Fatal diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'C1002'); + assert.strictEqual(marker.message, 'Fatal diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Error); + }); + + test('matches info diagnostics with codes', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = '2>/workspace/app.cs(20,5): info INF1001: Informational diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'INF1001'); + assert.strictEqual(marker.message, 'Informational diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Info); + }); + + test('matches lines with subcategory prefixes', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'Main.cs(17,20): subcategory warning CS0168: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS0168'); + assert.strictEqual(marker.message, 'The variable \'x\' is declared but never used'); + assert.strictEqual(marker.severity, MarkerSeverity.Warning); + }); + + test('matches complex diagnostics with all qualifiers', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = ' 12>c:/workspace/Main.cs(42,7,43,2): subcategory fatal error CS9999: Complex diagnostics'; + const result = matcher.handle([line]); + assert.ok(result.match); + const marker = result.match!.marker; + assert.strictEqual(marker.code, 'CS9999'); + assert.strictEqual(marker.message, 'Complex diagnostics'); + assert.strictEqual(marker.severity, MarkerSeverity.Error); + assert.strictEqual(marker.startLineNumber, 42); + assert.strictEqual(marker.startColumn, 7); + assert.strictEqual(marker.endLineNumber, 43); + assert.strictEqual(marker.endColumn, 2); + }); + + test('ignores diagnostics without origin', () => { + const matcher = matchers.createLineMatcher({ + owner: 'msCompile', + applyTo: matchers.ApplyToKind.allDocuments, + fileLocation: matchers.FileLocationKind.Absolute, + pattern: matchers.ProblemPatternRegistry.get('msCompile') + }); + const line = 'warning: The variable \'x\' is declared but never used'; + const result = matcher.handle([line]); + assert.strictEqual(result.match, null); + }); +}); diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index ecd1ca5eb37..399ff208b72 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -40,6 +40,7 @@ import { Categories } from '../../../../platform/action/common/actionCommonCateg import { IOutputService } from '../../../services/output/common/output.js'; import { ILoggerResource, ILoggerService, LogLevel } from '../../../../platform/log/common/log.js'; import { VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { TerminalContribSettingId } from '../../terminal/terminalContribExports.js'; type TelemetryData = { mimeType: TelemetryTrustedValue; @@ -426,6 +427,23 @@ class ConfigurationTelemetryContribution extends Disposable implements IWorkbenc source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'source of the setting' }; }>('extensions.autoRestart', { settingValue: this.getValueToReport(key, target), source }); return; + case TerminalContribSettingId.OutputLocation: + this.telemetryService.publicLog2('terminal.integrated.chatAgentTools.outputLocation', { settingValue: this.getValueToReport(key, target), source }); + return; + case TerminalContribSettingId.SuggestEnabled: + + this.telemetryService.publicLog2('terminal.integrated.suggest.enabled', { settingValue: this.getValueToReport(key, target), source }); + return; } } diff --git a/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts index 70ff99db0cf..41ec0b1a611 100644 --- a/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/baseTerminalBackend.ts @@ -6,6 +6,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; +import { isNumber, isObject } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; import { ICrossVersionSerializedTerminalState, IPtyHostController, ISerializedTerminalState, ITerminalLogService } from '../../../../platform/terminal/common/terminal.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; @@ -105,20 +106,27 @@ export abstract class BaseTerminalBackend extends Disposable { if (serializedState === undefined) { return undefined; } - const parsedUnknown = JSON.parse(serializedState); - if (!('version' in parsedUnknown) || !('state' in parsedUnknown) || !Array.isArray(parsedUnknown.state)) { - this._logService.warn('Could not revive serialized processes, wrong format', parsedUnknown); + const crossVersionState = JSON.parse(serializedState) as unknown; + if (!isCrossVersionSerializedTerminalState(crossVersionState)) { + this._logService.warn('Could not revive serialized processes, wrong format', crossVersionState); return undefined; } - const parsedCrossVersion = parsedUnknown as ICrossVersionSerializedTerminalState; - if (parsedCrossVersion.version !== 1) { - this._logService.warn(`Could not revive serialized processes, wrong version "${parsedCrossVersion.version}"`, parsedCrossVersion); + if (crossVersionState.version !== 1) { + this._logService.warn(`Could not revive serialized processes, wrong version "${crossVersionState.version}"`, crossVersionState); return undefined; } - return parsedCrossVersion.state as ISerializedTerminalState[]; + return crossVersionState.state as ISerializedTerminalState[]; } protected _getWorkspaceId(): string { return this._workspaceContextService.getWorkspace().id; } } + +function isCrossVersionSerializedTerminalState(obj: unknown): obj is ICrossVersionSerializedTerminalState { + return ( + isObject(obj) && + 'version' in obj && isNumber(obj.version) && + 'state' in obj && Array.isArray(obj.state) + ); +} diff --git a/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts new file mode 100644 index 00000000000..64715a47835 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/chatTerminalCommandMirror.ts @@ -0,0 +1,733 @@ +/*--------------------------------------------------------------------------------------------- + * 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, toDisposable } from '../../../../base/common/lifecycle.js'; +import { CancellationError } from '../../../../base/common/errors.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import type { IMarker as IXtermMarker, Terminal as RawXtermTerminal } from '@xterm/xterm'; +import type { ITerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; +import { ITerminalService, type IDetachedTerminalInstance } from './terminal.js'; +import { DetachedProcessInfo } from './detachedTerminal.js'; +import { XtermTerminal } from './xterm/xtermTerminal.js'; +import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js'; +import { PANEL_BACKGROUND } from '../../../common/theme.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; +import { Color } from '../../../../base/common/color.js'; +import type { IChatTerminalToolInvocationData } from '../../chat/common/chatService/chatService.js'; +import type { IColorTheme } from '../../../../platform/theme/common/themeService.js'; +import { ICurrentPartialCommand } from '../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; + +function getChatTerminalBackgroundColor(theme: IColorTheme, contextKeyService: IContextKeyService, storedBackground?: string): Color | undefined { + if (storedBackground) { + const color = Color.fromHex(storedBackground); + if (color) { + return color; + } + } + + const terminalBackground = theme.getColor(TERMINAL_BACKGROUND_COLOR); + if (terminalBackground) { + return terminalBackground; + } + + const isInEditor = ChatContextKeys.inChatEditor.getValue(contextKeyService); + return theme.getColor(isInEditor ? editorBackground : PANEL_BACKGROUND); +} + +/** + * Computes the maximum column width of content in a terminal buffer. + * Iterates through each line and finds the rightmost non-empty cell. + * + * @param buffer The buffer to measure + * @param cols The terminal column count (used to clamp line length) + * @returns The maximum column width (number of columns used), or 0 if all lines are empty + */ +export function computeMaxBufferColumnWidth(buffer: { readonly length: number; getLine(y: number): { readonly length: number; getCell(x: number): { getChars(): string } | undefined } | undefined }, cols: number): number { + let maxWidth = 0; + + for (let y = 0; y < buffer.length; y++) { + const line = buffer.getLine(y); + if (!line) { + continue; + } + + // Find the last non-empty cell by iterating backwards + const lineLength = Math.min(line.length, cols); + for (let x = lineLength - 1; x >= 0; x--) { + if (line.getCell(x)?.getChars()) { + maxWidth = Math.max(maxWidth, x + 1); + break; + } + } + } + + return maxWidth; +} + +/** + * Checks if two VT strings match around a boundary where we would slice. + * This is an efficient O(1) check that verifies a small window of characters + * before the slice point to detect if the VT sequences have diverged (common on Windows). + * + * @param newVT The new VT text to compare. + * @param oldVT The old VT text to compare against. + * @param slicePoint The point where we would slice. Must be <= both string lengths. + * @param windowSize The number of characters before slicePoint to check (default 50). + * @returns True if the boundary matches, false if VT sequences have diverged. + */ +export function vtBoundaryMatches(newVT: string, oldVT: string, slicePoint: number, windowSize: number = 50): boolean { + const start = Math.max(0, slicePoint - windowSize); + const end = slicePoint; + for (let i = start; i < end; i++) { + if (newVT.charCodeAt(i) !== oldVT.charCodeAt(i)) { + return false; + } + } + return true; +} + +export interface IDetachedTerminalCommandMirrorRenderResult { + lineCount?: number; + maxColumnWidth?: number; +} + +interface IDetachedTerminalCommandMirror { + attach(container: HTMLElement): Promise; + renderCommand(): Promise; + onDidUpdate: Event; + onDidInput: Event; +} + +const enum ChatTerminalMirrorMetrics { + MirrorRowCount = 10, + MirrorColCountFallback = 80, + /** + * Maximum number of lines for which we compute the max column width. + * Computing max column width iterates the entire buffer, so we skip it + * for large outputs to avoid performance issues. + */ + MaxLinesForColumnWidthComputation = 100 +} + +/** + * Computes the line count for terminal output between start and end lines. + * The end line is exclusive (points to the line after output ends). + */ +function computeOutputLineCount(startLine: number, endLine: number): number { + return Math.max(endLine - startLine, 0); +} + +export async function getCommandOutputSnapshot( + xtermTerminal: XtermTerminal, + command: ITerminalCommand, + log?: (reason: 'fallback' | 'primary', error: unknown) => void +): Promise<{ text: string; lineCount: number } | undefined> { + const executedMarker = command.executedMarker; + const endMarker = command.endMarker; + + if (!endMarker || endMarker.isDisposed) { + return undefined; + } + + if (!executedMarker || executedMarker.isDisposed) { + const raw = xtermTerminal.raw; + const buffer = raw.buffer.active; + const offsets = [ + -(buffer.baseY + buffer.cursorY), + -buffer.baseY, + 0 + ]; + let startMarker: IXtermMarker | undefined; + for (const offset of offsets) { + startMarker = raw.registerMarker(offset); + if (startMarker) { + break; + } + } + if (!startMarker || startMarker.isDisposed) { + return { text: '', lineCount: 0 }; + } + const startLine = startMarker.line; + let text: string | undefined; + try { + text = await xtermTerminal.getRangeAsVT(startMarker, endMarker, true); + } catch (error) { + log?.('fallback', error); + return undefined; + } finally { + startMarker.dispose(); + } + if (!text) { + return { text: '', lineCount: 0 }; + } + const endLine = endMarker.line; + const lineCount = computeOutputLineCount(startLine, endLine); + return { text, lineCount }; + } + + const startLine = executedMarker.line; + const endLine = endMarker.line; + const lineCount = computeOutputLineCount(startLine, endLine); + + let text: string | undefined; + try { + text = await xtermTerminal.getRangeAsVT(executedMarker, endMarker, true); + } catch (error) { + log?.('primary', error); + return undefined; + } + if (!text) { + return { text: '', lineCount: 0 }; + } + + return { text, lineCount }; +} + +/** + * Mirrors a terminal command's output into a detached terminal instance. + * Used in the chat terminal tool progress part to show command output. + */ +export class DetachedTerminalCommandMirror extends Disposable implements IDetachedTerminalCommandMirror { + // Streaming approach + // ------------------ + // The mirror maintains a VT snapshot of the command's output and incrementally updates a + // detached xterm instance instead of re-rendering the whole range on every change. + // + // - A *dirty range* is the set of buffer rows that may have diverged between the source + // terminal and the detached mirror. It is tracked by: + // - `_lastUpToDateCursorY`: the last cursor row in the source buffer for which the + // mirror is known to be fully up to date. + // - `_lowestDirtyCursorY`: the smallest (top-most) cursor row that has been affected + // by new data or cursor movement since the last flush. + // + // - When new data arrives or the cursor moves, xterm events and `onData` callbacks are + // used to update `_lowestDirtyCursorY`. This effectively marks everything from that row + // downwards as potentially stale. + // + // - If the dirty range starts exactly at the previous end of the mirrored output (that is, + // `_lowestDirtyCursorY` is at or after `_lastUpToDateCursorY` and no earlier rows have + // changed), the mirror can *append* VT that corresponds only to the new rows. + // + // - If the cursor moves or data is written above the previously mirrored end (for example, + // when the command rewrites lines, uses carriage returns, or modifies earlier rows), + // `_lowestDirtyCursorY` will be before `_lastUpToDateCursorY`. In that case the mirror + // cannot safely append and instead falls back to taking a fresh VT snapshot of the + // entire command range and *rewrites* the detached terminal content. + + private _detachedTerminal: IDetachedTerminalInstance | undefined; + private _detachedTerminalPromise: Promise | undefined; + private _attachedContainer: HTMLElement | undefined; + private readonly _streamingDisposables = this._register(new DisposableStore()); + private readonly _onDidUpdateEmitter = this._register(new Emitter()); + public readonly onDidUpdate: Event = this._onDidUpdateEmitter.event; + private readonly _onDidInputEmitter = this._register(new Emitter()); + public readonly onDidInput: Event = this._onDidInputEmitter.event; + + private _lastVT = ''; + private _lineCount = 0; + private _maxColumnWidth = 0; + private _lastUpToDateCursorY: number | undefined; + private _lowestDirtyCursorY: number | undefined; + private _flushPromise: Promise | undefined; + private _dirtyScheduled = false; + private _isStreaming = false; + private _sourceRaw: RawXtermTerminal | undefined; + + constructor( + private readonly _xtermTerminal: XtermTerminal, + private readonly _command: ITerminalCommand, + @ITerminalService private readonly _terminalService: ITerminalService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService + ) { + super(); + this._register(toDisposable(() => { + this._stopStreaming(); + })); + } + + async attach(container: HTMLElement): Promise { + if (this._store.isDisposed) { + return; + } + let terminal: IDetachedTerminalInstance; + try { + terminal = await this._getOrCreateTerminal(); + } catch (error) { + if (error instanceof CancellationError) { + return; + } + throw error; + } + if (this._store.isDisposed) { + return; + } + if (this._attachedContainer !== container) { + container.classList.add('chat-terminal-output-terminal'); + terminal.attachToElement(container, { enableGpu: false }); + this._attachedContainer = container; + } + } + + async renderCommand(): Promise { + if (this._store.isDisposed) { + return undefined; + } + let detached: IDetachedTerminalInstance; + try { + detached = await this._getOrCreateTerminal(); + } catch (error) { + if (error instanceof CancellationError) { + return undefined; + } + throw error; + } + if (this._store.isDisposed) { + return undefined; + } + let vt; + try { + vt = await this._getCommandOutputAsVT(this._xtermTerminal); + } catch { + // ignore and treat as no output + } + if (!vt) { + return undefined; + } + if (this._store.isDisposed) { + return undefined; + } + + await new Promise(resolve => { + // Only append if the boundary around the slice point matches; otherwise rewrite. + // This is an efficient constant-time check (checking up to 50 characters) instead of comparing the entire prefix. + // On Windows, VT sequences can differ even for equivalent content, causing corruption + // if we blindly append. + const canAppend = !!this._lastVT && vt.text.length >= this._lastVT.length && this._vtBoundaryMatches(vt.text, this._lastVT.length); + if (!canAppend) { + // Reset the terminal if we had previous content (can't append, need full rewrite) + if (this._lastVT) { + detached.xterm.reset(); + } + if (vt.text) { + detached.xterm.write(vt.text, resolve); + } else { + resolve(); + } + } else { + const appended = vt.text.slice(this._lastVT.length); + if (appended) { + detached.xterm.write(appended, resolve); + } else { + resolve(); + } + } + }); + + this._lastVT = vt.text; + + const sourceRaw = this._xtermTerminal.raw; + if (sourceRaw) { + this._sourceRaw = sourceRaw; + this._lastUpToDateCursorY = this._getAbsoluteCursorY(sourceRaw); + if (!this._isStreaming && (!this._command.endMarker || this._command.endMarker.isDisposed)) { + this._startStreaming(sourceRaw); + } + } + + this._lineCount = this._getRenderedLineCount(); + // Only compute max column width after the command finishes and for small outputs + const commandFinished = this._command.endMarker && !this._command.endMarker.isDisposed; + if (commandFinished && this._lineCount <= ChatTerminalMirrorMetrics.MaxLinesForColumnWidthComputation) { + this._maxColumnWidth = this._computeMaxColumnWidth(); + } + + return { lineCount: this._lineCount, maxColumnWidth: this._maxColumnWidth }; + } + + private async _getCommandOutputAsVT(source: XtermTerminal): Promise<{ text: string } | undefined> { + if (this._store.isDisposed) { + return undefined; + } + const executedMarker = this._command.executedMarker ?? (this._command as unknown as ICurrentPartialCommand).commandExecutedMarker; + if (!executedMarker) { + return undefined; + } + + const endMarker = this._command.endMarker; + const text = await source.getRangeAsVT(executedMarker, endMarker, endMarker?.line !== executedMarker.line); + if (this._store.isDisposed) { + return undefined; + } + if (!text) { + return { text: '' }; + } + + return { text }; + } + + private _getRenderedLineCount(): number { + // Calculate line count from the command's markers when available + const endMarker = this._command.endMarker; + if (this._command.executedMarker && endMarker && !endMarker.isDisposed) { + const startLine = this._command.executedMarker.line; + const endLine = endMarker.line; + return computeOutputLineCount(startLine, endLine); + } + + // During streaming (no end marker), calculate from the source terminal buffer + const executedMarker = this._command.executedMarker ?? (this._command as unknown as ICurrentPartialCommand).commandExecutedMarker; + if (executedMarker && this._sourceRaw) { + const buffer = this._sourceRaw.buffer.active; + const currentLine = buffer.baseY + buffer.cursorY; + return computeOutputLineCount(executedMarker.line, currentLine); + } + + return this._lineCount; + } + + private _computeMaxColumnWidth(): number { + const detached = this._detachedTerminal; + if (!detached) { + return 0; + } + return computeMaxBufferColumnWidth(detached.xterm.buffer.active, detached.xterm.cols); + } + + private async _getOrCreateTerminal(): Promise { + if (this._detachedTerminal) { + return this._detachedTerminal; + } + if (this._detachedTerminalPromise) { + return this._detachedTerminalPromise; + } + if (this._store.isDisposed) { + throw new CancellationError(); + } + const createPromise = (async () => { + const colorProvider = { + getBackgroundColor: (theme: IColorTheme) => getChatTerminalBackgroundColor(theme, this._contextKeyService) + }; + const processInfo = new DetachedProcessInfo({ initialCwd: '' }); + const detached = await this._terminalService.createDetachedTerminal({ + cols: this._xtermTerminal.raw.cols ?? ChatTerminalMirrorMetrics.MirrorColCountFallback, + rows: ChatTerminalMirrorMetrics.MirrorRowCount, + readonly: false, + processInfo, + disableOverviewRuler: true, + colorProvider + }); + if (this._store.isDisposed) { + processInfo.dispose(); + detached.dispose(); + throw new CancellationError(); + } + this._detachedTerminal = detached; + this._register(processInfo); + this._register(detached); + + // Forward input from the mirror terminal to the source terminal + this._register(detached.onData(data => this._onDidInputEmitter.fire(data))); + return detached; + })(); + this._detachedTerminalPromise = createPromise; + return createPromise; + } + + private _startStreaming(raw: RawXtermTerminal): void { + if (this._store.isDisposed || this._isStreaming) { + return; + } + this._isStreaming = true; + this._streamingDisposables.add(Event.any(raw.onCursorMove, raw.onLineFeed, raw.onWriteParsed)(() => this._handleCursorEvent())); + this._streamingDisposables.add(raw.onData(() => this._handleCursorEvent())); + } + + private _stopStreaming(): void { + if (!this._isStreaming) { + return; + } + this._streamingDisposables.clear(); + this._isStreaming = false; + this._lowestDirtyCursorY = undefined; + this._sourceRaw = undefined; + } + + private _handleCursorEvent(): void { + if (this._store.isDisposed || !this._sourceRaw) { + return; + } + const cursorY = this._getAbsoluteCursorY(this._sourceRaw); + this._lowestDirtyCursorY = this._lowestDirtyCursorY === undefined ? cursorY : Math.min(this._lowestDirtyCursorY, cursorY); + this._scheduleFlush(); + } + + private _scheduleFlush(): void { + if (this._dirtyScheduled || this._store.isDisposed) { + return; + } + this._dirtyScheduled = true; + queueMicrotask(() => { + this._dirtyScheduled = false; + if (this._store.isDisposed) { + return; + } + this._flushDirtyRange(); + }); + } + + private _flushDirtyRange(): void { + if (this._store.isDisposed || this._flushPromise) { + return; + } + this._flushPromise = this._doFlushDirtyRange().finally(() => { + this._flushPromise = undefined; + }); + } + + private async _doFlushDirtyRange(): Promise { + if (this._store.isDisposed) { + return; + } + const sourceRaw = this._xtermTerminal.raw; + let detached = this._detachedTerminal; + if (!detached) { + try { + detached = await this._getOrCreateTerminal(); + } catch (error) { + if (error instanceof CancellationError) { + return; + } + throw error; + } + } + if (this._store.isDisposed) { + return; + } + const detachedRaw = detached?.xterm; + if (!sourceRaw || !detachedRaw) { + return; + } + + this._sourceRaw = sourceRaw; + const currentCursor = this._getAbsoluteCursorY(sourceRaw); + const previousCursor = this._lastUpToDateCursorY ?? currentCursor; + const startCandidate = this._lowestDirtyCursorY ?? currentCursor; + this._lowestDirtyCursorY = undefined; + + const startLine = Math.min(previousCursor, startCandidate); + // Ensure we resolve any pending flush even when no actual new output is available. + const vt = await this._getCommandOutputAsVT(this._xtermTerminal); + if (!vt) { + return; + } + if (this._store.isDisposed) { + return; + } + + if (vt.text === this._lastVT) { + this._lastUpToDateCursorY = currentCursor; + if (this._command.endMarker && !this._command.endMarker.isDisposed) { + this._stopStreaming(); + } + return; + } + + // Only append if: (1) cursor hasn't moved backwards, and (2) boundary around slice point matches. + // This is an efficient O(1) check instead of comparing the entire prefix. + // On Windows, VT sequences can differ even for equivalent content, so we must verify. + const canAppend = !!this._lastVT && startLine >= previousCursor && vt.text.length >= this._lastVT.length && this._vtBoundaryMatches(vt.text, this._lastVT.length); + await new Promise(resolve => { + if (!canAppend) { + // Reset the terminal if we had previous content (can't append, need full rewrite) + if (this._lastVT) { + detachedRaw.reset(); + } + if (vt.text) { + detachedRaw.write(vt.text, resolve); + } else { + resolve(); + } + } else { + const appended = vt.text.slice(this._lastVT.length); + if (appended) { + detachedRaw.write(appended, resolve); + } else { + resolve(); + } + } + }); + + this._lastVT = vt.text; + this._lineCount = this._getRenderedLineCount(); + this._lastUpToDateCursorY = currentCursor; + + const commandFinished = this._command.endMarker && !this._command.endMarker.isDisposed; + if (commandFinished) { + // Only compute max column width after the command finishes and for small outputs + if (this._lineCount <= ChatTerminalMirrorMetrics.MaxLinesForColumnWidthComputation) { + this._maxColumnWidth = this._computeMaxColumnWidth(); + } + this._stopStreaming(); + } + + this._onDidUpdateEmitter.fire({ lineCount: this._lineCount, maxColumnWidth: this._maxColumnWidth }); + } + + private _getAbsoluteCursorY(raw: RawXtermTerminal): number { + return raw.buffer.active.baseY + raw.buffer.active.cursorY; + } + + /** + * Checks if the new VT text matches the old VT around the boundary where we would slice. + */ + private _vtBoundaryMatches(newVT: string, slicePoint: number): boolean { + return vtBoundaryMatches(newVT, this._lastVT, slicePoint); + } +} + +/** + * Mirrors a terminal output snapshot into a detached terminal instance. + * Used when the terminal has been disposed of but we still want to show the output. + */ +export class DetachedTerminalSnapshotMirror extends Disposable { + private _detachedTerminal: Promise | undefined; + private _attachedContainer: HTMLElement | undefined; + + private _output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined; + private _container: HTMLElement | undefined; + private _dirty = true; + private _lastRenderedLineCount: number | undefined; + private _lastRenderedMaxColumnWidth: number | undefined; + + constructor( + output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined, + private readonly _getTheme: () => IChatTerminalToolInvocationData['terminalTheme'] | undefined, + @ITerminalService private readonly _terminalService: ITerminalService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + ) { + super(); + this._output = output; + const processInfo = this._register(new DetachedProcessInfo({ initialCwd: '' })); + this._detachedTerminal = this._terminalService.createDetachedTerminal({ + cols: ChatTerminalMirrorMetrics.MirrorColCountFallback, + rows: ChatTerminalMirrorMetrics.MirrorRowCount, + readonly: true, + processInfo, + disableOverviewRuler: true, + colorProvider: { + getBackgroundColor: theme => { + const storedBackground = this._getTheme()?.background; + return getChatTerminalBackgroundColor(theme, this._contextKeyService, storedBackground); + } + } + }).then(terminal => { + // If the store is already disposed, dispose the terminal immediately + if (this._store.isDisposed) { + terminal.dispose(); + return terminal; + } + return this._register(terminal); + }); + } + + private async _getTerminal(): Promise { + if (!this._detachedTerminal) { + throw new Error('Detached terminal not initialized'); + } + return this._detachedTerminal; + } + + public setOutput(output: IChatTerminalToolInvocationData['terminalCommandOutput'] | undefined): void { + this._output = output; + this._dirty = true; + } + + public async attach(container: HTMLElement): Promise { + const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return; + } + container.classList.add('chat-terminal-output-terminal'); + const needsAttach = this._attachedContainer !== container || container.firstChild === null; + if (needsAttach) { + terminal.attachToElement(container, { enableGpu: false }); + this._attachedContainer = container; + } + + this._container = container; + this._applyTheme(container); + } + + public async render(): Promise<{ lineCount?: number; maxColumnWidth?: number } | undefined> { + const output = this._output; + if (!output) { + return undefined; + } + if (!this._dirty) { + return { lineCount: this._lastRenderedLineCount ?? output.lineCount, maxColumnWidth: this._lastRenderedMaxColumnWidth }; + } + const terminal = await this._getTerminal(); + if (this._store.isDisposed) { + return undefined; + } + if (this._container) { + this._applyTheme(this._container); + } + const text = output.text ?? ''; + const lineCount = output.lineCount ?? this._estimateLineCount(text); + if (!text) { + this._dirty = false; + this._lastRenderedLineCount = lineCount; + this._lastRenderedMaxColumnWidth = 0; + return { lineCount: 0, maxColumnWidth: 0 }; + } + await new Promise(resolve => terminal.xterm.write(text, resolve)); + if (this._store.isDisposed) { + return undefined; + } + this._dirty = false; + this._lastRenderedLineCount = lineCount; + // Only compute max column width for small outputs to avoid performance issues + if (this._shouldComputeMaxColumnWidth(lineCount)) { + this._lastRenderedMaxColumnWidth = this._computeMaxColumnWidth(terminal); + } + return { lineCount, maxColumnWidth: this._lastRenderedMaxColumnWidth }; + } + + private _computeMaxColumnWidth(terminal: IDetachedTerminalInstance): number { + return computeMaxBufferColumnWidth(terminal.xterm.buffer.active, terminal.xterm.cols); + } + + private _estimateLineCount(text: string): number { + if (!text) { + return 0; + } + const sanitized = text.replace(/\r/g, ''); + const segments = sanitized.split('\n'); + const count = sanitized.endsWith('\n') ? segments.length - 1 : segments.length; + return Math.max(count, 1); + } + + private _shouldComputeMaxColumnWidth(lineCount: number): boolean { + return lineCount <= ChatTerminalMirrorMetrics.MaxLinesForColumnWidthComputation; + } + + private _applyTheme(container: HTMLElement): void { + const theme = this._getTheme(); + if (!theme) { + container.style.removeProperty('background-color'); + container.style.removeProperty('color'); + return; + } + if (theme.background) { + container.style.backgroundColor = theme.background; + } + if (theme.foreground) { + container.style.color = theme.foreground; + } + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts b/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts index 250021d2ffa..ff94e3496e8 100644 --- a/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/detachedTerminal.ts @@ -6,10 +6,11 @@ import * as dom from '../../../../base/browser/dom.js'; import { Delayer } from '../../../../base/common/async.js'; import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { OperatingSystem } from '../../../../base/common/platform.js'; import { MicrotaskDelay } from '../../../../base/common/symbols.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalCapabilityStore } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalCapabilityStore } from '../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; import { IMergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariable.js'; import { ITerminalBackend } from '../../../../platform/terminal/common/terminal.js'; @@ -19,17 +20,20 @@ import { TerminalWidgetManager } from './widgets/widgetManager.js'; import { XtermTerminal } from './xterm/xtermTerminal.js'; import { IEnvironmentVariableInfo } from '../common/environmentVariable.js'; import { ITerminalProcessInfo, ProcessState } from '../common/terminal.js'; +import { Event } from '../../../../base/common/event.js'; export class DetachedTerminal extends Disposable implements IDetachedTerminalInstance { private readonly _widgets = this._register(new TerminalWidgetManager()); - public readonly capabilities = new TerminalCapabilityStore(); + public readonly capabilities: ITerminalCapabilityStore; private readonly _contributions: Map = new Map(); + private readonly _attachDisposables = this._register(new MutableDisposable()); public domElement?: HTMLElement; public get xterm(): IDetachedXtermTerminal { return this._xterm; } + public readonly onData: Event; constructor( private readonly _xterm: XtermTerminal, @@ -37,6 +41,10 @@ export class DetachedTerminal extends Disposable implements IDetachedTerminalIns @IInstantiationService instantiationService: IInstantiationService, ) { super(); + this.onData = this._xterm.raw.onData; + const capabilities = options.capabilities ?? new TerminalCapabilityStore(); + this._register(capabilities); + this.capabilities = capabilities; this._register(_xterm); // Initialize contributions @@ -95,6 +103,14 @@ export class DetachedTerminal extends Disposable implements IDetachedTerminalIns this.domElement = container; const screenElement = this._xterm.attachToElement(container, options); this._widgets.attachToElement(screenElement); + + const attachStore = new DisposableStore(); + const scheduleFocus = () => { + // Defer so scrollable containers can handle focus first; ensures textarea focus sticks + setTimeout(() => this.focus(true), 0); + }; + attachStore.add(dom.addDisposableListener(container, dom.EventType.MOUSE_DOWN, scheduleFocus)); + this._attachDisposables.value = attachStore; } forceScrollbarVisibility(): void { @@ -115,7 +131,7 @@ export class DetachedTerminal extends Disposable implements IDetachedTerminalIns * properties are stubbed. Properties are mutable and can be updated by * the instantiator. */ -export class DetachedProcessInfo implements ITerminalProcessInfo { +export class DetachedProcessInfo extends Disposable implements ITerminalProcessInfo { processState = ProcessState.Running; ptyProcessReady = Promise.resolve(); shellProcessId: number | undefined; @@ -129,11 +145,13 @@ export class DetachedProcessInfo implements ITerminalProcessInfo { hasWrittenData = false; hasChildProcesses = false; backend: ITerminalBackend | undefined; - capabilities = new TerminalCapabilityStore(); + capabilities: ITerminalCapabilityStore; shellIntegrationNonce = ''; extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined; constructor(initialValues: Partial) { + super(); Object.assign(this, initialValues); + this.capabilities = this._register(new TerminalCapabilityStore()); } } diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 501aa899b71..c005756ed46 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -144,8 +144,8 @@ position: relative; } -.monaco-workbench .terminal-editor .terminal-wrapper > div, -.monaco-workbench .pane-body.integrated-terminal .terminal-wrapper > div { +.monaco-workbench .terminal-editor .terminal-wrapper > .terminal-xterm-host, +.monaco-workbench .pane-body.integrated-terminal .terminal-wrapper > .terminal-xterm-host { height: 100%; } @@ -304,6 +304,13 @@ height: 100%; overflow: hidden; position: relative; + display: flex; + flex-direction: column; +} + +.monaco-workbench .pane-body.integrated-terminal .tabs-list-container > .tabs-list { + flex: 1; + min-height: 0; } .monaco-workbench .pane-body.integrated-terminal .tabs-container > .monaco-toolbar { @@ -330,10 +337,7 @@ } .monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry { - position: absolute; - left: 0; - right: 0; - bottom: 0; + flex-shrink: 0; height: 22px; line-height: 22px; display: flex; @@ -347,11 +351,13 @@ .monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry .terminal-tabs-entry { display: flex; align-items: center; - justify-content: center; - gap: 4px; + justify-content: flex-start; + gap: 6px; width: 100%; height: 100%; padding: 0; + box-sizing: border-box; + overflow: hidden; } .monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry .terminal-tabs-entry:hover { @@ -370,7 +376,7 @@ } .monaco-workbench .pane-body.integrated-terminal .tabs-container.has-text .terminal-tabs-chat-entry .terminal-tabs-entry { - justify-content: center; + justify-content: flex-start; padding: 0 10px; } @@ -378,6 +384,36 @@ display: none; } +.monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry .terminal-tabs-chat-entry-delete { + display: none; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + padding: 2px; + margin-left: auto; + cursor: pointer; + opacity: 0.8; + flex-shrink: 0; +} + +.monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry:hover .terminal-tabs-chat-entry-delete, +.monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry:focus-within .terminal-tabs-chat-entry-delete { + display: flex; +} + +.monaco-workbench .pane-body.integrated-terminal .terminal-tabs-chat-entry .terminal-tabs-chat-entry-delete:hover { + opacity: 1; + background-color: var(--vscode-toolbar-hoverBackground); + border-radius: 3px; +} + +.monaco-workbench .pane-body.integrated-terminal .tabs-container:not(.has-text) .terminal-tabs-chat-entry .terminal-tabs-chat-entry-delete, +.monaco-workbench .pane-body.integrated-terminal .tabs-container:not(.has-text) .terminal-tabs-chat-entry:hover .terminal-tabs-chat-entry-delete, +.monaco-workbench .pane-body.integrated-terminal .tabs-container:not(.has-text) .terminal-tabs-chat-entry:focus-within .terminal-tabs-chat-entry-delete { + display: none; +} + .monaco-workbench .pane-body.integrated-terminal .tabs-list .terminal-tabs-entry { text-align: center; } @@ -414,6 +450,20 @@ display: none; } +.monaco-workbench .pane-body.integrated-terminal .tabs-list .editable-tab .monaco-inputbox { + min-width: 0; + width: 100%; + box-sizing: border-box; + height: 22px; +} + +.monaco-workbench .pane-body.integrated-terminal .tabs-list .editable-tab .monaco-inputbox > .ibwrapper > .input { + padding: 0 6px; + height: 100%; + line-height: 22px; + box-sizing: border-box; +} + .monaco-workbench .pane-body.integrated-terminal .tabs-list .actions .action-label { padding: 2px; } @@ -523,6 +573,7 @@ box-sizing: border-box; transform: translateX(3px); pointer-events: none; + margin-left: -20px; } .terminal-command-guide.top { border-top-left-radius: 1px; diff --git a/src/vs/workbench/contrib/terminal/browser/remotePty.ts b/src/vs/workbench/contrib/terminal/browser/remotePty.ts index 70933f071ec..e27137d903c 100644 --- a/src/vs/workbench/contrib/terminal/browser/remotePty.ts +++ b/src/vs/workbench/contrib/terminal/browser/remotePty.ts @@ -8,6 +8,7 @@ import { ITerminalLaunchResult, IProcessPropertyMap, ITerminalChildProcess, ITer import { BasePty } from '../common/basePty.js'; import { RemoteTerminalChannelClient } from '../common/remote/remoteTerminalChannel.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { hasKey } from '../../../../base/common/types.js'; export class RemotePty extends BasePty implements ITerminalChildProcess { private readonly _startBarrier: Barrier; @@ -35,7 +36,7 @@ export class RemotePty extends BasePty implements ITerminalChildProcess { const startResult = await this._remoteTerminalChannel.start(this.id); - if (startResult && 'message' in startResult) { + if (startResult && hasKey(startResult, { message: true })) { // An error occurred return startResult; } @@ -116,10 +117,6 @@ export class RemotePty extends BasePty implements ITerminalChildProcess { return this._remoteTerminalChannel.setUnicodeVersion(this.id, version); } - async setNextCommandId(commandLine: string, commandId: string): Promise { - return this._remoteTerminalChannel.setNextCommandId(this.id, commandLine, commandId); - } - async refreshProperty(type: T): Promise { return this._remoteTerminalChannel.refreshProperty(this.id, type); } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts index 313148f646f..31b2ac595af 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalBackend.ts @@ -16,7 +16,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { IRemoteAuthorityResolverService } from '../../../../platform/remote/common/remoteAuthorityResolver.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ISerializedTerminalCommand } from '../../../../platform/terminal/common/capabilities/capabilities.js'; -import { IPtyHostLatencyMeasurement, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalBackend, ITerminalBackendRegistry, ITerminalChildProcess, ITerminalEnvironment, ITerminalLogService, ITerminalProcessOptions, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalExtensions, TerminalIcon, TerminalSettingId, TitleEventSource } from '../../../../platform/terminal/common/terminal.js'; +import { IPtyHostLatencyMeasurement, IShellLaunchConfig, IShellLaunchConfigDto, ITerminalBackend, ITerminalBackendRegistry, ITerminalChildProcess, ITerminalEnvironment, ITerminalLogService, ITerminalProcessOptions, ITerminalProfile, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, ProcessPropertyType, TerminalExtensions, TerminalIcon, TerminalSettingId, TitleEventSource, type IProcessPropertyMap } from '../../../../platform/terminal/common/terminal.js'; import { IProcessDetails } from '../../../../platform/terminal/common/terminalProcess.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; @@ -257,7 +257,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack ]; } - async updateProperty(id: number, property: T, value: any): Promise { + async updateProperty(id: number, property: T, value: IProcessPropertyMap[T]): Promise { await this._remoteTerminalChannel.updateProperty(id, property, value); } @@ -269,6 +269,10 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack await this._remoteTerminalChannel.updateIcon(id, userInitiated, icon, color); } + async setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + await this._remoteTerminalChannel.setNextCommandId(id, commandLine, commandId); + } + async getDefaultSystemShell(osOverride?: OperatingSystem): Promise { return this._remoteTerminalChannel.getDefaultSystemShell(osOverride) || ''; } @@ -349,7 +353,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); } } catch (e: unknown) { - this._logService.warn('RemoteTerminalBackend#getTerminalLayoutInfo Error', e && typeof e === 'object' && 'message' in e ? e.message : e); + this._logService.warn('RemoteTerminalBackend#getTerminalLayoutInfo Error', (<{ message?: string }>e).message ?? e); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 99d4879266b..c4bcfd5b70b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -28,13 +28,13 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte import { GroupIdentifier } from '../../../common/editor.js'; import { ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP_TYPE, SIDE_GROUP_TYPE } from '../../../services/editor/common/editorService.js'; import type { ICurrentPartialCommand } from '../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; -import type { IXtermCore } from './xterm-private.js'; +import type { IXtermCore, IBufferSet } from './xterm-private.js'; import type { IMenu } from '../../../../platform/actions/common/actions.js'; import type { IProgressState } from '@xterm/addon-progress'; import type { IEditorOptions } from '../../../../platform/editor/common/editor.js'; import type { TerminalEditorInput } from './terminalEditorInput.js'; import type { MaybePromise } from '../../../../base/common/async.js'; -import type { SingleOrMany } from '../../../../base/common/types.js'; +import { isNumber, type SingleOrMany } from '../../../../base/common/types.js'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalConfigurationService = createDecorator('terminalConfigurationService'); @@ -106,6 +106,15 @@ export interface ITerminalInstanceService { * Service enabling communication between the chat tool implementation in terminal contrib and workbench contribs. * Acts as a communication mechanism for chat-related terminal features. */ +export interface IChatTerminalToolProgressPart { + readonly elementIndex: number; + readonly contentIndex: number; + focusTerminal(): Promise; + toggleOutputFromKeyboard(): Promise; + focusOutput(): void; + getCommandAndOutputAsText(): string | undefined; +} + export interface ITerminalChatService { readonly _serviceBrand: undefined; @@ -142,21 +151,106 @@ export interface ITerminalChatService { getToolSessionIdForInstance(instance: ITerminalInstance): string | undefined; /** - * Associate a chat session ID with a terminal instance. This is used to retrieve the chat + * Associate a chat session with a terminal instance. This is used to retrieve the chat * session title for display purposes. - * @param chatSessionId The chat session ID + * @param chatSessionResource The chat session resource URI * @param instance The terminal instance */ - registerTerminalInstanceWithChatSession(chatSessionId: string, instance: ITerminalInstance): void; + registerTerminalInstanceWithChatSession(chatSessionResource: URI, instance: ITerminalInstance): void; /** + * Returns the chat session resource for a given terminal instance, if it has been registered. + * @param instance The terminal instance to look up + * @returns The chat session resource if found, undefined otherwise + */ + getChatSessionResourceForInstance(instance: ITerminalInstance): URI | undefined; + /** + * @deprecated Use getChatSessionResourceForInstance instead * Returns the chat session ID for a given terminal instance, if it has been registered. * @param instance The terminal instance to look up * @returns The chat session ID if found, undefined otherwise */ getChatSessionIdForInstance(instance: ITerminalInstance): string | undefined; + /** + * Check if a terminal is a background terminal (tool-driven terminal that may be hidden from + * normal UI). + * @param terminalToolSessionId The tool session ID to check, if provided + * @returns True if the terminal is a background terminal, false otherwise + */ isBackgroundTerminal(terminalToolSessionId?: string): boolean; + + /** + * Register a chat terminal tool progress part for tracking and focus management. + * @param part The progress part to register + * @returns A disposable that unregisters the progress part when disposed + */ + registerProgressPart(part: IChatTerminalToolProgressPart): IDisposable; + + /** + * Set the currently focused progress part. + * @param part The progress part to focus + */ + setFocusedProgressPart(part: IChatTerminalToolProgressPart): void; + + /** + * Clear the focused state from a progress part. + * @param part The progress part to clear focus from + */ + clearFocusedProgressPart(part: IChatTerminalToolProgressPart): void; + + /** + * Get the currently focused progress part, if any. + * @returns The focused progress part or undefined if none is focused + */ + getFocusedProgressPart(): IChatTerminalToolProgressPart | undefined; + + /** + * Get the most recently registered progress part, if any. + * @returns The most recent progress part or undefined if none exist + */ + getMostRecentProgressPart(): IChatTerminalToolProgressPart | undefined; + + /** + * Enable or disable auto approval for all commands in a specific session. + * @param chatSessionResource The chat session resource URI + * @param enabled Whether to enable or disable session auto approval + */ + setChatSessionAutoApproval(chatSessionResource: URI, enabled: boolean): void; + + /** + * Check if a session has auto approval enabled for all commands. + * @param chatSessionResource The chat session resource URI + * @returns True if the session has auto approval enabled + */ + hasChatSessionAutoApproval(chatSessionResource: URI): boolean; + + /** + * Add a session-scoped auto-approve rule. + * @param chatSessionResource The chat session resource URI + * @param key The rule key (command or regex pattern) + * @param value The rule value (approval boolean or object with approve and matchCommandLine) + */ + addSessionAutoApproveRule(chatSessionResource: URI, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void; + + /** + * Get all session-scoped auto-approve rules for a specific chat session. + * @param chatSessionResource The chat session resource URI + * @returns A record of all session-scoped auto-approve rules for the session + */ + getSessionAutoApproveRules(chatSessionResource: URI): Readonly>; + + /** + * Signal that a foreground terminal tool invocation should continue in the background. + * This causes the tool to return its current output immediately while the terminal keeps running. + * @param terminalToolSessionId The tool session ID to continue in background + */ + continueInBackground(terminalToolSessionId: string): void; + + /** + * Event fired when a terminal tool invocation should continue in the background. + */ + readonly onDidContinueInBackground: Event; } /** @@ -269,9 +363,10 @@ export interface IDetachedXTermOptions { cols: number; rows: number; colorProvider: IXtermColorProvider; - capabilities?: ITerminalCapabilityStore; + capabilities?: ITerminalCapabilityStore & IDisposable; readonly?: boolean; processInfo: ITerminalProcessInfo; + disableOverviewRuler?: boolean; } /** @@ -335,6 +430,11 @@ export interface IBaseTerminalInstance { export interface IDetachedTerminalInstance extends IDisposable, IBaseTerminalInstance { readonly xterm: IDetachedXtermTerminal; + /** + * Event fired when data is received from the terminal. + */ + onData: Event; + /** * Attached the terminal to the given element. This should be preferred over * calling {@link IXtermTerminal.attachToElement} so that extra DOM elements @@ -346,7 +446,7 @@ export interface IDetachedTerminalInstance extends IDisposable, IBaseTerminalIns attachToElement(container: HTMLElement, options?: Partial): void; } -export const isDetachedTerminalInstance = (t: ITerminalInstance | IDetachedTerminalInstance): t is IDetachedTerminalInstance => typeof (t as ITerminalInstance).instanceId !== 'number'; +export const isDetachedTerminalInstance = (t: ITerminalInstance | IDetachedTerminalInstance): t is IDetachedTerminalInstance => !isNumber((t as ITerminalInstance).instanceId); export interface ITerminalService extends ITerminalInstanceHost { readonly _serviceBrand: undefined; @@ -432,6 +532,7 @@ export interface ITerminalService extends ITerminalInstanceHost { moveIntoNewEditor(source: ITerminalInstance): void; moveToTerminalView(source: ITerminalInstance | URI): Promise; getPrimaryBackend(): ITerminalBackend | undefined; + setNextCommandId(id: number, commandLine: string, commandId: string): Promise; /** * Fire the onActiveTabChanged event, this will trigger the terminal dropdown to be updated, @@ -665,7 +766,7 @@ export interface ITerminalInstanceHost { /** * Reveal and focus the instance, regardless of its location. */ - focusInstance(instance: ITerminalInstance): void; + focusInstance(instance: ITerminalInstance): Promise; /** * Reveal and focus the active instance, regardless of its location. */ @@ -1228,6 +1329,16 @@ export interface IXtermTerminal extends IDisposable { */ readonly onDidChangeFocus: Event; + /** + * Fires after a search is performed. + */ + readonly onAfterSearch: Event; + + /** + * Fires before a search is performed. + */ + readonly onBeforeSearch: Event; + /** * Gets a view of the current texture atlas used by the renderers. */ @@ -1282,6 +1393,14 @@ export interface IXtermTerminal extends IDisposable { */ getFont(): ITerminalFont; + /** + * Gets the content between two markers as VT sequences. + * @param startMarker The marker to start from. When not provided, will start from 0. + * @param endMarker The marker to end at. When not provided, will end at the last line. + * @param skipLastLine Whether the last line should be skipped (e.g. when it's the prompt line) + */ + getRangeAsVT(startMarker?: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise; + /** * Gets whether there's any terminal selection. */ @@ -1381,6 +1500,22 @@ export interface IDetachedXtermTerminal extends IXtermTerminal { * Resizes the terminal. */ resize(columns: number, rows: number): void; + + /** + * Performs a full reset (RIS) of the terminal, clearing all content + * and resetting cursor position to the origin. + */ + reset(): void; + + /** + * Access to the terminal buffer for reading cursor position and content. + */ + readonly buffer: IBufferSet; + + /** + * The number of columns in the terminal. + */ + readonly cols: number; } export interface IInternalXtermTerminal { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 53c281e498c..79023c406f4 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -14,7 +14,7 @@ import { Schemas } from '../../../../base/common/network.js'; import { isAbsolute } from '../../../../base/common/path.js'; import { isWindows } from '../../../../base/common/platform.js'; import { dirname } from '../../../../base/common/resources.js'; -import { isObject, isString } from '../../../../base/common/types.js'; +import { hasKey, isObject, isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; @@ -47,7 +47,7 @@ import { IConfigurationResolverService } from '../../../services/configurationRe import { ConfigurationResolverExpression } from '../../../services/configurationResolver/common/configurationResolverExpression.js'; import { editorGroupToColumn } from '../../../services/editor/common/editorGroupColumn.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; -import { AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { ACTIVE_GROUP, AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; @@ -63,8 +63,8 @@ import { killTerminalIcon, newTerminalIcon } from './terminalIcons.js'; import { ITerminalQuickPickItem } from './terminalProfileQuickpick.js'; import { TerminalTabList } from './terminalTabsList.js'; import { ResourceContextKey } from '../../../common/contextkeys.js'; +import { SeparatorSelectOption } from '../../../../base/browser/ui/selectBox/selectBox.js'; -export const switchTerminalActionViewItemSeparator = '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500'; export const switchTerminalShowTabsTitle = localize('showTerminalTabs', "Show Tabs"); const category = terminalStrings.actionCategory; @@ -316,7 +316,10 @@ export function registerTerminalActions() { id: TerminalCommandId.CreateTerminalEditor, title: localize2('workbench.action.terminal.createTerminalEditor', 'Create New Terminal in Editor Area'), run: async (c, _, args) => { - const options = (isObject(args) && 'location' in args) ? args as ICreateTerminalOptions : { location: TerminalLocation.Editor }; + function isCreateTerminalOptions(obj: unknown): obj is ICreateTerminalOptions { + return isObject(obj) && 'location' in obj; + } + const options = isCreateTerminalOptions(args) ? args : { location: { viewColumn: ACTIVE_GROUP } }; const instance = await c.service.createTerminal(options); await instance.focusWhenReady(); } @@ -506,7 +509,7 @@ export function registerTerminalActions() { return; } c.service.setActiveInstance(instance); - focusActiveTerminal(instance, c); + await focusActiveTerminal(instance, c); } }); @@ -998,7 +1001,7 @@ export function registerTerminalActions() { }] }, run: async (c, _, args) => { - const cwd = isObject(args) && 'cwd' in args ? toOptionalString(args.cwd) : undefined; + const cwd = args ? toOptionalString((<{ cwd?: string }>args).cwd) : undefined; const instance = await c.service.createTerminal({ cwd }); if (!instance) { return; @@ -1032,7 +1035,7 @@ export function registerTerminalActions() { f1: false, run: async (activeInstance, c, accessor, args) => { const notificationService = accessor.get(INotificationService); - const name = isObject(args) && 'name' in args ? toOptionalString(args.name) : undefined; + const name = args ? toOptionalString((<{ name?: string }>args).name) : undefined; if (!name) { notificationService.warn(localize('workbench.action.terminal.renameWithArg.noName', "No name argument provided")); return; @@ -1406,7 +1409,7 @@ export function registerTerminalActions() { if (!item) { return; } - if (item === switchTerminalActionViewItemSeparator) { + if (item === SeparatorSelectOption.text) { c.service.refreshActiveGroup(); return; } @@ -1511,9 +1514,13 @@ export function validateTerminalName(name: string): { content: string; severity: return null; } +function isTerminalProfile(obj: unknown): obj is ITerminalProfile { + return isObject(obj) && 'profileName' in obj; +} + function convertOptionsOrProfileToOptions(optionsOrProfile?: ICreateTerminalOptions | ITerminalProfile): ICreateTerminalOptions | undefined { - if (isObject(optionsOrProfile) && 'profileName' in optionsOrProfile) { - return { config: optionsOrProfile as ITerminalProfile, location: (optionsOrProfile as ICreateTerminalOptions).location }; + if (isTerminalProfile(optionsOrProfile)) { + return { config: optionsOrProfile, location: (optionsOrProfile as ICreateTerminalOptions).location }; } return optionsOrProfile; } @@ -1574,13 +1581,16 @@ export function refreshTerminalActions(detectedProfiles: ITerminalProfile[]): ID let instance: ITerminalInstance | undefined; let cwd: string | URI | undefined; - if (isObject(eventOrOptionsOrProfile) && eventOrOptionsOrProfile && 'profileName' in eventOrOptionsOrProfile) { + if (isObject(eventOrOptionsOrProfile) && eventOrOptionsOrProfile && hasKey(eventOrOptionsOrProfile, { profileName: true })) { const config = c.profileService.availableProfiles.find(profile => profile.profileName === eventOrOptionsOrProfile.profileName); if (!config) { throw new Error(`Could not find terminal profile "${eventOrOptionsOrProfile.profileName}"`); } options = { config }; - if ('location' in eventOrOptionsOrProfile) { + function isSimpleArgs(obj: unknown): obj is { profileName: string; location?: 'view' | 'editor' | unknown } { + return isObject(obj) && 'location' in obj; + } + if (isSimpleArgs(eventOrOptionsOrProfile)) { switch (eventOrOptionsOrProfile.location) { case 'editor': options.location = TerminalLocation.Editor; break; case 'view': options.location = TerminalLocation.Panel; break; @@ -1712,13 +1722,17 @@ export function shrinkWorkspaceFolderCwdPairs(pairs: WorkspaceFolderCwdPair[]): } async function focusActiveTerminal(instance: ITerminalInstance | undefined, c: ITerminalServicesCollection): Promise { - // TODO@meganrogge: Is this the right logic for when instance is undefined? - if (instance?.target === TerminalLocation.Editor) { - await c.editorService.revealActiveEditor(); - await instance.focusWhenReady(true); - } else { - await c.groupService.showPanel(true); + const target = instance + ?? c.service.activeInstance + ?? c.editorService.activeInstance + ?? c.groupService.activeInstance; + if (!target) { + if (c.groupService.instances.length > 0) { + await c.groupService.showPanel(true); + } + return; } + await c.service.focusInstance(target); } async function renameWithQuickPick(c: ITerminalServicesCollection, accessor: ServicesAccessor, resource?: unknown) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts index d39f1a91159..326d159c078 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigurationService.ts @@ -13,6 +13,8 @@ import type { IXtermCore } from './xterm-private.js'; import { DEFAULT_BOLD_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, FontWeight, ITerminalConfiguration, MAXIMUM_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MINIMUM_LETTER_SPACING, TERMINAL_CONFIG_SECTION, type ITerminalFont } from '../common/terminal.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { TerminalLocation, TerminalLocationConfigValue } from '../../../../platform/terminal/common/terminal.js'; +import { isString } from '../../../../base/common/types.js'; +import { clamp } from '../../../../base/common/numbers.js'; // #region TerminalConfigurationService @@ -60,7 +62,7 @@ export class TerminalConfigurationService extends Disposable implements ITermina this._onConfigChanged.fire(); } - private _normalizeFontWeight(input: any, defaultWeight: FontWeight): FontWeight { + private _normalizeFontWeight(input: FontWeight, defaultWeight: FontWeight): FontWeight { if (input === 'normal' || input === 'bold') { return input; } @@ -244,18 +246,14 @@ export class TerminalFontMetrics extends Disposable { // #region Utils -function clampInt(source: any, minimum: number, maximum: number, fallback: T): number | T { - let r = parseInt(source, 10); - if (isNaN(r)) { +function clampInt(source: string | number, minimum: number, maximum: number, fallback: T): number | T { + if (source === null || source === undefined) { return fallback; } - if (typeof minimum === 'number') { - r = Math.max(minimum, r); - } - if (typeof maximum === 'number') { - r = Math.min(maximum, r); + const r = isString(source) ? parseInt(source, 10) : source; + if (isNaN(r)) { + return fallback; } - return r; + return clamp(r, minimum, maximum); } - // #endregion Utils diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts index 0bbc6c9e896..8b40d469375 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditor.ts @@ -47,6 +47,8 @@ export class TerminalEditor extends EditorPane { private readonly _newDropdown: MutableDisposable = this._register(new MutableDisposable()); + private readonly _sessionDisposables = this._register(new DisposableStore()); + private readonly _disposableStore = this._register(new DisposableStore()); constructor( @@ -84,13 +86,14 @@ export class TerminalEditor extends EditorPane { // since the editor does not monitor focus changes, for ex. between the terminal // panel and the editors, this is needed so that the active instance gets set // when focus changes between them. - this._register(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance())); + this._sessionDisposables.add(this._editorInput.terminalInstance.onDidFocus(() => this._setActiveInstance())); this._editorInput.setCopyLaunchConfig(this._editorInput.terminalInstance.shellLaunchConfig); } } override clearInput(): void { super.clearInput(); + this._sessionDisposables.clear(); if (this._overflowGuardElement && this._editorInput?.terminalInstance?.domElement.parentElement === this._overflowGuardElement) { this._editorInput.terminalInstance?.detachFromElement(); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts index 8b81b895b1d..d0c3a794561 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorInput.ts @@ -244,4 +244,8 @@ export class TerminalEditorInput extends EditorInput implements IEditorCloseHand } }; } + + public override canReopen(): boolean { + return false; + } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts index d59f618e611..b0544972b48 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorSerializer.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { isNumber, isObject } from '../../../../base/common/types.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IEditorSerializer } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; -import { ISerializedTerminalEditorInput, ITerminalEditorService, ITerminalInstance } from './terminal.js'; +import { ISerializedTerminalEditorInput, ITerminalEditorService, ITerminalInstance, type IDeserializedTerminalEditorInput } from './terminal.js'; import { TerminalEditorInput } from './terminalEditorInput.js'; export class TerminalInputSerializer implements IEditorSerializer { @@ -15,7 +16,7 @@ export class TerminalInputSerializer implements IEditorSerializer { ) { } public canSerialize(editorInput: TerminalEditorInput): editorInput is TerminalEditorInput & { readonly terminalInstance: ITerminalInstance } { - return typeof editorInput.terminalInstance?.persistentProcessId === 'number' && editorInput.terminalInstance.shouldPersist; + return isNumber(editorInput.terminalInstance?.persistentProcessId) && editorInput.terminalInstance.shouldPersist; } public serialize(editorInput: TerminalEditorInput): string | undefined { @@ -26,8 +27,11 @@ export class TerminalInputSerializer implements IEditorSerializer { } public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined { - const terminalInstance = JSON.parse(serializedEditorInput); - return this._terminalEditorService.reviveInput(terminalInstance); + const editorInput = JSON.parse(serializedEditorInput) as unknown; + if (!isDeserializedTerminalEditorInput(editorInput)) { + throw new Error(`Could not revive terminal editor input, ${editorInput}`); + } + return this._terminalEditorService.reviveInput(editorInput); } private _toJson(instance: ITerminalInstance): ISerializedTerminalEditorInput { @@ -47,3 +51,7 @@ export class TerminalInputSerializer implements IEditorSerializer { }; } } + +function isDeserializedTerminalEditorInput(obj: unknown): obj is IDeserializedTerminalEditorInput { + return isObject(obj) && 'id' in obj && 'pid' in obj; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts index 6190a6693fa..4f8a381ebee 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalEditorService.ts @@ -125,7 +125,12 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor } async focusInstance(instance: ITerminalInstance): Promise { - return instance.focusWhenReady(true); + if (!this.instances.includes(instance)) { + return; + } + this.setActiveInstance(instance); + await this._revealEditor(instance); + await instance.focusWhenReady(true); } async focusActiveInstance(): Promise { @@ -231,15 +236,11 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor } reviveInput(deserializedInput: IDeserializedTerminalEditorInput): EditorInput { - if ('pid' in deserializedInput) { - const newDeserializedInput = { ...deserializedInput, findRevivedId: true }; - const instance = this._terminalInstanceService.createInstance({ attachPersistentProcess: newDeserializedInput }, TerminalLocation.Editor); - const input = this._instantiationService.createInstance(TerminalEditorInput, instance.resource, instance); - this._registerInstance(instance.resource.path, input, instance); - return input; - } else { - throw new Error(`Could not revive terminal editor input, ${deserializedInput}`); - } + const newDeserializedInput = { ...deserializedInput, findRevivedId: true }; + const instance = this._terminalInstanceService.createInstance({ attachPersistentProcess: newDeserializedInput }, TerminalLocation.Editor); + const input = this._instantiationService.createInstance(TerminalEditorInput, instance.resource, instance); + this._registerInstance(instance.resource.path, input, instance); + return input; } detachInstance(instance: ITerminalInstance) { @@ -258,14 +259,22 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor if (!instance) { return; } + await this._revealEditor(instance, preserveFocus); + } + private async _revealEditor(instance: ITerminalInstance, preserveFocus?: boolean): Promise { // If there is an active openEditor call for this instance it will be revealed by that if (this._activeOpenEditorRequest?.instanceId === instance.instanceId) { + await this._activeOpenEditorRequest.promise; + return; + } + + const editorInput = this._editorInputs.get(instance.resource.path); + if (!editorInput) { return; } - const editorInput = this._editorInputs.get(instance.resource.path)!; - this._editorService.openEditor( + await this._editorService.openEditor( editorInput, { pinned: true, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts b/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts index d37dcbd684d..613d73281ac 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalExtensions.ts @@ -41,7 +41,7 @@ export type ITerminalContributionDescription = { readonly id: string } & ( */ export function registerTerminalContribution(id: string, ctor: { new(ctx: ITerminalContributionContext, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals?: false): void; export function registerTerminalContribution(id: string, ctor: { new(ctx: IDetachedCompatibleTerminalContributionContext, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals: true): void; -export function registerTerminalContribution(id: string, ctor: { new(ctx: any, ...services: Services): ITerminalContribution }, canRunInDetachedTerminals: boolean = false): void { +export function registerTerminalContribution(id: string, ctor: TerminalContributionCtor | DetachedCompatibleTerminalContributionCtor, canRunInDetachedTerminals: boolean = false): void { // eslint-disable-next-line local/code-no-dangerous-type-assertions TerminalContributionRegistry.INSTANCE.registerTerminalContribution({ id, ctor, canRunInDetachedTerminals } as ITerminalContributionDescription); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts index 6be0f71d5e9..bead415e2a7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroup.ts @@ -16,7 +16,7 @@ import { TerminalStatus } from './terminalStatusList.js'; import { getWindow } from '../../../../base/browser/dom.js'; import { getPartByLocation } from '../../../services/views/browser/viewsService.js'; import { asArray } from '../../../../base/common/arrays.js'; -import type { SingleOrMany } from '../../../../base/common/types.js'; +import { hasKey, isNumber, type SingleOrMany } from '../../../../base/common/types.js'; const enum Constants { /** @@ -132,7 +132,7 @@ class SplitPaneContainer extends Disposable { private _addChild(instance: ITerminalInstance, index: number): void { const child = new SplitPane(instance, this.orientation === Orientation.HORIZONTAL ? this._height : this._width); child.orientation = this.orientation; - if (typeof index === 'number') { + if (isNumber(index)) { this._children.splice(index, 0, child); } else { this._children.push(child); @@ -303,7 +303,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { // if a parent terminal is provided, find it // otherwise, parent is the active terminal const parentIndex = parentTerminalId ? this._terminalInstances.findIndex(t => t.instanceId === parentTerminalId) : this._activeInstanceIndex; - if ('instanceId' in shellLaunchConfigOrInstance) { + if (hasKey(shellLaunchConfigOrInstance, { instanceId: true })) { instance = shellLaunchConfigOrInstance; } else { instance = this._terminalInstanceService.createInstance(shellLaunchConfigOrInstance, TerminalLocation.Panel); @@ -338,7 +338,7 @@ export class TerminalGroup extends Disposable implements ITerminalGroup { } getLayoutInfo(isActive: boolean): ITerminalTabLayoutInfoById { - const instances = this.terminalInstances.filter(instance => typeof instance.persistentProcessId === 'number' && instance.shouldPersist); + const instances = this.terminalInstances.filter(instance => isNumber(instance.persistentProcessId) && instance.shouldPersist); const totalSize = instances.map(t => this._splitPaneContainer?.getPaneSize(t) || 0).reduce((total, size) => total += size, 0); return { isActive: isActive, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts index 4cd6a3f9eae..506dd04965b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalGroupService.ts @@ -148,8 +148,11 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe pane?.terminalTabbedView?.focusHover(); } - async focusInstance(_: ITerminalInstance): Promise { - return this.showPanel(true); + async focusInstance(instance: ITerminalInstance): Promise { + if (this.instances.includes(instance)) { + this.setActiveInstance(instance); + } + await this.showPanel(true); } async focusActiveInstance(): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts index 1f21581a19d..d615b1020e2 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIcon.ts @@ -16,6 +16,7 @@ import { ITerminalProfileResolverService } from '../common/terminal.js'; import { ansiColorMap } from '../common/terminalColorRegistry.js'; import { createStyleSheet } from '../../../../base/browser/domStylesheets.js'; import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { isString } from '../../../../base/common/types.js'; export function getColorClass(colorKey: string): string; @@ -24,7 +25,7 @@ export function getColorClass(terminal: ITerminalInstance): string | undefined; export function getColorClass(extensionTerminalProfile: IExtensionTerminalProfile): string | undefined; export function getColorClass(terminalOrColorKey: ITerminalInstance | IExtensionTerminalProfile | ITerminalProfile | string): string | undefined { let color = undefined; - if (typeof terminalOrColorKey === 'string') { + if (isString(terminalOrColorKey)) { color = terminalOrColorKey; } else if (terminalOrColorKey.color) { color = terminalOrColorKey.color.replace(/\./g, '_'); @@ -101,16 +102,16 @@ export function getUriClasses(terminal: ITerminalInstance | IExtensionTerminalPr let uri = undefined; if (extensionContributed) { - if (typeof icon === 'string' && (icon.startsWith('$(') || getIconRegistry().getIcon(icon))) { + if (isString(icon) && (icon.startsWith('$(') || getIconRegistry().getIcon(icon))) { return iconClasses; - } else if (typeof icon === 'string') { + } else if (isString(icon)) { uri = URI.parse(icon); } } - if (icon instanceof URI) { + if (URI.isUri(icon)) { uri = icon; - } else if (icon instanceof Object && 'light' in icon && 'dark' in icon) { + } else if (!ThemeIcon.isThemeIcon(icon) && !isString(icon)) { uri = isDark(colorScheme) ? icon.dark : icon.light; } if (uri instanceof URI) { @@ -123,8 +124,11 @@ export function getUriClasses(terminal: ITerminalInstance | IExtensionTerminalPr } export function getIconId(accessor: ServicesAccessor, terminal: ITerminalInstance | IExtensionTerminalProfile | ITerminalProfile): string { - if (!terminal.icon || (terminal.icon instanceof Object && !('id' in terminal.icon))) { - return accessor.get(ITerminalProfileResolverService).getDefaultIcon().id; + if (isString(terminal.icon)) { + return terminal.icon; } - return typeof terminal.icon === 'string' ? terminal.icon : terminal.icon.id; + if (ThemeIcon.isThemeIcon(terminal.icon)) { + return terminal.icon.id; + } + return accessor.get(ITerminalProfileResolverService).getDefaultIcon().id; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts index fd222eda77e..47f4757ec0a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalIconPicker.ts @@ -8,7 +8,7 @@ import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js' import { codiconsLibrary } from '../../../../base/common/codiconsLibrary.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import type { ThemeIcon } from '../../../../base/common/themables.js'; +import { ThemeIcon } from '../../../../base/common/themables.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; @@ -23,7 +23,7 @@ const icons = new Lazy(() => { if (e.id === codiconsLibrary.blank.id) { return false; } - if (!('fontCharacter' in e.defaults)) { + if (ThemeIcon.isThemeIcon(e.defaults)) { return false; } if (includedChars.has(e.defaults.fontCharacter)) { @@ -66,7 +66,7 @@ export class TerminalIconPicker extends Disposable { target: { targetElements: [body], x: bodyRect.left + (bodyRect.width - dimension.width) / 2, - y: bodyRect.top + this._layoutService.activeContainerOffset.quickPickTop - 2 + y: bodyRect.top + this._layoutService.activeContainerOffset.top }, position: { hoverPosition: HoverPosition.BELOW, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 35fc962eece..3a1a2c0c388 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -36,11 +36,11 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js'; -import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IMarkProperties, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalCapabilityStoreMultiplexer } from '../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; @@ -69,7 +69,7 @@ import { TerminalWidgetManager } from './widgets/widgetManager.js'; import { LineDataEventAddon } from './xterm/lineDataEventAddon.js'; import { XtermTerminal, getXtermScaledDimensions } from './xterm/xtermTerminal.js'; import { IEnvironmentVariableInfo } from '../common/environmentVariable.js'; -import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TERMINAL_CREATION_COMMANDS, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js'; +import { DEFAULT_COMMANDS_TO_SKIP_SHELL, ITerminalProcessManager, ITerminalProfileResolverService, ProcessState, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js'; import { TERMINAL_BACKGROUND_COLOR } from '../common/terminalColorRegistry.js'; import { TerminalContextKeys } from '../common/terminalContextKey.js'; import { getUriLabelForShell, getShellIntegrationTimeout, getWorkspaceForTerminal, preparePathForShell } from '../common/terminalEnvironment.js'; @@ -90,9 +90,9 @@ import type { IMenu } from '../../../../platform/actions/common/actions.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { TerminalContribCommandId } from '../terminalContribExports.js'; import type { IProgressState } from '@xterm/addon-progress'; -import { refreshShellIntegrationInfoStatus } from './terminalTooltip.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { PromptInputState } from '../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; +import { hasKey, isNumber, isString } from '../../../../base/common/types.js'; const enum Constants { /** @@ -185,7 +185,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _widgetManager: TerminalWidgetManager; private readonly _dndObserver: MutableDisposable = this._register(new MutableDisposable()); private _lastLayoutDimensions: dom.Dimension | undefined; - private _hasHadInput: boolean; private _description?: string; private _processName: string = ''; private _sequence?: string; @@ -374,16 +373,16 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IFileService private readonly _fileService: IFileService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, - @IPreferencesService private readonly _preferencesService: IPreferencesService, + @IPreferencesService _preferencesService: IPreferencesService, @IViewsService private readonly _viewsService: IViewsService, @IThemeService private readonly _themeService: IThemeService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ITerminalLogService private readonly _logService: ITerminalLogService, - @IStorageService private readonly _storageService: IStorageService, + @IStorageService _storageService: IStorageService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IProductService private readonly _productService: IProductService, + @IProductService _productService: IProductService, @IQuickInputService private readonly _quickInputService: IQuickInputService, - @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IEditorService private readonly _editorService: IEditorService, @IWorkspaceTrustRequestService private readonly _workspaceTrustRequestService: IWorkspaceTrustRequestService, @@ -406,7 +405,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._hadFocusOnExit = false; this._isVisible = false; this._instanceId = TerminalInstance._instanceIdCounter++; - this._hasHadInput = false; this._fixedRows = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.rows; this._fixedCols = _shellLaunchConfig.attachPersistentProcess?.fixedDimensions?.cols; this._shellLaunchConfig.shellIntegrationEnvironmentReporting = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnvironmentReporting); @@ -430,7 +428,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } if (this.shellLaunchConfig.cwd) { - const cwdUri = typeof this._shellLaunchConfig.cwd === 'string' ? URI.from({ + const cwdUri = isString(this._shellLaunchConfig.cwd) ? URI.from({ scheme: Schemas.file, path: this._shellLaunchConfig.cwd }) : this._shellLaunchConfig.cwd; @@ -464,7 +462,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { capabilityListeners.get(e.id)?.dispose(); const refreshInfo = () => { this._labelComputer?.refreshLabel(this); - refreshShellIntegrationInfoStatus(this); + this._refreshShellIntegrationInfoStatus(this); }; switch (e.id) { case TerminalCapability.CwdDetection: { @@ -498,7 +496,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } } })); - this._register(this.onDidChangeShellType(() => refreshShellIntegrationInfoStatus(this))); + this._register(this.onDidChangeShellType(() => this._refreshShellIntegrationInfoStatus(this))); this._register(this.capabilities.onDidRemoveCapability(e => { capabilityListeners.get(e.id)?.dispose(); })); @@ -508,7 +506,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // which would result in the wrong profile being selected and the wrong icon being // permanently attached to the terminal. This also doesn't work when the default profile // setting is set to null, that's handled after the process is created. - if (!this.shellLaunchConfig.executable && !workbenchEnvironmentService.remoteAuthority) { + if (!this.shellLaunchConfig.executable && !this._workbenchEnvironmentService.remoteAuthority) { this._terminalProfileResolverService.resolveIcon(this._shellLaunchConfig, OS); } this._icon = _shellLaunchConfig.attachPersistentProcess?.icon || _shellLaunchConfig.icon; @@ -645,13 +643,6 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(this.onDisposed(() => { contribution.dispose(); this._contributions.delete(desc.id); - // Just in case to prevent potential future memory leaks due to cyclic dependency. - if ('instance' in contribution) { - delete contribution.instance; - } - if ('_instance' in contribution) { - delete contribution._instance; - } })); } } @@ -862,7 +853,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { await this._handleOnData(data); })); this._register(xterm.raw.onBinary(data => this._processManager.processBinary(data))); - // Init winpty compat and link handler after process creation as they rely on the + // Init conpty compat and link handler after process creation as they rely on the // underlying process OS this._register(this._processManager.onProcessReady(async (processTraits) => { // Respond to DA1 with basic conformance. Note that including this is required to avoid @@ -894,7 +885,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Register and update the terminal's shell integration status this._register(Event.runAndSubscribe(xterm.shellIntegration.onDidChangeSeenSequences, () => { if (xterm.shellIntegration.seenSequences.size > 0) { - refreshShellIntegrationInfoStatus(this); + this._refreshShellIntegrationInfoStatus(this); } })); @@ -928,6 +919,54 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return xterm; } + // Debounce this to avoid impacting input latency while typing into the prompt + @debounce(500) + private _refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { + if (!instance.xterm) { + return; + } + const cmdDetectionType = ( + instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection + ? nls.localize('shellIntegration.rich', 'Rich') + : instance.capabilities.has(TerminalCapability.CommandDetection) + ? nls.localize('shellIntegration.basic', 'Basic') + : instance.usedShellIntegrationInjection + ? nls.localize('shellIntegration.injectionFailed', "Injection failed to activate") + : nls.localize('shellIntegration.no', 'No') + ); + + const detailedAdditions: string[] = []; + if (instance.shellType) { + detailedAdditions.push(`Shell type: \`${instance.shellType}\``); + } + const cwd = instance.cwd; + if (cwd) { + detailedAdditions.push(`Current working directory: \`${cwd}\``); + } + const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); + if (seenSequences.length > 0) { + detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); + } + const promptType = instance.capabilities.get(TerminalCapability.PromptTypeDetection)?.promptType; + if (promptType) { + detailedAdditions.push(`Prompt type: \`${promptType}\``); + } + const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString(); + if (combinedString !== undefined) { + detailedAdditions.push(`Prompt input: \`\`\`${combinedString}\`\`\``); + } + const detailedAdditionsString = detailedAdditions.length > 0 + ? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n') + : ''; + + instance.statusList.add({ + id: TerminalStatus.ShellIntegrationInfo, + severity: Severity.Info, + tooltip: `${nls.localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`, + detailedTooltip: `${nls.localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${detailedAdditionsString}` + }); + } + async runCommand(commandLine: string, shouldExecute: boolean, commandId?: string): Promise { let commandDetection = this.capabilities.get(TerminalCapability.CommandDetection); const siInjectionEnabled = this._configurationService.getValue(TerminalSettingId.ShellIntegrationEnabled) === true; @@ -966,10 +1005,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { await this._processManager.setNextCommandId(commandLine, commandId); } - // Determine whether to send ETX (ctrl+c) before running the command. This should always - // happen unless command detection can reliably say that a command is being entered and - // there is no content in the prompt - if (!commandDetection || commandDetection.promptInputModel.value.length > 0) { + // Determine whether to send ETX (ctrl+c) before running the command. Only do this when the + // command will be executed immediately or when command detection shows the prompt contains text. + if (shouldExecute && (!commandDetection || commandDetection.promptInputModel.value.length > 0)) { await this.sendText('\x03', false); // Wait a little before running the command to avoid the sequences being echoed while the ^C // is being evaluated @@ -1027,8 +1065,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { throw new Error('A container element needs to be set with `attachToElement` and be part of the DOM before calling `_open`'); } - const xtermElement = document.createElement('div'); - this._wrapperElement.appendChild(xtermElement); + const xtermHost = document.createElement('div'); + xtermHost.classList.add('terminal-xterm-host'); + this._wrapperElement.appendChild(xtermHost); this._container.appendChild(this._wrapperElement); @@ -1037,7 +1076,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Attach the xterm object to the DOM, exposing it to the smoke tests this._wrapperElement.xterm = xterm.raw; - const screenElement = xterm.attachToElement(xtermElement); + const screenElement = xterm.attachToElement(xtermHost); // Fire xtermOpen on all contributions for (const contribution of this._contributions.values()) { @@ -1084,39 +1123,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return false; } - const SHOW_TERMINAL_CONFIG_PROMPT_KEY = 'terminal.integrated.showTerminalConfigPrompt'; - const EXCLUDED_KEYS = ['RightArrow', 'LeftArrow', 'UpArrow', 'DownArrow', 'Space', 'Meta', 'Control', 'Shift', 'Alt', '', 'Delete', 'Backspace', 'Tab']; - - // only keep track of input if prompt hasn't already been shown - if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) && - !EXCLUDED_KEYS.includes(event.key) && - !event.ctrlKey && - !event.shiftKey && - !event.altKey) { - this._hasHadInput = true; - } - - // for keyboard events that resolve to commands described - // within commandsToSkipShell, either alert or skip processing by xterm.js - if (resolveResult.kind === ResultKind.KbFound && resolveResult.commandId && this._skipTerminalCommands.some(k => k === resolveResult.commandId) && !this._terminalConfigurationService.config.sendKeybindingsToShell) { - // don't alert when terminal is opened or closed - if (this._storageService.getBoolean(SHOW_TERMINAL_CONFIG_PROMPT_KEY, StorageScope.APPLICATION, true) && - this._hasHadInput && - !TERMINAL_CREATION_COMMANDS.includes(resolveResult.commandId)) { - this._notificationService.prompt( - Severity.Info, - nls.localize('keybindingHandling', "Some keybindings don't go to the terminal by default and are handled by {0} instead.", this._productService.nameLong), - [ - { - label: nls.localize('configureTerminalSettings', "Configure Terminal Settings"), - run: () => { - this._preferencesService.openSettings({ jsonEditor: false, query: `@id:${TerminalSettingId.CommandsToSkipShell},${TerminalSettingId.SendKeybindingsToShell},${TerminalSettingId.AllowChords}` }); - } - } satisfies IPromptChoice - ] - ); - this._storageService.store(SHOW_TERMINAL_CONFIG_PROMPT_KEY, false, StorageScope.APPLICATION, StorageTarget.USER); - } + // Skip processing by xterm.js of keyboard events that resolve to commands defined in + // the commandsToSkipShell setting. Ensure sendKeybindingsToShell is respected here + // which will disable this special handling and always opt to send the keystroke to the + // shell process + if (!this._terminalConfigurationService.config.sendKeybindingsToShell && resolveResult.kind === ResultKind.KbFound && resolveResult.commandId && this._skipTerminalCommands.some(k => k === resolveResult.commandId)) { event.preventDefault(); return false; } @@ -1474,36 +1485,36 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._register(processManager.onDidChangeProperty(({ type, value }) => { switch (type) { case ProcessPropertyType.Cwd: - this._cwd = value; + this._cwd = value as IProcessPropertyMap[ProcessPropertyType.Cwd]; this._labelComputer?.refreshLabel(this); break; case ProcessPropertyType.InitialCwd: - this._initialCwd = value; + this._initialCwd = value as IProcessPropertyMap[ProcessPropertyType.InitialCwd]; this._cwd = this._initialCwd; this._setTitle(this.title, TitleEventSource.Config); this._icon = this._shellLaunchConfig.attachPersistentProcess?.icon || this._shellLaunchConfig.icon; this._onIconChanged.fire({ instance: this, userInitiated: false }); break; case ProcessPropertyType.Title: - this._setTitle(value ?? '', TitleEventSource.Process); + this._setTitle(value as IProcessPropertyMap[ProcessPropertyType.Title] ?? '', TitleEventSource.Process); break; case ProcessPropertyType.OverrideDimensions: - this.setOverrideDimensions(value, true); + this.setOverrideDimensions(value as IProcessPropertyMap[ProcessPropertyType.OverrideDimensions], true); break; case ProcessPropertyType.ResolvedShellLaunchConfig: - this._setResolvedShellLaunchConfig(value); + this._setResolvedShellLaunchConfig(value as IProcessPropertyMap[ProcessPropertyType.ResolvedShellLaunchConfig]); break; case ProcessPropertyType.ShellType: - this.setShellType(value); + this.setShellType(value as IProcessPropertyMap[ProcessPropertyType.ShellType]); break; case ProcessPropertyType.HasChildProcesses: - this._onDidChangeHasChildProcesses.fire(value); + this._onDidChangeHasChildProcesses.fire(value as IProcessPropertyMap[ProcessPropertyType.HasChildProcesses]); break; case ProcessPropertyType.UsedShellIntegrationInjection: this._usedShellIntegrationInjection = true; break; case ProcessPropertyType.ShellIntegrationInjectionFailureReason: - this._shellIntegrationInjectionInfo = value; + this._shellIntegrationInjectionInfo = value as IProcessPropertyMap[ProcessPropertyType.ShellIntegrationInjectionFailureReason]; break; } })); @@ -1536,13 +1547,11 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this.isDisposed) { return; } - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); - if (activeWorkspaceRootUri) { - const trusted = await this._trust(); - if (!trusted) { - this._onProcessExit({ message: nls.localize('workspaceNotTrustedCreateTerminal', "Cannot launch a terminal process in an untrusted workspace") }); - } - } else if (this._cwd && this._userHome && this._cwd !== this._userHome) { + const trusted = await this._trust(); + // Allow remote and local terminals from remote to be created in untrusted remote workspace + if (!trusted && !this.remoteAuthority && !this._workbenchEnvironmentService.remoteAuthority) { + this._onProcessExit({ message: nls.localize('workspaceNotTrustedCreateTerminal', "Cannot launch a terminal process in an untrusted workspace") }); + } else if (this._workspaceContextService.getWorkspace().folders.length === 0 && this._cwd && this._userHome && this._cwd !== this._userHome) { // something strange is going on if cwd is not userHome in an empty workspace this._onProcessExit({ message: nls.localize('workspaceNotTrustedCreateTerminalCwd', "Cannot launch a terminal process in an untrusted workspace with cwd {0} and userHome {1}", this._cwd, this._userHome) @@ -1556,9 +1565,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { const originalIcon = this.shellLaunchConfig.icon; await this._processManager.createProcess(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows).then(result => { if (result) { - if ('message' in result) { + if (hasKey(result, { message: true })) { this._onProcessExit(result); - } else if ('injectedArgs' in result) { + } else if (hasKey(result, { injectedArgs: true })) { this._injectedArgs = result.injectedArgs; } } @@ -1775,10 +1784,10 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { callback?.(); return; } - const text = typeof this._shellLaunchConfig.initialText === 'string' + const text = isString(this._shellLaunchConfig.initialText) ? this._shellLaunchConfig.initialText : this._shellLaunchConfig.initialText?.text; - if (typeof this._shellLaunchConfig.initialText === 'string') { + if (isString(this._shellLaunchConfig.initialText)) { xterm.raw.writeln(text, callback); } else { if (this._shellLaunchConfig.initialText.trailingNewLine) { @@ -1832,9 +1841,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._shellLaunchConfig = shell; // Must be done before calling _createProcess() await this._processManager.relaunch(this._shellLaunchConfig, this._cols || Constants.DefaultCols, this._rows || Constants.DefaultRows, reset).then(result => { if (result) { - if ('message' in result) { + if (hasKey(result, { message: true })) { this._onProcessExit(result); - } else if ('injectedArgs' in result) { + } else if (hasKey(result, { injectedArgs: true })) { this._injectedArgs = result.injectedArgs; } } @@ -1843,7 +1852,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @debounce(1000) relaunch(): void { - this.reuseTerminal(this._shellLaunchConfig, true); + // Clear the attachPersistentProcess flag to ensure we create a new process + // instead of trying to reattach to the existing one during relaunch. + const shellLaunchConfig = { ...this._shellLaunchConfig }; + delete shellLaunchConfig.attachPersistentProcess; + + this.reuseTerminal(shellLaunchConfig, true); } private _onTitleChange(title: string): void { @@ -1853,10 +1867,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } private async _trust(): Promise { - return (await this._workspaceTrustRequestService.requestWorkspaceTrust( - { - message: nls.localize('terminal.requestTrust', "Creating a terminal process requires executing code") - })) === true; + if (this._configurationService.getValue(TerminalSettingId.AllowInUntrustedWorkspace)) { + this._logService.info(`Workspace trust check bypassed due to ${TerminalSettingId.AllowInUntrustedWorkspace}`); + return true; + } + const trustRequest = await this._workspaceTrustRequestService.requestWorkspaceTrust({ + message: nls.localize('terminal.requestTrust', "Creating a terminal process requires executing code") + }); + return trustRequest === true; } @debounce(2000) @@ -1867,7 +1885,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // reset cwd if it has changed, so file based url paths can be resolved try { const cwd = await this._refreshProperty(ProcessPropertyType.Cwd); - if (typeof cwd !== 'string') { + if (!isString(cwd)) { throw new Error(`cwd is not a string ${cwd}`); } } catch (e: unknown) { @@ -2237,8 +2255,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { !this._shellLaunchConfig.isExtensionOwnedTerminal && // Not a reconnected or revived terminal !this._shellLaunchConfig.attachPersistentProcess && - // Not a Windows remote using ConPTY (#187084) - !(this._processManager.remoteAuthority && this._terminalConfigurationService.config.windowsEnableConpty && (await this._processManager.getBackendOS()) === OperatingSystem.Windows) + // Not a Windows remote using ConPTY which cannot relaunch (#187084). ConPTY is used on + // Windows builds 18309+. + !(this._processManager.remoteAuthority && (await this._processManager.getBackendOS()) === OperatingSystem.Windows && this._processManager.processTraits?.windowsPty?.buildNumber && this._processManager.processTraits.windowsPty.buildNumber >= 18309) ) { this.relaunch(); return; @@ -2708,7 +2727,7 @@ export function parseExitResult( return { code: exitCodeOrError, message: undefined }; } - const code = typeof exitCodeOrError === 'number' ? exitCodeOrError : exitCodeOrError.code; + const code = isNumber(exitCodeOrError) ? exitCodeOrError : exitCodeOrError.code; // Create exit code message let message: string | undefined = undefined; @@ -2717,7 +2736,7 @@ export function parseExitResult( let commandLine: string | undefined = undefined; if (shellLaunchConfig.executable) { commandLine = shellLaunchConfig.executable; - if (typeof shellLaunchConfig.args === 'string') { + if (isString(shellLaunchConfig.args)) { commandLine += ` ${shellLaunchConfig.args}`; } else if (shellLaunchConfig.args && shellLaunchConfig.args.length) { commandLine += shellLaunchConfig.args.map(a => ` '${a}'`).join(); @@ -2799,7 +2818,8 @@ function guessShellTypeFromExecutable(os: OperatingSystem, executable: string): [GeneralShellType.Node, /^node$/], [GeneralShellType.NuShell, /^nu$/], [GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/], - [GeneralShellType.Python, /^py(?:thon)?$/] + [GeneralShellType.Python, /^py(?:thon)?$/], + [GeneralShellType.Xonsh, /^xonsh/] ]); for (const [shellType, pattern] of generalShellTypeMap) { if (exeBasename.match(pattern)) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index d1f7b5d8adc..acb65754e1a 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -16,6 +16,7 @@ import { TerminalContextKeys } from '../common/terminalContextKey.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { promiseWithResolvers } from '../../../../base/common/async.js'; +import { hasKey } from '../../../../base/common/types.js'; export class TerminalInstanceService extends Disposable implements ITerminalInstanceService { declare _serviceBrand: undefined; @@ -54,7 +55,7 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile?: IShellLaunchConfig | ITerminalProfile, cwd?: string | URI): IShellLaunchConfig { // Profile was provided - if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) { + if (shellLaunchConfigOrProfile && hasKey(shellLaunchConfigOrProfile, { profileName: true })) { const profile = shellLaunchConfigOrProfile; if (!profile.path) { return shellLaunchConfigOrProfile; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts index 20358187449..b0860ecc69c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalMenus.ts @@ -19,6 +19,8 @@ import { terminalStrings } from '../common/terminalStrings.js'; import { ACTIVE_GROUP, AUX_WINDOW_GROUP, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; import { HasSpeechProvider } from '../../speech/common/speechService.js'; +import { hasKey } from '../../../../base/common/types.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; export const enum TerminalContextMenuGroup { Chat = '0_chat', @@ -409,7 +411,7 @@ export function setupTerminalMenus(): void { group: 'navigation', order: 0, when: ContextKeyExpr.and( - ContextKeyExpr.equals('hasChatTerminals', false), + ContextKeyExpr.not(TerminalContribContextKeyStrings.ChatHasHiddenTerminals), ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.has(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.or( @@ -758,7 +760,7 @@ export function setupTerminalMenus(): void { command: { id: TerminalCommandId.StartVoice, title: localize('workbench.action.terminal.startVoiceEditor', "Start Dictation"), - icon: Codicon.run + icon: Codicon.mic }, group: 'navigation', order: 9, @@ -787,7 +789,7 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro } { const dropdownActions: IAction[] = []; const submenuActions: IAction[] = []; - const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && 'viewColumn' in location && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; + const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && hasKey(location, { viewColumn: true }) && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; if (location === TerminalLocation.Editor) { location = { viewColumn: ACTIVE_GROUP }; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts index 3f25d0aaf70..7838a529449 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessExtHostProxy.ts @@ -34,7 +34,7 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal readonly onRequestInitialCwd: Event = this._onRequestInitialCwd.event; private readonly _onRequestCwd = this._register(new Emitter()); readonly onRequestCwd: Event = this._onRequestCwd.event; - private readonly _onDidChangeProperty = this._register(new Emitter>()); + private readonly _onDidChangeProperty = this._register(new Emitter()); readonly onDidChangeProperty = this._onDidChangeProperty.event; private readonly _onProcessExit = this._register(new Emitter()); readonly onProcessExit: Event = this._onProcessExit.event; @@ -63,22 +63,22 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal this._onProcessReady.fire({ pid, cwd, windowsPty: undefined }); } - emitProcessProperty({ type, value }: IProcessProperty): void { + emitProcessProperty({ type, value }: IProcessProperty): void { switch (type) { case ProcessPropertyType.Cwd: - this.emitCwd(value); + this.emitCwd(value as IProcessPropertyMap[ProcessPropertyType.Cwd]); break; case ProcessPropertyType.InitialCwd: - this.emitInitialCwd(value); + this.emitInitialCwd(value as IProcessPropertyMap[ProcessPropertyType.InitialCwd]); break; case ProcessPropertyType.Title: - this.emitTitle(value); + this.emitTitle(value as IProcessPropertyMap[ProcessPropertyType.Title]); break; case ProcessPropertyType.OverrideDimensions: - this.emitOverrideDimensions(value); + this.emitOverrideDimensions(value as IProcessPropertyMap[ProcessPropertyType.OverrideDimensions]); break; case ProcessPropertyType.ResolvedShellLaunchConfig: - this.emitResolvedShellLaunchConfig(value); + this.emitResolvedShellLaunchConfig(value as IProcessPropertyMap[ProcessPropertyType.ResolvedShellLaunchConfig]); break; } } @@ -140,10 +140,6 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal // No-op } - async setNextCommandId(commandLine: string, commandId: string): Promise { - // No-op - } - async processBinary(data: string): Promise { // Disabled for extension terminals this._onBinary.fire(data); @@ -163,8 +159,9 @@ export class TerminalProcessExtHostProxy extends Disposable implements ITerminal }); } - async refreshProperty(type: T): Promise { + async refreshProperty(type: T): Promise { // throws if called in extHostTerminalService + throw new Error('refreshProperty not implemented on extension host'); } async updateProperty(type: T, value: IProcessPropertyMap[T]): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 35fe84b73dd..02b12985084 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -22,7 +22,7 @@ import { FlowControlConstants, ITerminalLaunchResult, IProcessDataEvent, IProces import { TerminalRecorder } from '../../../../platform/terminal/common/terminalRecorder.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from './environmentVariableInfo.js'; -import { ITerminalConfigurationService, ITerminalInstanceService } from './terminal.js'; +import { ITerminalConfigurationService, ITerminalInstanceService, ITerminalService } from './terminal.js'; import { IEnvironmentVariableInfo, IEnvironmentVariableService } from '../common/environmentVariable.js'; import { MergedEnvironmentVariableCollection } from '../../../../platform/terminal/common/environmentVariableCollection.js'; import { serializeEnvironmentVariableCollections } from '../../../../platform/terminal/common/environmentVariableShared.js'; @@ -45,6 +45,7 @@ import { TerminalContribSettingId } from '../terminalContribExports.js'; import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import type { MaybePromise } from '../../../../base/common/async.js'; +import { isString } from '../../../../base/common/types.js'; const enum ProcessConstants { /** @@ -67,7 +68,7 @@ const enum ProcessType { * * Internal definitions: * - Process: The process launched with the terminalProcess.ts file, or the pty as a whole - * - Pty Process: The pseudoterminal parent process (or the conpty/winpty agent process) + * - Pty Process: The pseudoterminal parent process (or the conpty agent process) * - Shell Process: The pseudoterminal child process (ie. the shell) */ export class TerminalProcessManager extends Disposable implements ITerminalProcessManager { @@ -117,7 +118,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce readonly onProcessData = this._onProcessData.event; private readonly _onProcessReplayComplete = this._register(new Emitter()); readonly onProcessReplayComplete = this._onProcessReplayComplete.event; - private readonly _onDidChangeProperty = this._register(new Emitter>()); + private readonly _onDidChangeProperty = this._register(new Emitter()); readonly onDidChangeProperty = this._onDidChangeProperty.event; private readonly _onEnvironmentVariableInfoChange = this._register(new Emitter()); readonly onEnvironmentVariableInfoChanged = this._onEnvironmentVariableInfoChange.event; @@ -156,7 +157,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @ITerminalService private readonly _terminalService: ITerminalService ) { super(); this._cwdWorkspaceFolder = terminalEnvironment.getWorkspaceForTerminal(cwd, this._workspaceContextService, this._historyService); @@ -164,15 +166,15 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._ackDataBufferer = new AckDataBufferer(e => this._process?.acknowledgeDataEvent(e)); this._dataFilter = this._register(this._instantiationService.createInstance(SeamlessRelaunchDataFilter)); this._register(this._dataFilter.onProcessData(ev => { - const data = (typeof ev === 'string' ? ev : ev.data); + const data = (isString(ev) ? ev : ev.data); const beforeProcessDataEvent: IBeforeProcessDataEvent = { data }; this._onBeforeProcessData.fire(beforeProcessDataEvent); if (beforeProcessDataEvent.data && beforeProcessDataEvent.data.length > 0) { // This event is used by the caller so the object must be reused - if (typeof ev !== 'string') { + if (!isString(ev)) { ev.data = beforeProcessDataEvent.data; } - this._onProcessData.fire(typeof ev !== 'string' ? ev : { data: beforeProcessDataEvent.data, trackCommit: false }); + this._onProcessData.fire(!isString(ev) ? ev : { data: beforeProcessDataEvent.data, trackCommit: false }); } })); @@ -300,7 +302,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce suggestEnabled: this._configurationService.getValue(TerminalContribSettingId.SuggestEnabled), nonce: this.shellIntegrationNonce }, - windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, windowsUseConptyDll: this._terminalConfigurationService.config.windowsUseConptyDll ?? false, environmentVariableCollections: this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined, workspaceFolder: this._cwdWorkspaceFolder, @@ -386,7 +387,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce newProcess.onDidChangeProperty(({ type, value }) => { switch (type) { case ProcessPropertyType.HasChildProcesses: - this._hasChildProcesses = value; + this._hasChildProcesses = value as IProcessPropertyMap[ProcessPropertyType.HasChildProcesses]; break; case ProcessPropertyType.FailedShellIntegrationActivation: this._telemetryService?.publicLog2<{}, { owner: 'meganrogge'; comment: 'Indicates shell integration was not activated because of custom args' }>('terminal/shellIntegrationActivationFailureCustomArgs'); @@ -507,7 +508,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce suggestEnabled: this._configurationService.getValue(TerminalContribSettingId.SuggestEnabled), nonce: this.shellIntegrationNonce }, - windowsEnableConpty: this._terminalConfigurationService.config.windowsEnableConpty, windowsUseConptyDll: this._terminalConfigurationService.config.windowsUseConptyDll ?? false, environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined, workspaceFolder: this._cwdWorkspaceFolder, @@ -562,6 +562,9 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } } })); + this._register(toDisposable(() => { + this.ptyProcessReady = undefined!; + })); } async getBackendOS(): Promise { @@ -595,10 +598,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce async setNextCommandId(commandLine: string, commandId: string): Promise { await this.ptyProcessReady; const process = this._process; - if (!process) { + if (!process?.id) { return; } - await process.setNextCommandId(commandLine, commandId); + await this._terminalService.setNextCommandId(process.id, commandLine, commandId); } private _resize(cols: number, rows: number) { @@ -863,7 +866,7 @@ class SeamlessRelaunchDataFilter extends Disposable { private _createRecorder(process: ITerminalChildProcess): [TerminalRecorder, IDisposable] { const recorder = new TerminalRecorder(0, 0); - const disposable = process.onProcessData(e => recorder.handleData(typeof e === 'string' ? e : e.data)); + const disposable = process.onProcessData(e => recorder.handleData(isString(e) ? e : e.data)); return [recorder, disposable]; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index eff79840baf..b3b306e07f8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -18,6 +18,7 @@ import { IPickerQuickAccessItem } from '../../../../platform/quickinput/browser/ import { getIconRegistry } from '../../../../platform/theme/common/iconRegistry.js'; import { basename } from '../../../../base/common/path.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { hasKey, isString } from '../../../../base/common/types.js'; type DefaultProfileName = string; @@ -40,9 +41,7 @@ export class TerminalProfileQuickpick { return; } if (type === 'setDefault') { - if ('command' in result.profile) { - return; // Should never happen - } else if ('id' in result.profile) { + if (hasKey(result.profile, { id: true })) { // extension contributed profile await this._configurationService.updateValue(defaultProfileKey, result.profile.title, ConfigurationTarget.USER); return { @@ -60,7 +59,7 @@ export class TerminalProfileQuickpick { } // Add the profile to settings if necessary - if ('isAutoDetected' in result.profile) { + if (hasKey(result.profile, { profileName: true })) { const profilesConfig = await this._configurationService.getValue(profilesKey); if (typeof profilesConfig === 'object') { const newProfile: ITerminalProfileObject = { @@ -76,7 +75,7 @@ export class TerminalProfileQuickpick { // Set the default profile await this._configurationService.updateValue(defaultProfileKey, result.profileName, ConfigurationTarget.USER); } else if (type === 'createInstance') { - if ('id' in result.profile) { + if (hasKey(result.profile, { id: true })) { return { config: { extensionIdentifier: result.profile.extensionIdentifier, @@ -94,7 +93,7 @@ export class TerminalProfileQuickpick { } } // for tests - return 'profileName' in result.profile ? result.profile.profileName : result.profile.title; + return hasKey(result.profile, { profileName: true }) ? result.profile.profileName : result.profile.title; } private async _createAndShow(type: 'setDefault' | 'createInstance'): Promise { @@ -110,13 +109,10 @@ export class TerminalProfileQuickpick { if (!await this._isProfileSafe(context.item.profile)) { return; } - if ('command' in context.item.profile) { + if (hasKey(context.item.profile, { id: true })) { return; } - if ('id' in context.item.profile) { - return; - } - const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); + const configProfiles: { [key: string]: ITerminalExecutable | null | undefined } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); const existingProfiles = !!configProfiles ? Object.keys(configProfiles) : []; const name = await this._quickInputService.input({ prompt: nls.localize('enterTerminalProfileName', "Enter terminal profile name"), @@ -131,7 +127,7 @@ export class TerminalProfileQuickpick { if (!name) { return; } - const newConfigValue: { [key: string]: ITerminalExecutable } = { + const newConfigValue: { [key: string]: ITerminalExecutable | null | undefined } = { ...configProfiles, [name]: this._createNewProfileConfig(context.item.profile) }; @@ -154,7 +150,7 @@ export class TerminalProfileQuickpick { const contributedProfiles: IProfileQuickPickItem[] = []; for (const contributed of this._terminalProfileService.contributedProfiles) { let icon: ThemeIcon | undefined; - if (typeof contributed.icon === 'string') { + if (isString(contributed.icon)) { if (contributed.icon.startsWith('$(')) { icon = ThemeIcon.fromString(contributed.icon); } else { @@ -223,8 +219,8 @@ export class TerminalProfileQuickpick { } private async _isProfileSafe(profile: ITerminalProfile | IExtensionTerminalProfile): Promise { - const isUnsafePath = 'isUnsafePath' in profile && profile.isUnsafePath; - const requiresUnsafePath = 'requiresUnsafePath' in profile && profile.requiresUnsafePath; + const isUnsafePath = hasKey(profile, { profileName: true }) && profile.isUnsafePath; + const requiresUnsafePath = hasKey(profile, { profileName: true }) && profile.requiresUnsafePath; if (!isUnsafePath && !requiresUnsafePath) { return true; } @@ -270,7 +266,7 @@ export class TerminalProfileQuickpick { } if (profile.args) { - if (typeof profile.args === 'string') { + if (isString(profile.args)) { return { label, description: `${profile.path} ${profile.args}`, profile, profileName: profile.profileName, buttons, iconClasses }; } const argsString = profile.args.map(e => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts index 0bc0700b6fd..8e24851fbe7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -22,7 +22,7 @@ import { isUriComponents, URI } from '../../../../base/common/uri.js'; import { deepClone } from '../../../../base/common/objects.js'; import { ITerminalInstanceService } from './terminal.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import type { SingleOrMany } from '../../../../base/common/types.js'; +import { isString, type SingleOrMany } from '../../../../base/common/types.js'; export interface IProfileContextProvider { getDefaultSystemShell(remoteAuthority: string | undefined, os: OperatingSystem): Promise; @@ -132,7 +132,7 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl // Verify the icon is valid, and fallback correctly to the generic terminal id if there is // an issue - const resource = shellLaunchConfig === undefined || typeof shellLaunchConfig.cwd === 'string' ? undefined : shellLaunchConfig.cwd; + const resource = shellLaunchConfig === undefined || isString(shellLaunchConfig.cwd) ? undefined : shellLaunchConfig.cwd; shellLaunchConfig.icon = this._getCustomIcon(shellLaunchConfig.icon) || this._getCustomIcon(resolvedProfile.icon) || this.getDefaultIcon(resource); @@ -169,11 +169,11 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl return this._context.getEnvironment(remoteAuthority); } - private _getCustomIcon(icon?: unknown): TerminalIcon | undefined { + private _getCustomIcon(icon?: TerminalIcon): TerminalIcon | undefined { if (!icon) { return undefined; } - if (typeof icon === 'string') { + if (isString(icon)) { return ThemeIcon.fromId(icon); } if (ThemeIcon.isThemeIcon(icon)) { @@ -182,11 +182,8 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl if (URI.isUri(icon) || isUriComponents(icon)) { return URI.revive(icon); } - if (typeof icon === 'object' && 'light' in icon && 'dark' in icon) { - const castedIcon = (icon as { light: unknown; dark: unknown }); - if ((URI.isUri(castedIcon.light) || isUriComponents(castedIcon.light)) && (URI.isUri(castedIcon.dark) || isUriComponents(castedIcon.dark))) { - return { light: URI.revive(castedIcon.light), dark: URI.revive(castedIcon.dark) }; - } + if ((URI.isUri(icon.light) || isUriComponents(icon.light)) && (URI.isUri(icon.dark) || isUriComponents(icon.dark))) { + return { light: URI.revive(icon.light), dark: URI.revive(icon.dark) }; } return undefined; } @@ -303,7 +300,7 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl // Resolve args variables if (profile.args) { - if (typeof profile.args === 'string') { + if (isString(profile.args)) { profile.args = await this._resolveVariables(profile.args, env, lastActiveWorkspace); } else { profile.args = await Promise.all(profile.args.map(arg => this._resolveVariables(arg, env, lastActiveWorkspace))); @@ -351,7 +348,7 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl if (profile === null || profile === undefined || typeof profile !== 'object') { return false; } - if ('path' in profile && typeof (profile as { path: unknown }).path === 'string') { + if ('path' in profile && isString((profile as { path: unknown }).path)) { return true; } return false; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 0a7f1950e25..0cbef05b922 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -23,6 +23,7 @@ import { ITerminalContributionService } from '../common/terminalExtensionPoints. import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; +import { hasKey, isString } from '../../../../base/common/types.js'; /* * Links TerminalService with TerminalProfileResolverService @@ -115,7 +116,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi let defaultProfileName: string | undefined; if (os) { defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${this._getOsKey(os)}`); - if (!defaultProfileName || typeof defaultProfileName !== 'string') { + if (!defaultProfileName || !isString(defaultProfileName)) { return undefined; } } else { @@ -168,7 +169,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi private async _updateContributedProfiles(): Promise { const platformKey = await this.getPlatformKey(); const excludedContributedProfiles: string[] = []; - const configProfiles: { [key: string]: any } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); + const configProfiles: { [key: string]: ITerminalExecutable | null | undefined } = this._configurationService.getValue(TerminalSettingPrefix.Profiles + platformKey); for (const [profileName, value] of Object.entries(configProfiles)) { if (value === null) { excludedContributedProfiles.push(profileName); @@ -244,7 +245,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi async getContributedDefaultProfile(shellLaunchConfig: IShellLaunchConfig): Promise { // prevents recursion with the MainThreadTerminalService call to create terminal // and defers to the provided launch config when an executable is provided - if (shellLaunchConfig && !shellLaunchConfig.extHostTerminalId && !('executable' in shellLaunchConfig)) { + if (shellLaunchConfig && !shellLaunchConfig.extHostTerminalId && !hasKey(shellLaunchConfig, { executable: true })) { const key = await this.getPlatformKey(); const defaultProfileName = this._configurationService.getValue(`${TerminalSettingPrefix.DefaultProfile}${key}`); const contributedDefaultProfile = this.contributedProfiles.find(p => p.title === defaultProfileName); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index f3c393fc2b4..a01da1a710c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -56,6 +56,7 @@ import { createInstanceCapabilityEventMultiplexer } from './terminalEvents.js'; import { isAuxiliaryWindow, mainWindow } from '../../../../base/browser/window.js'; import { GroupIdentifier } from '../../../common/editor.js'; import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { hasKey, isString } from '../../../../base/common/types.js'; interface IBackgroundTerminal { instance: ITerminalInstance; @@ -242,7 +243,7 @@ export class TerminalService extends Disposable implements ITerminalService { if (!result) { return; } - if (typeof result === 'string') { + if (isString(result)) { return; } const keyMods: IKeyMods | undefined = result.keyMods; @@ -251,14 +252,14 @@ export class TerminalService extends Disposable implements ITerminalService { const defaultLocation = this._terminalConfigurationService.defaultLocation; let instance; - if (result.config && 'id' in result?.config) { + if (result.config && hasKey(result.config, { id: true })) { await this.createContributedTerminalProfile(result.config.extensionIdentifier, result.config.id, { icon: result.config.options?.icon, color: result.config.options?.color, location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation }); return; - } else if (result.config && 'profileName' in result.config) { + } else if (result.config && hasKey(result.config, { profileName: true })) { if (keyMods?.alt && activeInstance) { // create split, only valid if there's an active instance instance = await this.createTerminal({ location: { parentTerminal: activeInstance }, config: result.config, cwd }); @@ -339,6 +340,13 @@ export class TerminalService extends Disposable implements ITerminalService { return this._primaryBackend; } + async setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + if (!this._primaryBackend || id <= 0) { + return; + } + await this._primaryBackend.setNextCommandId(id, commandLine, commandId); + } + private _forwardInstanceHostEvents(host: ITerminalInstanceHost) { this._register(host.onDidChangeInstances(this._onDidChangeInstances.fire, this._onDidChangeInstances)); this._register(host.onDidDisposeInstance(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)); @@ -388,10 +396,14 @@ export class TerminalService extends Disposable implements ITerminalService { } async focusInstance(instance: ITerminalInstance): Promise { + if (this._activeInstance !== instance) { + this.setActiveInstance(instance); + } if (instance.target === TerminalLocation.Editor) { - return this._terminalEditorService.focusInstance(instance); + await this._terminalEditorService.focusInstance(instance); + return; } - return this._terminalGroupService.focusInstance(instance); + await this._terminalGroupService.focusInstance(instance); } async focusActiveInstance(): Promise { @@ -951,9 +963,9 @@ export class TerminalService extends Disposable implements ITerminalService { if (location === TerminalLocation.Editor) { return this._terminalEditorService; } else if (typeof location === 'object') { - if ('viewColumn' in location) { + if (hasKey(location, { viewColumn: true })) { return this._terminalEditorService; - } else if ('parentTerminal' in location) { + } else if (hasKey(location, { parentTerminal: true })) { return (await location.parentTerminal).target === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; } } else { @@ -967,9 +979,9 @@ export class TerminalService extends Disposable implements ITerminalService { // Await the initialization of available profiles as long as this is not a pty terminal or a // local terminal in a remote workspace as profile won't be used in those cases and these // terminals need to be launched before remote connections are established. + const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.file; if (this._terminalProfileService.availableProfiles.length === 0) { - const isPtyTerminal = options?.config && 'customPtyImplementation' in options.config; - const isLocalInRemoteTerminal = this._remoteAgentService.getConnection() && URI.isUri(options?.cwd) && options?.cwd.scheme === Schemas.vscodeFileResource; + const isPtyTerminal = options?.config && hasKey(options.config, { customPtyImplementation: true }); if (!isPtyTerminal && !isLocalInRemoteTerminal) { if (this._connectionState === TerminalConnectionState.Connecting) { mark(`code/terminal/willGetProfiles`); @@ -981,13 +993,26 @@ export class TerminalService extends Disposable implements ITerminalService { } } - const config = options?.config || this._terminalProfileService.getDefaultProfile(); - const shellLaunchConfig = config && 'extensionIdentifier' in config ? {} : this._terminalInstanceService.convertProfileToShellLaunchConfig(config || {}); + let config = options?.config; + if (!config && isLocalInRemoteTerminal) { + const backend = await this._terminalInstanceService.getBackend(undefined); + const executable = await backend?.getDefaultSystemShell(); + if (executable) { + config = { executable }; + } + } + + if (!config) { + config = this._terminalProfileService.getDefaultProfile(); + } + const shellLaunchConfig = config && hasKey(config, { extensionIdentifier: true }) ? {} : this._terminalInstanceService.convertProfileToShellLaunchConfig(config || {}); // Get the contributed profile if it was provided const contributedProfile = options?.skipContributedProfileCheck ? undefined : await this._getContributedProfile(shellLaunchConfig, options); - const splitActiveTerminal = typeof options?.location === 'object' && 'splitActiveTerminal' in options.location ? options.location.splitActiveTerminal : typeof options?.location === 'object' ? 'parentTerminal' in options.location : false; + const splitActiveTerminal = typeof options?.location === 'object' && hasKey(options.location, { splitActiveTerminal: true }) + ? options.location.splitActiveTerminal + : typeof options?.location === 'object' ? hasKey(options.location, { parentTerminal: true }) : false; await this._resolveCwd(shellLaunchConfig, splitActiveTerminal, options); @@ -1000,7 +1025,7 @@ export class TerminalService extends Disposable implements ITerminalService { if (splitActiveTerminal) { location = resolvedLocation === TerminalLocation.Editor ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true }; } else { - location = typeof options?.location === 'object' && 'viewColumn' in options.location ? options.location : resolvedLocation; + location = typeof options?.location === 'object' && hasKey(options.location, { viewColumn: true }) ? options.location : resolvedLocation; } await this.createContributedTerminalProfile(contributedProfile.extensionIdentifier, contributedProfile.id, { icon: contributedProfile.icon, @@ -1064,7 +1089,7 @@ export class TerminalService extends Disposable implements ITerminalService { } private async _getContributedProfile(shellLaunchConfig: IShellLaunchConfig, options?: ICreateTerminalOptions): Promise { - if (options?.config && 'extensionIdentifier' in options.config) { + if (options?.config && hasKey(options.config, { extensionIdentifier: true })) { return options.config; } @@ -1073,18 +1098,20 @@ export class TerminalService extends Disposable implements ITerminalService { async createDetachedTerminal(options: IDetachedXTermOptions): Promise { const ctor = await TerminalInstance.getXtermConstructor(this._keybindingService, this._contextKeyService); + const capabilities = options.capabilities ?? new TerminalCapabilityStore(); const xterm = this._instantiationService.createInstance(XtermTerminal, undefined, ctor, { cols: options.cols, rows: options.rows, xtermColorProvider: options.colorProvider, - capabilities: options.capabilities || new TerminalCapabilityStore(), + capabilities, + disableOverviewRuler: options.disableOverviewRuler, }, undefined); if (options.readonly) { xterm.raw.attachCustomKeyEventHandler(() => false); } - const instance = new DetachedTerminal(xterm, options, this._instantiationService); + const instance = new DetachedTerminal(xterm, { ...options, capabilities }, this._instantiationService); this._detachedXterms.add(instance); const l = xterm.onDidDispose(() => { this._detachedXterms.delete(instance); @@ -1101,7 +1128,7 @@ export class TerminalService extends Disposable implements ITerminalService { shellLaunchConfig.cwd = options.cwd; } else if (splitActiveTerminal && options?.location) { let parent = this.activeInstance; - if (typeof options.location === 'object' && 'parentTerminal' in options.location) { + if (typeof options.location === 'object' && hasKey(options.location, { parentTerminal: true })) { parent = await options.location.parentTerminal; } if (!parent) { @@ -1153,13 +1180,13 @@ export class TerminalService extends Disposable implements ITerminalService { async resolveLocation(location?: ITerminalLocationOptions): Promise { if (location && typeof location === 'object') { - if ('parentTerminal' in location) { + if (hasKey(location, { parentTerminal: true })) { // since we don't set the target unless it's an editor terminal, this is necessary const parentTerminal = await location.parentTerminal; return !parentTerminal.target ? TerminalLocation.Panel : parentTerminal.target; - } else if ('viewColumn' in location) { + } else if (hasKey(location, { viewColumn: true })) { return TerminalLocation.Editor; - } else if ('splitActiveTerminal' in location) { + } else if (hasKey(location, { splitActiveTerminal: true })) { // since we don't set the target unless it's an editor terminal, this is necessary return !this._activeInstance?.target ? TerminalLocation.Panel : this._activeInstance?.target; } @@ -1168,16 +1195,16 @@ export class TerminalService extends Disposable implements ITerminalService { } private async _getSplitParent(location?: ITerminalLocationOptions): Promise { - if (location && typeof location === 'object' && 'parentTerminal' in location) { + if (location && typeof location === 'object' && hasKey(location, { parentTerminal: true })) { return location.parentTerminal; - } else if (location && typeof location === 'object' && 'splitActiveTerminal' in location) { + } else if (location && typeof location === 'object' && hasKey(location, { splitActiveTerminal: true })) { return this.activeInstance; } return undefined; } private _getEditorOptions(location?: ITerminalLocationOptions): TerminalEditorLocation | undefined { - if (location && typeof location === 'object' && 'viewColumn' in location) { + if (location && typeof location === 'object' && hasKey(location, { viewColumn: true })) { // Terminal-specific workaround to resolve the active group in auxiliary windows to // override the locked editor behavior. if (location.viewColumn === ACTIVE_GROUP && isAuxiliaryWindow(getActiveWindow())) { @@ -1193,7 +1220,7 @@ export class TerminalService extends Disposable implements ITerminalService { private _evaluateLocalCwd(shellLaunchConfig: IShellLaunchConfig) { // Add welcome message and title annotation for local terminals launched within remote or // virtual workspaces - if (typeof shellLaunchConfig.cwd !== 'string' && shellLaunchConfig.cwd?.scheme === Schemas.file) { + if (!isString(shellLaunchConfig.cwd) && shellLaunchConfig.cwd?.scheme === Schemas.file) { if (VirtualWorkspaceContext.getValue(this._contextKeyService)) { shellLaunchConfig.initialText = formatMessageForTerminal(nls.localize('localTerminalVirtualWorkspace', "This shell is open to a {0}local{1} folder, NOT to the virtual folder", '\x1b[3m', '\x1b[23m'), { excludeLeadingNewLine: true, loudFormatting: true }); shellLaunchConfig.type = 'Local'; @@ -1298,7 +1325,7 @@ class TerminalEditorStyle extends Themable { let uri = undefined; if (icon instanceof URI) { uri = icon; - } else if (icon instanceof Object && 'light' in icon && 'dark' in icon) { + } else if (icon instanceof Object && hasKey(icon, { light: true, dark: true })) { uri = isDark(colorTheme.type) ? icon.dark : icon.light; } const iconClasses = getUriClasses(instance, colorTheme.type); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index bdbbe66141e..6c1111d7481 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -14,6 +14,7 @@ import { spinningLoading } from '../../../../platform/theme/common/iconRegistry. import { ThemeIcon } from '../../../../base/common/themables.js'; import { ITerminalStatus } from '../common/terminal.js'; import { mainWindow } from '../../../../base/browser/window.js'; +import { isString } from '../../../../base/common/types.js'; /** * The set of _internal_ terminal statuses, other components building on the terminal should put @@ -112,7 +113,7 @@ export class TerminalStatusList extends Disposable implements ITerminalStatusLis remove(status: ITerminalStatus): void; remove(statusId: string): void; remove(statusOrId: ITerminalStatus | string): void { - const status = typeof statusOrId === 'string' ? this._statuses.get(statusOrId) : statusOrId; + const status = isString(statusOrId) ? this._statuses.get(statusOrId) : statusOrId; // Verify the status is the same as the one passed in if (status && this._statuses.get(status.id)) { const wasPrimary = this.primary?.id === status.id; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index 95b836afefb..7a3dd584458 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { LayoutPriority, Orientation, Sizing, SplitView } from '../../../../base/browser/ui/splitview/splitview.js'; -import { Disposable, DisposableStore, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, dispose, IDisposable } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; @@ -15,7 +15,7 @@ import { Action, IAction, Separator } from '../../../../base/common/actions.js'; import { IMenu, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; +import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { localize } from '../../../../nls.js'; import { openContextMenu } from './terminalContextMenu.js'; @@ -27,6 +27,7 @@ import { TerminalTabsChatEntry } from './terminalTabsChatEntry.js'; import { containsDragType } from '../../../../platform/dnd/browser/dnd.js'; import { getTerminalResourcesFromDragEvent, parseTerminalUri } from './terminalUri.js'; import type { IProcessDetails } from '../../../../platform/terminal/common/terminalProcess.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; const $ = dom.$; @@ -96,17 +97,11 @@ export class TerminalTabbedView extends Disposable { tabListContainer.appendChild(this._tabListElement); this._tabContainer.appendChild(tabListContainer); - this._register(dom.addDisposableListener(this._tabContainer, dom.EventType.DBLCLICK, async () => { - const instance = await this._terminalService.createTerminal({ location: TerminalLocation.Panel }); - this._terminalGroupService.setActiveInstance(instance); - await instance.focusWhenReady(); - })); - this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalInstanceContext, contextKeyService)); this._tabsListMenu = this._register(menuService.createMenu(MenuId.TerminalTabContext, contextKeyService)); this._tabsListEmptyMenu = this._register(menuService.createMenu(MenuId.TerminalTabEmptyAreaContext, contextKeyService)); - this._tabList = this._register(this._instantiationService.createInstance(TerminalTabList, this._tabListElement, this._register(new DisposableStore()))); + this._tabList = this._register(this._instantiationService.createInstance(TerminalTabList, this._tabListElement)); this._tabListDomElement = this._tabList.getHTMLElement(); this._chatEntry = this._register(this._instantiationService.createInstance(TerminalTabsChatEntry, tabListContainer, this._tabContainer)); @@ -143,13 +138,13 @@ export class TerminalTabbedView extends Disposable { this._updateChatTerminalsEntry(); })); - this._register(Event.any(this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession, this._terminalService.onDidChangeInstances)(() => { + this._register(Event.any(this._terminalChatService.onDidRegisterTerminalInstanceWithToolSession, this._terminalService.onDidChangeInstances, this._terminalService.onDidDisposeInstance)(() => { this._refreshShowTabs(); this._updateChatTerminalsEntry(); })); this._register(contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(new Set(['hasHiddenChatTerminals']))) { + if (e.affectsSome(new Set([TerminalContribContextKeyStrings.ChatHasHiddenTerminals]))) { this._refreshShowTabs(); this._updateChatTerminalsEntry(); } @@ -181,22 +176,20 @@ export class TerminalTabbedView extends Disposable { return true; } - if (hide === 'never') { - return true; - } - - if (this._terminalGroupService.instances.length) { - return true; - } - - if (hide === 'singleTerminal' && this._terminalGroupService.instances.length > 1) { - return true; - } - - if (hide === 'singleGroup' && this._terminalGroupService.groups.length > 1) { - return true; + switch (hide) { + case 'never': + return true; + case 'singleTerminal': + if (this._terminalGroupService.instances.length > 1) { + return true; + } + break; + case 'singleGroup': + if (this._terminalGroupService.groups.length > 1) { + return true; + } + break; } - return false; } @@ -599,13 +592,12 @@ export class TerminalTabbedView extends Disposable { // be focused. So wait for connection to finish, then focus. const previousActiveElement = this._tabListElement.ownerDocument.activeElement; if (previousActiveElement) { - // TODO: Improve lifecycle management this event should be disposed after first fire - this._register(this._terminalService.onDidChangeConnectionState(() => { + const listener = this._register(Event.once(this._terminalService.onDidChangeConnectionState)(() => { // Only focus the terminal if the activeElement has not changed since focus() was called - // TODO: Hack if (dom.isActiveElement(previousActiveElement)) { this._focus(); } + this._store.delete(listener); })); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsChatEntry.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsChatEntry.ts index a536c00ea8d..7e6e39b2f31 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsChatEntry.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsChatEntry.ts @@ -9,17 +9,19 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { $ } from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ITerminalChatService } from './terminal.js'; +import { ITerminalChatService, ITerminalService } from './terminal.js'; import * as dom from '../../../../base/browser/dom.js'; export class TerminalTabsChatEntry extends Disposable { private readonly _entry: HTMLElement; private readonly _label: HTMLElement; + private readonly _deleteButton: HTMLElement; override dispose(): void { this._entry.remove(); this._label.remove(); + this._deleteButton.remove(); super.dispose(); } @@ -28,6 +30,7 @@ export class TerminalTabsChatEntry extends Disposable { private readonly _tabContainer: HTMLElement, @ICommandService private readonly _commandService: ICommandService, @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, + @ITerminalService private readonly _terminalService: ITerminalService, ) { super(); @@ -40,10 +43,22 @@ export class TerminalTabsChatEntry extends Disposable { icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.commentDiscussionSparkle)); this._label = dom.append(entry, $('.terminal-tabs-chat-entry-label')); + // Add delete button (right-aligned via CSS margin-left: auto) + this._deleteButton = dom.append(entry, $('.terminal-tabs-chat-entry-delete')); + this._deleteButton.classList.add(...ThemeIcon.asClassNameArray(Codicon.trashcan)); + this._deleteButton.tabIndex = 0; + this._deleteButton.setAttribute('role', 'button'); + this._deleteButton.setAttribute('aria-label', localize('terminal.tabs.chatEntryDeleteAriaLabel', "Kill all hidden chat terminals")); + this._deleteButton.setAttribute('title', localize('terminal.tabs.chatEntryDeleteTooltip', "Kill all hidden chat terminals")); + const runChatTerminalsCommand = () => { void this._commandService.executeCommand('workbench.action.terminal.chat.viewHiddenChatTerminals'); }; this._register(dom.addDisposableListener(this._entry, dom.EventType.CLICK, e => { + // Don't trigger if clicking on the delete button + if (e.target === this._deleteButton || this._deleteButton.contains(e.target as Node)) { + return; + } e.preventDefault(); runChatTerminalsCommand(); })); @@ -53,9 +68,31 @@ export class TerminalTabsChatEntry extends Disposable { runChatTerminalsCommand(); } })); + + // Delete button click handler + this._register(dom.addDisposableListener(this._deleteButton, dom.EventType.CLICK, async (e) => { + e.preventDefault(); + e.stopPropagation(); + await this._deleteAllHiddenTerminals(); + })); + + // Delete button keyboard handler + this._register(dom.addDisposableListener(this._deleteButton, dom.EventType.KEY_DOWN, async (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + await this._deleteAllHiddenTerminals(); + } + })); + this.update(); } + private async _deleteAllHiddenTerminals(): Promise { + const hiddenTerminals = this._terminalChatService.getToolSessionTerminalInstances(true); + await Promise.all(hiddenTerminals.map(terminal => this._terminalService.safeDisposeTerminal(terminal))); + } + get element(): HTMLElement { return this._entry; } @@ -66,11 +103,14 @@ export class TerminalTabsChatEntry extends Disposable { this._entry.style.display = 'none'; this._label.textContent = ''; this._entry.removeAttribute('aria-label'); + this._entry.removeAttribute('title'); return; } this._entry.style.display = ''; + const tooltip = localize('terminal.tabs.chatEntryTooltip', "Show hidden chat terminals"); + this._entry.setAttribute('title', tooltip); const hasText = this._tabContainer.classList.contains('has-text'); if (hasText) { this._label.textContent = hiddenChatTerminalCount === 1 diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 05cb619a218..21118dd74ed 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -74,9 +74,14 @@ export class TerminalTabList extends WorkbenchList { private _terminalTabsSingleSelectedContextKey: IContextKey; private _isSplitContextKey: IContextKey; + private _hasText: boolean = true; + get hasText(): boolean { return this._hasText; } + + private _hasActionBar: boolean = true; + get hasActionBar(): boolean { return this._hasActionBar; } + constructor( container: HTMLElement, - disposableStore: DisposableStore, @IContextKeyService contextKeyService: IContextKeyService, @IListService listService: IListService, @IConfigurationService private readonly _configurationService: IConfigurationService, @@ -95,7 +100,7 @@ export class TerminalTabList extends WorkbenchList { getHeight: () => TerminalTabsListSizes.TabHeight, getTemplateId: () => 'terminal.tabs' }, - [disposableStore.add(instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER), () => this.getSelectedElements()))], + [instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER), () => this.getSelectedElements(), () => this.hasText, () => this.hasActionBar)], { horizontalScrolling: false, supportDynamicHeights: false, @@ -149,19 +154,21 @@ export class TerminalTabList extends WorkbenchList { })); this.disposables.add(this.onMouseDblClick(async e => { - const focus = this.getFocus(); - if (focus.length === 0) { + if (!e.element) { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); const instance = await this._terminalService.createTerminal({ location: TerminalLocation.Panel }); this._terminalGroupService.setActiveInstance(instance); await instance.focusWhenReady(); + return; } - if (this._terminalEditingService.getEditingTerminal()?.instanceId === e.element?.instanceId) { + if (this._terminalEditingService.getEditingTerminal()?.instanceId === e.element.instanceId) { return; } if (this._getFocusMode() === 'doubleClick' && this.getFocus().length === 1) { - e.element?.focus(true); + e.element.focus(true); } })); @@ -247,15 +254,29 @@ export class TerminalTabList extends WorkbenchList { const instance = this.getFocusedElements(); this._isSplitContextKey.set(instance.length > 0 && this._terminalGroupService.instanceIsSplit(instance[0])); } + + override layout(height?: number, width?: number): void { + super.layout(height, width); + const actualWidth = width ?? this.getHTMLElement().clientWidth; + const newHasText = actualWidth >= TerminalTabsListSizes.MidpointViewWidth; + const newHasActionBar = actualWidth > TerminalTabsListSizes.ActionbarMinimumWidth; + if (this._hasText !== newHasText || this._hasActionBar !== newHasActionBar) { + this._hasText = newHasText; + this._hasActionBar = newHasActionBar; + this.refresh(); + } + } } -class TerminalTabsRenderer extends Disposable implements IListRenderer { +class TerminalTabsRenderer implements IListRenderer { templateId = 'terminal.tabs'; constructor( - private readonly _container: HTMLElement, + _container: HTMLElement, private readonly _labels: ResourceLabels, private readonly _getSelection: () => ITerminalInstance[], + private readonly _getHasText: () => boolean, + private readonly _getHasActionBar: () => boolean, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService, @ITerminalService private readonly _terminalService: ITerminalService, @@ -269,13 +290,14 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer action instanceof MenuItemAction - ? this._register(this._instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate })) + ? templateDisposables.add(this._instantiationService.createInstance(MenuEntryActionViewItem, action, { hoverDelegate: options.hoverDelegate })) : undefined })); @@ -313,28 +337,13 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer this._cachedContainerWidth = -1); - } - return this._cachedContainerWidth; - } - renderElement(instance: ITerminalInstance, index: number, template: ITerminalTabEntryTemplate): void { - const hasText = !this.shouldHideText(); + const hasText = this._getHasText(); + const hasActionBar = this._getHasActionBar(); const group = this._terminalGroupService.getGroupForInstance(instance); if (!group) { @@ -360,7 +369,6 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer { + template.elementDisposables.add(new Action(TerminalCommandId.SplitActiveTab, terminalStrings.split.short, ThemeIcon.asClassName(Codicon.splitHorizontal), true, async () => { this._runForSelectionOrInstance(instance, async e => { this._terminalService.createTerminal({ location: { parentTerminal: e } }); }); @@ -524,12 +531,12 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer { + actions.push(template.elementDisposables.add(new Action(action.id, action.label, action.icon ? ThemeIcon.asClassName(action.icon) : undefined, true, async () => { this._runForSelectionOrInstance(instance, e => this._commandService.executeCommand(action.id, instance)); }))); } } - actions.push(this._register(new Action(TerminalCommandId.KillActiveTab, terminalStrings.kill.short, ThemeIcon.asClassName(Codicon.trashcan), true, async () => { + actions.push(template.elementDisposables.add(new Action(TerminalCommandId.KillActiveTab, terminalStrings.kill.short, ThemeIcon.asClassName(Codicon.trashcan), true, async () => { this._runForSelectionOrInstance(instance, e => this._terminalService.safeDisposeTerminal(e)); }))); // TODO: Cache these in a way that will use the correct instance @@ -563,6 +570,7 @@ interface ITerminalTabEntryTemplate { hoverActions?: IHoverAction[]; }; readonly elementDisposables: DisposableStore; + readonly templateDisposables: DisposableStore; } @@ -644,9 +652,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop ( - isObject(e) && 'instanceId' in e - )) as ITerminalInstance[]; + const terminals = (dndData as unknown[]).filter(isTerminalInstance); if (terminals.length > 0) { originalEvent.dataTransfer.setData(TerminalDataTransfers.Terminals, JSON.stringify(terminals.map(e => e.resource.toString()))); } @@ -735,7 +741,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop arg === '-l' || arg === '--login') ?? false, + isLoginShell: (isString(slc.args) ? slc.args.split(' ') : slc.args)?.some(arg => arg === '-l' || arg === '--login') ?? false, isReconnect: !!slc.attachPersistentProcess, hasRemoteAuthority: instance.hasRemoteAuthority, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts index ed400202bb9..966892a8ea3 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTooltip.ts @@ -3,18 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../nls.js'; -import { ITerminalInstance } from './terminal.js'; +import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js'; import { asArray } from '../../../../base/common/arrays.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import type { IHoverAction } from '../../../../base/browser/ui/hover/hover.js'; -import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; -import { TerminalStatus } from './terminalStatusList.js'; -import Severity from '../../../../base/common/severity.js'; +import { basename } from '../../../../base/common/path.js'; +import { localize } from '../../../../nls.js'; import { StorageScope, StorageTarget, type IStorageService } from '../../../../platform/storage/common/storage.js'; -import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; import type { ITerminalStatusHoverAction } from '../common/terminal.js'; -import { basename } from '../../../../base/common/path.js'; +import { TerminalStorageKeys } from '../common/terminalStorageKeys.js'; +import { ITerminalInstance } from './terminal.js'; export function getInstanceHoverInfo(instance: ITerminalInstance, storageService: IStorageService): { content: MarkdownString; actions: IHoverAction[] } { const showDetailed = parseInt(storageService.get(TerminalStorageKeys.TabsShowDetailed, StorageScope.APPLICATION) ?? '0'); @@ -77,48 +74,3 @@ export function getShellProcessTooltip(instance: ITerminalInstance, showDetailed return lines.length ? `\n\n---\n\n${lines.join('\n')}` : ''; } -export function refreshShellIntegrationInfoStatus(instance: ITerminalInstance) { - if (!instance.xterm) { - return; - } - const cmdDetectionType = ( - instance.capabilities.get(TerminalCapability.CommandDetection)?.hasRichCommandDetection - ? localize('shellIntegration.rich', 'Rich') - : instance.capabilities.has(TerminalCapability.CommandDetection) - ? localize('shellIntegration.basic', 'Basic') - : instance.usedShellIntegrationInjection - ? localize('shellIntegration.injectionFailed', "Injection failed to activate") - : localize('shellIntegration.no', 'No') - ); - - const detailedAdditions: string[] = []; - if (instance.shellType) { - detailedAdditions.push(`Shell type: \`${instance.shellType}\``); - } - const cwd = instance.cwd; - if (cwd) { - detailedAdditions.push(`Current working directory: \`${cwd}\``); - } - const seenSequences = Array.from(instance.xterm.shellIntegration.seenSequences); - if (seenSequences.length > 0) { - detailedAdditions.push(`Seen sequences: ${seenSequences.map(e => `\`${e}\``).join(', ')}`); - } - const promptType = instance.capabilities.get(TerminalCapability.PromptTypeDetection)?.promptType; - if (promptType) { - detailedAdditions.push(`Prompt type: \`${promptType}\``); - } - const combinedString = instance.capabilities.get(TerminalCapability.CommandDetection)?.promptInputModel.getCombinedString(); - if (combinedString !== undefined) { - detailedAdditions.push(`Prompt input: \`${combinedString}\``); - } - const detailedAdditionsString = detailedAdditions.length > 0 - ? '\n\n' + detailedAdditions.map(e => `- ${e}`).join('\n') - : ''; - - instance.statusList.add({ - id: TerminalStatus.ShellIntegrationInfo, - severity: Severity.Info, - tooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}`, - detailedTooltip: `${localize('shellIntegration', "Shell integration")}: ${cmdDetectionType}${detailedAdditionsString}` - }); -} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 5eed1d4f713..9a745727415 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -13,7 +13,7 @@ import { IContextMenuService, IContextViewService } from '../../../../platform/c import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IThemeService, Themable } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; -import { switchTerminalActionViewItemSeparator, switchTerminalShowTabsTitle } from './terminalActions.js'; +import { switchTerminalShowTabsTitle } from './terminalActions.js'; import { INotificationService, IPromptChoice, Severity } from '../../../../platform/notification/common/notification.js'; import { ICreateTerminalOptions, ITerminalConfigurationService, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalConnectionState, TerminalDataTransfers } from './terminal.js'; import { ViewPane, IViewPaneOptions } from '../../../browser/parts/views/viewPane.js'; @@ -26,7 +26,7 @@ import { ITerminalProfileResolverService, ITerminalProfileService, TerminalComma import { TerminalSettingId, ITerminalProfile, TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { ActionViewItem, IBaseActionViewItemOptions, SelectActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { asCssVariable, selectBorder } from '../../../../platform/theme/common/colorRegistry.js'; -import { ISelectOptionItem } from '../../../../base/browser/ui/selectBox/selectBox.js'; +import { ISelectOptionItem, SeparatorSelectOption } from '../../../../base/browser/ui/selectBox/selectBox.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { TerminalTabbedView } from './terminalTabbedView.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; @@ -52,6 +52,7 @@ import { InstanceContext, TerminalContextActionRunner } from './terminalContextM import { MicrotaskDelay } from '../../../../base/common/symbols.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { hasNativeContextMenu } from '../../../../platform/window/common/window.js'; +import { hasKey } from '../../../../base/common/types.js'; export class TerminalViewPane extends ViewPane { private _parentDomElement: HTMLElement | undefined; @@ -336,13 +337,12 @@ export class TerminalViewPane extends ViewPane { // be focused. So wait for connection to finish, then focus. const previousActiveElement = this.element.ownerDocument.activeElement; if (previousActiveElement) { - // TODO: Improve lifecycle management this event should be disposed after first fire - this._register(this._terminalService.onDidChangeConnectionState(() => { + const listener = this._register(Event.once(this._terminalService.onDidChangeConnectionState)(() => { // Only focus the terminal if the activeElement has not changed since focus() was called - // TODO: Hack if (previousActiveElement && dom.isActiveElement(previousActiveElement)) { this._terminalGroupService.showPanel(true); } + this._store.delete(listener); })); } } @@ -397,7 +397,7 @@ function getTerminalSelectOpenItems(terminalService: ITerminalService, terminalG } else { items = [{ text: nls.localize('terminalConnectingLabel', "Starting...") }]; } - items.push({ text: switchTerminalActionViewItemSeparator, isDisabled: true }); + items.push(SeparatorSelectOption); items.push({ text: switchTerminalShowTabsTitle }); return items; } @@ -624,7 +624,7 @@ class TerminalThemeIconStyle extends Themable { let uri = undefined; if (icon instanceof URI) { uri = icon; - } else if (icon instanceof Object && 'light' in icon && 'dark' in icon) { + } else if (icon instanceof Object && hasKey(icon, { light: true, dark: true })) { uri = isDark(colorTheme.type) ? icon.dark : icon.light; } const iconClasses = getUriClasses(instance, colorTheme.type); diff --git a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts index 23729e0c8d1..176f0378394 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm-private.d.ts @@ -31,3 +31,19 @@ export interface IXtermCore { }; }; } + +export interface IBufferLine { + readonly length: number; + getCell(x: number): { getChars(): string } | undefined; + translateToString(trimRight?: boolean): string; +} + +export interface IBufferSet { + readonly active: { + readonly baseY: number; + readonly cursorY: number; + readonly cursorX: number; + readonly length: number; + getLine(y: number): IBufferLine | undefined; + }; +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts index c0d71ee566a..68e2c80e2ca 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationAddon.ts @@ -21,23 +21,22 @@ import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quic import { CommandInvalidationReason, ICommandDetectionCapability, IMarkProperties, ITerminalCapabilityStore, ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalSettingId, type IDecorationAddon } from '../../../../../platform/terminal/common/terminal.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { terminalDecorationError, terminalDecorationIncomplete, terminalDecorationMark, terminalDecorationSuccess } from '../terminalIcons.js'; -import { DecorationSelector, getTerminalDecorationHoverContent, updateLayout } from './decorationStyles.js'; +import { terminalDecorationMark } from '../terminalIcons.js'; +import { DecorationSelector, getTerminalCommandDecorationState, getTerminalDecorationHoverContent, updateLayout } from './decorationStyles.js'; import { TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR, TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR } from '../../common/terminalColorRegistry.js'; import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { IChatContextPickService } from '../../../chat/browser/chatContextPickService.js'; -import { IChatWidgetService, showChatView } from '../../../chat/browser/chat.js'; +import { IChatContextPickService } from '../../../chat/browser/attachments/chatContextPickService.js'; +import { IChatWidgetService } from '../../../chat/browser/chat.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { TerminalContext } from '../../../chat/browser/actions/chatContext.js'; import { getTerminalUri, parseTerminalUri } from '../terminalUri.js'; import { URI } from '../../../../../base/common/uri.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; +import { isString } from '../../../../../base/common/types.js'; -interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; exitCode?: number; markProperties?: IMarkProperties } +interface IDisposableDecoration { decoration: IDecoration; disposables: IDisposable[]; command?: ITerminalCommand; markProperties?: IMarkProperties } export class DecorationAddon extends Disposable implements ITerminalAddon, IDecorationAddon { protected _terminal: Terminal | undefined; @@ -69,9 +68,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco @IHoverService private readonly _hoverService: IHoverService, @IChatContextPickService private readonly _contextPickService: IChatContextPickService, @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IViewsService private readonly _viewsService: IViewsService, - @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._register(toDisposable(() => this._dispose())); @@ -178,7 +175,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco private _refreshStyles(refreshOverviewRulerColors?: boolean): void { if (refreshOverviewRulerColors) { for (const decoration of this._decorations.values()) { - const color = this._getDecorationCssColor(decoration)?.toString() ?? ''; + const color = this._getDecorationCssColor(decoration.command)?.toString() ?? ''; if (decoration.decoration.options?.overviewRulerOptions) { decoration.decoration.options.overviewRulerOptions.color = color; } else if (decoration.decoration.options) { @@ -188,7 +185,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco } this._updateClasses(this._placeholderDecoration?.element); for (const decoration of this._decorations.values()) { - this._updateClasses(decoration.decoration.element, decoration.exitCode, decoration.markProperties); + this._updateClasses(decoration.decoration.element, decoration.command, decoration.markProperties); } } @@ -319,14 +316,14 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco { decoration, disposables: this._createDisposables(element, command, markProperties), - exitCode: command?.exitCode, - markProperties: command?.markProperties + command, + markProperties: command?.markProperties || markProperties }); } if (!element.classList.contains(DecorationSelector.Codicon) || command?.marker?.line === 0) { // first render or buffer was cleared updateLayout(this._configurationService, element); - this._updateClasses(element, command?.exitCode, command?.markProperties || markProperties); + this._updateClasses(element, command, command?.markProperties || markProperties); } }); return decoration; @@ -363,11 +360,11 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco private _createHover(element: HTMLElement, command: ITerminalCommand | undefined, hoverMessage?: string) { return this._hoverService.setupDelayedHover(element, () => ({ - content: new MarkdownString(getTerminalDecorationHoverContent(command, hoverMessage)) + content: new MarkdownString(getTerminalDecorationHoverContent(command, hoverMessage, true)) })); } - private _updateClasses(element?: HTMLElement, exitCode?: number, markProperties?: IMarkProperties): void { + private _updateClasses(element?: HTMLElement, command?: ITerminalCommand, markProperties?: IMarkProperties): void { if (!element) { return; } @@ -384,17 +381,15 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco } } else { // command decoration + const state = getTerminalCommandDecorationState(command); this._updateCommandDecorationVisibility(element); - if (exitCode === undefined) { - element.classList.add(DecorationSelector.DefaultColor, DecorationSelector.Default); - element.classList.add(...ThemeIcon.asClassNameArray(terminalDecorationIncomplete)); - } else if (exitCode) { - element.classList.add(DecorationSelector.ErrorColor); - element.classList.add(...ThemeIcon.asClassNameArray(terminalDecorationError)); - } else { - element.classList.add(...ThemeIcon.asClassNameArray(terminalDecorationSuccess)); + for (const className of state.classNames) { + element.classList.add(className); } + element.classList.add(...ThemeIcon.asClassNameArray(state.icon)); } + element.removeAttribute('title'); + element.removeAttribute('aria-label'); } private _createContextMenu(element: HTMLElement, command: ITerminalCommand): IDisposable[] { @@ -481,7 +476,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco class: undefined, tooltip: labelCopyCommandAndOutput, id: 'terminal.copyCommandAndOutput', label: labelCopyCommandAndOutput, enabled: true, run: () => { const output = command.getOutput(); - if (typeof output === 'string') { + if (isString(output)) { this._clipboardService.writeText(`${command.command !== '' ? command.command + '\n' : ''}${output}`); } } @@ -491,7 +486,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco class: undefined, tooltip: labelText, id: 'terminal.copyOutput', label: labelText, enabled: true, run: () => { const text = command.getOutput(); - if (typeof text === 'string') { + if (isString(text)) { this._clipboardService.writeText(text); } } @@ -527,6 +522,10 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco } private _createAttachToChatAction(command: ITerminalCommand): IAction | undefined { + const chatIsEnabled = this._chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).some(w => w.attachmentCapabilities.supportsTerminalAttachments); + if (!chatIsEnabled) { + return undefined; + } const labelAttachToChat = localize("terminal.attachToChat", 'Attach To Chat'); return { class: undefined, tooltip: labelAttachToChat, id: 'terminal.attachToChat', label: labelAttachToChat, enabled: true, @@ -535,7 +534,7 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco // If no widget found (e.g., after window reload when chat hasn't been focused), open chat view if (!widget) { - widget = await showChatView(this._viewsService, this._layoutService); + widget = await this._chatWidgetService.revealWidget(); } if (!widget) { @@ -607,12 +606,12 @@ export class DecorationAddon extends Disposable implements ITerminalAddon, IDeco quickPick.show(); } - private _getDecorationCssColor(decorationOrCommand?: IDisposableDecoration | ITerminalCommand): string | undefined { + private _getDecorationCssColor(command?: ITerminalCommand): string | undefined { let colorId: string; - if (decorationOrCommand?.exitCode === undefined) { + if (command?.exitCode === undefined) { colorId = TERMINAL_COMMAND_DECORATION_DEFAULT_BACKGROUND_COLOR; } else { - colorId = decorationOrCommand.exitCode ? TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR : TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR; + colorId = command.exitCode ? TERMINAL_COMMAND_DECORATION_ERROR_BACKGROUND_COLOR : TERMINAL_COMMAND_DECORATION_SUCCESS_BACKGROUND_COLOR; } return this._themeService.getColorTheme().getColor(colorId)?.toString(); } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts index f19b9cb7162..c3c89f4b167 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/decorationStyles.ts @@ -4,10 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { fromNow, getDurationString } from '../../../../../base/common/date.js'; +import { isNumber } from '../../../../../base/common/types.js'; +import type { ThemeIcon } from '../../../../../base/common/themables.js'; import { localize } from '../../../../../nls.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ITerminalCommand } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import type { ITerminalCommand } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js'; +import { terminalDecorationError, terminalDecorationIncomplete, terminalDecorationSuccess } from '../terminalIcons.js'; const enum DecorationStyles { DefaultDimension = 16, @@ -25,9 +28,8 @@ export const enum DecorationSelector { OverviewRuler = '.xterm-decoration-overview-ruler', } -export function getTerminalDecorationHoverContent(command: ITerminalCommand | undefined, hoverMessage?: string): string { - let hoverContent = `${localize('terminalPromptContextMenu', "Show Command Actions")}`; - hoverContent += '\n\n---\n\n'; +export function getTerminalDecorationHoverContent(command: ITerminalCommand | undefined, hoverMessage?: string, showCommandActions?: boolean): string { + let hoverContent = showCommandActions ? `${localize('terminalPromptContextMenu', "Show Command Actions")}\n\n---\n\n` : ''; if (!command) { if (hoverMessage) { hoverContent = hoverMessage; @@ -41,7 +43,7 @@ export function getTerminalDecorationHoverContent(command: ITerminalCommand | un return ''; } } else { - if (command.duration) { + if (isNumber(command.duration)) { const durationText = getDurationString(command.duration); if (command.exitCode) { if (command.exitCode === -1) { @@ -60,13 +62,167 @@ export function getTerminalDecorationHoverContent(command: ITerminalCommand | un hoverContent += localize('terminalPromptCommandFailedWithExitCode', 'Command executed {0} and failed (Exit Code {1})', fromNow(command.timestamp, true), command.exitCode); } } else { - hoverContent += localize('terminalPromptCommandSuccess', 'Command executed {0}', fromNow(command.timestamp, true)); + hoverContent += localize('terminalPromptCommandSuccess', 'Command executed {0} now'); } } } return hoverContent; } +export interface ITerminalCommandDecorationPersistedState { + exitCode?: number; + timestamp?: number; + duration?: number; +} + +export const enum TerminalCommandDecorationStatus { + Unknown = 'unknown', + Running = 'running', + Success = 'success', + Error = 'error' +} + +export interface ITerminalCommandDecorationState { + status: TerminalCommandDecorationStatus; + icon: ThemeIcon; + classNames: string[]; + exitCode?: number; + exitCodeText: string; + startTimestamp?: number; + startText: string; + duration?: number; + durationText: string; + hoverMessage: string; +} + +const unknownText = localize('terminalCommandDecoration.unknown', 'Unknown'); +const runningText = localize('terminalCommandDecoration.running', 'Running'); + +export function getTerminalCommandDecorationTooltip(command?: ITerminalCommand, storedState?: ITerminalCommandDecorationPersistedState): string { + if (command) { + return getTerminalDecorationHoverContent(command); + } + if (!storedState) { + return ''; + } + const timestamp = storedState.timestamp; + const exitCode = storedState.exitCode; + const duration = storedState.duration; + if (typeof timestamp !== 'number' || timestamp === undefined) { + return ''; + } + let hoverContent = ''; + const fromNowText = fromNow(timestamp, true); + if (typeof duration === 'number') { + const durationText = getDurationString(Math.max(duration, 0)); + if (exitCode) { + if (exitCode === -1) { + hoverContent += localize('terminalPromptCommandFailed.duration', 'Command executed {0}, took {1} and failed', fromNowText, durationText); + } else { + hoverContent += localize('terminalPromptCommandFailedWithExitCode.duration', 'Command executed {0}, took {1} and failed (Exit Code {2})', fromNowText, durationText, exitCode); + } + } else { + hoverContent += localize('terminalPromptCommandSuccess.duration', 'Command executed {0} and took {1}', fromNowText, durationText); + } + } else { + if (exitCode) { + if (exitCode === -1) { + hoverContent += localize('terminalPromptCommandFailed', 'Command executed {0} and failed', fromNowText); + } else { + hoverContent += localize('terminalPromptCommandFailedWithExitCode', 'Command executed {0} and failed (Exit Code {1})', fromNowText, exitCode); + } + } else { + hoverContent += localize('terminalPromptCommandSuccess.', 'Command executed {0} ', fromNowText); + } + } + return hoverContent; +} + +export function getTerminalCommandDecorationState( + command: ITerminalCommand | undefined, + storedState?: ITerminalCommandDecorationPersistedState, + now: number = Date.now() +): ITerminalCommandDecorationState { + let status = TerminalCommandDecorationStatus.Unknown; + const exitCode: number | undefined = command?.exitCode ?? storedState?.exitCode; + let exitCodeText = unknownText; + const startTimestamp: number | undefined = command?.timestamp ?? storedState?.timestamp; + let startText = unknownText; + let durationMs: number | undefined; + let durationText = unknownText; + + if (typeof startTimestamp === 'number') { + startText = new Date(startTimestamp).toLocaleString(); + } + + if (command) { + if (command.exitCode === undefined) { + status = TerminalCommandDecorationStatus.Running; + exitCodeText = runningText; + durationMs = startTimestamp !== undefined ? Math.max(0, now - startTimestamp) : undefined; + } else if (command.exitCode !== 0) { + status = TerminalCommandDecorationStatus.Error; + exitCodeText = String(command.exitCode); + durationMs = command.duration ?? (startTimestamp !== undefined ? Math.max(0, now - startTimestamp) : undefined); + } else { + status = TerminalCommandDecorationStatus.Success; + exitCodeText = String(command.exitCode); + durationMs = command.duration ?? (startTimestamp !== undefined ? Math.max(0, now - startTimestamp) : undefined); + } + } else if (storedState) { + if (storedState.exitCode === undefined) { + status = TerminalCommandDecorationStatus.Running; + exitCodeText = runningText; + durationMs = startTimestamp !== undefined ? Math.max(0, now - startTimestamp) : undefined; + } else if (storedState.exitCode !== 0) { + status = TerminalCommandDecorationStatus.Error; + exitCodeText = String(storedState.exitCode); + durationMs = storedState.duration; + } else { + status = TerminalCommandDecorationStatus.Success; + exitCodeText = String(storedState.exitCode); + durationMs = storedState.duration; + } + } + + if (typeof durationMs === 'number') { + durationText = getDurationString(Math.max(durationMs, 0)); + } + + const classNames: string[] = []; + let icon = terminalDecorationIncomplete; + switch (status) { + case TerminalCommandDecorationStatus.Running: + case TerminalCommandDecorationStatus.Unknown: + classNames.push(DecorationSelector.DefaultColor, DecorationSelector.Default); + icon = terminalDecorationIncomplete; + break; + case TerminalCommandDecorationStatus.Error: + classNames.push(DecorationSelector.ErrorColor); + icon = terminalDecorationError; + break; + case TerminalCommandDecorationStatus.Success: + classNames.push('success'); + icon = terminalDecorationSuccess; + break; + } + + const hoverMessage = getTerminalCommandDecorationTooltip(command, storedState); + + return { + status, + icon, + classNames, + exitCode, + exitCodeText, + startTimestamp, + startText, + duration: durationMs, + durationText, + hoverMessage + }; +} + export function updateLayout(configurationService: IConfigurationService, element?: HTMLElement): void { if (!element) { return; @@ -74,7 +230,7 @@ export function updateLayout(configurationService: IConfigurationService, elemen const fontSize = configurationService.inspect(TerminalSettingId.FontSize).value; const defaultFontSize = configurationService.inspect(TerminalSettingId.FontSize).defaultValue; const lineHeight = configurationService.inspect(TerminalSettingId.LineHeight).value; - if (typeof fontSize === 'number' && typeof defaultFontSize === 'number' && typeof lineHeight === 'number') { + if (isNumber(fontSize) && isNumber(defaultFontSize) && isNumber(lineHeight)) { const scalar = (fontSize / defaultFontSize) <= 1 ? (fontSize / defaultFontSize) : 1; // must be inlined to override the inlined styles from xterm element.style.width = `${scalar * DecorationStyles.DefaultDimension}px`; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts index f308a522277..523748df685 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/markNavigationAddon.ts @@ -12,7 +12,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { TERMINAL_OVERVIEW_RULER_CURSOR_FOREGROUND_COLOR } from '../../common/terminalColorRegistry.js'; import { getWindow } from '../../../../../base/browser/dom.js'; -import { ICurrentPartialCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; +import { ICurrentPartialCommand, isFullTerminalCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalContribSettingId } from '../../terminalContribExports.js'; @@ -260,7 +260,7 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe } revealCommand(command: ITerminalCommand | ICurrentPartialCommand, position: ScrollPosition = ScrollPosition.Middle): void { - const marker = 'getOutput' in command ? command.marker : command.commandStartMarker; + const marker = isFullTerminalCommand(command) ? command.marker : command.commandStartMarker; if (!this._terminal || !marker) { return; } @@ -331,9 +331,6 @@ export class MarkNavigationAddon extends Disposable implements IMarkTracker, ITe element.classList.add('bottom'); } } - if (this._terminal?.element) { - element.style.marginLeft = `-${getWindow(this._terminal.element).getComputedStyle(this._terminal.element).paddingLeft}`; - } })); } } diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index b4f743df09c..8428e99b5c4 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -46,6 +46,7 @@ import { equals } from '../../../../../base/common/objects.js'; import type { IProgressState } from '@xterm/addon-progress'; import type { CommandDetectionCapability } from '../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; import { URI } from '../../../../../base/common/uri.js'; +import { isNumber } from '../../../../../base/common/types.js'; const enum RenderConstants { SmoothScrollDuration = 125 @@ -83,6 +84,8 @@ export interface IXtermTerminalOptions { disableShellIntegrationReporting?: boolean; /** The object that imports xterm addons, set this to inject an importer in tests. */ xtermAddonImporter?: XtermAddonImporter; + /** Whether to disable the overview ruler. */ + disableOverviewRuler?: boolean; } /** @@ -104,6 +107,8 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach get lastInputEvent(): string | undefined { return this._lastInputEvent; } private _progressState: IProgressState = { state: 0, value: 0 }; get progressState(): IProgressState { return this._progressState; } + get buffer() { return this.raw.buffer; } + get cols() { return this.raw.cols; } // Always on addons private _markNavigationAddon: MarkNavigationAddon; @@ -117,6 +122,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach private _searchAddon?: SearchAddonType; private _unicode11Addon?: Unicode11AddonType; private _webglAddon?: WebglAddonType; + private _webglAddonCustomGlyphs?: boolean = false; private _serializeAddon?: SerializeAddonType; private _imageAddon?: ImageAddonType; private readonly _ligaturesAddon: MutableDisposable = this._register(new MutableDisposable()); @@ -140,6 +146,10 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach readonly onDidRequestRefreshDimensions = this._onDidRequestRefreshDimensions.event; private readonly _onDidChangeFindResults = this._register(new Emitter<{ resultIndex: number; resultCount: number }>()); readonly onDidChangeFindResults = this._onDidChangeFindResults.event; + private readonly _onBeforeSearch = this._register(new Emitter()); + readonly onBeforeSearch = this._onBeforeSearch.event; + private readonly _onAfterSearch = this._register(new Emitter()); + readonly onAfterSearch = this._onAfterSearch.event; private readonly _onDidChangeSelection = this._register(new Emitter()); readonly onDidChangeSelection = this._onDidChangeSelection.event; private readonly _onDidChangeFocus = this._register(new Emitter()); @@ -229,12 +239,16 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach scrollSensitivity: config.mouseWheelScrollSensitivity, scrollOnEraseInDisplay: true, wordSeparator: config.wordSeparators, - overviewRuler: { + overviewRuler: options.disableOverviewRuler ? { width: 0 } : { width: 14, showTopBorder: true, }, ignoreBracketedPasteMode: config.ignoreBracketedPasteMode, rescaleOverlappingGlyphs: config.rescaleOverlappingGlyphs, + vtExtensions: { + kittyKeyboard: config.enableKittyKeyboardProtocol, + win32InputMode: config.enableWin32InputMode, + }, windowOptions: { getWinSizePixels: true, getCellSizePixels: true, @@ -527,13 +541,13 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this.raw.options.macOptionClickForcesSelection = config.macOptionClickForcesSelection; this.raw.options.rightClickSelectsWord = config.rightClickBehavior === 'selectWord'; this.raw.options.wordSeparator = config.wordSeparators; - this.raw.options.customGlyphs = config.customGlyphs; this.raw.options.ignoreBracketedPasteMode = config.ignoreBracketedPasteMode; this.raw.options.rescaleOverlappingGlyphs = config.rescaleOverlappingGlyphs; - this.raw.options.overviewRuler = { - width: 14, - showTopBorder: true, + this.raw.options.vtExtensions = { + kittyKeyboard: config.enableKittyKeyboardProtocol, + win32InputMode: config.enableWin32InputMode, }; + this._updateSmoothScrolling(); if (this._attached) { if (this._attached.options.enableGpu) { @@ -614,6 +628,12 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this._lastFindResult = results; this._onDidChangeFindResults.fire(results); }); + this._searchAddon.onBeforeSearch(() => { + this._onBeforeSearch.fire(); + }); + this._searchAddon.onAfterSearch(() => { + this._onAfterSearch.fire(); + }); return this._searchAddon; }); } @@ -701,6 +721,10 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach this._accessibilitySignalService.playSignal(AccessibilitySignal.clear); } + reset(): void { + this.raw.reset(); + } + hasSelection(): boolean { return this.raw.hasSelection(); } @@ -740,11 +764,13 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach if (this.hasSelection() || (asHtml && command)) { if (asHtml) { const textAsHtml = await this.getSelectionAsHtml(command); - function listener(e: any) { - if (!e.clipboardData.types.includes('text/plain')) { - e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); + function listener(e: ClipboardEvent) { + if (e.clipboardData) { + if (!e.clipboardData.types.includes('text/plain')) { + e.clipboardData.setData('text/plain', command?.getOutput() ?? ''); + } + e.clipboardData.setData('text/html', textAsHtml); } - e.clipboardData.setData('text/html', textAsHtml); e.preventDefault(); } const doc = dom.getDocument(this.raw.element); @@ -787,12 +813,20 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } private async _enableWebglRenderer(): Promise { - if (!this.raw.element || this._webglAddon) { + // Currently webgl options can only be specified on addon creation + if (!this.raw.element || this._webglAddon && this._webglAddonCustomGlyphs === this._terminalConfigurationService.config.customGlyphs) { return; } + // Dispose of existing addon before creating a new one to avoid leaking WebGL contexts + this._disposeOfWebglRenderer(); + + this._webglAddonCustomGlyphs = this._terminalConfigurationService.config.customGlyphs; + const Addon = await this._xtermAddonLoader.importAddon('webgl'); - this._webglAddon = new Addon(); + this._webglAddon = new Addon({ + customGlyphs: this._terminalConfigurationService.config.customGlyphs + }); try { this.raw.loadAddon(this._webglAddon); this._logService.trace('Webgl was loaded'); @@ -876,18 +910,45 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach } private _disposeOfWebglRenderer(): void { + if (!this._webglAddon) { + return; + } try { this._webglAddon?.dispose(); } catch { // ignore } this._webglAddon = undefined; + this._webglAddonCustomGlyphs = undefined; this._refreshImageAddon(); // WebGL renderer cell dimensions differ from the DOM renderer, make sure the terminal // gets resized after the webgl addon is disposed this._onDidRequestRefreshDimensions.fire(); } + async getRangeAsVT(startMarker?: IXtermMarker, endMarker?: IXtermMarker, skipLastLine?: boolean): Promise { + if (!this._serializeAddon) { + const Addon = await this._xtermAddonLoader.importAddon('serialize'); + this._serializeAddon = new Addon(); + this.raw.loadAddon(this._serializeAddon); + } + + const hasValidEndMarker = isNumber(endMarker?.line); + const start = isNumber(startMarker?.line) && startMarker?.line > -1 ? startMarker.line : 0; + let end = hasValidEndMarker ? endMarker.line : this.raw.buffer.active.length - 1; + if (skipLastLine && hasValidEndMarker) { + end = end - 1; + } + end = Math.max(end, start); + return this._serializeAddon.serialize({ + range: { + start: startMarker?.line ?? 0, + end + } + }); + } + + getXtermTheme(theme?: IColorTheme): ITheme { if (!theme) { theme = this._themeService.getColorTheme(); @@ -963,6 +1024,7 @@ export class XtermTerminal extends Disposable implements IXtermTerminal, IDetach override dispose(): void { this._anyTerminalFocusContextKey.reset(); this._anyFocusedTerminalHasSelection.reset(); + this._disposeOfWebglRenderer(); this._onDidDispose.fire(); super.dispose(); } diff --git a/src/vs/workbench/contrib/terminal/common/basePty.ts b/src/vs/workbench/contrib/terminal/common/basePty.ts index a851f56225e..ace591d0ee3 100644 --- a/src/vs/workbench/contrib/terminal/common/basePty.ts +++ b/src/vs/workbench/contrib/terminal/common/basePty.ts @@ -6,6 +6,7 @@ import { Emitter } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { mark } from '../../../../base/common/performance.js'; +import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import type { IPtyHostProcessReplayEvent, ISerializedCommandDetectionCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { ProcessPropertyType, type IProcessDataEvent, type IProcessProperty, type IProcessPropertyMap, type IProcessReadyEvent, type ITerminalChildProcess } from '../../../../platform/terminal/common/terminal.js'; @@ -37,7 +38,7 @@ export abstract class BasePty extends Disposable implements Partial()); readonly onProcessReady = this._onProcessReady.event; - protected readonly _onDidChangeProperty = this._register(new Emitter>()); + protected readonly _onDidChangeProperty = this._register(new Emitter()); readonly onDidChangeProperty = this._onDidChangeProperty.event; protected readonly _onProcessExit = this._register(new Emitter()); readonly onProcessExit = this._onProcessExit.event; @@ -68,18 +69,21 @@ export abstract class BasePty extends Disposable implements Partial) { + handleDidChangeProperty({ type, value }: IProcessProperty) { switch (type) { case ProcessPropertyType.Cwd: - this._properties.cwd = value; + this._properties.cwd = value as IProcessPropertyMap[ProcessPropertyType.Cwd]; break; case ProcessPropertyType.InitialCwd: - this._properties.initialCwd = value; + this._properties.initialCwd = value as IProcessPropertyMap[ProcessPropertyType.InitialCwd]; break; - case ProcessPropertyType.ResolvedShellLaunchConfig: - if (value.cwd && typeof value.cwd !== 'string') { - value.cwd = URI.revive(value.cwd); + case ProcessPropertyType.ResolvedShellLaunchConfig: { + const cast = value as IProcessPropertyMap[ProcessPropertyType.ResolvedShellLaunchConfig]; + if (cast.cwd && !isString(cast.cwd)) { + cast.cwd = URI.revive(cast.cwd); } + break; + } } this._onDidChangeProperty.fire({ type, value }); } diff --git a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts index c31ec299525..b728f908338 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/remoteTerminalChannel.ts @@ -21,7 +21,7 @@ import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs import { IProcessEnvironment, OperatingSystem } from '../../../../../base/common/platform.js'; import { ICompleteTerminalConfiguration } from '../terminal.js'; import { IPtyHostProcessReplayEvent } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ISerializableEnvironmentDescriptionMap as ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js'; +import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js'; import type * as performance from '../../../../../base/common/performance.js'; import { RemoteTerminalChannelEvent, RemoteTerminalChannelRequest } from './terminal.js'; import { ConfigurationResolverExpression } from '../../../../services/configurationResolver/common/configurationResolverExpression.js'; @@ -90,14 +90,14 @@ export class RemoteTerminalChannelClient implements IPtyHostController { get onProcessOrphanQuestion(): Event<{ id: number }> { return this._channel.listen<{ id: number }>(RemoteTerminalChannelEvent.OnProcessOrphanQuestion); } - get onExecuteCommand(): Event<{ reqId: number; persistentProcessId: number; commandId: string; commandArgs: any[] }> { - return this._channel.listen<{ reqId: number; persistentProcessId: number; commandId: string; commandArgs: any[] }>(RemoteTerminalChannelEvent.OnExecuteCommand); + get onExecuteCommand(): Event<{ reqId: number; persistentProcessId: number; commandId: string; commandArgs: unknown[] }> { + return this._channel.listen<{ reqId: number; persistentProcessId: number; commandId: string; commandArgs: unknown[] }>(RemoteTerminalChannelEvent.OnExecuteCommand); } get onDidRequestDetach(): Event<{ requestId: number; workspaceId: string; instanceId: number }> { return this._channel.listen<{ requestId: number; workspaceId: string; instanceId: number }>(RemoteTerminalChannelEvent.OnDidRequestDetach); } - get onDidChangeProperty(): Event<{ id: number; property: IProcessProperty }> { - return this._channel.listen<{ id: number; property: IProcessProperty }>(RemoteTerminalChannelEvent.OnDidChangeProperty); + get onDidChangeProperty(): Event<{ id: number; property: IProcessProperty }> { + return this._channel.listen<{ id: number; property: IProcessProperty }>(RemoteTerminalChannelEvent.OnDidChangeProperty); } constructor( @@ -246,7 +246,7 @@ export class RemoteTerminalChannelClient implements IPtyHostController { orphanQuestionReply(id: number): Promise { return this._channel.call(RemoteTerminalChannelRequest.OrphanQuestionReply, [id]); } - sendCommandResult(reqId: number, isError: boolean, payload: any): Promise { + sendCommandResult(reqId: number, isError: boolean, payload: unknown): Promise { return this._channel.call(RemoteTerminalChannelRequest.SendCommandResult, [reqId, isError, payload]); } freePortKillProcess(port: string): Promise<{ port: string; processId: string }> { diff --git a/src/vs/workbench/contrib/terminal/common/remote/terminal.ts b/src/vs/workbench/contrib/terminal/common/remote/terminal.ts index c980fad5598..fdf6140e90d 100644 --- a/src/vs/workbench/contrib/terminal/common/remote/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/remote/terminal.ts @@ -6,7 +6,7 @@ import { UriComponents } from '../../../../../base/common/uri.js'; import { IShellLaunchConfigDto, ITerminalProcessOptions } from '../../../../../platform/terminal/common/terminal.js'; import { ICompleteTerminalConfiguration } from '../terminal.js'; -import { ISerializableEnvironmentDescriptionMap as ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js'; +import { ISerializableEnvironmentDescriptionMap, ISerializableEnvironmentVariableCollection } from '../../../../../platform/terminal/common/environmentVariable.js'; export const REMOTE_TERMINAL_CHANNEL_NAME = 'remoteterminal'; diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh index 8ec742e124f..87e3a63fe0a 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh @@ -62,6 +62,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + export HISTCONTROL="ignorespace" + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # Apply EnvironmentVariableCollections if needed if [ -n "${VSCODE_ENV_REPLACE:-}" ]; then IFS=':' read -ra ADDR <<< "$VSCODE_ENV_REPLACE" diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh index f32b74f1927..5389bd95b12 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration-rc.zsh @@ -98,6 +98,12 @@ if [ -z "$VSCODE_SHELL_INTEGRATION" ]; then builtin return fi +# Prevent AI-executed commands from polluting shell history +if [ "${VSCODE_PREVENT_SHELL_HISTORY:-}" = "1" ]; then + builtin setopt HIST_IGNORE_SPACE + builtin unset VSCODE_PREVENT_SHELL_HISTORY +fi + # The property (P) and command (E) codes embed values which require escaping. # Backslashes are doubled. Non-alphanumeric characters are converted to escaped hex. __vsc_escape_value() { diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish index 51e0d3a7d6e..0e0b6798c16 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.fish @@ -23,6 +23,13 @@ or exit set --global VSCODE_SHELL_INTEGRATION 1 set --global __vscode_shell_env_reporting $VSCODE_SHELL_ENV_REPORTING set -e VSCODE_SHELL_ENV_REPORTING + +# Prevent AI-executed commands from polluting shell history +if test "$VSCODE_PREVENT_SHELL_HISTORY" = "1" + set -g fish_private_mode 1 + set -e VSCODE_PREVENT_SHELL_HISTORY +end + set -g envVarsToReport if test -n "$__vscode_shell_env_reporting" set envVarsToReport (string split "," "$__vscode_shell_env_reporting") diff --git a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 index 3c37defc7b2..e89f6ec24c4 100644 --- a/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 +++ b/src/vs/workbench/contrib/terminal/common/scripts/shellIntegration.ps1 @@ -259,4 +259,13 @@ function Set-MappedKeyHandlers { if ($Global:__VSCodeState.HasPSReadLine) { Set-MappedKeyHandlers + + # Prevent AI-executed commands from polluting shell history + if ($env:VSCODE_PREVENT_SHELL_HISTORY -eq "1") { + Set-PSReadLineOption -AddToHistoryHandler { + param([string]$line) + return $false + } + $env:VSCODE_PREVENT_SHELL_HISTORY = $null + } } diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 45d392f1aae..bd69a9fde5b 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -178,7 +178,6 @@ export interface ITerminalConfiguration { environmentChangesRelaunch: boolean; showExitAlert: boolean; splitCwd: 'workspaceRoot' | 'initial' | 'inherited'; - windowsEnableConpty: boolean; windowsUseConptyDll?: boolean; wordSeparators: string; enableFileLinks: 'off' | 'on' | 'notRemote'; @@ -208,6 +207,8 @@ export interface ITerminalConfiguration { smoothScrolling: boolean; ignoreBracketedPasteMode: boolean; rescaleOverlappingGlyphs: boolean; + enableKittyKeyboardProtocol: boolean; + enableWin32InputMode: boolean; fontLigatures?: { enabled: boolean; featureSettings: string; @@ -288,7 +289,7 @@ export interface ITerminalProcessManager extends IDisposable, ITerminalProcessIn readonly onProcessData: Event; readonly onProcessReplayComplete: Event; readonly onEnvironmentVariableInfoChanged: Event; - readonly onDidChangeProperty: Event>; + readonly onDidChangeProperty: Event; readonly onProcessExit: Event; readonly onRestoreCommands: Event; @@ -336,7 +337,7 @@ export interface ITerminalProcessExtHostProxy extends IDisposable { readonly instanceId: number; emitData(data: string): void; - emitProcessProperty(property: IProcessProperty): void; + emitProcessProperty(property: IProcessProperty): void; emitReady(pid: number, cwd: string, windowsPty: IProcessReadyWindowsPty | undefined): void; emitExit(exitCode: number | undefined): void; @@ -547,6 +548,7 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ TerminalCommandId.FocusHover, AccessibilityCommandId.OpenAccessibilityHelp, TerminalCommandId.StopVoice, + TerminalCommandId.SendSignal, 'workbench.action.tasks.rerunForActiveTerminal', 'editor.action.toggleTabFocusMode', 'notifications.hideList', diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index bd3ef396fc1..7431c14b493 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -7,6 +7,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import type { IStringDictionary } from '../../../../base/common/collections.js'; import { IJSONSchemaSnippet } from '../../../../base/common/jsonSchema.js'; import { isMacintosh, isWindows } from '../../../../base/common/platform.js'; +import { isString } from '../../../../base/common/types.js'; import { localize } from '../../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationRegistry, type IConfigurationPropertySchema } from '../../../../platform/configuration/common/configurationRegistry.js'; import product from '../../../../platform/product/common/product.js'; @@ -469,10 +470,14 @@ const terminalConfiguration: IStringDictionary = { default: true }, [TerminalSettingId.WindowsUseConptyDll]: { - markdownDescription: localize('terminal.integrated.windowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.22.250204002) shipped with VS Code, instead of the one bundled with Windows."), + restricted: true, + markdownDescription: localize('terminal.integrated.windowsUseConptyDll', "Whether to use the experimental conpty.dll (v1.23.251008001) shipped with VS Code, instead of the one bundled with Windows."), type: 'boolean', tags: ['preview'], - default: false + default: false, + experiment: { + mode: 'auto' + }, }, [TerminalSettingId.SplitCwd]: { description: localize('terminal.integrated.splitCwd', "Controls the working directory a split terminal starts with."), @@ -485,11 +490,6 @@ const terminalConfiguration: IStringDictionary = { ], default: 'inherited' }, - [TerminalSettingId.WindowsEnableConpty]: { - description: localize('terminal.integrated.windowsEnableConpty', "Whether to use ConPTY for Windows terminal process communication (requires Windows 10 build number 18309+). Winpty will be used if this is false."), - type: 'boolean', - default: true - }, [TerminalSettingId.WordSeparators]: { markdownDescription: localize('terminal.integrated.wordSeparators', "A string containing all characters to be considered word separators when double-clicking to select word and in the fallback 'word' link detection. Since this is used for link detection, including characters such as `:` that are used when detecting links will cause the line and column part of links like `file:10:5` to be ignored."), type: 'string', @@ -557,7 +557,7 @@ const terminalConfiguration: IStringDictionary = { localize('hideOnStartup.whenEmpty', "Only hide the terminal when there are no persistent sessions restored."), localize('hideOnStartup.always', "Always hide the terminal, even when there are persistent sessions restored.") ], - default: 'never' + default: 'never', }, [TerminalSettingId.HideOnLastClosed]: { description: localize('terminal.integrated.hideOnLastClosed', "Whether to hide the terminal view when the last terminal is closed. This will only happen when the terminal is the only visible view in the view container."), @@ -565,7 +565,15 @@ const terminalConfiguration: IStringDictionary = { default: true }, [TerminalSettingId.CustomGlyphs]: { - markdownDescription: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs for block element and box drawing characters instead of using the font, which typically yields better rendering with continuous lines. Note that this doesn't work when {0} is disabled.", `\`#${TerminalSettingId.GpuAcceleration}#\``), + markdownDescription: localize('terminal.integrated.customGlyphs', "Whether to draw custom glyphs instead of using the font for the following unicode ranges:\n\n{0}\n\nThis will typically result in better rendering with continuous lines, even when line height and letter spacing is used. This feature only works when {1} is enabled.", [ + '- Box Drawing (U+2500-U+257F)', + '- Block Elements (U+2580-U+259F)', + '- Braille Patterns (U+2800-U+28FF)', + '- Powerline Symbols (U+E0A0-U+E0D4, Private Use Area)', + '- Progress Indicators (U+EE00-U+EE0B, Private Use Area)', + '- Git Branch Symbols (U+F5D0-U+F60D, Private Use Area)', + '- Symbols for Legacy Computing (U+1FB00-U+1FBFF)' + ].join('\n'), `\`#${TerminalSettingId.GpuAcceleration}#\``), type: 'boolean', default: true }, @@ -574,6 +582,26 @@ const terminalConfiguration: IStringDictionary = { type: 'boolean', default: true }, + [TerminalSettingId.EnableKittyKeyboardProtocol]: { + restricted: true, + markdownDescription: localize('terminal.integrated.enableKittyKeyboardProtocol', "Whether to enable the kitty keyboard protocol, which provides more detailed keyboard input reporting to the terminal."), + type: 'boolean', + default: false, + tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } + }, + [TerminalSettingId.EnableWin32InputMode]: { + restricted: true, + markdownDescription: localize('terminal.integrated.enableWin32InputMode', "Whether to enable the win32 input mode, which provides enhanced keyboard input support on Windows."), + type: 'boolean', + default: false, + tags: ['experimental', 'advanced'], + experiment: { + mode: 'auto' + } + }, [TerminalSettingId.ShellIntegrationEnabled]: { restricted: true, markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", '`#terminal.integrated.shellIntegration.decorationsEnabled#`', '`#editor.accessibilitySupport#`'), @@ -639,6 +667,12 @@ const terminalConfiguration: IStringDictionary = { localize('terminal.integrated.focusAfterRun.none', "Do nothing."), ] }, + [TerminalSettingId.AllowInUntrustedWorkspace]: { + restricted: true, + markdownDescription: localize('terminal.integrated.allowInUntrustedWorkspace', "Controls whether terminals can be created in an untrusted workspace.\n\n**This feature bypasses a security protection that prevents terminals from launching in untrusted workspaces. The reason this is a security risk is because shells are often set up to potentially execute code automatically based on the contents of the current working directory. This should be safe to use provided your shell is set up in such a way that code execution in the folder never happens.**"), + type: 'boolean', + default: false + }, [TerminalSettingId.DeveloperPtyHostLatency]: { description: localize('terminal.integrated.developer.ptyHost.latency', "Simulated latency in milliseconds applied to all calls made to the pty host. This is useful for testing terminal behavior under high latency conditions."), type: 'number', @@ -680,7 +714,7 @@ Registry.as(WorkbenchExtensions.ConfigurationMi migrateFn: (enableBell, accessor) => { const configurationKeyValuePairs: ConfigurationKeyValuePairs = []; let announcement = accessor('accessibility.signals.terminalBell')?.announcement ?? accessor('accessibility.alert.terminalBell'); - if (announcement !== undefined && typeof announcement !== 'string') { + if (announcement !== undefined && !isString(announcement)) { announcement = announcement ? 'auto' : 'off'; } configurationKeyValuePairs.push(['accessibility.signals.terminalBell', { value: { sound: enableBell ? 'on' : 'off', announcement } }]); diff --git a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts index b78bfd70c77..82b0adcfc2d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalContextKey.ts @@ -7,6 +7,7 @@ import { localize } from '../../../../nls.js'; import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { TerminalSettingId } from '../../../../platform/terminal/common/terminal.js'; import { TERMINAL_VIEW_ID } from './terminal.js'; +import { TerminalContribContextKeyStrings } from '../terminalContribExports.js'; export const enum TerminalContextKeyStrings { IsOpen = 'terminalIsOpen', @@ -146,7 +147,7 @@ export namespace TerminalContextKeys { export const shouldShowViewInlineActions = ContextKeyExpr.and( ContextKeyExpr.equals('view', TERMINAL_VIEW_ID), ContextKeyExpr.notEquals(`config.${TerminalSettingId.TabsHideCondition}`, 'never'), - ContextKeyExpr.equals('hasChatTerminals', false), + ContextKeyExpr.not(TerminalContribContextKeyStrings.ChatHasHiddenTerminals), ContextKeyExpr.or( ContextKeyExpr.not(`config.${TerminalSettingId.TabsEnabled}`), ContextKeyExpr.and( diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index a0e95b1b934..a724394106a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -52,7 +52,7 @@ export function mergeEnvironments(parent: IProcessEnvironment, other: ITerminalE } function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: string | null): void { - if (typeof value === 'string') { + if (isString(value)) { env[key] = value; } else { delete env[key]; @@ -84,7 +84,7 @@ function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment async function resolveConfigurationVariables(variableResolver: VariableResolver, env: ITerminalEnvironment): Promise { await Promise.all(Object.entries(env).map(async ([key, value]) => { - if (typeof value === 'string') { + if (isString(value)) { try { env[key] = await variableResolver(value); } catch (e) { @@ -173,7 +173,7 @@ export function getLangEnvVariable(locale?: string): string { uk: 'UA', zh: 'CN', }; - if (parts[0] in languageVariants) { + if (Object.prototype.hasOwnProperty.call(languageVariants, parts[0])) { parts.push(languageVariants[parts[0]]); } } else { @@ -380,7 +380,7 @@ export async function preparePathForShell(resource: string | URI, executable: st } export function getWorkspaceForTerminal(cwd: URI | string | undefined, workspaceContextService: IWorkspaceContextService, historyService: IHistoryService): IWorkspaceFolder | undefined { - const cwdUri = typeof cwd === 'string' ? URI.parse(cwd) : cwd; + const cwdUri = isString(cwd) ? URI.parse(cwd) : cwd; let workspaceFolder = cwdUri ? workspaceContextService.getWorkspaceFolder(cwdUri) ?? undefined : undefined; if (!workspaceFolder) { // fallback to last active workspace if cwd is not available or it is not in workspace @@ -392,7 +392,7 @@ export function getWorkspaceForTerminal(cwd: URI | string | undefined, workspace } export async function getUriLabelForShell(uri: URI | string, backend: Pick, shellType?: TerminalShellType, os?: OperatingSystem, isWindowsFrontend: boolean = isWindows): Promise { - let path = typeof uri === 'string' ? uri : uri.fsPath; + let path = isString(uri) ? uri : uri.fsPath; if (os === OperatingSystem.Windows) { if (shellType === WindowsShellType.Wsl) { return backend.getWslPath(path.replaceAll('/', '\\'), 'win-to-unix'); @@ -401,7 +401,7 @@ export async function getUriLabelForShell(uri: URI | string, backend: Pick(terminalContributionsDescriptor); @@ -63,13 +64,16 @@ export class TerminalContributionService implements ITerminalContributionService } function hasValidTerminalIcon(profile: ITerminalProfileContribution): boolean { - return !profile.icon || - ( - typeof profile.icon === 'string' || - URI.isUri(profile.icon) || - ( - 'light' in profile.icon && 'dark' in profile.icon && - URI.isUri(profile.icon.light) && URI.isUri(profile.icon.dark) - ) + function isValidDarkLightIcon(obj: unknown): obj is { light: URI; dark: URI } { + return ( + isObject(obj) && + 'light' in obj && URI.isUri(obj.light) && + 'dark' in obj && URI.isUri(obj.dark) ); + } + return !profile.icon || ( + isString(profile.icon) || + URI.isUri(profile.icon) || + isValidDarkLightIcon(profile.icon) + ); } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts index 93886005251..6405af52054 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/localPty.ts @@ -91,10 +91,6 @@ export class LocalPty extends BasePty implements ITerminalChildProcess { return this._proxy.setUnicodeVersion(this.id, version); } - setNextCommandId(commandLine: string, commandId: string): Promise { - return this._proxy.setNextCommandId(this.id, commandLine, commandId); - } - handleOrphanQuestion() { this._proxy.orphanQuestionReply(this.id); } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts index e5e629a5612..4435552b283 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts @@ -192,6 +192,10 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke await this._proxy.updateIcon(id, userInitiated, icon, color); } + async setNextCommandId(id: number, commandLine: string, commandId: string): Promise { + await this._proxy.setNextCommandId(id, commandLine, commandId); + } + async updateProperty(id: number, property: ProcessPropertyType, value: IProcessPropertyMap[T]): Promise { return this._proxy.updateProperty(id, property, value); } @@ -348,7 +352,7 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke this._storageService.remove(TerminalStorageKeys.TerminalLayoutInfo, StorageScope.WORKSPACE); } } catch (e: unknown) { - this._logService.warn('LocalTerminalBackend#getTerminalLayoutInfo Error', e && typeof e === 'object' && 'message' in e ? e.message : e); + this._logService.warn('LocalTerminalBackend#getTerminalLayoutInfo Error', (<{ message?: string }>e).message ?? e); } } diff --git a/src/vs/workbench/contrib/terminal/terminal.all.ts b/src/vs/workbench/contrib/terminal/terminal.all.ts index 9cbb67bd019..6f08c629347 100644 --- a/src/vs/workbench/contrib/terminal/terminal.all.ts +++ b/src/vs/workbench/contrib/terminal/terminal.all.ts @@ -22,15 +22,16 @@ import '../terminalContrib/find/browser/terminal.find.contribution.js'; import '../terminalContrib/chat/browser/terminal.chat.contribution.js'; import '../terminalContrib/commandGuide/browser/terminal.commandGuide.contribution.js'; import '../terminalContrib/history/browser/terminal.history.contribution.js'; +import '../terminalContrib/inlineHint/browser/terminal.initialHint.contribution.js'; import '../terminalContrib/links/browser/terminal.links.contribution.js'; import '../terminalContrib/zoom/browser/terminal.zoom.contribution.js'; import '../terminalContrib/stickyScroll/browser/terminal.stickyScroll.contribution.js'; import '../terminalContrib/quickAccess/browser/terminal.quickAccess.contribution.js'; import '../terminalContrib/quickFix/browser/terminal.quickFix.contribution.js'; import '../terminalContrib/typeAhead/browser/terminal.typeAhead.contribution.js'; +import '../terminalContrib/resizeDimensionsOverlay/browser/terminal.resizeDimensionsOverlay.contribution.js'; import '../terminalContrib/sendSequence/browser/terminal.sendSequence.contribution.js'; import '../terminalContrib/sendSignal/browser/terminal.sendSignal.contribution.js'; import '../terminalContrib/suggest/browser/terminal.suggest.contribution.js'; -import '../terminalContrib/chat/browser/terminal.initialHint.contribution.js'; import '../terminalContrib/wslRecommendation/browser/terminal.wslRecommendation.contribution.js'; import '../terminalContrib/voice/browser/terminal.voice.contribution.js'; diff --git a/src/vs/workbench/contrib/terminal/terminalContribExports.ts b/src/vs/workbench/contrib/terminal/terminalContribExports.ts index 852825cdac1..5692332ebc9 100644 --- a/src/vs/workbench/contrib/terminal/terminalContribExports.ts +++ b/src/vs/workbench/contrib/terminal/terminalContribExports.ts @@ -7,7 +7,8 @@ import type { IConfigurationNode } from '../../../platform/configuration/common/ import { TerminalAccessibilityCommandId, defaultTerminalAccessibilityCommandsToSkipShell } from '../terminalContrib/accessibility/common/terminal.accessibility.js'; import { terminalAccessibilityConfiguration } from '../terminalContrib/accessibility/common/terminalAccessibilityConfiguration.js'; import { terminalAutoRepliesConfiguration } from '../terminalContrib/autoReplies/common/terminalAutoRepliesConfiguration.js'; -import { terminalInitialHintConfiguration } from '../terminalContrib/chat/common/terminalInitialHintConfiguration.js'; +import { TerminalChatCommandId, TerminalChatContextKeyStrings } from '../terminalContrib/chat/browser/terminalChat.js'; +import { terminalInitialHintConfiguration } from '../terminalContrib/inlineHint/common/terminalInitialHintConfiguration.js'; import { terminalChatAgentToolsConfiguration, TerminalChatAgentToolsSettingId } from '../terminalContrib/chatAgentTools/common/terminalChatAgentToolsConfiguration.js'; import { terminalCommandGuideConfiguration } from '../terminalContrib/commandGuide/common/terminalCommandGuideConfiguration.js'; import { TerminalDeveloperCommandId } from '../terminalContrib/developer/common/terminal.developer.js'; @@ -25,6 +26,13 @@ import { terminalZoomConfiguration } from '../terminalContrib/zoom/common/termin export const enum TerminalContribCommandId { A11yFocusAccessibleBuffer = TerminalAccessibilityCommandId.FocusAccessibleBuffer, DeveloperRestartPtyHost = TerminalDeveloperCommandId.RestartPtyHost, + OpenTerminalSettingsLink = TerminalChatCommandId.OpenTerminalSettingsLink, + DisableSessionAutoApproval = TerminalChatCommandId.DisableSessionAutoApproval, + FocusMostRecentChatTerminalOutput = TerminalChatCommandId.FocusMostRecentChatTerminalOutput, + FocusMostRecentChatTerminal = TerminalChatCommandId.FocusMostRecentChatTerminal, + ToggleChatTerminalOutput = TerminalChatCommandId.ToggleChatTerminalOutput, + FocusChatInstanceAction = TerminalChatCommandId.FocusChatInstanceAction, + ContinueInBackground = TerminalChatCommandId.ContinueInBackground, } // HACK: Export some settings from `terminalContrib/` that are depended upon elsewhere. These are @@ -36,6 +44,16 @@ export const enum TerminalContribSettingId { AutoApprove = TerminalChatAgentToolsSettingId.AutoApprove, EnableAutoApprove = TerminalChatAgentToolsSettingId.EnableAutoApprove, ShellIntegrationTimeout = TerminalChatAgentToolsSettingId.ShellIntegrationTimeout, + OutputLocation = TerminalChatAgentToolsSettingId.OutputLocation, +} + +// HACK: Export some context key strings from `terminalContrib/` that are depended upon elsewhere. +// These are soft layer breakers between `terminal/` and `terminalContrib/` but there are +// difficulties in removing the dependency. These are explicitly defined here to avoid an eslint +// line override. +export const enum TerminalContribContextKeyStrings { + ChatHasTerminals = TerminalChatContextKeyStrings.ChatHasTerminals, + ChatHasHiddenTerminals = TerminalChatContextKeyStrings.ChatHasHiddenTerminals, } // Export configuration schemes from terminalContrib - this is an exception to the eslint rule since diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts index d824e989eab..729ef0a01dc 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/commandDetectionCapability.test.ts @@ -10,6 +10,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/ import { ITerminalCommand } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { CommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/commandDetectionCapability.js'; import { writeP } from '../../../browser/terminalTestHelpers.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; type TestTerminalCommandMatch = Pick & { marker: { line: number } }; @@ -66,7 +67,7 @@ suite('CommandDetectionCapability', () => { setup(async () => { const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; - xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80 })); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, logger: TestXtermLogger })); const instantiationService = workbenchInstantiationService(undefined, store); capability = store.add(instantiationService.createInstance(TestCommandDetectionCapability, xterm)); addEvents = []; diff --git a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts index 00e99bbac9e..e506e153495 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/capabilities/partialCommandDetectionCapability.test.ts @@ -9,6 +9,7 @@ import { importAMDNodeModule } from '../../../../../../amdX.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { PartialCommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/partialCommandDetectionCapability.js'; import { writeP } from '../../../browser/terminalTestHelpers.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; import { Emitter } from '../../../../../../base/common/event.js'; suite('PartialCommandDetectionCapability', () => { @@ -27,7 +28,7 @@ suite('PartialCommandDetectionCapability', () => { setup(async () => { const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; - xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80 }) as Terminal); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, logger: TestXtermLogger }) as Terminal); onDidExecuteTextEmitter = store.add(new Emitter()); capability = store.add(new PartialCommandDetectionCapability(xterm, onDidExecuteTextEmitter.event)); addEvents = []; diff --git a/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts new file mode 100644 index 00000000000..851c3d5f702 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/chatTerminalCommandMirror.test.ts @@ -0,0 +1,558 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Terminal } from '@xterm/xterm'; +import { strictEqual } from 'assert'; +import { importAMDNodeModule } from '../../../../../amdX.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import type { IEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { TerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; +import { XtermTerminal } from '../../browser/xterm/xtermTerminal.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestXtermAddonImporter } from './xterm/xtermTestUtils.js'; +import { computeMaxBufferColumnWidth, vtBoundaryMatches } from '../../browser/chatTerminalCommandMirror.js'; + +const defaultTerminalConfig = { + fontFamily: 'monospace', + fontWeight: 'normal', + fontWeightBold: 'normal', + gpuAcceleration: 'off', + scrollback: 10, + fastScrollSensitivity: 2, + mouseWheelScrollSensitivity: 1, + unicodeVersion: '6' +}; + +suite('Workbench - ChatTerminalCommandMirror', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + suite('VT mirroring with XtermTerminal', () => { + let instantiationService: TestInstantiationService; + let configurationService: TestConfigurationService; + let XTermBaseCtor: typeof Terminal; + + async function createXterm(cols = 80, rows = 10, scrollback = 10): Promise { + const capabilities = store.add(new TerminalCapabilityStore()); + return store.add(instantiationService.createInstance(XtermTerminal, undefined, XTermBaseCtor, { + cols, + rows, + xtermColorProvider: { getBackgroundColor: () => undefined }, + capabilities, + disableShellIntegrationReporting: true, + xtermAddonImporter: new TestXtermAddonImporter(), + }, undefined)); + } + + function write(xterm: XtermTerminal, data: string): Promise { + return new Promise(resolve => xterm.write(data, resolve)); + } + + function getBufferText(xterm: XtermTerminal): string { + const buffer = xterm.raw.buffer.active; + const lines: string[] = []; + for (let i = 0; i < buffer.length; i++) { + const line = buffer.getLine(i); + lines.push(line?.translateToString(true) ?? ''); + } + // Trim trailing empty lines + while (lines.length > 0 && lines[lines.length - 1] === '') { + lines.pop(); + } + return lines.join('\n'); + } + + async function mirrorViaVT(source: XtermTerminal, startLine = 0): Promise { + const startMarker = source.raw.registerMarker(startLine - source.raw.buffer.active.baseY - source.raw.buffer.active.cursorY); + const vt = await source.getRangeAsVT(startMarker ?? undefined, undefined, true); + startMarker?.dispose(); + + const mirror = await createXterm(source.raw.cols, source.raw.rows); + if (vt) { + await write(mirror, vt); + } + return mirror; + } + + setup(async () => { + configurationService = new TestConfigurationService({ + editor: { + fastScrollSensitivity: 2, + mouseWheelScrollSensitivity: 1 + } as Partial, + files: {}, + terminal: { + integrated: defaultTerminalConfig + }, + }); + + instantiationService = workbenchInstantiationService({ + configurationService: () => configurationService + }, store); + + XTermBaseCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; + }); + + test('single character', async () => { + const source = await createXterm(); + await write(source, 'X'); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('single line', async () => { + const source = await createXterm(); + await write(source, 'hello world'); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('multiple lines', async () => { + const source = await createXterm(); + await write(source, 'line 1\r\nline 2\r\nline 3'); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('wrapped line', async () => { + const source = await createXterm(20, 10); // narrow terminal + const longLine = 'a'.repeat(50); // exceeds 20 cols + await write(source, longLine); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('content with special characters', async () => { + const source = await createXterm(); + await write(source, 'hello\ttab\r\nspaces here\r\n$pecial!@#%^&*'); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('content with ANSI colors', async () => { + const source = await createXterm(); + await write(source, '\x1b[31mred\x1b[0m \x1b[32mgreen\x1b[0m \x1b[34mblue\x1b[0m'); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('content filling visible area', async () => { + const source = await createXterm(80, 5); + for (let i = 1; i <= 5; i++) { + await write(source, `line ${i}\r\n`); + } + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('content with scrollback (partial buffer)', async () => { + const source = await createXterm(80, 5, 5); // 5 rows visible, 5 scrollback = 10 total + // Write enough to push into scrollback + for (let i = 1; i <= 12; i++) { + await write(source, `line ${i}\r\n`); + } + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('empty content', async () => { + const source = await createXterm(); + + const mirror = await mirrorViaVT(source); + + strictEqual(getBufferText(mirror), getBufferText(source)); + }); + + test('content from marker to cursor', async () => { + const source = await createXterm(); + await write(source, 'before\r\n'); + const startMarker = source.raw.registerMarker(0)!; + await write(source, 'output line 1\r\noutput line 2'); + + const vt = await source.getRangeAsVT(startMarker, undefined, true); + const mirror = await createXterm(); + if (vt) { + await write(mirror, vt); + } + startMarker.dispose(); + + // Mirror should contain just the content from marker onwards + const mirrorText = getBufferText(mirror); + strictEqual(mirrorText.includes('output line 1'), true); + strictEqual(mirrorText.includes('output line 2'), true); + strictEqual(mirrorText.includes('before'), false); + }); + + test('incremental mirroring appends correctly', async () => { + const source = await createXterm(); + const marker = source.raw.registerMarker(0)!; + await write(source, 'initial\r\n'); + + // First mirror with initial content + const vt1 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + const mirror = await createXterm(); + await write(mirror, vt1); + + // Add more content to source + await write(source, 'added\r\n'); + const vt2 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + // Append only the new part to mirror + const appended = vt2.slice(vt1.length); + if (appended) { + await write(mirror, appended); + } + + // Create a fresh mirror with full VT to compare against + const freshMirror = await createXterm(); + await write(freshMirror, vt2); + + marker.dispose(); + + // Incremental mirror should match fresh mirror + strictEqual(getBufferText(mirror), getBufferText(freshMirror)); + }); + + test('VT divergence detection prevents corruption (Windows scenario)', async () => { + // This test simulates the Windows issue where VT sequences can differ + // between calls even for equivalent visual content. On Windows, the + // serializer can produce different escape sequences (e.g., different + // line endings or cursor positioning) causing the prefix to diverge. + // + // Without boundary checking, blindly slicing would corrupt output: + // - vt1: "Line1\r\nLine2" (length 13) + // - vt2: "Line1\nLine2\nLine3" (different format, but starts similarly) + // - slice(13) on vt2 would give "ine3" instead of the full new content + + const mirror = await createXterm(); + + // Simulate first VT snapshot + const vt1 = 'Line1\r\nLine2'; + await write(mirror, vt1); + strictEqual(getBufferText(mirror), 'Line1\nLine2'); + + // Simulate divergent VT snapshot (different escape sequences for same content) + // This mimics what can happen on Windows where the VT serializer + // produces different output between calls + const vt2 = 'DifferentPrefix' + 'Line3'; + + // Use the actual utility function to test boundary checking + const boundaryMatches = vtBoundaryMatches(vt2, vt1, vt1.length); + + // Boundary should NOT match because the prefix diverged + strictEqual(boundaryMatches, false, 'Boundary check should detect divergence'); + + // When boundary doesn't match, the fix does a full reset + rewrite + // instead of corrupting the output by blind slicing + mirror.raw.reset(); + await write(mirror, vt2); + + // Final content should be the complete new VT, not corrupted + strictEqual(getBufferText(mirror), 'DifferentPrefixLine3'); + }); + + test('boundary check allows append when VT prefix matches', async () => { + const mirror = await createXterm(); + + // First VT snapshot + const vt1 = 'Line1\r\nLine2\r\n'; + await write(mirror, vt1); + + // Second VT snapshot that properly extends the first + const vt2 = vt1 + 'Line3\r\n'; + + // Use the actual utility function to test boundary checking + const boundaryMatches = vtBoundaryMatches(vt2, vt1, vt1.length); + + strictEqual(boundaryMatches, true, 'Boundary check should pass when prefix matches'); + + // Append should work correctly + const appended = vt2.slice(vt1.length); + await write(mirror, appended); + + strictEqual(getBufferText(mirror), 'Line1\nLine2\nLine3'); + }); + + test('incremental updates use append path (not full rewrite) in normal operation', async () => { + // This test verifies that in normal operation (VT prefix matches), + // we use the efficient append path rather than full rewrite. + + const source = await createXterm(); + const marker = source.raw.registerMarker(0)!; + + // Build up content incrementally, simulating streaming output + const writes: string[] = []; + + // Step 1: Initial content + await write(source, 'output line 1\r\n'); + const vt1 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + const mirror = await createXterm(); + await write(mirror, vt1); + writes.push(vt1); + + // Step 2: Add more content - should use append path + await write(source, 'output line 2\r\n'); + const vt2 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + // Verify VT extends properly (prefix matches) + strictEqual(vt2.startsWith(vt1), true, 'VT2 should start with VT1'); + + // Append only the new part (this is what the append path does) + const appended2 = vt2.slice(vt1.length); + strictEqual(appended2.length > 0, true, 'Should have new content to append'); + strictEqual(appended2.length < vt2.length, true, 'Append should be smaller than full rewrite'); + await write(mirror, appended2); + writes.push(appended2); + + // Step 3: Add more content - should continue using append path + await write(source, 'output line 3\r\n'); + const vt3 = await source.getRangeAsVT(marker, undefined, true) ?? ''; + + strictEqual(vt3.startsWith(vt2), true, 'VT3 should start with VT2'); + + const appended3 = vt3.slice(vt2.length); + strictEqual(appended3.length > 0, true, 'Should have new content to append'); + strictEqual(appended3.length < vt3.length, true, 'Append should be smaller than full rewrite'); + await write(mirror, appended3); + writes.push(appended3); + + marker.dispose(); + + // Verify final content is correct + strictEqual(getBufferText(mirror), 'output line 1\noutput line 2\noutput line 3'); + + // Verify we used the append path (total bytes written should be roughly + // equal to total VT, not 3x the total due to full rewrites) + const totalWritten = writes.reduce((sum, w) => sum + w.length, 0); + const fullRewriteWouldBe = vt1.length + vt2.length + vt3.length; + strictEqual(totalWritten < fullRewriteWouldBe, true, + `Append path should write less (${totalWritten}) than full rewrites would (${fullRewriteWouldBe})`); + }); + }); + + suite('computeMaxBufferColumnWidth', () => { + + /** + * Creates a mock buffer with the given lines. + * Each string represents a line; characters are cells, spaces are empty cells. + */ + function createMockBuffer(lines: string[], cols: number = 80): { readonly length: number; getLine(y: number): { readonly length: number; getCell(x: number): { getChars(): string } | undefined } | undefined } { + return { + length: lines.length, + getLine(y: number) { + if (y < 0 || y >= lines.length) { + return undefined; + } + const lineContent = lines[y]; + return { + length: Math.max(lineContent.length, cols), + getCell(x: number) { + if (x < 0 || x >= lineContent.length) { + return { getChars: () => '' }; + } + const char = lineContent[x]; + return { getChars: () => char === ' ' ? '' : char }; + } + }; + } + }; + } + + test('returns 0 for empty buffer', () => { + const buffer = createMockBuffer([]); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 0); + }); + + test('returns 0 for buffer with only empty lines', () => { + const buffer = createMockBuffer(['', '', '']); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 0); + }); + + test('returns correct width for single character', () => { + const buffer = createMockBuffer(['X']); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 1); + }); + + test('returns correct width for single line', () => { + const buffer = createMockBuffer(['hello']); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 5); + }); + + test('returns max width across multiple lines', () => { + const buffer = createMockBuffer([ + 'short', + 'much longer line', + 'mid' + ]); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 16); + }); + + test('ignores trailing spaces (empty cells)', () => { + // Spaces are treated as empty cells in our mock + const buffer = createMockBuffer(['hello ']); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 5); + }); + + test('respects cols parameter to clamp line length', () => { + const buffer = createMockBuffer(['abcdefghijklmnop']); // 16 chars, no spaces + strictEqual(computeMaxBufferColumnWidth(buffer, 10), 10); + }); + + test('handles lines with content at different positions', () => { + const buffer = createMockBuffer([ + 'a', // width 1 + ' b', // content at col 2, but width is 3 + ' c', // content at col 4, but width is 5 + ' d' // content at col 6, width is 7 + ]); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 7); + }); + + test('handles buffer with undefined lines gracefully', () => { + const buffer = { + length: 3, + getLine(y: number) { + if (y === 1) { + return undefined; + } + return { + length: 5, + getCell(x: number) { + return x < 3 ? { getChars: () => 'X' } : { getChars: () => '' }; + } + }; + } + }; + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 3); + }); + + test('handles line with all empty cells', () => { + const buffer = createMockBuffer([' ']); // all spaces = empty cells + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 0); + }); + + test('handles mixed empty and non-empty lines', () => { + const buffer = createMockBuffer([ + '', + 'content', + '', + 'more', + '' + ]); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 7); + }); + + test('returns correct width for line exactly at 80 cols', () => { + const line80 = 'a'.repeat(80); + const buffer = createMockBuffer([line80]); + strictEqual(computeMaxBufferColumnWidth(buffer, 80), 80); + }); + + test('returns correct width for line exceeding 80 cols with higher cols value', () => { + const line100 = 'a'.repeat(100); + const buffer = createMockBuffer([line100], 120); + strictEqual(computeMaxBufferColumnWidth(buffer, 120), 100); + }); + + test('handles wide terminal with long content', () => { + const buffer = createMockBuffer([ + 'short', + 'a'.repeat(150), + 'medium content here' + ], 200); + strictEqual(computeMaxBufferColumnWidth(buffer, 200), 150); + }); + + test('max of multiple lines where longest exceeds default cols', () => { + const buffer = createMockBuffer([ + 'a'.repeat(50), + 'b'.repeat(120), + 'c'.repeat(90) + ], 150); + strictEqual(computeMaxBufferColumnWidth(buffer, 150), 120); + }); + }); + + suite('vtBoundaryMatches', () => { + + test('returns true when strings match at boundary', () => { + const oldVT = 'Line1\r\nLine2\r\n'; + const newVT = oldVT + 'Line3\r\n'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + + test('returns false when strings diverge at boundary', () => { + const oldVT = 'Line1\r\nLine2'; + const newVT = 'DifferentPrefixLine3'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('returns false when single character differs in window', () => { + const oldVT = 'AAAAAAAAAA'; + const newVT = 'AAAAABAAAA' + 'NewContent'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('returns true for empty strings', () => { + strictEqual(vtBoundaryMatches('', '', 0), true); + }); + + test('returns true when slicePoint is 0', () => { + const oldVT = ''; + const newVT = 'SomeContent'; + strictEqual(vtBoundaryMatches(newVT, oldVT, 0), true); + }); + + test('handles strings shorter than window size', () => { + const oldVT = 'Short'; + const newVT = 'Short' + 'Added'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + + test('respects custom window size parameter', () => { + // With default window (50), this would match since the diff is at position 70 + const prefix = 'A'.repeat(80); + const oldVT = prefix; + const newVT = 'X' + 'A'.repeat(79) + 'NewContent'; // differs at position 0 + + // With window of 50, only checks chars 30-80, which would match + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length, 50), true); + + // With window of 100, would check chars 0-80, which would NOT match + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length, 100), false); + }); + + test('detects divergence in escape sequences (Windows scenario)', () => { + // Simulates Windows issue where VT escape sequences differ + const oldVT = '\x1b[0m\x1b[1mBold\x1b[0m\r\n'; + const newVT = '\x1b[0m\x1b[22mBold\x1b[0m\r\nMore'; // Different escape code for bold + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), false); + }); + + test('handles matching escape sequences', () => { + const oldVT = '\x1b[31mRed\x1b[0m\r\n'; + const newVT = '\x1b[31mRed\x1b[0m\r\nGreen'; + strictEqual(vtBoundaryMatches(newVT, oldVT, oldVT.length), true); + }); + }); +}); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 9aee99544fc..81bc2516e21 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -18,7 +18,7 @@ import { TerminalCapabilityStore } from '../../../../../platform/terminal/common import { GeneralShellType, ITerminalChildProcess, ITerminalProfile, TitleEventSource, type IShellLaunchConfig, type ITerminalBackend, type ITerminalProcessOptions } from '../../../../../platform/terminal/common/terminal.js'; import { IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; import { IViewDescriptorService } from '../../../../common/views.js'; -import { ITerminalConfigurationService, ITerminalInstance, ITerminalInstanceService } from '../../browser/terminal.js'; +import { ITerminalConfigurationService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from '../../browser/terminal.js'; import { TerminalConfigurationService } from '../../browser/terminalConfigurationService.js'; import { parseExitResult, TerminalInstance, TerminalLabelComputer } from '../../browser/terminalInstance.js'; import { IEnvironmentVariableService } from '../../common/environmentVariable.js'; @@ -88,7 +88,6 @@ class TestTerminalChildProcess extends Disposable implements ITerminalChildProce clearBuffer(): void { } acknowledgeDataEvent(charCount: number): void { } async setUnicodeVersion(version: '6' | '11'): Promise { } - async setNextCommandId(commandLine: string, commandId: string): Promise { } async getInitialCwd(): Promise { return ''; } async getCwd(): Promise { return ''; } async processBinary(data: string): Promise { } @@ -146,6 +145,7 @@ suite('Workbench - TerminalInstance', () => { instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService()); instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService))); instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService())); + instantiationService.stub(ITerminalService, { setNextCommandId: async () => { } } as Partial); terminalInstance = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {})); // //Wait for the teminalInstance._xtermReadyPromise to resolve await new Promise(resolve => setTimeout(resolve, 100)); @@ -174,6 +174,7 @@ suite('Workbench - TerminalInstance', () => { instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService()); instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService))); instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService())); + instantiationService.stub(ITerminalService, { setNextCommandId: async () => { } } as Partial); const taskTerminal = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, { type: 'Task', diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index d98fc0b3300..70ed6a4426e 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -11,7 +11,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes import { IConfigurationService, type IConfigurationChangeEvent } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { ITerminalChildProcess, type ITerminalBackend } from '../../../../../platform/terminal/common/terminal.js'; -import { ITerminalInstanceService } from '../../browser/terminal.js'; +import { ITerminalInstanceService, ITerminalService } from '../../browser/terminal.js'; import { TerminalProcessManager } from '../../browser/terminalProcessManager.js'; import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; @@ -44,7 +44,6 @@ class TestTerminalChildProcess implements ITerminalChildProcess { clearBuffer(): void { } acknowledgeDataEvent(charCount: number): void { } async setUnicodeVersion(version: '6' | '11'): Promise { } - async setNextCommandId(commandLine: string, commandId: string): Promise { } async getInitialCwd(): Promise { return ''; } async getCwd(): Promise { return ''; } async processBinary(data: string): Promise { } @@ -67,7 +66,7 @@ class TestTerminalInstanceService implements Partial { rows: number, unicodeVersion: '6' | '11', env: any, - windowsEnableConpty: boolean, + options: any, shouldPersist: boolean ) => new TestTerminalChildProcess(shouldPersist), getLatency: () => Promise.resolve([]) @@ -97,6 +96,7 @@ suite('Workbench - TerminalProcessManager', () => { affectsConfiguration: () => true, } satisfies Partial as unknown as IConfigurationChangeEvent); instantiationService.stub(ITerminalInstanceService, new TestTerminalInstanceService()); + instantiationService.stub(ITerminalService, { setNextCommandId: async () => { } } as Partial); manager = store.add(instantiationService.createInstance(TerminalProcessManager, 1, undefined, undefined, undefined)); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts index 8f74a602218..62d3ec2455e 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/decorationAddon.test.ts @@ -13,6 +13,7 @@ import { CommandDetectionCapability } from '../../../../../../platform/terminal/ import { TerminalCapabilityStore } from '../../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; import { DecorationAddon } from '../../../browser/xterm/decorationAddon.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; suite('DecorationAddon', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); @@ -50,7 +51,8 @@ suite('DecorationAddon', () => { xterm = store.add(new TestTerminal({ allowProposedApi: true, cols: 80, - rows: 30 + rows: 30, + logger: TestXtermLogger })); const capabilities = store.add(new TerminalCapabilityStore()); capabilities.add(TerminalCapability.CommandDetection, store.add(instantiationService.createInstance(CommandDetectionCapability, xterm))); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts index b8821513844..32de29b4beb 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/lineDataEventAddon.test.ts @@ -9,6 +9,7 @@ import { importAMDNodeModule } from '../../../../../../amdX.js'; import { OperatingSystem } from '../../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { writeP } from '../../../browser/terminalTestHelpers.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; import { LineDataEventAddon } from '../../../browser/xterm/lineDataEventAddon.js'; suite('LineDataEventAddon', () => { @@ -22,7 +23,7 @@ suite('LineDataEventAddon', () => { setup(async () => { const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; - xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 4 })); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 4, logger: TestXtermLogger })); lineDataEventAddon = store.add(new LineDataEventAddon()); xterm.loadAddon(lineDataEventAddon); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts index 984e205d9a5..fb03eb65f07 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/recordings/rich/macos_zsh_omz_echo_3_times.ts @@ -137,10 +137,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo a" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" @@ -245,10 +241,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo b" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" @@ -357,10 +349,6 @@ export const events = [ "type": "output", "data": "" }, - { - "type": "promptInputChange", - "data": "echo c" - }, { "type": "output", "data": "\u001b]633;P;Cwd=/Users/tyriar/playground/test1\u0007\u001b]633;EnvSingleStart;0;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\u001b]633;EnvSingleEnd;448d50d0-70fe-4ab5-842e-132f3b1c159a;\u0007\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\u001b]633;A\u0007tyriar@Mac test1 % \u001b]633;B\u0007\u001b[K\u001b[?2004h" diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts index 667de1c9a29..211eb8bbe9b 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.integrationTest.ts @@ -16,6 +16,7 @@ import type { TerminalCapabilityStore } from '../../../../../../platform/termina import { ShellIntegrationAddon } from '../../../../../../platform/terminal/common/xterm/shellIntegrationAddon.js'; import { workbenchInstantiationService, type TestTerminalConfigurationService } from '../../../../../test/browser/workbenchTestServices.js'; import { ITerminalConfigurationService } from '../../../../terminal/browser/terminal.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; import { NullTelemetryService } from '../../../../../../platform/telemetry/common/telemetryUtils.js'; import { events as rich_windows11_pwsh7_echo_3_times } from './recordings/rich/windows11_pwsh7_echo_3_times.js'; @@ -159,7 +160,7 @@ suite('Terminal Contrib Shell Integration Recordings', () => { terminalConfigurationService.setConfig(terminalConfig as unknown as Partial); const shellIntegrationAddon = store.add(new ShellIntegrationAddon('', true, undefined, NullTelemetryService, new NullLogService)); const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; - xterm = store.add(new TerminalCtor({ allowProposedApi: true })); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, logger: TestXtermLogger })); capabilities = shellIntegrationAddon.capabilities; const testContainer = document.createElement('div'); getActiveDocument().body.append(testContainer); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts index a18a4dff7b5..209e79c7e7d 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/shellIntegrationAddon.test.ts @@ -12,6 +12,7 @@ import { NullLogService } from '../../../../../../platform/log/common/log.js'; import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { deserializeVSCodeOscMessage, serializeVSCodeOscMessage, parseKeyValueAssignment, parseMarkSequence, ShellIntegrationAddon } from '../../../../../../platform/terminal/common/xterm/shellIntegrationAddon.js'; import { writeP } from '../../../browser/terminalTestHelpers.js'; +import { TestXtermLogger } from '../../../../../../platform/terminal/test/common/terminalTestHelpers.js'; class TestShellIntegrationAddon extends ShellIntegrationAddon { getCommandDetectionMock(terminal: Terminal): sinon.SinonMock { @@ -35,7 +36,7 @@ suite('ShellIntegrationAddon', () => { setup(async () => { const TerminalCtor = (await importAMDNodeModule('@xterm/xterm', 'lib/xterm.js')).Terminal; - xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30 })); + xterm = store.add(new TerminalCtor({ allowProposedApi: true, cols: 80, rows: 30, logger: TestXtermLogger })); shellIntegrationAddon = store.add(new TestShellIntegrationAddon('', true, undefined, undefined, new NullLogService())); xterm.loadAddon(shellIntegrationAddon); capabilities = shellIntegrationAddon.capabilities; diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index 83287031fbf..8db8fe5cffc 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { WebglAddon } from '@xterm/addon-webgl'; -import type { IEvent, Terminal } from '@xterm/xterm'; +import type { Terminal } from '@xterm/xterm'; import { deepStrictEqual, strictEqual } from 'assert'; import { importAMDNodeModule } from '../../../../../../amdX.js'; import { Color, RGBA } from '../../../../../../base/common/color.js'; @@ -22,40 +21,10 @@ import { XtermTerminal } from '../../../browser/xterm/xtermTerminal.js'; import { ITerminalConfiguration, TERMINAL_VIEW_ID } from '../../../common/terminal.js'; import { registerColors, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_INACTIVE_SELECTION_BACKGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR, TERMINAL_SELECTION_FOREGROUND_COLOR } from '../../../common/terminalColorRegistry.js'; import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; -import { IXtermAddonNameToCtor, XtermAddonImporter } from '../../../browser/xterm/xtermAddonImporter.js'; +import { TestWebglAddon, TestXtermAddonImporter } from './xtermTestUtils.js'; registerColors(); -class TestWebglAddon implements WebglAddon { - static shouldThrow = false; - static isEnabled = false; - readonly onChangeTextureAtlas = new Emitter().event as IEvent; - readonly onAddTextureAtlasCanvas = new Emitter().event as IEvent; - readonly onRemoveTextureAtlasCanvas = new Emitter().event as IEvent; - readonly onContextLoss = new Emitter().event as IEvent; - constructor(preserveDrawingBuffer?: boolean) { - } - activate() { - TestWebglAddon.isEnabled = !TestWebglAddon.shouldThrow; - if (TestWebglAddon.shouldThrow) { - throw new Error('Test webgl set to throw'); - } - } - dispose() { - TestWebglAddon.isEnabled = false; - } - clearTextureAtlas() { } -} - -class TestXtermAddonImporter extends XtermAddonImporter { - override async importAddon(name: T): Promise { - if (name === 'webgl') { - return TestWebglAddon as unknown as IXtermAddonNameToCtor[T]; - } - return super.importAddon(name); - } -} - export class TestViewDescriptorService implements Partial { private _location = ViewContainerLocation.Panel; private _onDidChangeLocation = new Emitter<{ views: IViewDescriptor[]; from: ViewContainerLocation; to: ViewContainerLocation }>(); diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTestUtils.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTestUtils.ts new file mode 100644 index 00000000000..bff945f742a --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTestUtils.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { WebglAddon } from '@xterm/addon-webgl'; +import type { IEvent } from '@xterm/xterm'; +import { Emitter } from '../../../../../../base/common/event.js'; +import { XtermAddonImporter, type IXtermAddonNameToCtor } from '../../../browser/xterm/xtermAddonImporter.js'; + +export class TestWebglAddon implements WebglAddon { + static shouldThrow = false; + static isEnabled = false; + private readonly _onChangeTextureAtlas = new Emitter(); + private readonly _onAddTextureAtlasCanvas = new Emitter(); + private readonly _onRemoveTextureAtlasCanvas = new Emitter(); + private readonly _onContextLoss = new Emitter(); + readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event as IEvent; + readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event as IEvent; + readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event as IEvent; + readonly onContextLoss = this._onContextLoss.event as IEvent; + constructor(preserveDrawingBuffer?: boolean) { + } + activate(): void { + TestWebglAddon.isEnabled = !TestWebglAddon.shouldThrow; + if (TestWebglAddon.shouldThrow) { + throw new Error('Test webgl set to throw'); + } + } + dispose(): void { + TestWebglAddon.isEnabled = false; + this._onChangeTextureAtlas.dispose(); + this._onAddTextureAtlasCanvas.dispose(); + this._onRemoveTextureAtlasCanvas.dispose(); + this._onContextLoss.dispose(); + } + clearTextureAtlas(): void { } +} + +export class TestXtermAddonImporter extends XtermAddonImporter { + override async importAddon(name: T): Promise { + if (name === 'webgl') { + return TestWebglAddon as unknown as IXtermAddonNameToCtor[T]; + } + return super.importAddon(name); + } +} + diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts index 865ea509b49..1f9bb62b4de 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalColorRegistry.test.ts @@ -24,6 +24,7 @@ function getMockTheme(type: ColorScheme): IColorTheme { defines: () => true, getTokenStyleMetadata: () => undefined, tokenColorMap: [], + tokenFontMap: [], semanticHighlighting: false }; return theme; diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts index 1ae0a82b5ca..20c04fdbde5 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalDataBuffering.test.ts @@ -14,21 +14,21 @@ suite('Workbench - TerminalDataBufferer', () => { const store = ensureNoDisposablesAreLeakedInTestSuite(); let bufferer: TerminalDataBufferer; - let counter: { [id: number]: number }; - let data: { [id: number]: string }; + let counter: Map; + let data: Map; setup(async () => { - counter = {}; - data = {}; + counter = new Map(); + data = new Map(); bufferer = store.add(new TerminalDataBufferer((id, e) => { - if (!(id in counter)) { - counter[id] = 0; + if (!counter.has(id)) { + counter.set(id, 0); } - counter[id]++; - if (!(id in data)) { - data[id] = ''; + counter.set(id, counter.get(id)! + 1); + if (!data.has(id)) { + data.set(id, ''); } - data[id] = e; + data.set(id, e); })); }); @@ -45,13 +45,13 @@ suite('Workbench - TerminalDataBufferer', () => { terminalOnData.fire('4'); - assert.strictEqual(counter[1], 1); - assert.strictEqual(data[1], '123'); + assert.strictEqual(counter.get(1), 1); + assert.strictEqual(data.get(1), '123'); await wait(0); - assert.strictEqual(counter[1], 2); - assert.strictEqual(data[1], '4'); + assert.strictEqual(counter.get(1), 2); + assert.strictEqual(data.get(1), '4'); }); test('start 2', async () => { @@ -69,17 +69,17 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.strictEqual(counter[1], undefined); - assert.strictEqual(data[1], undefined); - assert.strictEqual(counter[2], undefined); - assert.strictEqual(data[2], undefined); + assert.strictEqual(counter.get(1), undefined); + assert.strictEqual(data.get(1), undefined); + assert.strictEqual(counter.get(2), undefined); + assert.strictEqual(data.get(2), undefined); await wait(0); - assert.strictEqual(counter[1], 1); - assert.strictEqual(data[1], '123'); - assert.strictEqual(counter[2], 1); - assert.strictEqual(data[2], '4567'); + assert.strictEqual(counter.get(1), 1); + assert.strictEqual(data.get(1), '123'); + assert.strictEqual(counter.get(2), 1); + assert.strictEqual(data.get(2), '4567'); }); test('stop', async () => { @@ -94,8 +94,8 @@ suite('Workbench - TerminalDataBufferer', () => { bufferer.stopBuffering(1); await wait(0); - assert.strictEqual(counter[1], 1); - assert.strictEqual(data[1], '123'); + assert.strictEqual(counter.get(1), 1); + assert.strictEqual(data.get(1), '123'); }); test('start 2 stop 1', async () => { @@ -113,18 +113,18 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.strictEqual(counter[1], undefined); - assert.strictEqual(data[1], undefined); - assert.strictEqual(counter[2], undefined); - assert.strictEqual(data[2], undefined); + assert.strictEqual(counter.get(1), undefined); + assert.strictEqual(data.get(1), undefined); + assert.strictEqual(counter.get(2), undefined); + assert.strictEqual(data.get(2), undefined); bufferer.stopBuffering(1); await wait(0); - assert.strictEqual(counter[1], 1); - assert.strictEqual(data[1], '123'); - assert.strictEqual(counter[2], 1); - assert.strictEqual(data[2], '4567'); + assert.strictEqual(counter.get(1), 1); + assert.strictEqual(data.get(1), '123'); + assert.strictEqual(counter.get(2), 1); + assert.strictEqual(data.get(2), '4567'); }); test('dispose should flush remaining data events', async () => { @@ -142,17 +142,17 @@ suite('Workbench - TerminalDataBufferer', () => { terminal2OnData.fire('6'); terminal2OnData.fire('7'); - assert.strictEqual(counter[1], undefined); - assert.strictEqual(data[1], undefined); - assert.strictEqual(counter[2], undefined); - assert.strictEqual(data[2], undefined); + assert.strictEqual(counter.get(1), undefined); + assert.strictEqual(data.get(1), undefined); + assert.strictEqual(counter.get(2), undefined); + assert.strictEqual(data.get(2), undefined); bufferer.dispose(); await wait(0); - assert.strictEqual(counter[1], 1); - assert.strictEqual(data[1], '123'); - assert.strictEqual(counter[2], 1); - assert.strictEqual(data[2], '4567'); + assert.strictEqual(counter.get(1), 1); + assert.strictEqual(data.get(1), '123'); + assert.strictEqual(counter.get(2), 1); + assert.strictEqual(data.get(2), '4567'); }); }); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index eea2f252247..0888d1098d1 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -19,7 +19,7 @@ import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/cont import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ITerminalCommand, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ICurrentPartialCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; +import { ICurrentPartialCommand, isFullTerminalCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; import { TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js'; import { accessibleViewCurrentProviderId, accessibleViewIsShown } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibilityHelpAction, AccessibleViewAction } from '../../../accessibility/browser/accessibleViewActions.js'; @@ -226,9 +226,9 @@ export class TerminalAccessibleViewContribution extends Disposable implements IT return; } let line: number | undefined; - if ('marker' in command) { + if (isFullTerminalCommand(command)) { line = command.marker?.line; - } else if ('commandStartMarker' in command) { + } else { line = command.commandStartMarker?.line; } if (line === undefined || line < 0) { diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts index 6dcf9cb114b..2796901804d 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBufferProvider.ts @@ -8,7 +8,7 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IAccessibleViewContentProvider, AccessibleViewProviderId, IAccessibleViewOptions, AccessibleViewType, IAccessibleViewSymbol } from '../../../../../platform/accessibility/browser/accessibleView.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalCapability, ITerminalCommand } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; -import { ICurrentPartialCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; +import { ICurrentPartialCommand, isFullTerminalCommand } from '../../../../../platform/terminal/common/capabilities/commandDetection/terminalCommand.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { BufferContentTracker } from './bufferContentTracker.js'; @@ -98,9 +98,9 @@ export class TerminalAccessibleBufferProvider extends Disposable implements IAcc } private _getEditorLineForCommand(command: ITerminalCommand | ICurrentPartialCommand): number | undefined { let line: number | undefined; - if ('marker' in command) { + if (isFullTerminalCommand(command)) { line = command.marker?.line; - } else if ('commandStartMarker' in command) { + } else { line = command.commandStartMarker?.line; } if (line === undefined || line < 0) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css index bf5b51e3d49..508a44d5134 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalChatWidget.css @@ -31,6 +31,7 @@ .monaco-workbench .terminal-inline-chat .interactive-session { margin: initial; + max-width: unset; } .monaco-workbench .terminal-inline-chat.hide { @@ -71,3 +72,7 @@ border-image: linear-gradient(90deg, var(--vscode-editorGutter-addedBackground) var(--inline-chat-frame-progress), var(--vscode-button-background)) 1; animation: 3s shift linear infinite; } + +.monaco-workbench .terminal-inline-chat .inline-chat .chat-widget .interactive-session .interactive-input-part { + padding: 0 0 4px 0; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css b/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css deleted file mode 100644 index 08b471ab096..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/media/terminalInitialHint.css +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint, -.monaco-workbench .terminal-editor .terminal-initial-hint { - color: var(--vscode-terminal-initialHintForeground); -} -.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a, -.monaco-workbench .terminal-editor .terminal-initial-hint a { - cursor: pointer; -} - -.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint a, -.monaco-workbench .pane-body.integrated-terminal .terminal-initial-hint .detail, -.monaco-workbench .terminal-editor .terminal-initial-hint a, -.monaco-workbench .terminal-editor .terminal-initial-hint .detail { - font-style: italic; -} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts deleted file mode 100644 index 996b5c18ccb..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminal.initialHint.contribution.ts +++ /dev/null @@ -1,356 +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 { IDecoration, ITerminalAddon, Terminal as RawXtermTerminal } from '@xterm/xterm'; -import * as dom from '../../../../../base/browser/dom.js'; -import { IContentActionHandler, renderFormattedText } from '../../../../../base/browser/formattedTextRenderer.js'; -import { StandardMouseEvent } from '../../../../../base/browser/mouseEvent.js'; -import { status } from '../../../../../base/browser/ui/aria/aria.js'; -import { KeybindingLabel } from '../../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../../base/common/actions.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { OS } from '../../../../../base/common/platform.js'; -import { localize } from '../../../../../nls.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { ITerminalCapabilityStore, TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; -import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { IChatAgent, IChatAgentService } from '../../../chat/common/chatAgents.js'; -import { ChatAgentLocation } from '../../../chat/common/constants.js'; -import { IDetachedTerminalInstance, ITerminalContribution, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService, IXtermTerminal } from '../../../terminal/browser/terminal.js'; -import { registerTerminalContribution, type IDetachedCompatibleTerminalContributionContext, type ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; -import { TerminalInstance } from '../../../terminal/browser/terminalInstance.js'; -import { TerminalInitialHintSettingId } from '../common/terminalInitialHintConfiguration.js'; -import './media/terminalInitialHint.css'; -import { TerminalChatCommandId } from './terminalChat.js'; - -const $ = dom.$; - -const enum Constants { - InitialHintHideStorageKey = 'terminal.initialHint.hide' -} - -export class InitialHintAddon extends Disposable implements ITerminalAddon { - private readonly _onDidRequestCreateHint = this._register(new Emitter()); - get onDidRequestCreateHint(): Event { return this._onDidRequestCreateHint.event; } - private readonly _disposables = this._register(new MutableDisposable()); - - constructor(private readonly _capabilities: ITerminalCapabilityStore, - private readonly _onDidChangeAgents: Event) { - super(); - } - activate(terminal: RawXtermTerminal): void { - const store = this._register(new DisposableStore()); - this._disposables.value = store; - const capability = this._capabilities.get(TerminalCapability.CommandDetection); - if (capability) { - store.add(Event.once(capability.promptInputModel.onDidStartInput)(() => this._onDidRequestCreateHint.fire())); - } else { - this._register(this._capabilities.onDidAddCapability(e => { - if (e.id === TerminalCapability.CommandDetection) { - const capability = e.capability; - store.add(Event.once(capability.promptInputModel.onDidStartInput)(() => this._onDidRequestCreateHint.fire())); - if (!capability.promptInputModel.value) { - this._onDidRequestCreateHint.fire(); - } - } - })); - } - const agentListener = this._onDidChangeAgents((e) => { - if (e?.locations.includes(ChatAgentLocation.Terminal)) { - this._onDidRequestCreateHint.fire(); - agentListener.dispose(); - } - }); - this._disposables.value?.add(agentListener); - } -} - -export class TerminalInitialHintContribution extends Disposable implements ITerminalContribution { - static readonly ID = 'terminal.initialHint'; - - private _addon: InitialHintAddon | undefined; - - private _hintWidget: HTMLElement | undefined; - - static get(instance: ITerminalInstance | IDetachedTerminalInstance): TerminalInitialHintContribution | null { - return instance.getContribution(TerminalInitialHintContribution.ID); - } - private _decoration: IDecoration | undefined; - private _xterm: IXtermTerminal & { raw: RawXtermTerminal } | undefined; - - constructor( - private readonly _ctx: ITerminalContributionContext | IDetachedCompatibleTerminalContributionContext, - @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IStorageService private readonly _storageService: IStorageService, - @ITerminalEditorService private readonly _terminalEditorService: ITerminalEditorService, - @ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService, - ) { - super(); - - // Reset hint state when config changes - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled)) { - this._storageService.remove(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION); - } - })); - } - - xtermOpen(xterm: IXtermTerminal & { raw: RawXtermTerminal }): void { - // Don't show is the terminal was launched by an extension or a feature like debug - if ('shellLaunchConfig' in this._ctx.instance && (this._ctx.instance.shellLaunchConfig.isExtensionOwnedTerminal || this._ctx.instance.shellLaunchConfig.isFeatureTerminal)) { - return; - } - // Don't show if disabled - if (this._storageService.getBoolean(Constants.InitialHintHideStorageKey, StorageScope.APPLICATION, false)) { - return; - } - // Only show for the first terminal - if (this._terminalGroupService.instances.length + this._terminalEditorService.instances.length !== 1) { - return; - } - this._xterm = xterm; - this._addon = this._register(this._instantiationService.createInstance(InitialHintAddon, this._ctx.instance.capabilities, this._chatAgentService.onDidChangeAgents)); - this._xterm.raw.loadAddon(this._addon); - this._register(this._addon.onDidRequestCreateHint(() => this._createHint())); - } - - private _createHint(): void { - const instance = this._ctx.instance instanceof TerminalInstance ? this._ctx.instance : undefined; - const commandDetectionCapability = instance?.capabilities.get(TerminalCapability.CommandDetection); - if (!instance || !this._xterm || this._hintWidget || !commandDetectionCapability || commandDetectionCapability.promptInputModel.value || !!instance.shellLaunchConfig.attachPersistentProcess) { - return; - } - - if (!this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { - return; - } - - if (!this._decoration) { - const marker = this._xterm.raw.registerMarker(); - if (!marker) { - return; - } - - if (this._xterm.raw.buffer.active.cursorX === 0) { - return; - } - this._register(marker); - this._decoration = this._xterm.raw.registerDecoration({ - marker, - x: this._xterm.raw.buffer.active.cursorX + 1, - }); - if (this._decoration) { - this._register(this._decoration); - } - } - - this._register(this._xterm.raw.onKey(() => this.dispose())); - - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled) && !this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { - this.dispose(); - } - })); - - const inputModel = commandDetectionCapability.promptInputModel; - if (inputModel) { - this._register(inputModel.onDidChangeInput(() => { - if (inputModel.value) { - this.dispose(); - } - })); - } - - if (!this._decoration) { - return; - } - this._register(this._decoration); - this._register(this._decoration.onRender((e) => { - if (!this._hintWidget && this._xterm?.isFocused && this._terminalGroupService.instances.length + this._terminalEditorService.instances.length === 1) { - const terminalAgents = this._chatAgentService.getActivatedAgents().filter(candidate => candidate.locations.includes(ChatAgentLocation.Terminal)); - if (terminalAgents?.length) { - const widget = this._register(this._instantiationService.createInstance(TerminalInitialHintWidget, instance)); - this._addon?.dispose(); - this._hintWidget = widget.getDomNode(terminalAgents); - if (!this._hintWidget) { - return; - } - e.appendChild(this._hintWidget); - e.classList.add('terminal-initial-hint'); - const font = this._xterm.getFont(); - if (font) { - e.style.fontFamily = font.fontFamily; - e.style.fontSize = font.fontSize + 'px'; - } - } - } - if (this._hintWidget && this._xterm) { - const decoration = this._hintWidget.parentElement; - if (decoration) { - decoration.style.width = (this._xterm.raw.cols - this._xterm.raw.buffer.active.cursorX) / this._xterm!.raw.cols * 100 + '%'; - } - } - })); - } -} -registerTerminalContribution(TerminalInitialHintContribution.ID, TerminalInitialHintContribution, false); - -class TerminalInitialHintWidget extends Disposable { - - private _domNode: HTMLElement | undefined; - private readonly _toDispose: DisposableStore = this._register(new DisposableStore()); - private _isVisible = false; - private _ariaLabel: string = ''; - - constructor( - private readonly _instance: ITerminalInstance, - @ICommandService private readonly _commandService: ICommandService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IStorageService private readonly _storageService: IStorageService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @ITerminalService private readonly _terminalService: ITerminalService, - ) { - super(); - this._toDispose.add(_instance.onDidFocus(() => { - if (this._instance.hasFocus && this._isVisible && this._ariaLabel && this._configurationService.getValue(AccessibilityVerbositySettingId.TerminalChat)) { - status(this._ariaLabel); - } - })); - this._toDispose.add(_terminalService.onDidChangeInstances(() => { - if (this._terminalService.instances.length !== 1) { - this.dispose(); - } - })); - this._toDispose.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(TerminalInitialHintSettingId.Enabled) && !this._configurationService.getValue(TerminalInitialHintSettingId.Enabled)) { - this.dispose(); - } - })); - } - - private _getHintInlineChat(agents: IChatAgent[]) { - let ariaLabel = `Open chat.`; - - const handleClick = () => { - this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); - this._telemetryService.publicLog2('workbenchActionExecuted', { - id: 'terminalInlineChat.hintAction', - from: 'hint' - }); - this._commandService.executeCommand(TerminalChatCommandId.Start, { from: 'hint' }); - }; - this._toDispose.add(this._commandService.onDidExecuteCommand(e => { - if (e.commandId === TerminalChatCommandId.Start) { - this._storageService.store(Constants.InitialHintHideStorageKey, true, StorageScope.APPLICATION, StorageTarget.USER); - this.dispose(); - } - })); - - const hintHandler: IContentActionHandler = { - disposables: this._toDispose, - callback: (index, _event) => { - switch (index) { - case '0': - handleClick(); - break; - } - } - }; - - const hintElement = $('div.terminal-initial-hint'); - hintElement.style.display = 'block'; - - const keybindingHint = this._keybindingService.lookupKeybinding(TerminalChatCommandId.Start); - const keybindingHintLabel = keybindingHint?.getLabel(); - - if (keybindingHint && keybindingHintLabel) { - const actionPart = localize('emptyHintText', 'Open chat {0}. ', keybindingHintLabel); - - const [before, after] = actionPart.split(keybindingHintLabel).map((fragment) => { - const hintPart = $('a', undefined, fragment); - this._toDispose.add(dom.addDisposableListener(hintPart, dom.EventType.CLICK, handleClick)); - return hintPart; - }); - - hintElement.appendChild(before); - - const label = hintHandler.disposables.add(new KeybindingLabel(hintElement, OS)); - label.set(keybindingHint); - label.element.style.width = 'min-content'; - label.element.style.display = 'inline'; - - label.element.style.cursor = 'pointer'; - this._toDispose.add(dom.addDisposableListener(label.element, dom.EventType.CLICK, handleClick)); - - hintElement.appendChild(after); - - const typeToDismiss = localize('hintTextDismiss', 'Start typing to dismiss.'); - const textHint2 = $('span.detail', undefined, typeToDismiss); - hintElement.appendChild(textHint2); - - ariaLabel = actionPart.concat(typeToDismiss); - } else { - const hintMsg = localize({ - key: 'inlineChatHint', - comment: [ - 'Preserve double-square brackets and their order', - ] - }, '[[Open chat]] or start typing to dismiss.'); - const rendered = renderFormattedText(hintMsg, { actionHandler: hintHandler }); - hintElement.appendChild(rendered); - } - - return { ariaLabel, hintHandler, hintElement }; - } - - getDomNode(agents: IChatAgent[]): HTMLElement { - if (!this._domNode) { - this._domNode = $('.terminal-initial-hint'); - this._domNode!.style.paddingLeft = '4px'; - - const { hintElement, ariaLabel } = this._getHintInlineChat(agents); - this._domNode.append(hintElement); - this._ariaLabel = ariaLabel.concat(localize('disableHint', ' Toggle {0} in settings to disable this hint.', AccessibilityVerbositySettingId.TerminalChat)); - - this._toDispose.add(dom.addDisposableListener(this._domNode, 'click', () => { - this._domNode?.remove(); - this._domNode = undefined; - })); - - this._toDispose.add(dom.addDisposableListener(this._domNode, dom.EventType.CONTEXT_MENU, (e) => { - this._contextMenuService.showContextMenu({ - getAnchor: () => { return new StandardMouseEvent(dom.getActiveWindow(), e); }, - getActions: () => { - return [{ - id: 'workench.action.disableTerminalInitialHint', - label: localize('disableInitialHint', "Disable Initial Hint"), - tooltip: localize('disableInitialHint', "Disable Initial Hint"), - enabled: true, - class: undefined, - run: () => this._configurationService.updateValue(TerminalInitialHintSettingId.Enabled, false) - } - ]; - } - }); - })); - } - return this._domNode; - } - - override dispose(): void { - this._domNode?.remove(); - super.dispose(); - } -} diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts index 04f19f20ecd..bb5f4323288 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChat.ts @@ -19,6 +19,13 @@ export const enum TerminalChatCommandId { ViewInChat = 'workbench.action.terminal.chat.viewInChat', RerunRequest = 'workbench.action.terminal.chat.rerunRequest', ViewHiddenChatTerminals = 'workbench.action.terminal.chat.viewHiddenChatTerminals', + OpenTerminalSettingsLink = 'workbench.action.terminal.chat.openTerminalSettingsLink', + DisableSessionAutoApproval = 'workbench.action.terminal.chat.disableSessionAutoApproval', + FocusMostRecentChatTerminalOutput = 'workbench.action.terminal.chat.focusMostRecentChatTerminalOutput', + FocusMostRecentChatTerminal = 'workbench.action.terminal.chat.focusMostRecentChatTerminal', + ToggleChatTerminalOutput = 'workbench.action.terminal.chat.toggleChatTerminalOutput', + FocusChatInstanceAction = 'workbench.action.terminal.chat.focusChatInstance', + ContinueInBackground = 'workbench.action.terminal.chat.continueInBackground', } export const MENU_TERMINAL_CHAT_WIDGET_INPUT_SIDE_TOOLBAR = MenuId.for('terminalChatWidget'); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index 1b62a8d1b93..e48f5079315 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -32,7 +32,7 @@ export class TerminalChatAccessibilityHelp implements IAccessibleViewImplementat { type: AccessibleViewType.Help }, () => helpText, () => TerminalChatController.get(instance)?.terminalChatWidget?.focus(), - AccessibilityVerbositySettingId.TerminalChat, + AccessibilityVerbositySettingId.TerminalInlineChat, ); } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts index 93ff50574a2..02255c5d8aa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatActions.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { Codicon } from '../../../../../base/common/codicons.js'; +import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { localize, localize2 } from '../../../../../nls.js'; +import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { KeybindingsRegistry, KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ChatViewId, IChatWidgetService } from '../../../chat/browser/chat.js'; -import { ChatContextKeys } from '../../../chat/common/chatContextKeys.js'; -import { IChatService } from '../../../chat/common/chatService.js'; -import { LocalChatSessionUri } from '../../../chat/common/chatUri.js'; -import { ChatAgentLocation } from '../../../chat/common/constants.js'; -import { AbstractInline1ChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; +import { ChatContextKeys } from '../../../chat/common/actions/chatContextKeys.js'; +import { IChatService } from '../../../chat/common/chatService/chatService.js'; +import { LocalChatSessionUri } from '../../../chat/common/model/chatUri.js'; +import { ChatAgentLocation, ChatConfiguration } from '../../../chat/common/constants.js'; + import { isDetachedTerminalInstance, ITerminalChatService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { registerActiveXtermAction } from '../../../terminal/browser/terminalActions.js'; import { TerminalContextMenuGroup } from '../../../terminal/browser/terminalMenus.js'; @@ -25,6 +27,13 @@ import { IInstantiationService, ServicesAccessor } from '../../../../../platform import { getIconId } from '../../../terminal/browser/terminalIcon.js'; import { TerminalChatController } from './terminalChatController.js'; import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; +import { isString } from '../../../../../base/common/types.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { IPreferencesService, IOpenSettingsOptions } from '../../../../services/preferences/common/preferences.js'; +import { ConfigurationTarget } from '../../../../../platform/configuration/common/configuration.js'; +import { TerminalChatAgentToolsSettingId } from '../../chatAgentTools/common/terminalChatAgentToolsConfiguration.js'; +import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; +import { AbstractInlineChatAction } from '../../../inlineChat/browser/inlineChatActions.js'; registerActiveXtermAction({ id: TerminalChatCommandId.Start, @@ -54,26 +63,32 @@ registerActiveXtermAction({ } const contr = TerminalChatController.activeChatController || TerminalChatController.get(activeInstance); + if (!contr) { + return; + } if (opts) { - opts = typeof opts === 'string' ? { query: opts } : opts; - if (typeof opts === 'object' && opts !== null && 'query' in opts && typeof opts.query === 'string') { - contr?.updateInput(opts.query, false); - if (!('isPartialQuery' in opts && opts.isPartialQuery)) { - contr?.terminalChatWidget?.acceptInput(); + function isValidOptionsObject(obj: unknown): obj is { query: string; isPartialQuery?: boolean } { + return typeof obj === 'object' && obj !== null && 'query' in obj && isString(obj.query); + } + opts = isString(opts) ? { query: opts } : opts; + if (isValidOptionsObject(opts)) { + contr.updateInput(opts.query, false); + if (!opts.isPartialQuery) { + contr.terminalChatWidget?.acceptInput(); } } } - contr?.terminalChatWidget?.reveal(); + contr.terminalChatWidget?.reveal(); } }); registerActiveXtermAction({ id: TerminalChatCommandId.Close, title: localize2('closeChat', 'Close'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, keybinding: { primary: KeyCode.Escape, when: ContextKeyExpr.and( @@ -106,7 +121,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.RunCommand, title: localize2('runCommand', 'Run Chat Command'), shortTitle: localize2('run', 'Run'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -139,7 +154,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.RunFirstCommand, title: localize2('runFirstCommand', 'Run First Chat Command'), shortTitle: localize2('runFirst', 'Run First'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -171,7 +186,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.InsertCommand, title: localize2('insertCommand', 'Insert Chat Command'), shortTitle: localize2('insert', 'Insert'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, icon: Codicon.insert, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, @@ -205,7 +220,7 @@ registerActiveXtermAction({ id: TerminalChatCommandId.InsertFirstCommand, title: localize2('insertFirstCommand', 'Insert First Chat Command'), shortTitle: localize2('insertFirst', 'Insert First'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -238,7 +253,7 @@ registerActiveXtermAction({ title: localize2('chat.rerun.label', "Rerun Request"), f1: false, icon: Codicon.refresh, - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -280,7 +295,7 @@ registerActiveXtermAction({ registerActiveXtermAction({ id: TerminalChatCommandId.ViewInChat, title: localize2('viewInChat', 'View in Chat'), - category: AbstractInline1ChatAction.category, + category: AbstractInlineChatAction.category, precondition: ContextKeyExpr.and( ChatContextKeys.enabled, ContextKeyExpr.or(TerminalContextKeys.processSupported, TerminalContextKeys.terminalHasBeenCreated), @@ -350,9 +365,11 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { label: string; description: string | undefined; detail: string | undefined; + tooltip: string | IMarkdownString | undefined; id: string; } const lastCommandLocalized = (command: string) => localize2('chatTerminal.lastCommand', 'Last: {0}', command).value; + const MAX_DETAIL_LENGTH = 80; const metas: IItemMeta[] = []; for (const instance of all.values()) { @@ -364,20 +381,36 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { const chatSessionId = terminalChatService.getChatSessionIdForInstance(instance); let chatSessionTitle: string | undefined; if (chatSessionId) { - const sessionUri = LocalChatSessionUri.forSession(chatSessionId); - // Try to get title from active session first, then fall back to persisted title - chatSessionTitle = chatService.getSession(sessionUri)?.title || chatService.getPersistedSessionTitle(sessionUri); + chatSessionTitle = chatService.getSessionTitle(LocalChatSessionUri.forSession(chatSessionId)); } - let description: string | undefined; - if (chatSessionTitle) { - description = `${chatSessionTitle}`; + const description = chatSessionTitle; + let detail: string | undefined; + let tooltip: string | IMarkdownString | undefined; + if (lastCommand) { + // Take only the first line if the command spans multiple lines + const commandLines = lastCommand.split('\n'); + const firstLine = commandLines[0]; + const displayCommand = firstLine.length > MAX_DETAIL_LENGTH ? firstLine.substring(0, MAX_DETAIL_LENGTH) + '…' : firstLine; + detail = lastCommandLocalized(displayCommand); + // If the command was truncated or has multiple lines, provide a tooltip with the full command + const wasTruncated = firstLine.length > MAX_DETAIL_LENGTH; + const hasMultipleLines = commandLines.length > 1; + if (wasTruncated || hasMultipleLines) { + // Use markdown code block to preserve formatting for multi-line commands + if (hasMultipleLines) { + tooltip = { value: `\`\`\`\n${lastCommand}\n\`\`\``, supportThemeIcons: true }; + } else { + tooltip = lastCommandLocalized(lastCommand); + } + } } metas.push({ label, description, - detail: lastCommand ? lastCommandLocalized(lastCommand) : undefined, + detail, + tooltip, id: String(instance.instanceId), }); } @@ -387,6 +420,7 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { label: m.label, description: m.description, detail: m.detail, + tooltip: m.tooltip, id: m.id }); } @@ -398,7 +432,9 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { qp.title = localize2('showChatTerminals.title', 'Chat Terminals').value; qp.matchOnDescription = true; qp.matchOnDetail = true; - qp.onDidAccept(async () => { + const qpDisposables = new DisposableStore(); + qpDisposables.add(qp); + qpDisposables.add(qp.onDidAccept(async () => { const sel = qp.selectedItems[0]; if (sel) { const instance = all.get(Number(sel.id)); @@ -406,15 +442,108 @@ registerAction2(class ShowChatTerminalsAction extends Action2 { terminalService.setActiveInstance(instance); await terminalService.revealTerminal(instance); qp.hide(); - terminalService.focusInstance(instance); + await terminalService.focusInstance(instance); } else { qp.hide(); } } else { qp.hide(); } - }); - qp.onDidHide(() => qp.dispose()); + })); + qpDisposables.add(qp.onDidHide(() => { + qpDisposables.dispose(); + qp.dispose(); + })); qp.show(); } }); + + + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: TerminalChatCommandId.FocusMostRecentChatTerminal, + weight: KeybindingWeight.WorkbenchContrib, + when: ChatContextKeys.inChatSession, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyT, + handler: async (accessor: ServicesAccessor) => { + const terminalChatService = accessor.get(ITerminalChatService); + const part = terminalChatService.getMostRecentProgressPart(); + if (!part) { + return; + } + await part.focusTerminal(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: TerminalChatCommandId.FocusMostRecentChatTerminalOutput, + weight: KeybindingWeight.WorkbenchContrib, + when: ChatContextKeys.inChatSession, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyO, + handler: async (accessor: ServicesAccessor) => { + const terminalChatService = accessor.get(ITerminalChatService); + const part = terminalChatService.getMostRecentProgressPart(); + if (!part) { + return; + } + await part.toggleOutputFromKeyboard(); + } +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: TerminalChatCommandId.FocusMostRecentChatTerminal, + title: localize('chat.focusMostRecentTerminal', 'Chat: Focus Most Recent Terminal'), + }, + when: ChatContextKeys.inChatSession +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: TerminalChatCommandId.FocusMostRecentChatTerminalOutput, + title: localize('chat.focusMostRecentTerminalOutput', 'Chat: Focus Most Recent Terminal Output'), + }, + when: ChatContextKeys.inChatSession +}); + + +CommandsRegistry.registerCommand(TerminalChatCommandId.OpenTerminalSettingsLink, 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: TerminalChatAgentToolsSettingId.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:${TerminalChatAgentToolsSettingId.AutoApprove}`, + }); + break; + } + } + + } +}); + +CommandsRegistry.registerCommand(TerminalChatCommandId.DisableSessionAutoApproval, async (accessor, chatSessionResource: URI) => { + const terminalChatService = accessor.get(ITerminalChatService); + terminalChatService.setChatSessionAutoApproval(chatSessionResource, false); +}); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index bd1710a7f27..5160470b037 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -8,15 +8,13 @@ import { Lazy } from '../../../../../base/common/lazy.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, type ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatCodeBlockContextProviderService, showChatView } from '../../../chat/browser/chat.js'; -import { IChatService } from '../../../chat/common/chatService.js'; +import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../../../chat/browser/chat.js'; +import { IChatService } from '../../../chat/common/chatService/chatService.js'; import { isDetachedTerminalInstance, ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from '../../../terminal/browser/terminal.js'; import { TerminalChatWidget } from './terminalChatWidget.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; import type { ITerminalContributionContext } from '../../../terminal/browser/terminalExtensions.js'; -import type { IChatModel } from '../../../chat/common/chatModel.js'; +import type { IChatModel } from '../../../chat/common/model/chatModel.js'; import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; export class TerminalChatController extends Disposable implements ITerminalContribution { static readonly ID = 'terminal.chat'; @@ -154,11 +152,10 @@ export class TerminalChatController extends Disposable implements ITerminalContr } async function moveToPanelChat(accessor: ServicesAccessor, model: IChatModel | undefined) { - const viewsService = accessor.get(IViewsService); const chatService = accessor.get(IChatService); - const layoutService = accessor.get(IWorkbenchLayoutService); + const chatWidgetService = accessor.get(IChatWidgetService); - const widget = await showChatView(viewsService, layoutService); + const widget = await chatWidgetService.revealWidget(); if (widget && widget.viewModel && model) { for (const request of model.getRequests().slice()) { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts index cbdeafdd383..d971ecc21fa 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatEnabler.ts @@ -6,7 +6,7 @@ import { Event } from '../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IChatAgentService } from '../../../chat/common/chatAgents.js'; +import { IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { TerminalChatContextKeys } from './terminalChat.js'; diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts index 33672e6ffbe..e815d4e93cb 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatService.ts @@ -4,14 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableMap, IDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableMap, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; +import { URI } from '../../../../../base/common/uri.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; -import { ITerminalChatService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; +import { IChatTerminalToolProgressPart, ITerminalChatService, ITerminalInstance, ITerminalService } from '../../../terminal/browser/terminal.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { IChatService } from '../../../chat/common/chatService.js'; +import { IChatService } from '../../../chat/common/chatService/chatService.js'; import { TerminalChatContextKeys } from './terminalChat.js'; -import { LocalChatSessionUri } from '../../../chat/common/chatUri.js'; +import { chatSessionResourceToId, LocalChatSessionUri } from '../../../chat/common/model/chatUri.js'; +import { isNumber, isString } from '../../../../../base/common/types.js'; const enum StorageKeys { ToolSessionMappings = 'terminalChat.toolSessionMappings', @@ -27,12 +30,19 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ private readonly _terminalInstancesByToolSessionId = new Map(); private readonly _toolSessionIdByTerminalInstance = new Map(); - private readonly _chatSessionIdByTerminalInstance = new Map(); + private readonly _chatSessionResourceByTerminalInstance = new Map(); private readonly _terminalInstanceListenersByToolSessionId = this._register(new DisposableMap()); private readonly _chatSessionListenersByTerminalInstance = this._register(new DisposableMap()); - private readonly _onDidRegisterTerminalInstanceForToolSession = new Emitter(); + + private readonly _onDidContinueInBackground = this._register(new Emitter()); + readonly onDidContinueInBackground: Event = this._onDidContinueInBackground.event; + private readonly _onDidRegisterTerminalInstanceForToolSession = this._register(new Emitter()); readonly onDidRegisterTerminalInstanceWithToolSession: Event = this._onDidRegisterTerminalInstanceForToolSession.event; + private readonly _activeProgressParts = new Set(); + private _focusedProgressPart: IChatTerminalToolProgressPart | undefined; + private _mostRecentProgressPart: IChatTerminalToolProgressPart | undefined; + /** * Pending mappings restored from storage that have not yet been matched to a live terminal * instance (we match by persistentProcessId when it becomes available after reconnection). @@ -43,6 +53,18 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ private readonly _hasToolTerminalContext: IContextKey; private readonly _hasHiddenToolTerminalContext: IContextKey; + /** + * Tracks chat session resources that have auto approval enabled for all commands. This is a temporary + * approval that lasts only for the duration of the session. + */ + private readonly _sessionAutoApprovalEnabled = new ResourceMap(); + + /** + * Tracks session-scoped auto-approve rules per chat session. These are temporary rules that + * last only for the duration of the chat session (not persisted to disk). + */ + private readonly _sessionAutoApproveRules = new ResourceMap>(); + constructor( @ILogService private readonly _logService: ILogService, @ITerminalService private readonly _terminalService: ITerminalService, @@ -56,6 +78,14 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ this._hasHiddenToolTerminalContext = TerminalChatContextKeys.hasHiddenChatTerminals.bindTo(this._contextKeyService); this._restoreFromStorage(); + + // Clear session auto-approve rules when chat sessions end + this._register(this._chatService.onDidDisposeSession(e => { + for (const resource of e.sessionResource) { + this._sessionAutoApproveRules.delete(resource); + this._sessionAutoApprovalEnabled.delete(resource); + } + })); } registerTerminalInstanceWithToolSession(terminalToolSessionId: string | undefined, instance: ITerminalInstance): void { @@ -75,19 +105,23 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ })); this._register(this._chatService.onDidDisposeSession(e => { - if (LocalChatSessionUri.parseLocalSessionId(e.sessionResource) === terminalToolSessionId) { - this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); - this._toolSessionIdByTerminalInstance.delete(instance); - this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); - this._persistToStorage(); - this._updateHasToolTerminalContextKeys(); + for (const resource of e.sessionResource) { + if (LocalChatSessionUri.parseLocalSessionId(resource) === terminalToolSessionId) { + this._terminalInstancesByToolSessionId.delete(terminalToolSessionId); + this._toolSessionIdByTerminalInstance.delete(instance); + this._terminalInstanceListenersByToolSessionId.deleteAndDispose(terminalToolSessionId); + // Clean up session auto approval state + this._sessionAutoApprovalEnabled.delete(resource); + this._persistToStorage(); + this._updateHasToolTerminalContextKeys(); + } } })); // Update context keys when terminal instances change (including when terminals are created, disposed, revealed, or hidden) this._register(this._terminalService.onDidChangeInstances(() => this._updateHasToolTerminalContextKeys())); - if (typeof instance.shellLaunchConfig?.attachPersistentProcess?.id === 'number' || typeof instance.persistentProcessId === 'number') { + if (isNumber(instance.shellLaunchConfig?.attachPersistentProcess?.id) || isNumber(instance.persistentProcessId)) { this._persistToStorage(); } @@ -123,26 +157,32 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ return this._toolSessionIdByTerminalInstance.get(instance); } - registerTerminalInstanceWithChatSession(chatSessionId: string, instance: ITerminalInstance): void { - // If already registered with the same session ID, skip to avoid duplicate listeners - if (this._chatSessionIdByTerminalInstance.get(instance) === chatSessionId) { + registerTerminalInstanceWithChatSession(chatSessionResource: URI, instance: ITerminalInstance): void { + // If already registered with the same session, skip to avoid duplicate listeners + const existingResource = this._chatSessionResourceByTerminalInstance.get(instance); + if (existingResource && existingResource.toString() === chatSessionResource.toString()) { return; } // Clean up previous listener if the instance was registered with a different session this._chatSessionListenersByTerminalInstance.deleteAndDispose(instance); - this._chatSessionIdByTerminalInstance.set(instance, chatSessionId); + this._chatSessionResourceByTerminalInstance.set(instance, chatSessionResource); // Clean up when the instance is disposed const disposable = instance.onDisposed(() => { - this._chatSessionIdByTerminalInstance.delete(instance); + this._chatSessionResourceByTerminalInstance.delete(instance); this._chatSessionListenersByTerminalInstance.deleteAndDispose(instance); }); this._chatSessionListenersByTerminalInstance.set(instance, disposable); } + getChatSessionResourceForInstance(instance: ITerminalInstance): URI | undefined { + return this._chatSessionResourceByTerminalInstance.get(instance); + } + getChatSessionIdForInstance(instance: ITerminalInstance): string | undefined { - return this._chatSessionIdByTerminalInstance.get(instance); + const resource = this._chatSessionResourceByTerminalInstance.get(instance); + return resource ? chatSessionResourceToId(resource) : undefined; } isBackgroundTerminal(terminalToolSessionId?: string): boolean { @@ -156,6 +196,63 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ return this._terminalService.instances.includes(instance) && !this._terminalService.foregroundInstances.includes(instance); } + registerProgressPart(part: IChatTerminalToolProgressPart): IDisposable { + this._activeProgressParts.add(part); + if (this._isAfter(part, this._mostRecentProgressPart)) { + this._mostRecentProgressPart = part; + } + return toDisposable(() => { + this._activeProgressParts.delete(part); + if (this._focusedProgressPart === part) { + this._focusedProgressPart = undefined; + } + if (this._mostRecentProgressPart === part) { + this._mostRecentProgressPart = this._getLastActiveProgressPart(); + } + }); + } + + setFocusedProgressPart(part: IChatTerminalToolProgressPart): void { + this._focusedProgressPart = part; + } + + clearFocusedProgressPart(part: IChatTerminalToolProgressPart): void { + if (this._focusedProgressPart === part) { + this._focusedProgressPart = undefined; + } + } + + getFocusedProgressPart(): IChatTerminalToolProgressPart | undefined { + return this._focusedProgressPart; + } + + getMostRecentProgressPart(): IChatTerminalToolProgressPart | undefined { + if (!this._mostRecentProgressPart || !this._activeProgressParts.has(this._mostRecentProgressPart)) { + this._mostRecentProgressPart = this._getLastActiveProgressPart(); + } + return this._mostRecentProgressPart; + } + + private _getLastActiveProgressPart(): IChatTerminalToolProgressPart | undefined { + let latest: IChatTerminalToolProgressPart | undefined; + for (const part of this._activeProgressParts) { + if (this._isAfter(part, latest)) { + latest = part; + } + } + return latest; + } + + private _isAfter(candidate: IChatTerminalToolProgressPart, current: IChatTerminalToolProgressPart | undefined): boolean { + if (!current) { + return true; + } + if (candidate.elementIndex === current.elementIndex) { + return candidate.contentIndex >= current.contentIndex; + } + return candidate.elementIndex > current.elementIndex; + } + private _restoreFromStorage(): void { try { const raw = this._storageService.get(StorageKeys.ToolSessionMappings, StorageScope.WORKSPACE); @@ -164,7 +261,7 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ } const parsed: [string, number][] = JSON.parse(raw); for (const [toolSessionId, persistentProcessId] of parsed) { - if (typeof toolSessionId === 'string' && typeof persistentProcessId === 'number') { + if (isString(toolSessionId) && isNumber(persistentProcessId)) { this._pendingRestoredMappings.set(toolSessionId, persistentProcessId); } } @@ -201,8 +298,14 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ try { const entries: [string, number][] = []; for (const [toolSessionId, instance] of this._terminalInstancesByToolSessionId.entries()) { - if (typeof instance.persistentProcessId === 'number' && instance.shouldPersist) { - entries.push([toolSessionId, instance.persistentProcessId]); + // Use the live persistent process id when available, otherwise fall back to the id + // from the attached process so mappings survive early in the terminal lifecycle. + const persistentId = isNumber(instance.persistentProcessId) + ? instance.persistentProcessId + : instance.shellLaunchConfig.attachPersistentProcess?.id; + const shouldPersist = instance.shouldPersist || instance.shellLaunchConfig.forcePersist; + if (isNumber(persistentId) && shouldPersist) { + entries.push([toolSessionId, persistentId]); } } if (entries.length > 0) { @@ -221,4 +324,33 @@ export class TerminalChatService extends Disposable implements ITerminalChatServ const hiddenTerminalCount = this.getToolSessionTerminalInstances(true).length; this._hasHiddenToolTerminalContext.set(hiddenTerminalCount > 0); } + + setChatSessionAutoApproval(chatSessionResource: URI, enabled: boolean): void { + if (enabled) { + this._sessionAutoApprovalEnabled.set(chatSessionResource, true); + } else { + this._sessionAutoApprovalEnabled.delete(chatSessionResource); + } + } + + hasChatSessionAutoApproval(chatSessionResource: URI): boolean { + return this._sessionAutoApprovalEnabled.has(chatSessionResource); + } + + addSessionAutoApproveRule(chatSessionResource: URI, key: string, value: boolean | { approve: boolean; matchCommandLine?: boolean }): void { + let sessionRules = this._sessionAutoApproveRules.get(chatSessionResource); + if (!sessionRules) { + sessionRules = {}; + this._sessionAutoApproveRules.set(chatSessionResource, sessionRules); + } + sessionRules[key] = value; + } + + getSessionAutoApproveRules(chatSessionResource: URI): Readonly> { + return this._sessionAutoApproveRules.get(chatSessionResource) ?? {}; + } + + continueInBackground(terminalToolSessionId: string): void { + this._onDidContinueInBackground.fire(terminalToolSessionId); + } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts index f559d1b9100..d08f85abbe2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatWidget.ts @@ -8,7 +8,7 @@ import { Dimension, getActiveWindow, IFocusTracker, trackFocus } from '../../../ import { CancelablePromise, createCancelablePromise, DeferredPromise } from '../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { autorun, observableValue, type IObservable } from '../../../../../base/common/observable.js'; import { MicrotaskDelay } from '../../../../../base/common/symbols.js'; import { localize } from '../../../../../nls.js'; @@ -16,14 +16,11 @@ import { MenuId } from '../../../../../platform/actions/common/actions.js'; import { IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { IChatAcceptInputOptions, showChatView } from '../../../chat/browser/chat.js'; -import type { IChatViewState } from '../../../chat/browser/chatWidget.js'; -import { IChatAgentService } from '../../../chat/common/chatAgents.js'; -import { ChatModel, IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/chatModel.js'; +import { IChatAcceptInputOptions, IChatWidgetService } from '../../../chat/browser/chat.js'; +import { IChatAgentService } from '../../../chat/common/participants/chatAgents.js'; +import { IChatResponseModel, isCellTextEditOperationArray } from '../../../chat/common/model/chatModel.js'; import { ChatMode } from '../../../chat/common/chatModes.js'; -import { IChatProgress, IChatService } from '../../../chat/common/chatService.js'; +import { IChatModelReference, IChatProgress, IChatService } from '../../../chat/common/chatService/chatService.js'; import { ChatAgentLocation } from '../../../chat/common/constants.js'; import { InlineChatWidget } from '../../../inlineChat/browser/inlineChatWidget.js'; import { MENU_INLINE_CHAT_WIDGET_SECONDARY } from '../../../inlineChat/common/inlineChat.js'; @@ -84,7 +81,8 @@ export class TerminalChatWidget extends Disposable { private _terminalAgentName = 'terminal'; - private readonly _model: MutableDisposable = this._register(new MutableDisposable()); + private readonly _model: MutableDisposable = this._register(new MutableDisposable()); + private readonly _sessionDisposables: MutableDisposable = this._register(new MutableDisposable()); private _sessionCtor: CancelablePromise | undefined; @@ -101,10 +99,9 @@ export class TerminalChatWidget extends Disposable { @IContextKeyService contextKeyService: IContextKeyService, @IChatService private readonly _chatService: IChatService, @IStorageService private readonly _storageService: IStorageService, - @IViewsService private readonly _viewsService: IViewsService, @IInstantiationService instantiationService: IInstantiationService, @IChatAgentService private readonly _chatAgentService: IChatAgentService, - @IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService, ) { super(); @@ -154,6 +151,7 @@ export class TerminalChatWidget extends Disposable { this._inlineChatWidget.onDidChangeHeight, this._instance.onDimensionsChanged, this._inlineChatWidget.chatWidget.onDidChangeContentHeight, + Event.fromObservableLight(this._inlineChatWidget.chatWidget.input.selectedLanguageModel), Event.debounce(this._xterm.raw.onCursorMove, () => void 0, MicrotaskDelay), )(() => this._relayout())); @@ -235,8 +233,8 @@ export class TerminalChatWidget extends Disposable { this.inlineChatWidget.placeholder = defaultAgent?.description ?? localize('askAboutCommands', 'Ask about commands'); } - async reveal(viewState?: IChatViewState): Promise { - await this._createSession(viewState); + async reveal(): Promise { + await this._createSession(); this._doLayout(); this._container.classList.remove('hide'); this._visibleContextKey.set(true); @@ -328,38 +326,24 @@ export class TerminalChatWidget extends Disposable { return this._focusTracker; } - private async _createSession(viewState?: IChatViewState): Promise { + private async _createSession(): Promise { this._sessionCtor = createCancelablePromise(async token => { if (!this._model.value) { - this._model.value = this._chatService.startSession(ChatAgentLocation.Terminal, token); - const model = this._model.value; - if (model) { - this._inlineChatWidget.setChatModel(model, this._loadViewState()); - this._resetPlaceholder(); - } - if (!this._model.value) { - throw new Error('Failed to start chat session'); - } + const modelRef = this._chatService.startSession(ChatAgentLocation.Terminal); + this._model.value = modelRef; + const model = modelRef.object; + this._inlineChatWidget.setChatModel(model); + this._resetPlaceholder(); } }); - this._register(toDisposable(() => this._sessionCtor?.cancel())); - } - - private _loadViewState() { - const rawViewState = this._storageService.get(this._viewStateStorageKey, StorageScope.PROFILE, undefined); - let viewState: IChatViewState | undefined; - if (rawViewState) { - try { - viewState = JSON.parse(rawViewState); - } catch { - viewState = undefined; - } - } - return viewState; + this._sessionDisposables.value = toDisposable(() => this._sessionCtor?.cancel()); } private _saveViewState() { - this._storageService.store(this._viewStateStorageKey, JSON.stringify(this._inlineChatWidget.chatWidget.getViewState()), StorageScope.PROFILE, StorageTarget.USER); + const viewState = this._inlineChatWidget.chatWidget.getViewState(); + if (viewState) { + this._storageService.store(this._viewStateStorageKey, JSON.stringify(viewState), StorageScope.PROFILE, StorageTarget.USER); + } } clear(): void { @@ -432,7 +416,7 @@ export class TerminalChatWidget extends Disposable { } async viewInChat(): Promise { - const widget = await showChatView(this._viewsService, this._layoutService); + const widget = await this._chatWidgetService.revealWidget(); const currentRequest = this._inlineChatWidget.chatWidget.viewModel?.model.getRequests().find(r => r.id === this._currentRequestId); if (!widget || !currentRequest?.response) { return; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/alternativeRecommendation.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/alternativeRecommendation.ts index 265d7255eb5..1f6c62232b2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/alternativeRecommendation.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/alternativeRecommendation.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js'; +import type { ILanguageModelToolsService } from '../../../chat/common/tools/languageModelToolsService.js'; let previouslyRecommededInSession = false; @@ -27,7 +27,7 @@ const terminalCommands: { commands: RegExp[]; tags: string[] }[] = [ ]; export function getRecommendedToolsOverRunInTerminal(commandLine: string, languageModelToolsService: ILanguageModelToolsService): string | undefined { - const tools = languageModelToolsService.getTools(); + const tools = languageModelToolsService.getTools(undefined); if (!tools || previouslyRecommededInSession) { return; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.ts deleted file mode 100644 index b48404a18d5..00000000000 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandLineAutoApprover.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 { Disposable } from '../../../../../base/common/lifecycle.js'; -import type { OperatingSystem } from '../../../../../base/common/platform.js'; -import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../base/common/strings.js'; -import { isObject } from '../../../../../base/common/types.js'; -import { structuralEquals } from '../../../../../base/common/equals.js'; -import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js'; -import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; -import { isPowerShell } from './runInTerminalHelpers.js'; - -export interface IAutoApproveRule { - regex: RegExp; - regexCaseInsensitive: RegExp; - sourceText: string; - sourceTarget: ConfigurationTarget; - isDefaultRule: boolean; -} - -export interface ICommandApprovalResultWithReason { - result: ICommandApprovalResult; - reason: string; - rule?: IAutoApproveRule; -} - -export type ICommandApprovalResult = 'approved' | 'denied' | 'noMatch'; - -const neverMatchRegex = /(?!.*)/; -const transientEnvVarRegex = /^[A-Z_][A-Z0-9_]*=/i; - -export class CommandLineAutoApprover extends Disposable { - private _denyListRules: IAutoApproveRule[] = []; - private _allowListRules: IAutoApproveRule[] = []; - private _allowListCommandLineRules: IAutoApproveRule[] = []; - private _denyListCommandLineRules: IAutoApproveRule[] = []; - - constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - ) { - super(); - this.updateConfiguration(); - this._register(this._configurationService.onDidChangeConfiguration(e => { - if ( - e.affectsConfiguration(TerminalChatAgentToolsSettingId.AutoApprove) || - e.affectsConfiguration(TerminalChatAgentToolsSettingId.IgnoreDefaultAutoApproveRules) || - e.affectsConfiguration(TerminalChatAgentToolsSettingId.DeprecatedAutoApproveCompatible) - ) { - this.updateConfiguration(); - } - })); - } - - updateConfiguration() { - let configValue = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove); - const configInspectValue = this._configurationService.inspect(TerminalChatAgentToolsSettingId.AutoApprove); - const deprecatedValue = this._configurationService.getValue(TerminalChatAgentToolsSettingId.DeprecatedAutoApproveCompatible); - if (deprecatedValue && typeof deprecatedValue === 'object' && configValue && typeof configValue === 'object') { - configValue = { - ...configValue, - ...deprecatedValue - }; - } - - const { - denyListRules, - allowListRules, - allowListCommandLineRules, - denyListCommandLineRules - } = this._mapAutoApproveConfigToRules(configValue, configInspectValue); - this._allowListRules = allowListRules; - this._denyListRules = denyListRules; - this._allowListCommandLineRules = allowListCommandLineRules; - this._denyListCommandLineRules = denyListCommandLineRules; - } - - isCommandAutoApproved(command: string, shell: string, os: OperatingSystem): ICommandApprovalResultWithReason { - // Check if the command has a transient environment variable assignment prefix which we - // always deny for now as it can easily lead to execute other commands - if (transientEnvVarRegex.test(command)) { - return { - result: 'denied', - reason: `Command '${command}' is denied because it contains transient environment variables` - }; - } - - // Check the deny list to see if this command requires explicit approval - for (const rule of this._denyListRules) { - if (this._commandMatchesRule(rule, command, shell, os)) { - return { - result: 'denied', - rule, - reason: `Command '${command}' is denied by deny list rule: ${rule.sourceText}` - }; - } - } - - // Check the allow list to see if the command is allowed to run without explicit approval - for (const rule of this._allowListRules) { - if (this._commandMatchesRule(rule, command, shell, os)) { - return { - result: 'approved', - rule, - reason: `Command '${command}' is approved by allow list rule: ${rule.sourceText}` - }; - } - } - - // TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267 - - // Fallback is always to require approval - return { - result: 'noMatch', - reason: `Command '${command}' has no matching auto approve entries` - }; - } - - isCommandLineAutoApproved(commandLine: string): ICommandApprovalResultWithReason { - // Check the deny list first to see if this command line requires explicit approval - for (const rule of this._denyListCommandLineRules) { - if (rule.regex.test(commandLine)) { - return { - result: 'denied', - rule, - reason: `Command line '${commandLine}' is denied by deny list rule: ${rule.sourceText}` - }; - } - } - - // Check if the full command line matches any of the allow list command line regexes - for (const rule of this._allowListCommandLineRules) { - if (rule.regex.test(commandLine)) { - return { - result: 'approved', - rule, - reason: `Command line '${commandLine}' is approved by allow list rule: ${rule.sourceText}` - }; - } - } - return { - result: 'noMatch', - reason: `Command line '${commandLine}' has no matching auto approve entries` - }; - } - - private _commandMatchesRule(rule: IAutoApproveRule, command: string, shell: string, os: OperatingSystem): boolean { - const isPwsh = isPowerShell(shell, os); - - // PowerShell is case insensitive regardless of platform - if ((isPwsh ? rule.regexCaseInsensitive : rule.regex).test(command)) { - return true; - } else if (isPwsh && command.startsWith('(')) { - // Allow ignoring of the leading ( for PowerShell commands as it's a command pattern to - // operate on the output of a command. For example `(Get-Content README.md) ...` - if (rule.regexCaseInsensitive.test(command.slice(1))) { - return true; - } - } - return false; - } - - private _mapAutoApproveConfigToRules(config: unknown, configInspectValue: IConfigurationValue>): { - denyListRules: IAutoApproveRule[]; - allowListRules: IAutoApproveRule[]; - allowListCommandLineRules: IAutoApproveRule[]; - denyListCommandLineRules: IAutoApproveRule[]; - } { - if (!config || typeof config !== 'object') { - return { - denyListRules: [], - allowListRules: [], - allowListCommandLineRules: [], - denyListCommandLineRules: [] - }; - } - - const denyListRules: IAutoApproveRule[] = []; - const allowListRules: IAutoApproveRule[] = []; - const allowListCommandLineRules: IAutoApproveRule[] = []; - const denyListCommandLineRules: IAutoApproveRule[] = []; - - const ignoreDefaults = this._configurationService.getValue(TerminalChatAgentToolsSettingId.IgnoreDefaultAutoApproveRules) === true; - - for (const [key, value] of Object.entries(config)) { - const defaultValue = configInspectValue?.default?.value; - const isDefaultRule = !!( - isObject(defaultValue) && - key in defaultValue && - structuralEquals((defaultValue as Record)[key], value) - ); - function checkTarget(inspectValue: Readonly | undefined): boolean { - return ( - isObject(inspectValue) && - key in inspectValue && - structuralEquals((inspectValue as Record)[key], value) - ); - } - const sourceTarget = ( - checkTarget(configInspectValue.workspaceFolder) ? ConfigurationTarget.WORKSPACE_FOLDER - : checkTarget(configInspectValue.workspaceValue) ? ConfigurationTarget.WORKSPACE - : checkTarget(configInspectValue.userRemoteValue) ? ConfigurationTarget.USER_REMOTE - : checkTarget(configInspectValue.userLocalValue) ? ConfigurationTarget.USER_LOCAL - : checkTarget(configInspectValue.userValue) ? ConfigurationTarget.USER - : checkTarget(configInspectValue.applicationValue) ? ConfigurationTarget.APPLICATION - : ConfigurationTarget.DEFAULT - ); - - // If default rules are disabled, ignore entries that come from the default config - if (ignoreDefaults && isDefaultRule && sourceTarget === ConfigurationTarget.DEFAULT) { - continue; - } - - if (typeof value === 'boolean') { - const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); - // IMPORTANT: Only true and false are used, null entries need to be ignored - if (value === true) { - allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } else if (value === false) { - denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } - } else if (typeof value === 'object' && value !== null) { - // Handle object format like { approve: true/false, matchCommandLine: true/false } - const objectValue = value as { approve?: boolean; matchCommandLine?: boolean }; - if (typeof objectValue.approve === 'boolean') { - const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); - if (objectValue.approve === true) { - if (objectValue.matchCommandLine === true) { - allowListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } else { - allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } - } else if (objectValue.approve === false) { - if (objectValue.matchCommandLine === true) { - denyListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } else { - denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); - } - } - } - } - } - - return { - denyListRules, - allowListRules, - allowListCommandLineRules, - denyListCommandLineRules - }; - } - - private _convertAutoApproveEntryToRegex(value: string): { regex: RegExp; regexCaseInsensitive: RegExp } { - const regex = this._doConvertAutoApproveEntryToRegex(value); - if (regex.flags.includes('i')) { - return { regex, regexCaseInsensitive: regex }; - } - return { regex, regexCaseInsensitive: new RegExp(regex.source, regex.flags + 'i') }; - } - - private _doConvertAutoApproveEntryToRegex(value: string): RegExp { - // If it's wrapped in `/`, it's in regex format and should be converted directly - // Support all standard JavaScript regex flags: d, g, i, m, s, u, v, y - const regexMatch = value.match(/^\/(?.+)\/(?[dgimsuvy]*)$/); - const regexPattern = regexMatch?.groups?.pattern; - if (regexPattern) { - let flags = regexMatch.groups?.flags; - // Remove global flag as it changes how the regex state works which we need to handle - // internally - if (flags) { - flags = flags.replaceAll('g', ''); - } - - // Allow .* as users expect this would match everything - if (regexPattern === '.*') { - return new RegExp(regexPattern); - - } - - try { - const regex = new RegExp(regexPattern, flags || undefined); - if (regExpLeadsToEndlessLoop(regex)) { - return neverMatchRegex; - } - - return regex; - } catch (error) { - return neverMatchRegex; - } - } - - // The empty string should be ignored, rather than approve everything - if (value === '') { - return neverMatchRegex; - } - - let sanitizedValue: string; - - // Match both path separators it if looks like a path - if (value.includes('/') || value.includes('\\')) { - // Replace path separators with placeholders first, apply standard sanitization, then - // apply special path handling - let pattern = value.replace(/[/\\]/g, '%%PATH_SEP%%'); - pattern = escapeRegExpCharacters(pattern); - pattern = pattern.replace(/%%PATH_SEP%%*/g, '[/\\\\]'); - sanitizedValue = `^(?:\\.[/\\\\])?${pattern}`; - } - - // Escape regex special characters for non-path strings - else { - sanitizedValue = escapeRegExpCharacters(value); - } - - // Regular strings should match the start of the command line and be a word boundary - return new RegExp(`^${sanitizedValue}\\b`); - } -} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts new file mode 100644 index 00000000000..09d14d75e42 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/commandFileWriteParser.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Interface for command-specific file write parsers. + * Each parser is responsible for detecting when a specific command will write to files + * (beyond simple shell redirections which are handled separately via tree-sitter queries). + */ +export interface ICommandFileWriteParser { + /** + * The name of the command this parser handles (e.g., 'sed', 'tee'). + */ + readonly commandName: string; + + /** + * Checks if this parser can handle the given command text. + * Should return true only if the command would write to files. + * @param commandText The full text of a single command (not a pipeline). + */ + canHandle(commandText: string): boolean; + + /** + * Extracts the file paths that would be written to by this command. + * Should only be called if canHandle() returns true. + * @param commandText The full text of a single command (not a pipeline). + * @returns Array of file paths that would be modified. + */ + extractFileWrites(commandText: string): string[]; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts new file mode 100644 index 00000000000..f1442781c74 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/commandParsers/sedFileWriteParser.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICommandFileWriteParser } from './commandFileWriteParser.js'; + +/** + * Parser for detecting file writes from `sed` commands using in-place editing. + * + * Handles: + * - `sed -i 's/foo/bar/' file.txt` (GNU) + * - `sed -i.bak 's/foo/bar/' file.txt` (GNU with backup suffix) + * - `sed -i '' 's/foo/bar/' file.txt` (macOS/BSD with empty backup suffix) + * - `sed --in-place 's/foo/bar/' file.txt` (GNU long form) + * - `sed --in-place=.bak 's/foo/bar/' file.txt` (GNU long form with backup) + * - `sed -I 's/foo/bar/' file.txt` (BSD case-insensitive variant) + */ +export class SedFileWriteParser implements ICommandFileWriteParser { + readonly commandName = 'sed'; + + canHandle(commandText: string): boolean { + // Check if this is a sed command + if (!commandText.match(/^sed\s+/)) { + return false; + } + + // Check for -i, -I, or --in-place flag + const inPlaceRegex = /(?:^|\s)(-[a-zA-Z]*[iI][a-zA-Z]*\S*|--in-place(?:=\S*)?|(-i|-I)\s*'[^']*'|(-i|-I)\s*"[^"]*")(?:\s|$)/; + return inPlaceRegex.test(commandText); + } + + extractFileWrites(commandText: string): string[] { + const tokens = this._tokenizeCommand(commandText); + return this._extractFileTargets(tokens); + } + + /** + * Tokenizes a command into individual arguments, handling quotes and escapes. + */ + private _tokenizeCommand(commandText: string): string[] { + const tokens: string[] = []; + let current = ''; + let inSingleQuote = false; + let inDoubleQuote = false; + let escaped = false; + + for (let i = 0; i < commandText.length; i++) { + const char = commandText[i]; + + if (escaped) { + current += char; + escaped = false; + continue; + } + + if (char === '\\' && !inSingleQuote) { + escaped = true; + current += char; + continue; + } + + if (char === '\'' && !inDoubleQuote) { + inSingleQuote = !inSingleQuote; + current += char; + continue; + } + + if (char === '"' && !inSingleQuote) { + inDoubleQuote = !inDoubleQuote; + current += char; + continue; + } + + if (/\s/.test(char) && !inSingleQuote && !inDoubleQuote) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + + current += char; + } + + if (current) { + tokens.push(current); + } + + return tokens; + } + + /** + * Extracts file targets from tokenized sed command arguments. + * Files are generally the last non-option, non-script arguments. + */ + private _extractFileTargets(tokens: string[]): string[] { + if (tokens.length === 0 || tokens[0] !== 'sed') { + return []; + } + + const files: string[] = []; + let i = 1; // Skip 'sed' + let foundScript = false; + + while (i < tokens.length) { + const token = tokens[i]; + + // Long options + if (token.startsWith('--')) { + if (token === '--in-place' || token.startsWith('--in-place=')) { + // In-place flag (already verified we have one) + i++; + continue; + } + if (token === '--expression' || token === '--file') { + // Skip the option and its argument + i += 2; + foundScript = true; + continue; + } + if (token.startsWith('--expression=') || token.startsWith('--file=')) { + i++; + foundScript = true; + continue; + } + // Other long options like --sandbox, --debug, etc. + i++; + continue; + } + + // Short options + if (token.startsWith('-') && token.length > 1 && token[1] !== '-') { + // Could be combined flags like -ni or -i.bak + const flags = token.slice(1); + + // Check if this is -i with backup suffix attached (e.g., -i.bak) + const iIndex = flags.indexOf('i'); + const IIndex = flags.indexOf('I'); + const inPlaceIndex = iIndex >= 0 ? iIndex : IIndex; + + if (inPlaceIndex >= 0 && inPlaceIndex < flags.length - 1) { + // -i.bak style - backup suffix is attached + i++; + continue; + } + + // Check if -i or -I is the last flag and next token could be backup suffix + if ((flags.endsWith('i') || flags.endsWith('I')) && i + 1 < tokens.length) { + const nextToken = tokens[i + 1]; + // macOS/BSD style: -i '' or -i "" (empty string backup suffix) + // Only treat it as a backup suffix if it's empty or looks like a backup + // extension (starts with '.' and is short). Don't match sed scripts like 's/foo/bar/'. + if (nextToken === '\'\'' || nextToken === '""') { + i += 2; + continue; + } + // Check for quoted backup suffixes like '.bak' or ".backup" + if ((nextToken.startsWith('\'') && nextToken.endsWith('\'')) || (nextToken.startsWith('"') && nextToken.endsWith('"'))) { + const unquoted = nextToken.slice(1, -1); + // Backup suffixes typically start with '.' and are short extensions + if (unquoted.startsWith('.') && unquoted.length <= 10 && !unquoted.includes('/')) { + i += 2; + continue; + } + } + } + + // Check for -e or -f which take arguments + if (flags.includes('e') || flags.includes('f')) { + const eIndex = flags.indexOf('e'); + const fIndex = flags.indexOf('f'); + const optIndex = eIndex >= 0 ? eIndex : fIndex; + + // If -e or -f is not the last character, the rest of the token is the argument + if (optIndex < flags.length - 1) { + foundScript = true; + i++; + continue; + } + + // Otherwise, the next token is the argument + foundScript = true; + i += 2; + continue; + } + + i++; + continue; + } + + // Non-option argument + if (!foundScript) { + // First non-option is the script (unless -e/-f was used) + foundScript = true; + i++; + continue; + } + + // Subsequent non-option arguments are files + // Strip surrounding quotes from file path + let file = token; + if ((file.startsWith('\'') && file.endsWith('\'')) || (file.startsWith('"') && file.endsWith('"'))) { + file = file.slice(1, -1); + } + files.push(file); + i++; + } + + return files; + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts index b4f6102c4ff..de2f1ae7056 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/basicExecuteStrategy.ts @@ -6,14 +6,14 @@ import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../../../base/common/types.js'; import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import { trackIdleOnPrompt, waitForIdle, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when shell integration is enabled, but rich command detection was not @@ -37,11 +37,11 @@ import { setupRecreatingStartMarker } from './strategyHelpers.js'; * output. We lean on the LLM to be able to differentiate the actual output from prompts and bad * output when it's not ideal. */ -export class BasicExecuteStrategy implements ITerminalExecuteStrategy { +export class BasicExecuteStrategy extends Disposable implements ITerminalExecuteStrategy { readonly type = 'basic'; - private readonly _startMarker = new MutableDisposable(); + private readonly _startMarker = this._register(new MutableDisposable()); - private readonly _onDidCreateStartMarker = new Emitter; + private readonly _onDidCreateStartMarker = this._register(new Emitter); public onDidCreateStartMarker: Event = this._onDidCreateStartMarker.event; @@ -51,6 +51,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { private readonly _commandDetection: ICommandDetectionCapability, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { + super(); } async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { @@ -92,6 +93,7 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); @@ -113,6 +115,9 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { } // Execute the command + if (commandId) { + this._log(`In basic execute strategy: skipping pre-bound command id ${commandId} because basic shell integration executes via sendText`); + } // IMPORTANT: This uses `sendText` not `runCommand` since when basic shell integration // is used as it's more common to not recognize the prompt input which would result in // ^C being sent and also to return the exit code of 130 when from the shell when that @@ -123,11 +128,25 @@ export class BasicExecuteStrategy implements ITerminalExecuteStrategy { // Wait for the next end execution event - note that this may not correspond to the actual // execution requested this._log('Waiting for done event'); - const onDoneResult = await onDone; + const onDoneResult = await Promise.race([onDone, alternateBufferPromise.then(() => ({ type: 'alternateBuffer' } as const))]); if (onDoneResult && onDoneResult.type === 'disposal') { throw new Error('The terminal was closed'); } + if (onDoneResult && onDoneResult.type === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true + }; + } const finishedCommand = onDoneResult && onDoneResult.type === 'success' ? onDoneResult.command : undefined; + if (finishedCommand) { + this._log(`Finished command id=${finishedCommand.id ?? 'none'} for requested=${commandId ?? 'none'}`); + } else if (commandId) { + this._log(`No finished command surfaced for requested=${commandId}`); + } // Wait for the terminal to idle this._log('Waiting for idle'); diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts index a490ac06095..f95f297a958 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/executeStrategy.ts @@ -6,11 +6,11 @@ import { DeferredPromise, RunOnceScheduler } from '../../../../../../base/common/async.js'; import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; import type { Event } from '../../../../../../base/common/event.js'; -import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; +import { DisposableStore, type IDisposable } from '../../../../../../base/common/lifecycle.js'; import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; -export interface ITerminalExecuteStrategy { +export interface ITerminalExecuteStrategy extends IDisposable { readonly type: 'rich' | 'basic' | 'none'; /** * Executes a command line and gets a result designed to be passed directly to an LLM. The @@ -29,6 +29,7 @@ export interface ITerminalExecuteStrategyResult { additionalInformation?: string; exitCode?: number; error?: string; + didEnterAltBuffer?: boolean; } export async function waitForIdle(onData: Event, idleDurationMs: number): Promise { @@ -99,28 +100,6 @@ export function detectsCommonPromptPattern(cursorLine: string): IPromptDetection return { detected: false, reason: `No common prompt pattern found in last line: "${cursorLine}"` }; } - -// PowerShell-style multi-option line (supports [?] Help and optional default suffix) ending in whitespace -const PS_CONFIRM_RE = /\s*(?:\[[^\]]\]\s+[^\[]+\s*)+(?:\(default is\s+"[^"]+"\):)?\s+$/; - -// Bracketed/parenthesized yes/no pairs at end of line: (y/n), [Y/n], (yes/no), [no/yes] -const YN_PAIRED_RE = /(?:\(|\[)\s*(?:y(?:es)?\s*\/\s*n(?:o)?|n(?:o)?\s*\/\s*y(?:es)?)\s*(?:\]|\))\s+$/i; - -// Same as YN_PAIRED_RE but allows a preceding '?' or ':' and optional wrappers e.g. "Continue? (y/n)" or "Overwrite: [yes/no]" -const YN_AFTER_PUNCT_RE = /[?:]\s*(?:\(|\[)?\s*y(?:es)?\s*\/\s*n(?:o)?\s*(?:\]|\))?\s+$/i; - -// Confirmation prompts ending with (y) e.g. "Ok to proceed? (y)" -const CONFIRM_Y_RE = /\(y\)\s*$/i; - -const LINE_ENDS_WITH_COLON_RE = /:\s*$/; - -const END = /\(END\)$/; - -export function detectsInputRequiredPattern(cursorLine: string): boolean { - return PS_CONFIRM_RE.test(cursorLine) || YN_PAIRED_RE.test(cursorLine) || YN_AFTER_PUNCT_RE.test(cursorLine) || CONFIRM_Y_RE.test(cursorLine) || LINE_ENDS_WITH_COLON_RE.test(cursorLine.trim()) || END.test(cursorLine); -} - - /** * Enhanced version of {@link waitForIdle} that uses prompt detection heuristics. After the terminal * idles for the specified period, checks if the terminal's cursor line looks like a common prompt. @@ -187,6 +166,17 @@ export async function trackIdleOnPrompt( const scheduler = store.add(new RunOnceScheduler(() => { idleOnPrompt.complete(); }, idleDurationMs)); + let state: TerminalState = TerminalState.Initial; + + // Fallback in case prompt sequences are not seen but the terminal goes idle. + const promptFallbackScheduler = store.add(new RunOnceScheduler(() => { + if (state === TerminalState.Executing || state === TerminalState.PromptAfterExecuting) { + promptFallbackScheduler.cancel(); + return; + } + state = TerminalState.PromptAfterExecuting; + scheduler.schedule(); + }, 1000)); // Only schedule when a prompt sequence (A) is seen after an execute sequence (C). This prevents // cases where the command is executed before the prompt is written. While not perfect, sitting // on an A without a C following shortly after is a very good indicator that the command is done @@ -199,7 +189,6 @@ export async function trackIdleOnPrompt( Executing, PromptAfterExecuting, } - let state: TerminalState = TerminalState.Initial; store.add(onData(e => { // Update state // p10k fires C as `133;C;` @@ -217,9 +206,15 @@ export async function trackIdleOnPrompt( } // Re-schedule on every data event as we're tracking data idle if (state === TerminalState.PromptAfterExecuting) { + promptFallbackScheduler.cancel(); scheduler.schedule(); } else { scheduler.cancel(); + if (state === TerminalState.Initial || state === TerminalState.Prompt) { + promptFallbackScheduler.schedule(); + } else { + promptFallbackScheduler.cancel(); + } } })); return idleOnPrompt.p; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts index 8dc54fd8a53..ddc2a7f8939 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/noneExecuteStrategy.ts @@ -6,12 +6,12 @@ import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import { waitForIdle, waitForIdleWithPromptHeuristics, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; import { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when no shell integration is available. There are very few extension APIs @@ -19,12 +19,12 @@ import { setupRecreatingStartMarker } from './strategyHelpers.js'; * with `sendText` instead of `shellIntegration.executeCommand` and relying on idle events instead * of execution events. */ -export class NoneExecuteStrategy implements ITerminalExecuteStrategy { +export class NoneExecuteStrategy extends Disposable implements ITerminalExecuteStrategy { readonly type = 'none'; - private readonly _startMarker = new MutableDisposable(); + private readonly _startMarker = this._register(new MutableDisposable()); - private readonly _onDidCreateStartMarker = new Emitter; + private readonly _onDidCreateStartMarker = this._register(new Emitter); public onDidCreateStartMarker: Event = this._onDidCreateStartMarker.event; constructor( @@ -32,6 +32,7 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { private readonly _hasReceivedUserInput: () => boolean, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { + super(); } async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { @@ -47,6 +48,7 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); // Wait for the terminal to idle before executing the command this._log('Waiting for idle'); @@ -79,7 +81,21 @@ export class NoneExecuteStrategy implements ITerminalExecuteStrategy { // Assume the command is done when it's idle this._log('Waiting for idle with prompt heuristics'); - const promptResult = await waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000); + const promptResultOrAltBuffer = await Promise.race([ + waitForIdleWithPromptHeuristics(this._instance.onData, this._instance, 1000, 10000), + alternateBufferPromise.then(() => 'alternateBuffer' as const) + ]); + if (promptResultOrAltBuffer === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + additionalInformation: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true, + }; + } + const promptResult = promptResultOrAltBuffer; this._log(`Prompt detection result: ${promptResult.detected ? 'detected' : 'not detected'} - ${promptResult.reason}`); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts index 8417bcb01a8..b68f7a499d4 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/richExecuteStrategy.ts @@ -6,14 +6,14 @@ import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { CancellationError } from '../../../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; -import { DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../../../base/common/lifecycle.js'; import { isNumber } from '../../../../../../base/common/types.js'; import type { ICommandDetectionCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js'; import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js'; import type { ITerminalInstance } from '../../../../terminal/browser/terminal.js'; import { trackIdleOnPrompt, type ITerminalExecuteStrategy, type ITerminalExecuteStrategyResult } from './executeStrategy.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; -import { setupRecreatingStartMarker } from './strategyHelpers.js'; +import { createAltBufferPromise, setupRecreatingStartMarker } from './strategyHelpers.js'; /** * This strategy is used when the terminal has rich shell integration/command detection is @@ -22,11 +22,11 @@ import { setupRecreatingStartMarker } from './strategyHelpers.js'; * wrong in this state, minimal verification is done in this mode since rich command detection is a * strong signal that it's behaving correctly. */ -export class RichExecuteStrategy implements ITerminalExecuteStrategy { +export class RichExecuteStrategy extends Disposable implements ITerminalExecuteStrategy { readonly type = 'rich'; - private readonly _startMarker = new MutableDisposable(); + private readonly _startMarker = this._register(new MutableDisposable()); - private readonly _onDidCreateStartMarker = new Emitter; + private readonly _onDidCreateStartMarker = this._register(new Emitter); public onDidCreateStartMarker: Event = this._onDidCreateStartMarker.event; constructor( @@ -34,6 +34,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { private readonly _commandDetection: ICommandDetectionCapability, @ITerminalLogService private readonly _logService: ITerminalLogService, ) { + super(); } async execute(commandLine: string, token: CancellationToken, commandId?: string): Promise { @@ -45,6 +46,7 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { if (!xterm) { throw new Error('Xterm is not available'); } + const alternateBufferPromise = createAltBufferPromise(xterm, store, this._log.bind(this)); const onDone = Promise.race([ Event.toPromise(this._commandDetection.onCommandFinished, store).then(e => { @@ -80,10 +82,19 @@ export class RichExecuteStrategy implements ITerminalExecuteStrategy { // Wait for the terminal to idle this._log('Waiting for done event'); - const onDoneResult = await onDone; + const onDoneResult = await Promise.race([onDone, alternateBufferPromise.then(() => ({ type: 'alternateBuffer' } as const))]); if (onDoneResult && onDoneResult.type === 'disposal') { throw new Error('The terminal was closed'); } + if (onDoneResult && onDoneResult.type === 'alternateBuffer') { + this._log('Detected alternate buffer entry, skipping output capture'); + return { + output: undefined, + exitCode: undefined, + error: 'alternateBuffer', + didEnterAltBuffer: true + }; + } const finishedCommand = onDoneResult && onDoneResult.type === 'success' ? onDoneResult.command : undefined; if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts index 3ab743aa2b6..5c63b233ec2 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/executeStrategy/strategyHelpers.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DeferredPromise } from '../../../../../../base/common/async.js'; import { DisposableStore, MutableDisposable, toDisposable, type IDisposable } from '../../../../../../base/common/lifecycle.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; @@ -43,3 +44,29 @@ export function setupRecreatingStartMarker( })); store.add(startMarker); } + +export function createAltBufferPromise( + xterm: { raw: { buffer: { active: unknown; alternate: unknown; onBufferChange: (callback: () => void) => IDisposable } } }, + store: DisposableStore, + log?: (message: string) => void, +): Promise { + const deferred = new DeferredPromise(); + const complete = () => { + if (!deferred.isSettled) { + log?.('Detected alternate buffer entry'); + deferred.complete(); + } + }; + + if (xterm.raw.buffer.active === xterm.raw.buffer.alternate) { + complete(); + } else { + store.add(xterm.raw.buffer.onBufferChange(() => { + if (xterm.raw.buffer.active === xterm.raw.buffer.alternate) { + complete(); + } + })); + } + + return deferred.p; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts index 318a597ca1e..8741a9f1d39 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/outputHelpers.ts @@ -5,6 +5,9 @@ import { ITerminalInstance } from '../../../terminal/browser/terminal.js'; import type { IMarker as IXtermMarker } from '@xterm/xterm'; +import { truncateOutputKeepingTail } from './runInTerminalHelpers.js'; + +const MAX_OUTPUT_LENGTH = 16000; export function getOutput(instance: ITerminalInstance, startMarker?: IXtermMarker): string { if (!instance.xterm || !instance.xterm.raw) { @@ -21,8 +24,8 @@ export function getOutput(instance: ITerminalInstance, startMarker?: IXtermMarke } let output = lines.join('\n'); - if (output.length > 16000) { - output = output.slice(-16000); + if (output.length > MAX_OUTPUT_LENGTH) { + output = truncateOutputKeepingTail(output, MAX_OUTPUT_LENGTH); } return output; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts index 19330fc97ca..7cbf89ca9f3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/runInTerminalHelpers.ts @@ -9,9 +9,10 @@ import { posix as pathPosix, win32 as pathWin32 } from '../../../../../base/comm import { OperatingSystem } from '../../../../../base/common/platform.js'; import { escapeRegExpCharacters, removeAnsiEscapeCodes } from '../../../../../base/common/strings.js'; import { localize } from '../../../../../nls.js'; -import type { TerminalNewAutoApproveButtonData } from '../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; -import type { ToolConfirmationAction } from '../../../chat/common/languageModelToolsService.js'; -import type { ICommandApprovalResultWithReason } from './commandLineAutoApprover.js'; +import type { TerminalNewAutoApproveButtonData } from '../../../chat/browser/widget/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js'; +import type { ToolConfirmationAction } from '../../../chat/common/tools/languageModelToolsService.js'; +import type { ICommandApprovalResultWithReason } from './tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.js'; +import { isAutoApproveRule } from './tools/commandLineAnalyzer/commandLineAnalyzer.js'; export function isPowerShell(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { @@ -32,6 +33,13 @@ export function isZsh(envShell: string, os: OperatingSystem): boolean { return /^zsh$/.test(pathPosix.basename(envShell)); } +export function isBash(envShell: string, os: OperatingSystem): boolean { + if (os === OperatingSystem.Windows) { + return /^bash(?:\.exe)?$/i.test(pathWin32.basename(envShell)); + } + return /^bash$/.test(pathPosix.basename(envShell)); +} + export function isFish(envShell: string, os: OperatingSystem): boolean { if (os === OperatingSystem.Windows) { return /^fish(?:\.exe)?$/i.test(pathWin32.basename(envShell)); @@ -41,7 +49,20 @@ export function isFish(envShell: string, os: OperatingSystem): boolean { // Maximum output length to prevent context overflow const MAX_OUTPUT_LENGTH = 60000; // ~60KB limit to keep context manageable -const TRUNCATION_MESSAGE = '\n\n[... MIDDLE OF OUTPUT TRUNCATED ...]\n\n'; +export const TRUNCATION_MESSAGE = '\n\n[... PREVIOUS OUTPUT TRUNCATED ...]\n\n'; + +export function truncateOutputKeepingTail(output: string, maxLength: number): string { + if (output.length <= maxLength) { + return output; + } + const truncationMessageLength = TRUNCATION_MESSAGE.length; + if (truncationMessageLength >= maxLength) { + return TRUNCATION_MESSAGE.slice(TRUNCATION_MESSAGE.length - maxLength); + } + const availableLength = maxLength - truncationMessageLength; + const endPortion = output.slice(-availableLength); + return TRUNCATION_MESSAGE + endPortion; +} export function sanitizeTerminalOutput(output: string): string { let sanitized = removeAnsiEscapeCodes(output) @@ -50,15 +71,7 @@ export function sanitizeTerminalOutput(output: string): string { // Truncate if output is too long to prevent context overflow if (sanitized.length > MAX_OUTPUT_LENGTH) { - const truncationMessageLength = TRUNCATION_MESSAGE.length; - const availableLength = MAX_OUTPUT_LENGTH - truncationMessageLength; - const startLength = Math.floor(availableLength * 0.4); // Keep 40% from start - const endLength = availableLength - startLength; // Keep 60% from end - - const startPortion = sanitized.substring(0, startLength); - const endPortion = sanitized.substring(sanitized.length - endLength); - - sanitized = startPortion + TRUNCATION_MESSAGE + endPortion; + sanitized = truncateOutputKeepingTail(sanitized, MAX_OUTPUT_LENGTH); } return sanitized; @@ -94,33 +107,56 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str ]); // Commands where we want to suggest the sub-command (eg. `foo bar` instead of `foo`) - const commandsWithSubcommands = new Set(['git', 'npm', 'yarn', 'docker', 'kubectl', 'cargo', 'dotnet', 'mvn', 'gradle']); + const commandsWithSubcommands = new Set(['git', 'npm', 'npx', 'yarn', 'docker', 'kubectl', 'cargo', 'dotnet', 'mvn', 'gradle']); // Commands where we want to suggest the sub-command of a sub-command (eg. `foo bar baz` // instead of `foo`) const commandsWithSubSubCommands = new Set(['npm run', 'yarn run']); + // Helper function to find the first non-flag argument after a given index + const findNextNonFlagArg = (parts: string[], startIndex: number): number | undefined => { + for (let i = startIndex; i < parts.length; i++) { + if (!parts[i].startsWith('-')) { + return i; + } + } + return undefined; + }; + // For each unapproved sub-command (within the overall command line), decide whether to // suggest new rules for the command, a sub-command, a sub-command of a sub-command or to // not suggest at all. + // + // This includes support for detecting flags between the commands, so `mvn -DskipIT test a` + // would suggest `mvn -DskipIT test` as that's more useful than only suggesting the exact + // command line. const subCommandsToSuggest = Array.from(new Set(coalesce(unapprovedSubCommands.map(command => { const parts = command.trim().split(/\s+/); const baseCommand = parts[0].toLowerCase(); - const baseSubCommand = parts.length > 1 ? `${parts[0]} ${parts[1]}`.toLowerCase() : ''; // Security check: Never suggest auto-approval for dangerous interpreter commands if (neverAutoApproveCommands.has(baseCommand)) { return undefined; } - if (commandsWithSubSubCommands.has(baseSubCommand)) { - if (parts.length >= 3 && !parts[2].startsWith('-')) { - return `${parts[0]} ${parts[1]} ${parts[2]}`; - } - return undefined; - } else if (commandsWithSubcommands.has(baseCommand)) { - if (parts.length >= 2 && !parts[1].startsWith('-')) { - return `${parts[0]} ${parts[1]}`; + if (commandsWithSubcommands.has(baseCommand)) { + // Find the first non-flag argument after the command + const subCommandIndex = findNextNonFlagArg(parts, 1); + if (subCommandIndex !== undefined) { + // Check if this is a sub-sub-command case + const baseSubCommand = `${parts[0]} ${parts[subCommandIndex]}`.toLowerCase(); + if (commandsWithSubSubCommands.has(baseSubCommand)) { + // Look for the second non-flag argument after the first subcommand + const subSubCommandIndex = findNextNonFlagArg(parts, subCommandIndex + 1); + if (subSubCommandIndex !== undefined) { + // Include everything from command to sub-sub-command (including flags) + return parts.slice(0, subSubCommandIndex + 1).join(' '); + } + return undefined; + } else { + // Include everything from command to subcommand (including flags) + return parts.slice(0, subCommandIndex + 1).join(' '); + } } return undefined; } else { @@ -131,24 +167,50 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str if (subCommandsToSuggest.length > 0) { let subCommandLabel: string; if (subCommandsToSuggest.length === 1) { - subCommandLabel = localize('autoApprove.baseCommandSingle', 'Always Allow Command: {0}', subCommandsToSuggest[0]); + subCommandLabel = `\`${subCommandsToSuggest[0]} \u2026\``; } else { - const commandSeparated = subCommandsToSuggest.join(', '); - subCommandLabel = localize('autoApprove.baseCommand', 'Always Allow Commands: {0}', commandSeparated); + subCommandLabel = `Commands ${subCommandsToSuggest.map(e => `\`${e} \u2026\``).join(', ')}`; } actions.push({ - label: subCommandLabel, + label: `Allow ${subCommandLabel} in this Session`, data: { type: 'newRule', rule: subCommandsToSuggest.map(key => ({ key, - value: true + value: true, + scope: 'session' + })) + } satisfies TerminalNewAutoApproveButtonData + }); + actions.push({ + label: `Allow ${subCommandLabel} in this Workspace`, + data: { + type: 'newRule', + rule: subCommandsToSuggest.map(key => ({ + key, + value: true, + scope: 'workspace' + })) + } satisfies TerminalNewAutoApproveButtonData + }); + actions.push({ + label: `Always Allow ${subCommandLabel}`, + data: { + type: 'newRule', + rule: subCommandsToSuggest.map(key => ({ + key, + value: true, + scope: 'user' })) } satisfies TerminalNewAutoApproveButtonData }); } + if (actions.length > 0) { + actions.push(new Separator()); + } + // Allow exact command line, don't do this if it's just the first sub-command's first // word or if it's an exact match for special sub-commands const firstSubcommandFirstWord = unapprovedSubCommands.length > 0 ? unapprovedSubCommands[0].split(' ')[0] : ''; @@ -157,6 +219,34 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str !commandsWithSubcommands.has(commandLine) && !commandsWithSubSubCommands.has(commandLine) ) { + actions.push({ + label: localize('autoApprove.exactCommand1', 'Allow Exact Command Line in this Session'), + data: { + type: 'newRule', + rule: { + key: `/^${escapeRegExpCharacters(commandLine)}$/`, + value: { + approve: true, + matchCommandLine: true + }, + scope: 'session' + } + } satisfies TerminalNewAutoApproveButtonData + }); + actions.push({ + label: localize('autoApprove.exactCommand2', 'Allow Exact Command Line in this Workspace'), + data: { + type: 'newRule', + rule: { + key: `/^${escapeRegExpCharacters(commandLine)}$/`, + value: { + approve: true, + matchCommandLine: true + }, + scope: 'workspace' + } + } satisfies TerminalNewAutoApproveButtonData + }); actions.push({ label: localize('autoApprove.exactCommand', 'Always Allow Exact Command Line'), data: { @@ -166,7 +256,8 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str value: { approve: true, matchCommandLine: true - } + }, + scope: 'user' } } satisfies TerminalNewAutoApproveButtonData }); @@ -177,6 +268,18 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str actions.push(new Separator()); } + + // Allow all commands for this session + actions.push({ + label: localize('allowSession', 'Allow All Commands in this Session'), + tooltip: localize('allowSessionTooltip', 'Allow this tool to run in this session without confirmation.'), + data: { + type: 'sessionApproval' + } satisfies TerminalNewAutoApproveButtonData + }); + + actions.push(new Separator()); + // Always show configure option actions.push({ label: localize('autoApprove.configure', 'Configure Auto Approve...'), @@ -190,6 +293,42 @@ export function generateAutoApproveActions(commandLine: string, subCommands: str export function dedupeRules(rules: ICommandApprovalResultWithReason[]): ICommandApprovalResultWithReason[] { return rules.filter((result, index, array) => { - return result.rule && array.findIndex(r => r.rule && r.rule.sourceText === result.rule!.sourceText) === index; + if (!isAutoApproveRule(result.rule)) { + return false; + } + const sourceText = result.rule.sourceText; + return array.findIndex(r => isAutoApproveRule(r.rule) && r.rule.sourceText === sourceText) === index; }); } + +export interface IExtractedCdPrefix { + /** The directory path that was extracted from the cd command */ + directory: string; + /** The command to run after the cd */ + command: string; +} + +/** + * Extracts a cd prefix from a command line, returning the directory and remaining command. + * Does not check if the directory matches the current cwd - just extracts the pattern. + */ +export function extractCdPrefix(commandLine: string, shell: string, os: OperatingSystem): IExtractedCdPrefix | undefined { + const isPwsh = isPowerShell(shell, os); + + const cdPrefixMatch = commandLine.match( + isPwsh + ? /^(?:cd(?: \/d)?|Set-Location(?: -Path)?) (?[^\s]+) ?(?:&&|;)\s+(?.+)$/i + : /^cd (?[^\s]+) &&\s+(?.+)$/ + ); + const cdDir = cdPrefixMatch?.groups?.dir; + const cdSuffix = cdPrefixMatch?.groups?.suffix; + if (cdDir && cdSuffix) { + // Remove any surrounding quotes + let cdDirPath = cdDir; + if (cdDirPath.startsWith('"') && cdDirPath.endsWith('"')) { + cdDirPath = cdDirPath.slice(1, -1); + } + return { directory: cdDirPath, command: cdSuffix }; + } + return undefined; +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts index 29971d1984d..2c592de63d9 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/taskHelpers.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { timeout } from '../../../../../base/common/async.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { IStringDictionary } from '../../../../../base/common/collections.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; @@ -12,7 +13,7 @@ import { Range } from '../../../../../editor/common/core/range.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IMarkerData } from '../../../../../platform/markers/common/markers.js'; -import { IToolInvocationContext, ToolProgress } from '../../../chat/common/languageModelToolsService.js'; +import { IToolInvocationContext, ToolProgress } from '../../../chat/common/tools/languageModelToolsService.js'; import { ConfiguringTask, ITaskDependency, Task } from '../../../tasks/common/tasks.js'; import { ITaskService } from '../../../tasks/common/taskService.js'; import { ITerminalInstance } from '../../../terminal/browser/terminal.js'; @@ -21,6 +22,7 @@ import { OutputMonitor } from './tools/monitoring/outputMonitor.js'; import { IExecution, IPollingResult, OutputMonitorState } from './tools/monitoring/types.js'; import { Event } from '../../../../../base/common/event.js'; import { IReconnectionTaskData } from '../../../tasks/browser/terminalTaskSystem.js'; +import { isString } from '../../../../../base/common/types.js'; export function getTaskDefinition(id: string) { @@ -42,11 +44,31 @@ export function getTaskRepresentation(task: IConfiguredTask | Task): string { } else if ('script' in task && task.script) { return task.script; } else if ('command' in task && task.command) { - return typeof task.command === 'string' ? task.command : task.command.name?.toString() || ''; + return isString(task.command) ? task.command : task.command.name?.toString() || ''; } return ''; } +export function getTaskKey(task: Task): string { + return task.getKey() ?? task.getMapKey(); +} + +export function tasksMatch(a: Task, b: Task): boolean { + if (!a || !b) { + return false; + } + + if (getTaskKey(a) === getTaskKey(b)) { + return true; + } + + if (a.getCommonTaskId?.() === b.getCommonTaskId?.()) { + return true; + } + + return a._id === b._id; +} + export async function getTaskForTool(id: string | undefined, taskDefinition: { taskLabel?: string; taskType?: string }, workspaceFolder: string, configurationService: IConfigurationService, taskService: ITaskService, allowParentTask?: boolean): Promise { let index = 0; let task: IConfiguredTask | undefined; @@ -134,7 +156,7 @@ export async function resolveDependencyTasks(parentTask: Task, workspaceFolder: return undefined; } const dependencyTasks = await Promise.all(parentTask.configurationProperties.dependsOn.map(async (dep: ITaskDependency) => { - const depId: string | undefined = typeof dep.task === 'string' ? dep.task : dep.task?._key; + const depId: string | undefined = isString(dep.task) ? dep.task : dep.task?._key; if (!depId) { return undefined; } @@ -155,7 +177,8 @@ export async function collectTerminalResults( token: CancellationToken, disposableStore: DisposableStore, isActive?: (task: Task) => Promise, - dependencyTasks?: Task[] + dependencyTasks?: Task[], + taskService?: ITaskService ): Promise t.shellLaunchConfig.name ?? t.title ?? 'unknown'); + progress.report({ message: new MarkdownString(`Checking output for ${terminalNames.map(n => `\`${n}\``).join(', ')}`) }); + const terminalPromises = terminals.map(async (instance) => { let terminalTask = task; // For composite tasks, find the actual dependency task running in this terminal @@ -218,10 +243,26 @@ export async function collectTerminalResults( sessionId: invocationContext.sessionId }; + // For tasks with problem matchers, wait until the task becomes busy before creating the output monitor + if (terminalTask.configurationProperties.problemMatchers && terminalTask.configurationProperties.problemMatchers.length > 0 && taskService) { + const maxWaitTime = 1000; // Wait up to 1 second + const startTime = Date.now(); + while (!token.isCancellationRequested && Date.now() - startTime < maxWaitTime) { + const busyTasks = await taskService.getBusyTasks(); + if (busyTasks.some(t => tasksMatch(t, terminalTask))) { + break; + } + await timeout(100); + } + } + const outputMonitor = disposableStore.add(instantiationService.createInstance(OutputMonitor, execution, taskProblemPollFn, invocationContext, token, task._label)); - await Event.toPromise(outputMonitor.onDidFinishCommand); + await Promise.race([ + Event.toPromise(outputMonitor.onDidFinishCommand), + Event.toPromise(token.onCancellationRequested as Event) + ]); const pollingResult = outputMonitor.pollingResult; - results.push({ + return { name: instance.shellLaunchConfig.name ?? instance.title ?? 'unknown', output: pollingResult?.output ?? '', pollDurationMs: pollingResult?.pollDurationMs ?? 0, @@ -235,8 +276,11 @@ export async function collectTerminalResults( inputToolManualShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolManualShownCount ?? 0, inputToolFreeFormInputShownCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputShownCount ?? 0, inputToolFreeFormInputCount: outputMonitor.outputMonitorTelemetryCounters.inputToolFreeFormInputCount ?? 0, - }); - } + }; + }); + + const parallelResults = await Promise.all(terminalPromises); + results.push(...parallelResults); return results; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts index b277943ed58..d7e2dea9c4e 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/terminal.chatAgentTools.contribution.ts @@ -5,7 +5,6 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isNumber } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { MenuId } from '../../../../../platform/actions/common/actions.js'; @@ -14,17 +13,17 @@ import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contex import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js'; import { registerWorkbenchContribution2, WorkbenchPhase, type IWorkbenchContribution } from '../../../../common/contributions.js'; -import { IWorkbenchLayoutService } from '../../../../services/layout/browser/layoutService.js'; -import { IViewsService } from '../../../../services/views/common/viewsService.js'; -import { IChatWidgetService, showChatView } from '../../../chat/browser/chat.js'; -import { ChatContextKeys } from '../../../chat/common/chatContextKeys.js'; -import { ILanguageModelToolsService, ToolDataSource, VSCodeToolReference } from '../../../chat/common/languageModelToolsService.js'; +import { IChatWidgetService } from '../../../chat/browser/chat.js'; +import { ChatContextKeys } from '../../../chat/common/actions/chatContextKeys.js'; +import { ILanguageModelToolsService } from '../../../chat/common/tools/languageModelToolsService.js'; import { registerActiveInstanceAction, sharedWhenClause } from '../../../terminal/browser/terminalActions.js'; import { TerminalContextMenuGroup } from '../../../terminal/browser/terminalMenus.js'; import { TerminalContextKeys } from '../../../terminal/common/terminalContextKey.js'; import { TerminalChatAgentToolsCommandId } from '../common/terminal.chatAgentTools.js'; import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; +import { AwaitTerminalTool, AwaitTerminalToolData } from './tools/awaitTerminalTool.js'; import { GetTerminalLastCommandTool, GetTerminalLastCommandToolData } from './tools/getTerminalLastCommandTool.js'; +import { KillTerminalTool, KillTerminalToolData } from './tools/killTerminalTool.js'; import { GetTerminalOutputTool, GetTerminalOutputToolData } from './tools/getTerminalOutputTool.js'; import { GetTerminalSelectionTool, GetTerminalSelectionToolData } from './tools/getTerminalSelectionTool.js'; import { ConfirmTerminalCommandTool, ConfirmTerminalCommandToolData } from './tools/runInTerminalConfirmationTool.js'; @@ -32,6 +31,14 @@ import { RunInTerminalTool, createRunInTerminalToolData } from './tools/runInTer import { CreateAndRunTaskTool, CreateAndRunTaskToolData } from './tools/task/createAndRunTaskTool.js'; import { GetTaskOutputTool, GetTaskOutputToolData } from './tools/task/getTaskOutputTool.js'; import { RunTaskTool, RunTaskToolData } from './tools/task/runTaskTool.js'; +import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js'; +import { ITerminalSandboxService, TerminalSandboxService } from '../common/terminalSandboxService.js'; + +// #region Services + +registerSingleton(ITerminalSandboxService, TerminalSandboxService, InstantiationType.Delayed); + +// #endregion Services class ShellIntegrationTimeoutMigrationContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.shellIntegrationTimeoutMigration'; @@ -52,6 +59,22 @@ class ShellIntegrationTimeoutMigrationContribution extends Disposable implements } registerWorkbenchContribution2(ShellIntegrationTimeoutMigrationContribution.ID, ShellIntegrationTimeoutMigrationContribution, WorkbenchPhase.Eventually); +class OutputLocationMigrationContribution extends Disposable implements IWorkbenchContribution { + static readonly ID = 'terminal.outputLocationMigration'; + + constructor( + @IConfigurationService configurationService: IConfigurationService, + ) { + super(); + // Migrate legacy 'none' value to 'chat' + const currentValue = configurationService.getValue(TerminalChatAgentToolsSettingId.OutputLocation); + if (currentValue === 'none') { + configurationService.updateValue(TerminalChatAgentToolsSettingId.OutputLocation, 'chat'); + } + } +} +registerWorkbenchContribution2(OutputLocationMigrationContribution.ID, OutputLocationMigrationContribution, WorkbenchPhase.Eventually); + class ChatAgentToolsContribution extends Disposable implements IWorkbenchContribution { static readonly ID = 'terminal.chatAgentTools'; @@ -68,17 +91,20 @@ class ChatAgentToolsContribution extends Disposable implements IWorkbenchContrib this._register(toolsService.registerTool(ConfirmTerminalCommandToolData, confirmTerminalCommandTool)); const getTerminalOutputTool = instantiationService.createInstance(GetTerminalOutputTool); this._register(toolsService.registerTool(GetTerminalOutputToolData, getTerminalOutputTool)); + this._register(toolsService.executeToolSet.addTool(GetTerminalOutputToolData)); + + const awaitTerminalTool = instantiationService.createInstance(AwaitTerminalTool); + this._register(toolsService.registerTool(AwaitTerminalToolData, awaitTerminalTool)); + this._register(toolsService.executeToolSet.addTool(AwaitTerminalToolData)); - const runCommandsToolSet = this._register(toolsService.createToolSet(ToolDataSource.Internal, 'runCommands', VSCodeToolReference.runCommands, { - icon: ThemeIcon.fromId(Codicon.terminal.id), - description: localize('toolset.runCommands', 'Runs commands in the terminal') - })); - runCommandsToolSet.addTool(GetTerminalOutputToolData); + const killTerminalTool = instantiationService.createInstance(KillTerminalTool); + this._register(toolsService.registerTool(KillTerminalToolData, killTerminalTool)); + this._register(toolsService.executeToolSet.addTool(KillTerminalToolData)); instantiationService.invokeFunction(createRunInTerminalToolData).then(runInTerminalToolData => { const runInTerminalTool = instantiationService.createInstance(RunInTerminalTool); this._register(toolsService.registerTool(runInTerminalToolData, runInTerminalTool)); - runCommandsToolSet.addTool(runInTerminalToolData); + this._register(toolsService.executeToolSet.addTool(runInTerminalToolData)); }); const getTerminalSelectionTool = instantiationService.createInstance(GetTerminalSelectionTool); @@ -87,8 +113,8 @@ class ChatAgentToolsContribution extends Disposable implements IWorkbenchContrib const getTerminalLastCommandTool = instantiationService.createInstance(GetTerminalLastCommandTool); this._register(toolsService.registerTool(GetTerminalLastCommandToolData, getTerminalLastCommandTool)); - runCommandsToolSet.addTool(GetTerminalSelectionToolData); - runCommandsToolSet.addTool(GetTerminalLastCommandToolData); + this._register(toolsService.readToolSet.addTool(GetTerminalSelectionToolData)); + this._register(toolsService.readToolSet.addTool(GetTerminalLastCommandToolData)); // #endregion @@ -102,13 +128,9 @@ class ChatAgentToolsContribution extends Disposable implements IWorkbenchContrib const createAndRunTaskTool = instantiationService.createInstance(CreateAndRunTaskTool); this._register(toolsService.registerTool(CreateAndRunTaskToolData, createAndRunTaskTool)); - - const runTasksToolSet = this._register(toolsService.createToolSet(ToolDataSource.Internal, 'runTasks', 'runTasks', { - description: localize('toolset.runTasks', 'Runs tasks and gets their output for your workspace'), - })); - runTasksToolSet.addTool(RunTaskToolData); - runTasksToolSet.addTool(GetTaskOutputToolData); - runTasksToolSet.addTool(CreateAndRunTaskToolData); + this._register(toolsService.executeToolSet.addTool(RunTaskToolData)); + this._register(toolsService.executeToolSet.addTool(CreateAndRunTaskToolData)); + this._register(toolsService.readToolSet.addTool(GetTaskOutputToolData)); // #endregion } @@ -132,16 +154,14 @@ registerActiveInstanceAction({ }, ], run: async (activeInstance, _c, accessor) => { - const viewsService = accessor.get(IViewsService); const chatWidgetService = accessor.get(IChatWidgetService); - const layoutService = accessor.get(IWorkbenchLayoutService); const selection = activeInstance.selection; if (!selection) { return; } - const chatView = chatWidgetService.lastFocusedWidget || await showChatView(viewsService, layoutService); + const chatView = chatWidgetService.lastFocusedWidget ?? await chatWidgetService.revealWidget(); if (!chatView) { return; } diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts index a018f0aeddb..c87a59ab3b3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/toolTerminalCreator.ts @@ -9,14 +9,17 @@ import { Codicon } from '../../../../../base/common/codicons.js'; import { CancellationError } from '../../../../../base/common/errors.js'; import { Event } from '../../../../../base/common/event.js'; import { DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js'; +import { OperatingSystem } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { isNumber, isObject } from '../../../../../base/common/types.js'; +import { hasKey, isNumber, isObject, isString } from '../../../../../base/common/types.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { PromptInputState } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js'; import { ITerminalLogService, ITerminalProfile, TerminalSettingId, type IShellLaunchConfig } from '../../../../../platform/terminal/common/terminal.js'; import { ITerminalService, type ITerminalInstance } from '../../../terminal/browser/terminal.js'; import { getShellIntegrationTimeout } from '../../../terminal/common/terminalEnvironment.js'; +import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js'; +import { isBash, isFish, isPowerShell, isZsh } from './runInTerminalHelpers.js'; const enum ShellLaunchType { Unknown = 0, @@ -34,6 +37,7 @@ export interface IToolTerminal { instance: ITerminalInstance; shellIntegrationQuality: ShellIntegrationQuality; receivedUserInput?: boolean; + isBackground?: boolean; } export class ToolTerminalCreator { @@ -50,8 +54,8 @@ export class ToolTerminalCreator { ) { } - async createTerminal(shellOrProfile: string | ITerminalProfile, token: CancellationToken): Promise { - const instance = await this._createCopilotTerminal(shellOrProfile); + async createTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem, token: CancellationToken): Promise { + const instance = await this._createCopilotTerminal(shellOrProfile, os); const toolTerminal: IToolTerminal = { instance, shellIntegrationQuality: ShellIntegrationQuality.None, @@ -63,7 +67,7 @@ export class ToolTerminalCreator { instance.processReady.then(() => processReadyTimestamp = Date.now()), Event.toPromise(instance.onExit), ]); - if (!isNumber(initResult) && isObject(initResult) && 'message' in initResult) { + if (!isNumber(initResult) && isObject(initResult) && hasKey(initResult, { message: true })) { throw new Error(initResult.message); } @@ -98,7 +102,10 @@ export class ToolTerminalCreator { const commandDetection = instance.capabilities.get(TerminalCapability.CommandDetection); if (commandDetection?.promptInputModel.state === PromptInputState.Unknown) { this._logService.info(`ToolTerminalCreator#createTerminal: Waiting up to 2s for PromptInputModel state to change`); - await raceTimeout(Event.toPromise(commandDetection.onCommandStarted), 2000); + const didStart = await raceTimeout(Event.toPromise(commandDetection.onCommandStarted), 2000); + if (!didStart) { + this._logService.info(`ToolTerminalCreator#createTerminal: PromptInputModel state did not change within timeout`); + } } } @@ -135,18 +142,35 @@ export class ToolTerminalCreator { } } - private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) { + private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile, os: OperatingSystem) { + const shellPath = isString(shellOrProfile) ? shellOrProfile : shellOrProfile.path; + + const env: Record = { + // Avoid making `git diff` interactive when called from copilot + GIT_PAGER: 'cat', + }; + + const preventShellHistory = this._configurationService.getValue(TerminalChatAgentToolsSettingId.PreventShellHistory) === true; + if (preventShellHistory) { + // Check if the shell supports history exclusion via shell integration scripts + if ( + isBash(shellPath, os) || + isZsh(shellPath, os) || + isFish(shellPath, os) || + isPowerShell(shellPath, os) + ) { + env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; + } + } + const config: IShellLaunchConfig = { icon: ThemeIcon.fromId(Codicon.chatSparkle.id), hideFromUser: true, forcePersist: true, - env: { - // Avoid making `git diff` interactive when called from copilot - GIT_PAGER: 'cat', - } + env, }; - if (typeof shellOrProfile === 'string') { + if (isString(shellOrProfile)) { config.executable = shellOrProfile; } else { config.executable = shellOrProfile.path; diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/awaitTerminalTool.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/awaitTerminalTool.ts new file mode 100644 index 00000000000..4cd991c1497 --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/awaitTerminalTool.ts @@ -0,0 +1,128 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationError } from '../../../../../../base/common/errors.js'; +import type { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../../../base/common/codicons.js'; +import { Disposable } from '../../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../../nls.js'; +import { ToolDataSource, type CountTokensCallback, type IPreparedToolInvocation, type IToolData, type IToolImpl, type IToolInvocation, type IToolInvocationPreparationContext, type IToolResult, type ToolProgress } from '../../../../chat/common/tools/languageModelToolsService.js'; +import { RunInTerminalTool } from './runInTerminalTool.js'; +import { TerminalToolId } from './toolIds.js'; +import { raceCancellationError, timeout } from '../../../../../../base/common/async.js'; + +export const AwaitTerminalToolData: IToolData = { + id: TerminalToolId.AwaitTerminal, + toolReferenceName: 'awaitTerminal', + displayName: localize('awaitTerminalTool.displayName', 'Await Terminal'), + modelDescription: 'Wait for a background terminal command to complete. Returns the output, exit code, or timeout status.', + icon: Codicon.terminal, + source: ToolDataSource.Internal, + inputSchema: { + type: 'object', + properties: { + id: { + type: 'string', + description: `The ID of the terminal to await (returned by ${TerminalToolId.RunInTerminal} when isBackground=true).` + }, + timeout: { + type: 'number', + description: 'Timeout in milliseconds. If the command does not complete within this time, returns the output collected so far with a timeout indicator. Use 0 for no timeout.' + }, + }, + required: [ + 'id', + 'timeout', + ] + } +}; + +export interface IAwaitTerminalInputParams { + id: string; + timeout: number; +} + +export class AwaitTerminalTool extends Disposable implements IToolImpl { + async prepareToolInvocation(_context: IToolInvocationPreparationContext, _token: CancellationToken): Promise { + return { + invocationMessage: localize('await.progressive', "Awaiting terminal completion"), + pastTenseMessage: localize('await.past', "Awaited terminal completion"), + }; + } + + async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, _progress: ToolProgress, token: CancellationToken): Promise { + const args = invocation.parameters as IAwaitTerminalInputParams; + + const execution = RunInTerminalTool.getExecution(args.id); + if (!execution) { + return { + content: [{ + kind: 'text', + value: `Error: No active terminal execution found with ID ${args.id}. The terminal may have already completed or the ID is invalid.` + }] + }; + } + + try { + let result: { output?: string; exitCode?: number; error?: string; didEnterAltBuffer?: boolean }; + // Treat negative values as no timeout (same as 0) + const timeoutMs = Math.max(0, args.timeout); + const hasTimeout = timeoutMs > 0; + + if (hasTimeout) { + // Race completion against timeout and cancellation + const timeoutPromise = timeout(timeoutMs).then(() => ({ type: 'timeout' as const })); + const completionPromise = raceCancellationError(execution.completionPromise, token) + .then(r => ({ type: 'completed' as const, result: r })); + + const raceResult = await Promise.race([completionPromise, timeoutPromise]); + + if (raceResult.type === 'timeout') { + // Timeout reached - return partial output + const partialOutput = execution.getOutput(); + return { + toolMetadata: { + exitCode: undefined, + timedOut: true + }, + content: [{ + kind: 'text', + value: `Terminal ${args.id} timed out after ${timeoutMs}ms. Output collected so far:\n${partialOutput}` + }] + }; + } + + result = raceResult.result; + } else { + // No timeout - await completion directly with cancellation support + result = await raceCancellationError(execution.completionPromise, token); + } + + // Command completed + const output = execution.getOutput(); + const exitCodeText = result.exitCode !== undefined ? ` (exit code: ${result.exitCode})` : ''; + + return { + toolMetadata: { + exitCode: result.exitCode + }, + content: [{ + kind: 'text', + value: `Terminal ${args.id} completed${exitCodeText}:\n${output}` + }] + }; + } catch (e) { + if (e instanceof CancellationError) { + throw e; + } + return { + content: [{ + kind: 'text', + value: `Error awaiting terminal ${args.id}: ${e instanceof Error ? e.message : String(e)}` + }] + }; + } + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts new file mode 100644 index 00000000000..571eed32eac --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/commandLineAutoApprover.ts @@ -0,0 +1,398 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { structuralEquals } from '../../../../../../../../base/common/equals.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import type { OperatingSystem } from '../../../../../../../../base/common/platform.js'; +import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../../../../../../base/common/strings.js'; +import { isObject } from '../../../../../../../../base/common/types.js'; +import type { URI } from '../../../../../../../../base/common/uri.js'; +import { ConfigurationTarget, IConfigurationService, type IConfigurationValue } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IInstantiationService } from '../../../../../../../../platform/instantiation/common/instantiation.js'; +import { ITerminalChatService } from '../../../../../../terminal/browser/terminal.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; +import { isPowerShell } from '../../../runInTerminalHelpers.js'; +import type { IAutoApproveRule, INpmScriptAutoApproveRule } from '../commandLineAnalyzer.js'; +import { NpmScriptAutoApprover } from './npmScriptAutoApprover.js'; + +export interface ICommandApprovalResultWithReason { + result: ICommandApprovalResult; + reason: string; + rule?: IAutoApproveRule | INpmScriptAutoApproveRule; +} + +export type ICommandApprovalResult = 'approved' | 'denied' | 'noMatch'; + +const neverMatchRegex = /(?!.*)/; +const transientEnvVarRegex = /^[A-Z_][A-Z0-9_]*=/i; + +export class CommandLineAutoApprover extends Disposable { + private _denyListRules: IAutoApproveRule[] = []; + private _allowListRules: IAutoApproveRule[] = []; + private _allowListCommandLineRules: IAutoApproveRule[] = []; + private _denyListCommandLineRules: IAutoApproveRule[] = []; + private readonly _npmScriptAutoApprover: NpmScriptAutoApprover; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @ITerminalChatService private readonly _terminalChatService: ITerminalChatService, + ) { + super(); + this._npmScriptAutoApprover = this._register(instantiationService.createInstance(NpmScriptAutoApprover)); + this.updateConfiguration(); + this._register(this._configurationService.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration(TerminalChatAgentToolsSettingId.AutoApprove) || + e.affectsConfiguration(TerminalChatAgentToolsSettingId.IgnoreDefaultAutoApproveRules) || + e.affectsConfiguration(TerminalChatAgentToolsSettingId.DeprecatedAutoApproveCompatible) + ) { + this.updateConfiguration(); + } + })); + } + + updateConfiguration() { + let configValue = this._configurationService.getValue(TerminalChatAgentToolsSettingId.AutoApprove); + const configInspectValue = this._configurationService.inspect(TerminalChatAgentToolsSettingId.AutoApprove); + const deprecatedValue = this._configurationService.getValue(TerminalChatAgentToolsSettingId.DeprecatedAutoApproveCompatible); + if (deprecatedValue && typeof deprecatedValue === 'object' && configValue && typeof configValue === 'object') { + configValue = { + ...configValue, + ...deprecatedValue + }; + } + + const { + denyListRules, + allowListRules, + allowListCommandLineRules, + denyListCommandLineRules + } = this._mapAutoApproveConfigToRules(configValue, configInspectValue); + this._allowListRules = allowListRules; + this._denyListRules = denyListRules; + this._allowListCommandLineRules = allowListCommandLineRules; + this._denyListCommandLineRules = denyListCommandLineRules; + } + + async isCommandAutoApproved(command: string, shell: string, os: OperatingSystem, cwd: URI | undefined, chatSessionResource?: URI): Promise { + // Check if the command has a transient environment variable assignment prefix which we + // always deny for now as it can easily lead to execute other commands + if (transientEnvVarRegex.test(command)) { + return { + result: 'denied', + reason: `Command '${command}' is denied because it contains transient environment variables` + }; + } + + // Check the config deny list to see if this command requires explicit approval + for (const rule of this._denyListRules) { + if (this._commandMatchesRule(rule, command, shell, os)) { + return { + result: 'denied', + rule, + reason: `Command '${command}' is denied by deny list rule: ${rule.sourceText}` + }; + } + } + + // Check session allow rules (session deny rules can't exist) + for (const rule of this._getSessionRules(chatSessionResource).allowListRules) { + if (this._commandMatchesRule(rule, command, shell, os)) { + return { + result: 'approved', + rule, + reason: `Command '${command}' is approved by session allow list rule: ${rule.sourceText}` + }; + } + } + + // Check the config allow list to see if the command is allowed to run without explicit approval + for (const rule of this._allowListRules) { + if (this._commandMatchesRule(rule, command, shell, os)) { + return { + result: 'approved', + rule, + reason: `Command '${command}' is approved by allow list rule: ${rule.sourceText}` + }; + } + } + + // Check if this is an npm/yarn/pnpm script defined in package.json + const npmScriptResult = await this._npmScriptAutoApprover.isCommandAutoApproved(command, cwd); + if (npmScriptResult.isAutoApproved) { + return { + result: 'approved', + rule: { type: 'npmScript', npmScriptResult }, + reason: `Command '${command}' is approved as npm script '${npmScriptResult.scriptName}' is defined in package.json` + }; + } + + // TODO: LLM-based auto-approval https://github.com/microsoft/vscode/issues/253267 + + // Fallback is always to require approval + return { + result: 'noMatch', + reason: `Command '${command}' has no matching auto approve entries` + }; + } + + isCommandLineAutoApproved(commandLine: string, chatSessionResource?: URI): ICommandApprovalResultWithReason { + // Check the config deny list first to see if this command line requires explicit approval + for (const rule of this._denyListCommandLineRules) { + if (rule.regex.test(commandLine)) { + return { + result: 'denied', + rule, + reason: `Command line '${commandLine}' is denied by deny list rule: ${rule.sourceText}` + }; + } + } + + // Check session allow list (session deny rules can't exist) + for (const rule of this._getSessionRules(chatSessionResource).allowListCommandLineRules) { + if (rule.regex.test(commandLine)) { + return { + result: 'approved', + rule, + reason: `Command line '${commandLine}' is approved by session allow list rule: ${rule.sourceText}` + }; + } + } + + // Check if the full command line matches any of the config allow list command line regexes + for (const rule of this._allowListCommandLineRules) { + if (rule.regex.test(commandLine)) { + return { + result: 'approved', + rule, + reason: `Command line '${commandLine}' is approved by allow list rule: ${rule.sourceText}` + }; + } + } + return { + result: 'noMatch', + reason: `Command line '${commandLine}' has no matching auto approve entries` + }; + } + + private _getSessionRules(chatSessionResource?: URI): { + denyListRules: IAutoApproveRule[]; + allowListRules: IAutoApproveRule[]; + allowListCommandLineRules: IAutoApproveRule[]; + denyListCommandLineRules: IAutoApproveRule[]; + } { + const denyListRules: IAutoApproveRule[] = []; + const allowListRules: IAutoApproveRule[] = []; + const allowListCommandLineRules: IAutoApproveRule[] = []; + const denyListCommandLineRules: IAutoApproveRule[] = []; + + if (!chatSessionResource) { + return { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules }; + } + + const sessionRulesConfig = this._terminalChatService.getSessionAutoApproveRules(chatSessionResource); + for (const [key, value] of Object.entries(sessionRulesConfig)) { + if (typeof value === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + if (value === true) { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else if (value === false) { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } else if (typeof value === 'object' && value !== null) { + const objectValue = value as { approve?: boolean; matchCommandLine?: boolean }; + if (typeof objectValue.approve === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + if (objectValue.approve === true) { + if (objectValue.matchCommandLine === true) { + allowListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } else if (objectValue.approve === false) { + if (objectValue.matchCommandLine === true) { + denyListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } else { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget: 'session', isDefaultRule: false }); + } + } + } + } + } + + return { denyListRules, allowListRules, allowListCommandLineRules, denyListCommandLineRules }; + } + + private _commandMatchesRule(rule: IAutoApproveRule, command: string, shell: string, os: OperatingSystem): boolean { + const isPwsh = isPowerShell(shell, os); + + // PowerShell is case insensitive regardless of platform + if ((isPwsh ? rule.regexCaseInsensitive : rule.regex).test(command)) { + return true; + } else if (isPwsh && command.startsWith('(')) { + // Allow ignoring of the leading ( for PowerShell commands as it's a command pattern to + // operate on the output of a command. For example `(Get-Content README.md) ...` + if (rule.regexCaseInsensitive.test(command.slice(1))) { + return true; + } + } + return false; + } + + private _mapAutoApproveConfigToRules(config: unknown, configInspectValue: IConfigurationValue>): { + denyListRules: IAutoApproveRule[]; + allowListRules: IAutoApproveRule[]; + allowListCommandLineRules: IAutoApproveRule[]; + denyListCommandLineRules: IAutoApproveRule[]; + } { + if (!config || typeof config !== 'object') { + return { + denyListRules: [], + allowListRules: [], + allowListCommandLineRules: [], + denyListCommandLineRules: [] + }; + } + + const denyListRules: IAutoApproveRule[] = []; + const allowListRules: IAutoApproveRule[] = []; + const allowListCommandLineRules: IAutoApproveRule[] = []; + const denyListCommandLineRules: IAutoApproveRule[] = []; + + const ignoreDefaults = this._configurationService.getValue(TerminalChatAgentToolsSettingId.IgnoreDefaultAutoApproveRules) === true; + + for (const [key, value] of Object.entries(config)) { + const defaultValue = configInspectValue?.default?.value; + const isDefaultRule = !!( + isObject(defaultValue) && + Object.prototype.hasOwnProperty.call(defaultValue, key) && + structuralEquals((defaultValue as Record)[key], value) + ); + function checkTarget(inspectValue: Readonly | undefined): boolean { + return ( + isObject(inspectValue) && + Object.prototype.hasOwnProperty.call(inspectValue, key) && + structuralEquals((inspectValue as Record)[key], value) + ); + } + const sourceTarget = ( + checkTarget(configInspectValue.workspaceFolder) ? ConfigurationTarget.WORKSPACE_FOLDER + : checkTarget(configInspectValue.workspaceValue) ? ConfigurationTarget.WORKSPACE + : checkTarget(configInspectValue.userRemoteValue) ? ConfigurationTarget.USER_REMOTE + : checkTarget(configInspectValue.userLocalValue) ? ConfigurationTarget.USER_LOCAL + : checkTarget(configInspectValue.userValue) ? ConfigurationTarget.USER + : checkTarget(configInspectValue.applicationValue) ? ConfigurationTarget.APPLICATION + : ConfigurationTarget.DEFAULT + ); + + // If default rules are disabled, ignore entries that come from the default config + if (ignoreDefaults && isDefaultRule && sourceTarget === ConfigurationTarget.DEFAULT) { + continue; + } + + if (typeof value === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + // IMPORTANT: Only true and false are used, null entries need to be ignored + if (value === true) { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } else if (value === false) { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } + } else if (typeof value === 'object' && value !== null) { + // Handle object format like { approve: true/false, matchCommandLine: true/false } + const objectValue = value as { approve?: boolean; matchCommandLine?: boolean }; + if (typeof objectValue.approve === 'boolean') { + const { regex, regexCaseInsensitive } = this._convertAutoApproveEntryToRegex(key); + if (objectValue.approve === true) { + if (objectValue.matchCommandLine === true) { + allowListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } else { + allowListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } + } else if (objectValue.approve === false) { + if (objectValue.matchCommandLine === true) { + denyListCommandLineRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } else { + denyListRules.push({ regex, regexCaseInsensitive, sourceText: key, sourceTarget, isDefaultRule }); + } + } + } + } + } + + return { + denyListRules, + allowListRules, + allowListCommandLineRules, + denyListCommandLineRules + }; + } + + private _convertAutoApproveEntryToRegex(value: string): { regex: RegExp; regexCaseInsensitive: RegExp } { + const regex = this._doConvertAutoApproveEntryToRegex(value); + if (regex.flags.includes('i')) { + return { regex, regexCaseInsensitive: regex }; + } + return { regex, regexCaseInsensitive: new RegExp(regex.source, regex.flags + 'i') }; + } + + private _doConvertAutoApproveEntryToRegex(value: string): RegExp { + // If it's wrapped in `/`, it's in regex format and should be converted directly + // Support all standard JavaScript regex flags: d, g, i, m, s, u, v, y + const regexMatch = value.match(/^\/(?.+)\/(?[dgimsuvy]*)$/); + const regexPattern = regexMatch?.groups?.pattern; + if (regexPattern) { + let flags = regexMatch.groups?.flags; + // Remove global flag as it changes how the regex state works which we need to handle + // internally + if (flags) { + flags = flags.replaceAll('g', ''); + } + + // Allow .* as users expect this would match everything + if (regexPattern === '.*') { + return new RegExp(regexPattern); + + } + + try { + const regex = new RegExp(regexPattern, flags || undefined); + if (regExpLeadsToEndlessLoop(regex)) { + return neverMatchRegex; + } + + return regex; + } catch (error) { + return neverMatchRegex; + } + } + + // The empty string should be ignored, rather than approve everything + if (value === '') { + return neverMatchRegex; + } + + let sanitizedValue: string; + + // Match both path separators it if looks like a path + if (value.includes('/') || value.includes('\\')) { + // Replace path separators with placeholders first, apply standard sanitization, then + // apply special path handling + let pattern = value.replace(/[/\\]/g, '%%PATH_SEP%%'); + pattern = escapeRegExpCharacters(pattern); + pattern = pattern.replace(/%%PATH_SEP%%*/g, '[/\\\\]'); + sanitizedValue = `^(?:\\.[/\\\\])?${pattern}`; + } + + // Escape regex special characters for non-path strings + else { + sanitizedValue = escapeRegExpCharacters(value); + } + + // Regular strings should match the start of the command line and be a word boundary + return new RegExp(`^${sanitizedValue}\\b`); + } +} diff --git a/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts new file mode 100644 index 00000000000..57da0c5439c --- /dev/null +++ b/src/vs/workbench/contrib/terminalContrib/chatAgentTools/browser/tools/commandLineAnalyzer/autoApprove/npmScriptAutoApprover.ts @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MarkdownString, type IMarkdownString } from '../../../../../../../../base/common/htmlContent.js'; +import { visit, type JSONVisitor } from '../../../../../../../../base/common/json.js'; +import { Disposable } from '../../../../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../../../../base/common/uri.js'; +import { IUriIdentityService } from '../../../../../../../../platform/uriIdentity/common/uriIdentity.js'; +import { localize } from '../../../../../../../../nls.js'; +import { IConfigurationService } from '../../../../../../../../platform/configuration/common/configuration.js'; +import { IFileService } from '../../../../../../../../platform/files/common/files.js'; +import { IWorkspaceContextService, type IWorkspaceFolder } from '../../../../../../../../platform/workspace/common/workspace.js'; +import { TerminalChatAgentToolsSettingId } from '../../../../common/terminalChatAgentToolsConfiguration.js'; + +/** + * Regex patterns to match npm/yarn/pnpm run commands and extract the script name. + * Uses named capture groups: 'command' for the package manager, 'scriptName' for the script. + */ +const npmRunPatterns = [ + // npm run - - - - diff --git a/test/leaks/package.json b/test/leaks/package.json deleted file mode 100644 index 07bdd1d1830..00000000000 --- a/test/leaks/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "leaks", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "devDependencies": { - "koa": "^2.13.1", - "koa-mount": "^4.0.0", - "koa-static": "^5.0.0" - } -} diff --git a/test/leaks/server.js b/test/leaks/server.js deleted file mode 100644 index 18c3e7a0c04..00000000000 --- a/test/leaks/server.js +++ /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. - *--------------------------------------------------------------------------------------------*/ - -const Koa = require('koa'); -const serve = require('koa-static'); -const mount = require('koa-mount'); - -const app = new Koa(); - -app.use(serve('.')); -app.use(mount('/static', serve('../../out'))); - -app.listen(3000); -console.log('👉 http://localhost:3000'); diff --git a/test/mcp/package-lock.json b/test/mcp/package-lock.json index 37e2f795b02..75ce8f4867d 100644 --- a/test/mcp/package-lock.json +++ b/test/mcp/package-lock.json @@ -9,10 +9,10 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "1.18.1", - "@playwright/mcp": "^0.0.37", + "@modelcontextprotocol/sdk": "1.25.2", + "@playwright/mcp": "^0.0.40", "cors": "^2.8.5", - "express": "^5.1.0", + "express": "^5.2.1", "minimist": "^1.2.8", "ncp": "^2.0.0", "node-fetch": "^2.6.7" @@ -23,16 +23,30 @@ "@types/ncp": "2.0.1", "@types/node": "22.x", "@types/node-fetch": "^2.5.10", - "npm-run-all": "^4.1.5" + "npm-run-all2": "^8.0.4" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.18.1.tgz", - "integrity": "sha512-d//GE8/Yh7aC3e7p+kZG8JqqEAwwDUmAfvH1quogtbk+ksS6E0RR6toKKESPYYZVre0meqkJb27zb+dhqE9Sgw==", + "version": "1.25.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz", + "integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==", "license": "MIT", "dependencies": { - "ajv": "^6.12.6", + "@hono/node-server": "^1.19.7", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", @@ -40,23 +54,37 @@ "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", + "jose": "^6.1.1", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } } }, "node_modules/@playwright/mcp": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.37.tgz", - "integrity": "sha512-BnI2Ijim1rhIGhoFKJRCa+MaWtNr7M2lnLeDldDsR0n+ZB2G7zjt+MAMqy5eRD/mMiWsTaQsXlzZmXeixqBdsA==", + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.40.tgz", + "integrity": "sha512-gkaE0enMiRLKU3UdVZP2vUn9/rkLT01susE4XY7K10Wpl9vgOXeDCoTNwA2z82D8S2MX31lHx+uveEU4nHF3yw==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-09-06", - "playwright-core": "1.56.0-alpha-2025-09-06" + "playwright": "1.56.0-alpha-1758750661000", + "playwright-core": "1.56.0-alpha-1758750661000" }, "bin": { "mcp-server-playwright": "cli.js" @@ -217,81 +245,36 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "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-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" + "ajv": "^8.0.0" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "ajv": "^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "peerDependenciesMeta": { + "ajv": { + "optional": true + } } }, "node_modules/asynckit": { @@ -301,58 +284,28 @@ "dev": true, "license": "MIT" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -364,25 +317,6 @@ "node": ">= 0.8" } }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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", @@ -412,38 +346,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -457,13 +359,6 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", @@ -530,64 +425,10 @@ "node": ">= 8" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -601,42 +442,6 @@ } } }, - "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, - "license": "MIT", - "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-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -685,85 +490,6 @@ "node": ">= 0.8" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -810,40 +536,12 @@ "node": ">= 0.4" } }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -875,18 +573,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -937,11 +636,21 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "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==", - "license": "MIT" + "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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/finalhandler": { "version": "2.1.0", @@ -960,22 +669,6 @@ "node": ">= 0.8" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/form-data": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", @@ -1057,37 +750,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1125,41 +787,6 @@ "node": ">= 0.4" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1172,65 +799,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "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, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1271,456 +839,71 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true, - "license": "ISC" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, + "peer": true, "engines": { - "node": ">= 0.8" + "node": ">=16.9.0" } }, - "node_modules/http-errors/node_modules/statuses": { + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "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/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" + "node": ">= 0.8" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { - "call-bound": "^1.0.3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-weakset": { + "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.10" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, "node_modules/isexe": { @@ -1729,35 +912,37 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } }, - "node_modules/load-json-file": { + "node_modules/json-parse-even-better-errors": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, "engines": { - "node": ">=4" + "node": "^18.17.0 || >=20.5.0" } }, + "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==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1818,19 +1003,6 @@ "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -1864,13 +1036,6 @@ "node": ">= 0.6" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -1891,106 +1056,93 @@ } } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "node_modules/npm-run-all2": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz", + "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" + "picomatch": "^4.0.2", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" }, "bin": { "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js" }, "engines": { - "node": ">= 4" + "node": "^20.5.0 || >=22.0.0", + "npm": ">= 10" } }, - "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "node_modules/npm-run-all2/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", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, "engines": { - "node": ">=4.8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/npm-run-all/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=4" + "node": ">=16" } }, - "node_modules/npm-run-all/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/npm-run-all2/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" + "bin": { + "pidtree": "bin/pidtree.js" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-all/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=0.10" } }, - "node_modules/npm-run-all/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "which": "bin/which" + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/object-assign": { @@ -2014,37 +1166,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2066,38 +1187,6 @@ "wrappy": "1" } }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -2116,13 +1205,6 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", @@ -2132,40 +1214,17 @@ "node": ">=16" } }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, "engines": { - "node": ">=4" - } - }, - "node_modules/pidtree": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", - "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", - "dev": true, - "license": "MIT", - "bin": { - "pidtree": "bin/pidtree.js" + "node": ">=12" }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pkce-challenge": { @@ -2178,12 +1237,12 @@ } }, "node_modules/playwright": { - "version": "1.56.0-alpha-2025-09-06", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-06.tgz", - "integrity": "sha512-suVjiF5eeUtIqFq5E/5LGgkV0/bRSik87N+M7uLsjPQrKln9QHbZt3cy7Zybicj3ZqTBWWHvpN9b4cnpg6hS0g==", + "version": "1.56.0-alpha-1758750661000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-1758750661000.tgz", + "integrity": "sha512-15C/m7NPpAmBX2MFMrepCMj18ksBYvhbT90cvFjG2iBs2YPqO2U4f9OjcX207ITSmDAAJ8pWBlJutcZUYUERXg==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-09-06" + "playwright-core": "1.56.0-alpha-1758750661000" }, "bin": { "playwright": "cli.js" @@ -2196,9 +1255,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-09-06", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-06.tgz", - "integrity": "sha512-B2s/cuqYuu+mT4hIHG8gIOXjCSKh0Np1gJNCp0CrDk/UTLB74gThwXiyPAJU0fADIQH6Dv1glv8ZvKTDVT8Fng==", + "version": "1.56.0-alpha-1758750661000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-1758750661000.tgz", + "integrity": "sha512-ivP4xjc6EHkUqF80pMFfDRijKLEvO64qC6DTgyYrbsyCo8gugkqwKm6lFWn4W47g4S8juoUwQhlRVjM2BJ+ruA==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -2207,16 +1266,6 @@ "node": ">=18" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2230,19 +1279,10 @@ "node": ">= 0.10" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "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==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -2264,98 +1304,41 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.10" } }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, + "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==", "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/router": { @@ -2374,26 +1357,6 @@ "node": ">= 18" } }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2412,42 +1375,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", @@ -2455,16 +1383,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/send": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", @@ -2502,55 +1420,6 @@ "node": ">= 18" } }, - "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, - "license": "MIT", - "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/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2663,42 +1532,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -2708,134 +1541,6 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string.prototype.padend": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", - "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2865,103 +1570,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -2978,26 +1586,6 @@ "node": ">= 0.8" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "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/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3038,95 +1626,6 @@ "node": ">= 8" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3143,12 +1642,12 @@ } }, "node_modules/zod-to-json-schema": { - "version": "3.24.6", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", - "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", + "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", "license": "ISC", "peerDependencies": { - "zod": "^3.24.1" + "zod": "^3.25 || ^4" } } } diff --git a/test/mcp/package.json b/test/mcp/package.json index 0b17f1a6216..b32637f5fd7 100644 --- a/test/mcp/package.json +++ b/test/mcp/package.json @@ -8,14 +8,14 @@ "compile": "cd ../automation && npm run compile && cd ../mcp && node ../../node_modules/typescript/bin/tsc", "watch-automation": "cd ../automation && npm run watch", "watch-mcp": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput", - "watch": "npm-run-all -lp watch-automation watch-mcp", + "watch": "npm-run-all2 -lp watch-automation watch-mcp", "start-stdio": "echo 'Starting vscode-playwright-mcp... For customization and troubleshooting, see ./test/mcp/README.md' && npm ci && npm run -s compile && node ./out/stdio.js" }, "dependencies": { - "@modelcontextprotocol/sdk": "1.18.1", - "@playwright/mcp": "^0.0.37", + "@modelcontextprotocol/sdk": "1.25.2", + "@playwright/mcp": "^0.0.40", "cors": "^2.8.5", - "express": "^5.1.0", + "express": "^5.2.1", "minimist": "^1.2.8", "ncp": "^2.0.0", "node-fetch": "^2.6.7" @@ -26,6 +26,6 @@ "@types/ncp": "2.0.1", "@types/node": "22.x", "@types/node-fetch": "^2.5.10", - "npm-run-all": "^4.1.5" + "npm-run-all2": "^8.0.4" } } diff --git a/test/mcp/src/application.ts b/test/mcp/src/application.ts index a60c7b9764d..0af8959a237 100644 --- a/test/mcp/src/application.ts +++ b/test/mcp/src/application.ts @@ -232,7 +232,7 @@ async function setup(): Promise { logger.log('Smoketest setup done!\n'); } -export async function getApplication({ recordVideo }: { recordVideo?: boolean } = {}) { +export async function getApplication({ recordVideo, workspacePath }: { recordVideo?: boolean; workspacePath?: string } = {}) { const testCodePath = getDevElectronPath(); const electronPath = testCodePath; if (!fs.existsSync(electronPath || '')) { @@ -252,7 +252,8 @@ export async function getApplication({ recordVideo }: { recordVideo?: boolean } quality, version: parseVersion(version ?? '0.0.0'), codePath: opts.build, - workspacePath: rootPath, + // Use provided workspace path, or fall back to rootPath on CI (GitHub Actions) + workspacePath: workspacePath ?? (process.env.GITHUB_ACTIONS ? rootPath : undefined), logger, logsPath: logsRootPath, crashesPath: crashesRootPath, @@ -264,6 +265,7 @@ export async function getApplication({ recordVideo }: { recordVideo?: boolean } headless: opts.headless, browser: opts.browser, extraArgs: (opts.electronArgs || '').split(' ').map(arg => arg.trim()).filter(arg => !!arg), + extensionDevelopmentPath: opts.extensionDevelopmentPath, }); await application.start(); application.code.driver.currentPage.on('close', async () => { @@ -292,12 +294,12 @@ export class ApplicationService { return this._application; } - async getOrCreateApplication({ recordVideo }: { recordVideo?: boolean } = {}): Promise { + async getOrCreateApplication({ recordVideo, workspacePath }: { recordVideo?: boolean; workspacePath?: string } = {}): Promise { if (this._closing) { await this._closing; } if (!this._application) { - this._application = await getApplication({ recordVideo }); + this._application = await getApplication({ recordVideo, workspacePath }); this._application.code.driver.currentPage.on('close', () => { this._closing = (async () => { if (this._application) { diff --git a/test/mcp/src/automation.ts b/test/mcp/src/automation.ts index 9163af43e89..3263081ecfc 100644 --- a/test/mcp/src/automation.ts +++ b/test/mcp/src/automation.ts @@ -18,17 +18,18 @@ export async function getServer(appService: ApplicationService): Promise server.tool( 'vscode_automation_start', - 'Start VS Code Build', + 'Start VS Code Build. If workspacePath is not provided, VS Code will open with the last used workspace or an empty window.', { - recordVideo: z.boolean().optional() + recordVideo: z.boolean().optional().describe('Whether to record a video of the session'), + workspacePath: z.string().optional().describe('Optional path to a workspace or folder to open. If not provided, opens the last used workspace.') }, - async ({ recordVideo }) => { - const app = await appService.getOrCreateApplication({ recordVideo }); + async ({ recordVideo, workspacePath }) => { + const app = await appService.getOrCreateApplication({ recordVideo, workspacePath }); await app.startTracing(); return { content: [{ type: 'text' as const, - text: app ? `VS Code started successfully` : `Failed to start VS Code` + text: app ? `VS Code started successfully${workspacePath ? ` with workspace: ${workspacePath}` : ''}` : `Failed to start VS Code` }] }; } diff --git a/test/mcp/src/automationTools/core.ts b/test/mcp/src/automationTools/core.ts index 591d7437896..d18adf35ef0 100644 --- a/test/mcp/src/automationTools/core.ts +++ b/test/mcp/src/automationTools/core.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { z } from 'zod'; import { ApplicationService } from '../application'; /** @@ -12,25 +13,26 @@ import { ApplicationService } from '../application'; export function applyCoreTools(server: McpServer, appService: ApplicationService): RegisteredTool[] { const tools: RegisteredTool[] = []; - // Playwright keeps using this as a start... maybe it needs some massaging - // server.tool( - // 'vscode_automation_restart', - // 'Restart VS Code with optional workspace or folder and extra arguments', - // { - // workspaceOrFolder: z.string().optional().describe('Optional path to workspace or folder to open'), - // extraArgs: z.array(z.string()).optional().describe('Optional extra command line arguments') - // }, - // async (args) => { - // const { workspaceOrFolder, extraArgs } = args; - // await app.restart({ workspaceOrFolder, extraArgs }); - // return { - // content: [{ - // type: 'text' as const, - // text: `VS Code restarted successfully${workspaceOrFolder ? ` with workspace: ${workspaceOrFolder}` : ''}` - // }] - // }; - // } - // ); + tools.push(server.tool( + 'vscode_automation_restart', + 'Restart VS Code with optional workspace or folder and extra command-line arguments', + { + workspaceOrFolder: z.string().optional().describe('Path to a workspace or folder to open on restart'), + extraArgs: z.array(z.string()).optional().describe('Extra CLI arguments to pass on restart') + }, + async ({ workspaceOrFolder, extraArgs }) => { + const app = await appService.getOrCreateApplication(); + await app.restart({ workspaceOrFolder, extraArgs }); + const workspaceText = workspaceOrFolder ? ` with workspace: ${workspaceOrFolder}` : ''; + const argsText = extraArgs?.length ? ` (args: ${extraArgs.join(' ')})` : ''; + return { + content: [{ + type: 'text' as const, + text: `VS Code restarted successfully${workspaceText}${argsText}` + }] + }; + } + )); tools.push(server.tool( 'vscode_automation_stop', diff --git a/test/mcp/src/automationTools/settings.ts b/test/mcp/src/automationTools/settings.ts index 91502fe1cc9..46f91fe8fbf 100644 --- a/test/mcp/src/automationTools/settings.ts +++ b/test/mcp/src/automationTools/settings.ts @@ -37,7 +37,7 @@ export function applySettingsTools(server: McpServer, appService: ApplicationSer 'vscode_automation_settings_add_user_settings', 'Add multiple user settings at once', { - settings: z.array(z.array(z.string()).length(2)).describe('Array of [key, value] setting pairs') + settings: z.array(z.tuple([z.string(), z.string()])).describe('Array of [key, value] setting pairs') }, async (args) => { const { settings } = args; diff --git a/test/mcp/src/options.ts b/test/mcp/src/options.ts index eb81d9982d6..791d1368e19 100644 --- a/test/mcp/src/options.ts +++ b/test/mcp/src/options.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as minimist from 'minimist'; +import minimist from 'minimist'; const [, , ...args] = process.argv; export const opts = minimist(args, { @@ -12,7 +12,8 @@ export const opts = minimist(args, { 'stable-build', 'wait-time', 'test-repo', - 'electronArgs' + 'electronArgs', + 'extensionDevelopmentPath' ], boolean: [ 'verbose', @@ -36,4 +37,5 @@ export const opts = minimist(args, { electronArgs?: string; video?: boolean; autostart?: boolean; + extensionDevelopmentPath?: string; }; diff --git a/test/mcp/tsconfig.json b/test/mcp/tsconfig.json index 0d77fe42693..74fa2635bec 100644 --- a/test/mcp/tsconfig.json +++ b/test/mcp/tsconfig.json @@ -8,6 +8,7 @@ "strict": true, "noUnusedParameters": false, "noUnusedLocals": true, + "rootDir": "./src", "outDir": "out", "sourceMap": true, "skipLibCheck": true, diff --git a/test/monaco/esm-check/esm-check.js b/test/monaco/esm-check/esm-check.js index 3575e3d76ee..6b5fdf4fe93 100644 --- a/test/monaco/esm-check/esm-check.js +++ b/test/monaco/esm-check/esm-check.js @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check +// @ts-check const fs = require('fs'); const path = require('path'); -const util = require('../../../build/lib/util'); const playwright = require('@playwright/test'); const yaserver = require('yaserver'); const http = require('http'); +const { glob } = require('glob'); const DEBUG_TESTS = false; const SRC_DIR = path.join(__dirname, '../../../out-monaco-editor-core/esm'); @@ -76,10 +76,9 @@ async function startServer() { } async function extractSourcesWithoutCSS() { - await util.rimraf(DST_DIR); + fs.rmSync(DST_DIR, { recursive: true, force: true }); - const files = util.rreddir(SRC_DIR); - for (const file of files) { + for (const file of glob.sync('**/*', { cwd: SRC_DIR, nodir: true })) { const srcFilename = path.join(SRC_DIR, file); if (!/\.js$/.test(srcFilename)) { continue; @@ -90,7 +89,7 @@ async function extractSourcesWithoutCSS() { let contents = fs.readFileSync(srcFilename).toString(); contents = contents.replace(/import '[^']+\.css';/g, ''); - util.ensureDir(path.dirname(dstFilename)); + fs.mkdirSync(path.dirname(dstFilename), { recursive: true }); fs.writeFileSync(dstFilename, contents); } } diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index ada3052b87e..513d33eeb34 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -144,9 +144,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, diff --git a/test/sanity/.gitignore b/test/sanity/.gitignore new file mode 100644 index 00000000000..3ff554959d6 --- /dev/null +++ b/test/sanity/.gitignore @@ -0,0 +1,15 @@ +.config +.dbus +.dotnet +.DS_Store +.local +.mozilla +.pki +.var +.vscode* +Downloads +node_modules/ +npm-debug.log +out/ +results.xml +Thumbs.db diff --git a/test/sanity/README.md b/test/sanity/README.md new file mode 100644 index 00000000000..d854439a472 --- /dev/null +++ b/test/sanity/README.md @@ -0,0 +1,156 @@ +# VS Code Release Sanity Check Tests + +## Overview + +Automated end-to-end release sanity tests for published VS Code builds. +These tests verify critical functionality across different platforms and installation methods, +ensuring that published builds meet quality standards before reaching end users. + +See [Sanity Check wiki page](https://github.com/microsoft/vscode/wiki/Sanity-Check) for more details on sanity testing. + +## Usage + +Many tests will use the underlying platform to install and verify basic VS Code functionality. +Such tests will need to be run on the corresponding target OS/virtual machine and will fail if ran outside. +Use -g or -f command-line options to filter tests to match the host platform. + +### Command-Line Options + +|Option|Alias|Description| +|--------|-------|-------------| +|`--commit `|`-c`|The commit to test (required)| +|`--quality `|`-q`|The quality to test (required, "stable", "insider" or "exploration")| +|`--no-cleanup`||Do not cleanup downloaded files after each test| +|`--no-signing-check`||Skip Authenticode and codesign signature checks| +|`--no-headless`||Run tests with a visible UI (desktop tests only)| +|`--no-detection`||Enable all tests regardless of platform and skip executable runs| +|`--grep `|`-g`|Only run tests matching the given pattern (Mocha grep)| +|`--fgrep `|`-f`|Only run tests containing the given string (Mocha fgrep)| +|`--test-results `|`-t`|Output test results in JUnit format to the specified path| +|`--timeout `||Set the test-case timeout in seconds (default: 600 seconds)| +|`--verbose`|`-v`|Enable verbose logging| +|`--help`|`-h`|Show this help message| + +### Example + +To run CLI tests for all platforms on given commit of Insiders build, from the root directory run: + +```bash +npm run sanity-test -- --commit 19228f26df517fecbfda96c20956f7c521e072be --quality insider -g "cli*" +``` + +## Scripts + +Platform-specific scripts are provided in the `scripts/` directory to set up the environment and run tests: + +|Script|Platform|Description| +|--------|----------|-------------| +|`run-win32.cmd`|Windows|Runs tests using Edge as the Playwright browser| +|`run-macOS.sh`|macOS|Installs Playwright WebKit and runs tests| +|`run-ubuntu.sh`|Ubuntu|Sets up X11, Chromium, and Snap daemon, then runs tests| +|`run-docker.sh`|Linux (Docker)|Builds and runs tests inside a Docker container| +|`run-docker.cmd`|Windows (Docker)|Windows wrapper for Docker-based Linux tests| + +### Docker Script Options + +The `run-docker.sh` script accepts the following options: + +|Option|Description| +|--------|-------------| +|`--container `|Container dockerfile name (required, e.g., "ubuntu", "alpine")| +|`--arch `|Target architecture: amd64, arm64, or arm (default: amd64)| +|`--base-image `|Override the base Docker image (e.g., "ubuntu:24.04")| + +All other arguments are passed through to the sanity test runner. + +## Containers + +Docker container definitions are provided in the `containers/` directory for testing on various Linux distributions: + +|Container|Base Image|Description| +|-----------|------------|-------------| +|`alpine`|Alpine 3.x|Alpine Linux with musl libc| +|`centos`|CentOS Stream 9|RHEL-compatible distribution| +|`debian-10`|Debian 10 (Buster)|Older Debian with legacy library versions| +|`debian-12`|Debian 12 (Bookworm)|Current Debian stable| +|`fedora`|Fedora 36/40|Cutting-edge RPM-based distribution| +|`opensuse`|openSUSE Leap 16.0|SUSE-based enterprise distribution| +|`redhat`|Red Hat UBI 9|Red Hat Universal Base Image| +|`ubuntu`|Ubuntu 22.04/24.04|Popular Debian-based distribution| + +Each container includes: + +- Node.js 22.x runtime +- X11 server (Xvfb) for headless desktop testing +- D-Bus for desktop integration +- Architecture-specific VS Code dependencies + +Some containers include web browser used for validating web server targets. + +### Running Tests in a Container + +```bash +# Ubuntu 24.04 on amd64 +./scripts/run-docker.sh --container ubuntu --base-image ubuntu:24.04 -c -q insider + +# Alpine on arm64 +./scripts/run-docker.sh --container alpine --arch arm64 -c -q stable +``` + +## CI/CD Pipeline + +Sanity tests run in Azure Pipelines via the `product-sanity-tests.yml` pipeline. + +### Pipeline Parameters + +|Parameter|Description| +|-----------|-------------| +|`buildQuality`|The quality of the build to test: "exploration", "insider", or "stable"| +|`buildCommit`|The published build commit SHA| +|`npmRegistry`|Custom NPM registry URL (optional)| + +### Test Matrix + +The pipeline tests across multiple platforms and architectures: + +**Native Hosts:** + +- macOS arm64 +- Windows x64 +- Windows arm64 +- Ubuntu 22.04 x64 (native, with Snap support) + +**Partial Support:** + +For the following platforms only downloads are validated (and not install/runtime): + +- macOS x64 + +**Linux Containers (amd64 and arm64):** + +- Alpine 3.23 +- CentOS Stream 9 +- Debian 10 and 12 (also arm32) +- Fedora 36 and 40 +- openSUSE Leap 16.0 +- Red Hat UBI 9 +- Ubuntu 22.04 and 24.04 (also arm32) + +### Pipeline Files + +- [product-sanity-tests.yml](../../build/azure-pipelines/product-sanity-tests.yml) - Main pipeline definition +- [sanity-tests.yml](../../build/azure-pipelines/common/sanity-tests.yml) - Reusable job template + +## References + +The following public documentation pages provide details on end-user VS Code setup scenarios. + +- [Download VS Code](https://code.visualstudio.com/Download) +- [Requirements](https://code.visualstudio.com/docs/supporting/requirements) +- [Setup Overview](https://code.visualstudio.com/docs/setup/setup-overview) +- [Linux Setup](https://code.visualstudio.com/docs/setup/linux) +- [macOS Setup](https://code.visualstudio.com/docs/setup/mac) +- [Windows Setup](https://code.visualstudio.com/docs/setup/windows) +- [Portable Mode](https://code.visualstudio.com/docs/editor/portable) +- [VS Code Server](https://code.visualstudio.com/docs/remote/vscode-server) +- [Developing in WSL](https://code.visualstudio.com/docs/remote/wsl) diff --git a/test/sanity/containers/alpine.dockerfile b/test/sanity/containers/alpine.dockerfile new file mode 100644 index 00000000000..61ac9439a18 --- /dev/null +++ b/test/sanity/containers/alpine.dockerfile @@ -0,0 +1,9 @@ +ARG BASE_IMAGE=mcr.microsoft.com/devcontainers/base:alpine-3.22 +FROM ${BASE_IMAGE} + +# Node.js 22 +RUN apk add --no-cache nodejs + +# Chromium +RUN apk add --no-cache chromium +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser diff --git a/test/sanity/containers/centos.dockerfile b/test/sanity/containers/centos.dockerfile new file mode 100644 index 00000000000..6d46c33a5af --- /dev/null +++ b/test/sanity/containers/centos.dockerfile @@ -0,0 +1,24 @@ +ARG BASE_IMAGE=quay.io/centos/centos:stream9 +FROM ${BASE_IMAGE} + +# Node.js 22 +RUN dnf module enable -y nodejs:22 && \ + dnf install -y nodejs + +# Chromium +RUN dnf install -y epel-release && \ + dnf install -y chromium + +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Desktop Bus +RUN dnf install -y dbus-x11 && \ + mkdir -p /run/dbus + +# X11 Server +RUN dnf install -y xorg-x11-server-Xvfb + +# VS Code dependencies +RUN dnf install -y \ + ca-certificates \ + xdg-utils diff --git a/test/sanity/containers/debian-10.dockerfile b/test/sanity/containers/debian-10.dockerfile new file mode 100644 index 00000000000..61d8e713eb0 --- /dev/null +++ b/test/sanity/containers/debian-10.dockerfile @@ -0,0 +1,41 @@ +ARG MIRROR +ARG BASE_IMAGE=debian:10 +ARG TARGETARCH +FROM ${MIRROR}${BASE_IMAGE} + +# Update to archive repos since Debian 10 is EOL +RUN sed -i 's|http://deb.debian.org|http://archive.debian.org|g' /etc/apt/sources.list && \ + sed -i 's|http://security.debian.org|http://archive.debian.org|g' /etc/apt/sources.list && \ + sed -i '/buster-updates/d' /etc/apt/sources.list && \ + echo "deb http://archive.debian.org/debian bullseye main" >> /etc/apt/sources.list + +# Utilities +RUN apt-get update && \ + apt-get install -y curl + +# Upgrade libstdc++6 from bullseye (required by Node.js 22) +RUN apt-get install -y -t bullseye libstdc++6 + +# Node.js (arm32/arm64 use official builds, others use NodeSource) +RUN if [ "$TARGETARCH" = "arm" ]; then \ + curl -fsSL https://nodejs.org/dist/v20.18.3/node-v20.18.3-linux-armv7l.tar.gz | tar -xz -C /usr/local --strip-components=1; \ + elif [ "$TARGETARCH" = "arm64" ]; then \ + curl -fsSL https://nodejs.org/dist/v22.21.1/node-v22.21.1-linux-arm64.tar.gz | tar -xz -C /usr/local --strip-components=1; \ + else \ + curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs; \ + fi + +# Chromium +RUN apt-get install -y chromium +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium + +# Desktop Bus +RUN apt-get install -y dbus-x11 && \ + mkdir -p /run/dbus + +# X11 Server +RUN apt-get install -y xvfb + +# Install newer libxkbfile1 from Debian 11 since Debian 10 version is too old +RUN apt-get install -y -t bullseye libxkbfile1 diff --git a/test/sanity/containers/debian-12.dockerfile b/test/sanity/containers/debian-12.dockerfile new file mode 100644 index 00000000000..3163d9d8d92 --- /dev/null +++ b/test/sanity/containers/debian-12.dockerfile @@ -0,0 +1,22 @@ +ARG MIRROR +ARG BASE_IMAGE=debian:bookworm +FROM ${MIRROR}${BASE_IMAGE} + +# Utilities +RUN apt-get update && \ + apt-get install -y curl + +# Node.js 22 +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs + +# Chromium +RUN apt-get install -y chromium +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium + +# Desktop Bus +RUN apt-get install -y dbus-x11 && \ + mkdir -p /run/dbus + +# X11 Server +RUN apt-get install -y xvfb diff --git a/test/sanity/containers/entrypoint.sh b/test/sanity/containers/entrypoint.sh new file mode 100644 index 00000000000..bdf72651107 --- /dev/null +++ b/test/sanity/containers/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +echo "System: $(uname -s) $(uname -r) $(uname -m), page size: $(getconf PAGESIZE) bytes" +echo "Memory: $(awk '/MemTotal/ {t=$2} /MemAvailable/ {a=$2} END {printf "%.0f MB total, %.0f MB available", t/1024, a/1024}' /proc/meminfo)" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" + +if command -v Xvfb > /dev/null 2>&1; then + echo "Starting X11 Server" + export DISPLAY=:99 + Xvfb $DISPLAY -screen 0 1024x768x24 -ac -noreset & +fi + +if command -v dbus-daemon > /dev/null 2>&1; then + echo "Starting Desktop Bus" + dbus-daemon --system --fork +fi + +export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + +echo "Running sanity tests" +node /root/out/index.js "$@" diff --git a/test/sanity/containers/fedora.dockerfile b/test/sanity/containers/fedora.dockerfile new file mode 100644 index 00000000000..9b97ec60578 --- /dev/null +++ b/test/sanity/containers/fedora.dockerfile @@ -0,0 +1,21 @@ +ARG MIRROR +ARG BASE_IMAGE=fedora:36 +FROM ${MIRROR}${BASE_IMAGE} + +# Node.js 22 +RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \ + dnf install -y nodejs-22.21.1 + +# Chromium +RUN dnf install -y chromium +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Desktop Bus +RUN dnf install -y dbus-x11 && \ + mkdir -p /run/dbus + +# X11 Server +RUN dnf install -y xorg-x11-server-Xvfb + +# VS Code dependencies +RUN dnf install -y xdg-utils diff --git a/test/sanity/containers/opensuse.dockerfile b/test/sanity/containers/opensuse.dockerfile new file mode 100644 index 00000000000..4f53a6e9cfa --- /dev/null +++ b/test/sanity/containers/opensuse.dockerfile @@ -0,0 +1,21 @@ +ARG BASE_IMAGE=opensuse/leap:16.0 +FROM ${BASE_IMAGE} + +# Node.js 22 +RUN zypper install -y nodejs22 + +# Chromium +RUN zypper install -y chromium pciutils Mesa-libGL1 +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium + +# X11 Server +RUN zypper install -y xorg-x11-server-Xvfb + +# Desktop Bus +RUN zypper install -y dbus-1-x11 && \ + mkdir -p /run/dbus + +# VS Code dependencies +RUN zypper install -y \ + liberation-fonts \ + libgtk-3-0 diff --git a/test/sanity/containers/redhat.dockerfile b/test/sanity/containers/redhat.dockerfile new file mode 100644 index 00000000000..03fca173549 --- /dev/null +++ b/test/sanity/containers/redhat.dockerfile @@ -0,0 +1,6 @@ +ARG BASE_IMAGE=redhat/ubi9:9.7 +FROM ${BASE_IMAGE} + +# Node.js 22 +RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \ + dnf install -y nodejs-22.21.1 diff --git a/test/sanity/containers/ubuntu.dockerfile b/test/sanity/containers/ubuntu.dockerfile new file mode 100644 index 00000000000..028f916ff22 --- /dev/null +++ b/test/sanity/containers/ubuntu.dockerfile @@ -0,0 +1,47 @@ +ARG MIRROR +ARG BASE_IMAGE=ubuntu:22.04 +FROM ${MIRROR}${BASE_IMAGE} + +# Use Azure package mirrors +ARG TARGETARCH +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \ + sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list.d/ubuntu.sources; \ + else \ + sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list; \ + fi; \ + else \ + if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then \ + sed -i 's|http://ports.ubuntu.com|http://azure.ports.ubuntu.com|g' /etc/apt/sources.list.d/ubuntu.sources; \ + else \ + sed -i 's|http://ports.ubuntu.com|http://azure.ports.ubuntu.com|g' /etc/apt/sources.list; \ + fi; \ + fi + +# Utilities +RUN apt-get update && \ + apt-get install -y curl iproute2 + +# Node.js 22 +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \ + apt-get install -y nodejs + +# No UI on arm32 on Ubuntu 24.04 +ARG BASE_IMAGE +ARG TARGETARCH +RUN if [ "$TARGETARCH" != "arm" ] || [ "$BASE_IMAGE" != "ubuntu:24.04" ]; then \ + # X11 Server \ + apt-get install -y xvfb && \ + # Desktop Bus \ + apt-get install -y dbus-x11 && \ + mkdir -p /run/dbus; \ + fi + +# VS Code dependencies +RUN apt-get install -y libasound2 || apt-get install -y libasound2t64 && \ + apt-get install -y libgtk-3-0 || apt-get install -y libgtk-3-0t64 && \ + apt-get install -y libcurl4 || apt-get install -y libcurl4t64 && \ + apt-get install -y \ + libgbm1 \ + libnss3 \ + xdg-utils diff --git a/test/sanity/package-lock.json b/test/sanity/package-lock.json new file mode 100644 index 00000000000..27d7cd8110e --- /dev/null +++ b/test/sanity/package-lock.json @@ -0,0 +1,1387 @@ +{ + "name": "code-oss-dev-sanity-test", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-oss-dev-sanity-test", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.8", + "mocha": "^11.7.5", + "mocha-junit-reporter": "^2.2.1", + "node-fetch": "^3.3.2", + "playwright": "^1.57.0" + }, + "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/node": "22.x", + "typescript": "^6.0.0-dev.20251110" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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==", + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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/chalk/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/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==", + "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/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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "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==", + "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/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "ISC" + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha-junit-reporter": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz", + "integrity": "sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "md5": "^2.3.0", + "mkdirp": "^3.0.0", + "strip-ansi": "^6.0.1", + "xml": "^1.0.1" + }, + "peerDependencies": { + "mocha": ">=2.2.5" + } + }, + "node_modules/mocha-junit-reporter/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha-junit-reporter/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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==", + "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/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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/typescript": { + "version": "6.0.0-dev.20260113", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.0-dev.20260113.tgz", + "integrity": "sha512-frXm5LJtstQlM511cGZLCalQjX5YUdUhvNSQAEcI4EuHoflAaqvCa2KIzPKNbyH3KmFPjA3EOs9FphTSKNc4CQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "license": "Apache-2.0" + }, + "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==", + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "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==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/test/sanity/package.json b/test/sanity/package.json new file mode 100644 index 00000000000..0b281e7a2ca --- /dev/null +++ b/test/sanity/package.json @@ -0,0 +1,24 @@ +{ + "name": "code-oss-dev-sanity-test", + "version": "0.1.0", + "license": "MIT", + "type": "module", + "main": "./out/index.js", + "scripts": { + "compile": "tsc", + "start": "node ./out/index.js" + }, + "dependencies": { + "minimist": "^1.2.8", + "mocha": "^11.7.5", + "mocha-junit-reporter": "^2.2.1", + "node-fetch": "^3.3.2", + "playwright": "^1.57.0" + }, + "devDependencies": { + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/node": "22.x", + "typescript": "^6.0.0-dev.20251110" + } +} diff --git a/test/sanity/scripts/qemu-init.sh b/test/sanity/scripts/qemu-init.sh new file mode 100755 index 00000000000..c4c95755d42 --- /dev/null +++ b/test/sanity/scripts/qemu-init.sh @@ -0,0 +1,51 @@ +#!/bin/sh +set -e + +# Mount kernel filesystems (proc for process info, sysfs for device info) +echo "Mounting kernel filesystems" +mount -t proc proc /proc +mount -t sysfs sys /sys + +# Mount pseudo-terminal and shared memory filesystems +echo "Mounting PTY and shared memory" +mkdir -p /dev/pts +mount -t devpts devpts /dev/pts +mkdir -p /dev/shm +mount -t tmpfs tmpfs /dev/shm + +# Mount temporary directories with proper permissions +echo "Mounting temporary directories" +mount -t tmpfs tmpfs /tmp +chmod 1777 /tmp +mount -t tmpfs tmpfs /var/tmp + +# Mount runtime directory for services (D-Bus, XDG) +echo "Mounting runtime directories" +mount -t tmpfs tmpfs /run +mkdir -p /run/dbus +mkdir -p /run/user/0 +chmod 700 /run/user/0 + +echo "Setting up machine-id for D-Bus" +cat /proc/sys/kernel/random/uuid | tr -d '-' > /etc/machine-id + +echo "Setting system clock" +date -s "$(cat /host-time)" + +echo "Setting up networking" +ip link set lo up +ip link set eth0 up +ip addr add 10.0.2.15/24 dev eth0 +ip route add default via 10.0.2.2 +echo "nameserver 10.0.2.3" > /etc/resolv.conf + +export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +export XDG_RUNTIME_DIR=/run/user/0 + +echo "Starting entrypoint" +sh /root/containers/entrypoint.sh $(cat /test-args) +echo $? > /exit-code +sync + +echo "Powering off" +echo o > /proc/sysrq-trigger diff --git a/test/sanity/scripts/run-docker.cmd b/test/sanity/scripts/run-docker.cmd new file mode 100644 index 00000000000..fd1ab024eb8 --- /dev/null +++ b/test/sanity/scripts/run-docker.cmd @@ -0,0 +1,64 @@ +@echo off +setlocal enabledelayedexpansion + +set ROOT=%~dp0.. +set CONTAINER= +set ARCH=amd64 +set MIRROR=mcr.microsoft.com/mirror/docker/library/ +set BASE_IMAGE= +set ARGS= + +:parse_args +if "%~1"=="" goto :done_parsing +if "%~1"=="--container" ( + set CONTAINER=%~2 + shift & shift + goto :parse_args +) +if "%~1"=="--arch" ( + set ARCH=%~2 + shift & shift + goto :parse_args +) +if "%~1"=="--base-image" ( + set BASE_IMAGE=%~2 + shift & shift + goto :parse_args +) +set "ARGS=!ARGS! %~1" +shift +goto :parse_args + +:done_parsing +if "%CONTAINER%"=="" ( + echo Error: --container is required + exit /b 1 +) + +set HOST_ARCH=amd64 +if "%PROCESSOR_ARCHITECTURE%"=="ARM64" set HOST_ARCH=arm64 +if not "%ARCH%"=="%HOST_ARCH%" ( + echo Setting up QEMU emulation for %ARCH% on %HOST_ARCH% host + docker run --privileged --rm tonistiigi/binfmt --install all >nul 2>&1 +) + +set BASE_IMAGE_ARG= +if not "%BASE_IMAGE%"=="" set BASE_IMAGE_ARG=--build-arg "BASE_IMAGE=%BASE_IMAGE%" + +echo Building container image: %CONTAINER% +docker buildx build ^ + --platform "linux/%ARCH%" ^ + --build-arg "MIRROR=%MIRROR%" ^ + %BASE_IMAGE_ARG% ^ + --tag "%CONTAINER%" ^ + --file "%ROOT%\containers\%CONTAINER%.dockerfile" ^ + "%ROOT%\containers" + +echo Running sanity tests in container +docker run ^ + --rm ^ + --platform "linux/%ARCH%" ^ + --volume "%ROOT%:/root" ^ + --entrypoint sh ^ + "%CONTAINER%" ^ + /root/containers/entrypoint.sh %ARGS% diff --git a/test/sanity/scripts/run-docker.sh b/test/sanity/scripts/run-docker.sh new file mode 100755 index 00000000000..0007f9b98f0 --- /dev/null +++ b/test/sanity/scripts/run-docker.sh @@ -0,0 +1,62 @@ +#!/bin/sh +set -e + +CONTAINER="" +ARCH="amd64" +MIRROR="mcr.microsoft.com/mirror/docker/library/" +BASE_IMAGE="" +PAGE_SIZE="" +ARGS="" + +while [ $# -gt 0 ]; do + case "$1" in + --container) CONTAINER="$2"; shift 2 ;; + --arch) ARCH="$2"; shift 2 ;; + --base-image) BASE_IMAGE="$2"; shift 2 ;; + --page-size) PAGE_SIZE="$2"; shift 2 ;; + *) ARGS="$ARGS $1"; shift ;; + esac +done + +if [ -z "$CONTAINER" ]; then + echo "Error: --container is required" + exit 1 +fi + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/.." && pwd) + +# Only build if image doesn't exist (i.e., not loaded from cache) +if ! docker image inspect "$CONTAINER" > /dev/null 2>&1; then + if [ "$PAGE_SIZE" != "" ]; then + echo "Setting up QEMU user-mode emulation for $ARCH" + docker run --privileged --rm tonistiigi/binfmt --install "$ARCH" + fi + + echo "Building container image: $CONTAINER" + docker buildx build \ + --platform "linux/$ARCH" \ + --build-arg "MIRROR=$MIRROR" \ + ${BASE_IMAGE:+--build-arg "BASE_IMAGE=$BASE_IMAGE"} \ + --tag "$CONTAINER" \ + --file "$ROOT_DIR/containers/$CONTAINER.dockerfile" \ + "$ROOT_DIR/containers" +else + echo "Using cached container image: $CONTAINER" +fi + +# For 64K page size, use QEMU system emulation with a 64K kernel +if [ "$PAGE_SIZE" = "64k" ]; then + exec "$SCRIPT_DIR/run-qemu-64k.sh" \ + --container "$CONTAINER" \ + -- $ARGS +else + echo "Running sanity tests in container" + docker run \ + --rm \ + --platform "linux/$ARCH" \ + --volume "$ROOT_DIR:/root" \ + --entrypoint sh \ + "$CONTAINER" \ + /root/containers/entrypoint.sh $ARGS +fi diff --git a/test/sanity/scripts/run-macOS.sh b/test/sanity/scripts/run-macOS.sh new file mode 100755 index 00000000000..7ac2197ad25 --- /dev/null +++ b/test/sanity/scripts/run-macOS.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +echo "System: $(uname -s) $(uname -r) $(uname -m)" +echo "Memory: $(( $(sysctl -n hw.memsize) / 1024 / 1024 / 1024 )) GB" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" + +echo "Installing Playwright WebKit browser" +npx playwright install --with-deps webkit + +echo "Running sanity tests" +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +node "$SCRIPT_DIR/../out/index.js" $@ diff --git a/test/sanity/scripts/run-qemu-64k.sh b/test/sanity/scripts/run-qemu-64k.sh new file mode 100755 index 00000000000..55198489922 --- /dev/null +++ b/test/sanity/scripts/run-qemu-64k.sh @@ -0,0 +1,86 @@ +#!/bin/sh +set -e + +CONTAINER="" +ARGS="" + +while [ $# -gt 0 ]; do + case "$1" in + --container) CONTAINER="$2"; shift 2 ;; + --) shift; ARGS="$*"; break ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +if [ -z "$CONTAINER" ]; then + echo "Usage: $0 --container CONTAINER [-- ARGS...]" + exit 1 +fi + +echo "Installing QEMU system emulation and tools" +sudo apt-get update && sudo apt-get install -y qemu-system-arm binutils + +echo "Exporting container filesystem" +CONTAINER_ID=$(docker create --platform linux/arm64 "$CONTAINER") +ROOTFS_DIR=$(mktemp -d) +docker export "$CONTAINER_ID" | sudo tar -xf - -C "$ROOTFS_DIR" +docker rm -f "$CONTAINER_ID" + +# echo "Removing container image to free disk space" +# docker rmi "$CONTAINER" || true +docker system prune -f || true + +echo "Copying test files into root filesystem" +TEST_DIR=$(cd "$(dirname "$0")/.." && pwd) +sudo cp -r "$TEST_DIR"/* "$ROOTFS_DIR/root/" + +echo "Downloading Ubuntu 24.04 generic-64k kernel for ARM64" +KERNEL_URL="https://ports.ubuntu.com/ubuntu-ports/pool/main/l/linux/linux-image-unsigned-6.8.0-90-generic-64k_6.8.0-90.91_arm64.deb" +KERNEL_DIR=$(mktemp -d) +curl -fL "$KERNEL_URL" -o "$KERNEL_DIR/kernel.deb" + +echo "Extracting kernel" +cd "$KERNEL_DIR" && ar x kernel.deb && rm kernel.deb +tar xf data.tar* && rm -f debian-binary control.tar* data.tar* +VMLINUZ="$KERNEL_DIR/boot/vmlinuz-6.8.0-90-generic-64k" +if [ ! -f "$VMLINUZ" ]; then + echo "Error: Could not find kernel at $VMLINUZ" + exit 1 +fi + +echo "Storing test arguments and installing init script" +echo "$ARGS" > "$ROOTFS_DIR/test-args" +date -u '+%Y-%m-%d %H:%M:%S' > "$ROOTFS_DIR/host-time" +sudo mv "$ROOTFS_DIR/root/scripts/qemu-init.sh" "$ROOTFS_DIR/init" +sudo chmod +x "$ROOTFS_DIR/init" + +echo "Creating disk image with root filesystem" +DISK_IMG=$(mktemp) +dd if=/dev/zero of="$DISK_IMG" bs=1M count=2048 status=none +sudo mkfs.ext4 -q -d "$ROOTFS_DIR" "$DISK_IMG" +sudo rm -rf "$ROOTFS_DIR" + +echo "Starting QEMU VM with 64K page size kernel" +timeout 1800 qemu-system-aarch64 \ + -M virt \ + -cpu max,pauth-impdef=on \ + -accel tcg,thread=multi \ + -m 4096 \ + -smp 2 \ + -kernel "$VMLINUZ" \ + -append "console=ttyAMA0 root=/dev/vda rw init=/init net.ifnames=0" \ + -drive file="$DISK_IMG",format=raw,if=virtio \ + -netdev user,id=net0 \ + -device virtio-net-pci,netdev=net0 \ + -nographic \ + -no-reboot + +echo "Extracting test results from disk image" +MOUNT_DIR=$(mktemp -d) +sudo mount -o loop "$DISK_IMG" "$MOUNT_DIR" +sudo cp "$MOUNT_DIR/root/results.xml" "$TEST_DIR/results.xml" +sudo chown "$(id -u):$(id -g)" "$TEST_DIR/results.xml" + +EXIT_CODE=$(sudo cat "$MOUNT_DIR/exit-code" 2>/dev/null || echo 1) +sudo umount "$MOUNT_DIR" +exit $EXIT_CODE diff --git a/test/sanity/scripts/run-ubuntu.sh b/test/sanity/scripts/run-ubuntu.sh new file mode 100755 index 00000000000..884f292c38c --- /dev/null +++ b/test/sanity/scripts/run-ubuntu.sh @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +echo "System: $(uname -s) $(uname -r) $(uname -m)" +echo "Memory: $(free -h | awk '/^Mem:/ {print $2 " total, " $3 " used, " $7 " available"}')" +echo "Disk: $(df -h / | awk 'NR==2 {print $2 " total, " $3 " used, " $4 " available"}')" + +echo "Configuring Azure mirror" +sudo sed -i 's|http://archive.ubuntu.com|http://azure.archive.ubuntu.com|g' /etc/apt/sources.list + +echo "Installing dependencies" +sudo apt-get update +sudo apt-get install -y dbus-x11 x11-utils xvfb + +echo "Installing Chromium" +sudo snap install chromium +export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser + +echo "Starting X11 Server" +export DISPLAY=:99 +Xvfb $DISPLAY -screen 0 1024x768x24 -ac -noreset & + +echo "Starting Snap daemon" +sudo systemctl start snapd.socket +sudo systemctl start snapd.service + +echo "Running sanity tests" +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +node "$SCRIPT_DIR/../out/index.js" "$@" diff --git a/test/sanity/scripts/run-win32.cmd b/test/sanity/scripts/run-win32.cmd new file mode 100644 index 00000000000..beca60e057b --- /dev/null +++ b/test/sanity/scripts/run-win32.cmd @@ -0,0 +1,54 @@ +@echo off +setlocal + +for /f "tokens=*" %%a in ('powershell -NoProfile -Command "[int](Get-CimInstance Win32_Processor).Architecture"') do set ARCH=%%a +if "%ARCH%"=="12" (set "ARCH_NAME=ARM64") else if "%ARCH%"=="9" (set "ARCH_NAME=AMD64") else if "%ARCH%"=="5" (set "ARCH_NAME=ARM") else (set "ARCH_NAME=x86") + +echo System: %OS% %ARCH_NAME% +powershell -NoProfile -Command "$os = Get-CimInstance Win32_OperatingSystem; $total = $os.TotalVisibleMemorySize/1MB; $free = $os.FreePhysicalMemory/1MB; Write-Host ('Memory: {0:N0} GB free of {1:N0} GB' -f $free, $total)" +powershell -NoProfile -Command "$disk = Get-PSDrive C; Write-Host ('Disk C: {0:N0} GB free of {1:N0} GB' -f ($disk.Free/1GB), (($disk.Used+$disk.Free)/1GB))" + +echo Checking if WSL is installed +where wsl >nul 2>nul +if errorlevel 1 ( + echo WSL is not installed, skipping WSL setup +) else ( + echo Checking if Ubuntu is available on WSL + powershell -NoProfile -Command "if ((wsl -l -q) -contains 'Ubuntu') { exit 0 } else { exit 1 }" + if errorlevel 1 call :install_ubuntu +) + +set PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +set PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe + +echo Running sanity tests +node "%~dp0..\out\index.js" %* + +goto :eof + +REM ==================================================================================================================== + +:install_ubuntu + +echo Ubuntu image is not present in WSL + +if "%ARCH%"=="12" ( + set "ROOTFS_URL=https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-arm64-ubuntu22.04lts.rootfs.tar.gz" +) else ( + set "ROOTFS_URL=https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-amd64-ubuntu22.04lts.rootfs.tar.gz" +) + +set "ROOTFS_ZIP=%TEMP%\ubuntu-rootfs.tar.gz" +set "ROOTFS_DIR=%LOCALAPPDATA%\WSL\Ubuntu" + +echo Downloading Ubuntu rootfs from %ROOTFS_URL% to %ROOTFS_ZIP% +curl -L -o "%ROOTFS_ZIP%" "%ROOTFS_URL%" + +echo Importing Ubuntu into WSL at %ROOTFS_DIR% from %ROOTFS_ZIP% +mkdir "%ROOTFS_DIR%" 2>nul +wsl --import Ubuntu "%ROOTFS_DIR%" "%ROOTFS_ZIP%" + +echo Starting Ubuntu on WSL +wsl -d Ubuntu echo Ubuntu WSL is ready + +goto :eof diff --git a/test/sanity/src/cli.test.ts b/test/sanity/src/cli.test.ts new file mode 100644 index 00000000000..d22761ee92c --- /dev/null +++ b/test/sanity/src/cli.test.ts @@ -0,0 +1,150 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Browser } from 'playwright'; +import { TestContext } from './context.js'; +import { GitHubAuth } from './githubAuth.js'; +import { UITest } from './uiTest.js'; + +export function setup(context: TestContext) { + context.test('cli-alpine-arm64', ['alpine', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('cli-alpine-arm64'); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-alpine-x64', ['alpine', 'x64'], async () => { + const dir = await context.downloadAndUnpack('cli-alpine-x64'); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-darwin-arm64', ['darwin', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('cli-darwin-arm64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-darwin-x64', ['darwin', 'x64'], async () => { + const dir = await context.downloadAndUnpack('cli-darwin-x64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-linux-arm64', ['linux', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('cli-linux-arm64'); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-linux-armhf', ['linux', 'arm32'], async () => { + const dir = await context.downloadAndUnpack('cli-linux-armhf'); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-linux-x64', ['linux', 'x64'], async () => { + const dir = await context.downloadAndUnpack('cli-linux-x64'); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-win32-arm64', ['windows', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('cli-win32-arm64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + context.test('cli-win32-x64', ['windows', 'x64'], async () => { + const dir = await context.downloadAndUnpack('cli-win32-x64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getCliEntryPoint(dir); + await testCliApp(entryPoint); + }); + + async function testCliApp(entryPoint: string) { + if (context.options.downloadOnly) { + return; + } + + const result = context.runNoErrors(entryPoint, '--version'); + const version = result.stdout.trim().match(/\(commit ([a-f0-9]+)\)/)?.[1]; + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); + + if (!context.capabilities.has('github-account')) { + return; + } + + const cliDataDir = context.createTempDir(); + const test = new UITest(context); + const auth = new GitHubAuth(context); + let browser: Browser | undefined; + + context.log('Logging out of Dev Tunnel to ensure fresh authentication'); + context.run(entryPoint, '--cli-data-dir', cliDataDir, 'tunnel', 'user', 'logout'); + + context.log('Starting Dev Tunnel to local server using CLI'); + await context.runCliApp('CLI', entryPoint, + [ + '--cli-data-dir', cliDataDir, + 'tunnel', + '--accept-server-license-terms', + '--server-data-dir', context.createTempDir(), + '--extensions-dir', test.extensionsDir, + '--verbose' + ], + async (line) => { + const deviceCode = /To grant access .* use code ([A-Z0-9-]+)/.exec(line)?.[1]; + if (deviceCode) { + context.log(`Device code detected: ${deviceCode}, starting device flow authentication`); + browser = await context.launchBrowser(); + await auth.runDeviceCodeFlow(browser, deviceCode); + return; + } + + const tunnelUrl = /Open this link in your browser (https?:\/\/[^\s]+)/.exec(line)?.[1]; + if (tunnelUrl) { + const tunnelId = new URL(tunnelUrl).pathname.split('/').pop()!; + const url = context.getTunnelUrl(tunnelUrl, test.workspaceDir); + context.log(`CLI started successfully with tunnel URL: ${url}`); + + if (!browser) { + throw new Error('Browser instance is not available'); + } + + context.log(`Navigating to ${url}`); + const page = await context.getPage(browser.newPage()); + await page.goto(url); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + context.log('Selecting GitHub Account'); + await page.locator('span.monaco-highlighted-label', { hasText: 'GitHub' }).click(); + + context.log('Clicking Allow on confirmation dialog'); + await page.getByRole('button', { name: 'Allow' }).click(); + + await auth.runUserWebFlow(page); + + context.log('Waiting for connection to be established'); + await page.getByRole('button', { name: `remote ${tunnelId}` }).waitFor({ timeout: 5 * 60 * 1000 }); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; + } + } + ); + } +} diff --git a/test/sanity/src/context.ts b/test/sanity/src/context.ts new file mode 100644 index 00000000000..c0b053ee309 --- /dev/null +++ b/test/sanity/src/context.ts @@ -0,0 +1,1110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { spawn, spawnSync, SpawnSyncReturns } from 'child_process'; +import { createHash } from 'crypto'; +import fs from 'fs'; +import { test } from 'mocha'; +import fetch, { Response } from 'node-fetch'; +import os from 'os'; +import path from 'path'; +import { Browser, chromium, Page, webkit } from 'playwright'; +import { Capability, detectCapabilities } from './detectors.js'; + +/** + * Response from https://update.code.visualstudio.com/api/versions/commit:// + */ +interface ITargetMetadata { + url: string; + name: string; + version: string; + productVersion: string; + hash: string; + timestamp: number; + sha256hash: string; + supportsFastUpdate: boolean; +} + +/** + * Provides context and utilities for VS Code sanity tests. + */ +export class TestContext { + private static readonly authenticodeInclude = /^.+\.(exe|dll|sys|cab|cat|msi|jar|ocx|ps1|psm1|psd1|ps1xml|pssc1)$/i; + private static readonly codesignExclude = /node_modules\/(@parcel\/watcher\/build\/Release\/watcher\.node|@vscode\/deviceid\/build\/Release\/windows\.node|@vscode\/ripgrep\/bin\/rg|@vscode\/spdlog\/build\/Release\/spdlog.node|kerberos\/build\/Release\/kerberos.node|@vscode\/native-watchdog\/build\/Release\/watchdog\.node|node-pty\/build\/Release\/(pty\.node|spawn-helper)|vsda\/build\/Release\/vsda\.node|native-watchdog\/build\/Release\/watchdog\.node)$/; + private static readonly notarizeExclude = /extensions\/microsoft-authentication\/dist\/libmsalruntime\.dylib$/; + + private readonly tempDirs = new Set(); + private readonly wslTempDirs = new Set(); + private nextPort = 3010; + + public constructor(public readonly options: Readonly<{ + quality: 'stable' | 'insider' | 'exploration'; + commit: string; + verbose: boolean; + cleanup: boolean; + checkSigning: boolean; + headlessBrowser: boolean; + downloadOnly: boolean; + }>) { + } + + /** + * Returns true if the current process is running as root. + */ + public readonly isRootUser = process.getuid?.() === 0; + + /** + * Returns the detected capabilities of the current system. + */ + public readonly capabilities = detectCapabilities(); + + /** + * Returns the OS temp directory with expanded long names on Windows. + */ + public readonly osTempDir = (function () { + let tempDir = fs.realpathSync(os.tmpdir()); + + // On Windows, expand short 8.3 file names to long names + if (os.platform() === 'win32') { + const result = spawnSync('powershell', ['-Command', `(Get-Item "${tempDir}").FullName`], { encoding: 'utf-8' }); + if (result.status === 0 && result.stdout) { + tempDir = result.stdout.trim(); + } + } + + return tempDir; + })(); + + /** + * Runs a test only if the required capabilities are present. + * @param name The name of the test. + * @param require The required capabilities for the test. + * @param fn The test function. + * @returns The Mocha test object or void if the test is skipped. + */ + public test(name: string, require: Capability[], fn: () => Promise): Mocha.Test | void { + if (!this.options.downloadOnly && require.some(o => !this.capabilities.has(o))) { + return; + } + + const self = this; + return test(name, async function () { + self.log(`Starting test: ${name}`); + + const homeDir = os.homedir(); + process.chdir(homeDir); + self.log(`Changed working directory to: ${homeDir}`); + + try { + await fn(); + + } catch (error) { + self.log(`Test failed with error: ${error instanceof Error ? error.message : String(error)}`); + throw error; + + } finally { + process.chdir(homeDir); + self.log(`Changed working directory to: ${homeDir}`); + + if (self.options.cleanup) { + self.cleanup(); + } + + self.log(`Finished test: ${name}`); + } + }); + } + + /** + * The console outputs collected during the current test. + */ + public consoleOutputs: string[] = []; + + /** + * Logs a message with a timestamp. + */ + public log(message: string) { + const line = `[${new Date().toISOString()}] ${message}`; + this.consoleOutputs.push(line); + if (this.options.verbose) { + console.log(line); + } + } + + /** + * Logs an error message and throws an Error. + */ + public error(message: string): never { + const line = `[${new Date().toISOString()}] ERROR: ${message}`; + this.consoleOutputs.push(line); + console.error(line); + throw new Error(message); + } + + /** + * Creates a new temporary directory and returns its path. + */ + public createTempDir(): string { + const tempDir = fs.mkdtempSync(path.join(this.osTempDir, 'vscode-sanity')); + this.log(`Created temp directory: ${tempDir}`); + this.tempDirs.add(tempDir); + return tempDir; + } + + /** + * Creates a new temporary directory in WSL and returns its path. + */ + public createWslTempDir(): string { + const tempDir = `/tmp/vscode-sanity-${Date.now()}-${Math.random().toString(36).slice(2)}`; + this.log(`Creating WSL temp directory: ${tempDir}`); + this.runNoErrors('wsl', 'mkdir', '-p', tempDir); + this.wslTempDirs.add(tempDir); + return tempDir; + } + + /** + * Deletes a directory in WSL. + * @param dir The WSL directory path to delete. + */ + public deleteWslDir(dir: string): void { + this.log(`Deleting WSL directory: ${dir}`); + this.runNoErrors('wsl', 'rm', '-rf', dir); + } + + /** + * Converts a Windows path to a WSL path. + * @param windowsPath The Windows path to convert (e.g., 'C:\Users\test'). + * @returns The WSL path (e.g., '/mnt/c/Users/test'). + */ + public toWslPath(windowsPath: string): string { + return windowsPath + .replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`) + .replaceAll('\\', '/'); + } + + /** + * Returns the name of the default WSL distribution. + * @returns The default WSL distribution name (e.g., 'Ubuntu'). + */ + public getDefaultWslDistro(): string { + const result = this.runNoErrors('wsl', '--list', '--quiet'); + const distro = result.stdout.trim().split('\n')[0].replace(/\0/g, '').trim(); + if (!distro) { + this.error('No WSL distribution found'); + } + this.log(`Default WSL distribution: ${distro}`); + return distro; + } + + /** + * Ensures that the directory for the specified file path exists. + */ + public ensureDirExists(filePath: string) { + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + } + + /** + * Cleans up all temporary directories created during the test run. + */ + public cleanup() { + for (const dir of this.tempDirs) { + this.log(`Deleting temp directory: ${dir}`); + try { + fs.rmSync(dir, { recursive: true, force: true }); + this.log(`Deleted temp directory: ${dir}`); + } catch (error) { + this.log(`Failed to delete temp directory: ${dir}: ${error}`); + } + } + this.tempDirs.clear(); + + for (const dir of this.wslTempDirs) { + try { + this.deleteWslDir(dir); + } catch (error) { + this.log(`Failed to delete WSL temp directory: ${dir}: ${error}`); + } + } + this.wslTempDirs.clear(); + } + + /** + * Fetches a URL and ensures there are no errors. + * @param url The URL to fetch. + * @returns The fetch Response object. + */ + public async fetchNoErrors(url: string): Promise { + const maxRetries = 5; + let lastError: Error | undefined; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + if (attempt > 0) { + const delay = Math.pow(2, attempt - 1) * 1000; + this.log(`Retrying fetch (attempt ${attempt + 1}/${maxRetries}) after ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + + try { + const response = await fetch(url); + if (!response.ok) { + lastError = new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + continue; + } + + if (response.body === null) { + lastError = new Error(`Response body is null for ${url}`); + continue; + } + + return response as Response & { body: NodeJS.ReadableStream }; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + this.log(`Fetch attempt ${attempt + 1} failed: ${lastError.message}`); + } + } + + this.error(`Failed to fetch ${url} after ${maxRetries} attempts: ${lastError?.message}`); + } + + /** + * Fetches metadata for a specific VS Code release target. + * @param target The target platform (e.g., 'cli-linux-x64'). + * @returns The target metadata. + */ + public async fetchMetadata(target: string): Promise { + const url = `https://update.code.visualstudio.com/api/versions/commit:${this.options.commit}/${target}/${this.options.quality}`; + + this.log(`Fetching metadata for ${target} from ${url}`); + const response = await this.fetchNoErrors(url); + + const result = await response.json() as ITargetMetadata; + if (result.url === undefined || result.sha256hash === undefined) { + this.error(`Invalid metadata response for ${target}: ${JSON.stringify(result)}`); + } + + this.log(`Fetched metadata for ${target}: ${JSON.stringify(result)}`); + return result; + } + + /** + * Downloads installer for specified VS Code release target. + * @param target The target platform (e.g., 'cli-linux-x64'). + * @returns The path to the downloaded file. + */ + public async downloadTarget(target: string): Promise { + const { url, sha256hash } = await this.fetchMetadata(target); + const filePath = path.join(this.createTempDir(), path.basename(url)); + + this.log(`Downloading ${url} to ${filePath}`); + const { body } = await this.fetchNoErrors(url); + + const stream = fs.createWriteStream(filePath); + await new Promise((resolve, reject) => { + body.on('error', reject); + stream.on('error', reject); + stream.on('finish', resolve); + body.pipe(stream); + }); + + this.log(`Downloaded ${url} to ${filePath}`); + this.validateSha256Hash(filePath, sha256hash); + + return filePath; + } + + /** + * Validates the SHA-256 hash of a file. + * @param filePath The path to the file to validate. + * @param expectedHash The expected SHA-256 hash in hexadecimal format. + */ + public validateSha256Hash(filePath: string, expectedHash: string) { + this.log(`Validating SHA256 hash for ${filePath}`); + + const buffer = fs.readFileSync(filePath); + const hash = createHash('sha256').update(buffer).digest('hex'); + + if (hash !== expectedHash) { + this.error(`Hash mismatch for ${filePath}: expected ${expectedHash}, got ${hash}`); + } + } + + /** + * Validates the Authenticode signature of a Windows executable. + * @param filePath The path to the file to validate. + */ + public validateAuthenticodeSignature(filePath: string) { + if (!this.options.checkSigning || !this.capabilities.has('windows')) { + this.log(`Skipping Authenticode signature validation for ${filePath} (signing checks disabled)`); + return; + } + + this.log(`Validating Authenticode signature for ${filePath}`); + + const result = this.run('powershell', '-Command', `Get-AuthenticodeSignature "${filePath}" | Select-Object -ExpandProperty Status`); + if (result.error !== undefined) { + this.error(`Failed to run Get-AuthenticodeSignature: ${result.error.message}`); + } + + const status = result.stdout.trim(); + if (status !== 'Valid') { + this.error(`Authenticode signature is not valid for ${filePath}: ${status}`); + } + } + + /** + * Validates Authenticode signatures for all executable files in the specified directory. + * @param dir The directory to scan for executable files. + */ + public validateAllAuthenticodeSignatures(dir: string) { + if (!this.options.checkSigning || !this.capabilities.has('windows')) { + this.log(`Skipping Authenticode signature validation for ${dir} (signing checks disabled)`); + return; + } + + const files = fs.readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + const filePath = path.join(dir, file.name); + if (file.isDirectory()) { + this.validateAllAuthenticodeSignatures(filePath); + } else if (TestContext.authenticodeInclude.test(file.name)) { + this.validateAuthenticodeSignature(filePath); + } + } + } + + /** + * Validates the codesign signature of a macOS binary or app bundle. + * @param filePath The path to the file or app bundle to validate. + */ + public validateCodesignSignature(filePath: string) { + if (!this.options.checkSigning || !this.capabilities.has('darwin')) { + this.log(`Skipping codesign signature validation for ${filePath} (signing checks disabled)`); + return; + } + + this.log(`Validating codesign signature for ${filePath}`); + + const result = this.run('codesign', '--verify', '--deep', '--strict', '--verbose=2', filePath); + if (result.error !== undefined) { + this.error(`Failed to run codesign: ${result.error.message}`); + } + + if (result.status !== 0) { + this.error(`Codesign signature is not valid for ${filePath}: ${result.stderr}`); + } + + if (!TestContext.notarizeExclude.test(filePath)) { + this.log(`Validating notarization for ${filePath}`); + + const notaryResult = this.run('spctl', '--assess', '--type', 'open', '--context', 'context:primary-signature', '--verbose=2', filePath); + if (notaryResult.error !== undefined) { + this.error(`Failed to run spctl: ${notaryResult.error.message}`); + } + + if (notaryResult.status !== 0) { + this.error(`Notarization is not valid for ${filePath}: ${notaryResult.stderr}`); + } + } + } + + /** + * Validates codesign signatures for all Mach-O binaries in the specified directory. + * @param dir The directory to scan for Mach-O binaries. + */ + public validateAllCodesignSignatures(dir: string) { + if (!this.options.checkSigning || !this.capabilities.has('darwin')) { + this.log(`Skipping codesign signature validation for ${dir} (signing checks disabled)`); + return; + } + + const files = fs.readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + const filePath = path.join(dir, file.name); + if (TestContext.codesignExclude.test(filePath)) { + this.log(`Skipping codesign validation for excluded file: ${filePath}`); + } else if (file.isDirectory()) { + // For .app bundles, validate the bundle itself, not its contents + if (file.name.endsWith('.app') || file.name.endsWith('.framework')) { + this.validateCodesignSignature(filePath); + } else { + this.validateAllCodesignSignatures(filePath); + } + } else if (this.isMachOBinary(filePath)) { + this.validateCodesignSignature(filePath); + } + } + } + + /** + * Checks if a file is a Mach-O binary by examining its magic number. + * @param filePath The path to the file to check. + * @returns True if the file is a Mach-O binary. + */ + private isMachOBinary(filePath: string): boolean { + try { + const file = fs.openSync(filePath, 'r'); + const buffer = Buffer.alloc(4); + fs.readSync(file, buffer, 0, 4, 0); + fs.closeSync(file); + const magic = buffer.readUInt32BE(0); + return magic === 0xFEEDFACE || magic === 0xCEFAEDFE || magic === 0xFEEDFACF || + magic === 0xCFFAEDFE || magic === 0xCAFEBABE || magic === 0xBEBAFECA; + } catch { + return false; + } + } + + /** + * Downloads and unpacks the specified VS Code release target. + * @param target The target platform (e.g., 'cli-linux-x64'). + * @returns The path to the unpacked directory. + */ + public async downloadAndUnpack(target: string): Promise { + const filePath = await this.downloadTarget(target); + return this.unpackArchive(filePath); + } + + /** + * Unpacks a .zip or .tar.gz archive to a temporary directory. + * @param archivePath The path to the archive file. + * @returns The path to the temporary directory where the archive was unpacked. + */ + public unpackArchive(archivePath: string): string { + const dir = this.createTempDir(); + + this.log(`Unpacking ${archivePath} to ${dir}`); + this.runNoErrors('tar', '-xzf', archivePath, '-C', dir, '--no-same-permissions'); + this.log(`Unpacked ${archivePath} to ${dir}`); + + return dir; + } + + /** + * Mounts a macOS DMG file and returns the mount point. + * @param dmgPath The path to the DMG file. + * @returns The path to the mounted volume. + */ + public mountDmg(dmgPath: string): string { + this.log(`Mounting DMG ${dmgPath}`); + const result = this.runNoErrors('hdiutil', 'attach', dmgPath, '-nobrowse', '-readonly'); + + // Parse the output to find the mount point (last column of the last line) + const lines = result.stdout.trim().split('\n'); + const lastLine = lines[lines.length - 1]; + const mountPoint = lastLine.split('\t').pop()?.trim(); + + if (!mountPoint || !fs.existsSync(mountPoint)) { + this.error(`Failed to find mount point for DMG ${dmgPath}`); + } + + this.log(`Mounted DMG at ${mountPoint}`); + return mountPoint; + } + + /** + * Unmounts a macOS DMG volume. + * @param mountPoint The path to the mounted volume. + */ + public unmountDmg(mountPoint: string): void { + this.log(`Unmounting DMG ${mountPoint}`); + this.runNoErrors('hdiutil', 'detach', mountPoint); + this.log(`Unmounted DMG ${mountPoint}`); + } + + /** + * Runs a command synchronously. + * @param command The command to run. + * @param args Optional arguments for the command. + * @returns The result of the spawnSync call. + */ + public run(command: string, ...args: string[]): SpawnSyncReturns { + this.log(`Running command: ${command} ${args.join(' ')}`); + return spawnSync(command, args, { encoding: 'utf-8' }) as SpawnSyncReturns; + } + + /** + * Runs a command synchronously and ensures it succeeds. + * @param command The command to run. + * @param args Optional arguments for the command. + * @returns The result of the spawnSync call. + */ + public runNoErrors(command: string, ...args: string[]): SpawnSyncReturns { + const result = this.run(command, ...args); + if (result.error !== undefined) { + this.error(`Failed to run command: ${result.error.message}`); + } + + if (result.status !== 0) { + this.error(`Command exited with code ${result.status}: ${result.stderr}`); + } + + return result; + } + + /** + * Kills a process and all its child processes. + * @param pid The process ID to kill. + */ + public killProcessTree(pid: number): void { + this.log(`Killing process tree for PID: ${pid}`); + if (os.platform() === 'win32') { + spawnSync('taskkill', ['/T', '/F', '/PID', pid.toString()]); + } else { + process.kill(-pid, 'SIGKILL'); + } + this.log(`Killed process tree for PID: ${pid}`); + } + + /** + * Returns the Windows installation directory for VS Code based on the installation type and quality. + * @param type The type of installation ('user' or 'system'). + * @returns The path to the VS Code installation directory. + */ + private getWindowsInstallDir(type: 'user' | 'system'): string { + let parentDir: string; + if (type === 'system') { + parentDir = process.env['ProgramW6432'] || process.env['PROGRAMFILES'] || ''; + } else { + parentDir = path.join(process.env['LOCALAPPDATA'] || '', 'Programs'); + } + + switch (this.options.quality) { + case 'stable': + return path.join(parentDir, 'Microsoft VS Code'); + case 'insider': + return path.join(parentDir, 'Microsoft VS Code Insiders'); + case 'exploration': + return path.join(parentDir, 'Microsoft VS Code Exploration'); + } + } + + /** + * Installs a Microsoft Installer package silently. + * @param installerPath The path to the installer executable. + * @returns The path to the installed VS Code executable. + */ + public installWindowsApp(type: 'user' | 'system', installerPath: string): string { + this.log(`Installing ${installerPath} in silent mode`); + this.runNoErrors(installerPath, '/silent', '/mergetasks=!runcode'); + this.log(`Installed ${installerPath} successfully`); + + const appDir = this.getWindowsInstallDir(type); + let entryPoint: string; + switch (this.options.quality) { + case 'stable': + entryPoint = path.join(appDir, 'Code.exe'); + break; + case 'insider': + entryPoint = path.join(appDir, 'Code - Insiders.exe'); + break; + case 'exploration': + entryPoint = path.join(appDir, 'Code - Exploration.exe'); + break; + } + + if (!fs.existsSync(entryPoint)) { + this.error(`Desktop entry point does not exist: ${entryPoint}`); + } + + this.log(`Installed VS Code executable at: ${entryPoint}`); + return entryPoint; + } + + /** + * Uninstalls a Windows application silently. + * @param type The type of installation ('user' or 'system'). + */ + public async uninstallWindowsApp(type: 'user' | 'system') { + const appDir = this.getWindowsInstallDir(type); + const uninstallerPath = path.join(appDir, 'unins000.exe'); + if (!fs.existsSync(uninstallerPath)) { + this.error(`Uninstaller does not exist: ${uninstallerPath}`); + } + + this.log(`Uninstalling VS Code from ${appDir} in silent mode`); + this.runNoErrors(uninstallerPath, '/silent'); + this.log(`Uninstalled VS Code from ${appDir} successfully`); + + await new Promise(resolve => setTimeout(resolve, 2000)); + if (fs.existsSync(appDir)) { + this.error(`Installation directory still exists after uninstall: ${appDir}`); + } + } + + /** + * Installs VS Code Linux DEB package. + * @param packagePath The path to the DEB file. + * @returns The path to the installed VS Code executable. + */ + public installDeb(packagePath: string): string { + this.log(`Installing ${packagePath} using DEB package manager`); + if (this.isRootUser) { + this.runNoErrors('dpkg', '-i', packagePath); + } else { + this.runNoErrors('sudo', 'dpkg', '-i', packagePath); + } + this.log(`Installed ${packagePath} successfully`); + + const name = this.getLinuxBinaryName(); + const entryPoint = path.join('/usr/share', name, name); + this.log(`Installed VS Code executable at: ${entryPoint}`); + return entryPoint; + } + + /** + * Uninstalls VS Code Linux DEB package. + */ + public async uninstallDeb() { + const name = this.getLinuxBinaryName(); + const packagePath = path.join('/usr/share', name, name); + + this.log(`Uninstalling DEB package ${packagePath}`); + if (this.isRootUser) { + this.runNoErrors('dpkg', '-r', name); + } else { + this.runNoErrors('sudo', 'dpkg', '-r', name); + } + this.log(`Uninstalled DEB package ${packagePath} successfully`); + + await new Promise(resolve => setTimeout(resolve, 1000)); + if (fs.existsSync(packagePath)) { + this.error(`Package still exists after uninstall: ${packagePath}`); + } + } + + /** + * Installs VS Code Linux RPM package. + * @param packagePath The path to the RPM file. + * @returns The path to the installed VS Code executable. + */ + public installRpm(packagePath: string): string { + this.log(`Installing ${packagePath} using RPM package manager`); + if (this.isRootUser) { + this.runNoErrors('rpm', '-i', packagePath); + } else { + this.runNoErrors('sudo', 'rpm', '-i', packagePath); + } + this.log(`Installed ${packagePath} successfully`); + + const name = this.getLinuxBinaryName(); + const entryPoint = path.join('/usr/share', name, name); + this.log(`Installed VS Code executable at: ${entryPoint}`); + return entryPoint; + } + + /** + * Uninstalls VS Code Linux RPM package. + */ + public async uninstallRpm() { + const name = this.getLinuxBinaryName(); + const packagePath = path.join('/usr/bin', name); + + this.log(`Uninstalling RPM package ${packagePath}`); + if (this.isRootUser) { + this.runNoErrors('rpm', '-e', name); + } else { + this.runNoErrors('sudo', 'rpm', '-e', name); + } + this.log(`Uninstalled RPM package ${packagePath} successfully`); + + await new Promise(resolve => setTimeout(resolve, 1000)); + if (fs.existsSync(packagePath)) { + this.error(`Package still exists after uninstall: ${packagePath}`); + } + } + + /** + * Installs VS Code Linux Snap package. + * @param packagePath The path to the Snap file. + * @returns The path to the installed VS Code executable. + */ + public installSnap(packagePath: string): string { + this.log(`Installing ${packagePath} using Snap package manager`); + if (this.isRootUser) { + this.runNoErrors('snap', 'install', packagePath, '--classic', '--dangerous'); + } else { + this.runNoErrors('sudo', 'snap', 'install', packagePath, '--classic', '--dangerous'); + } + this.log(`Installed ${packagePath} successfully`); + + // Snap wrapper scripts are in /snap/bin, but actual Electron binary is in /snap//current/usr/share/ + const name = this.getLinuxBinaryName(); + const entryPoint = `/snap/${name}/current/usr/share/${name}/${name}`; + this.log(`Installed VS Code executable at: ${entryPoint}`); + return entryPoint; + } + + /** + * Uninstalls VS Code Linux Snap package. + */ + public async uninstallSnap() { + const name = this.getLinuxBinaryName(); + const packagePath = path.join('/snap/bin', name); + + this.log(`Uninstalling Snap package ${packagePath}`); + if (this.isRootUser) { + this.runNoErrors('snap', 'remove', name); + } else { + this.runNoErrors('sudo', 'snap', 'remove', name); + } + this.log(`Uninstalled Snap package ${packagePath} successfully`); + + await new Promise(resolve => setTimeout(resolve, 1000)); + if (fs.existsSync(packagePath)) { + this.error(`Package still exists after uninstall: ${packagePath}`); + } + } + + /** + * Returns the Linux binary name based on quality. + */ + private getLinuxBinaryName(): string { + switch (this.options.quality) { + case 'stable': + return 'code'; + case 'insider': + return 'code-insiders'; + case 'exploration': + return 'code-exploration'; + } + } + + /** + * Returns the entry point executable for the VS Code Desktop installation in the specified directory. + * @param dir The directory of the VS Code installation. + * @returns The path to the entry point executable. + */ + public getDesktopEntryPoint(dir: string): string { + let filePath: string = ''; + + switch (os.platform()) { + case 'darwin': { + let appName: string; + switch (this.options.quality) { + case 'stable': + appName = 'Visual Studio Code.app'; + break; + case 'insider': + appName = 'Visual Studio Code - Insiders.app'; + break; + case 'exploration': + appName = 'Visual Studio Code - Exploration.app'; + break; + } + filePath = path.join(dir, appName, 'Contents/MacOS/Electron'); + break; + } + case 'linux': { + let binaryName: string; + switch (this.options.quality) { + case 'stable': + binaryName = `code`; + break; + case 'insider': + binaryName = `code-insiders`; + break; + case 'exploration': + binaryName = `code-exploration`; + break; + } + filePath = path.join(dir, binaryName); + break; + } + case 'win32': { + let exeName: string; + switch (this.options.quality) { + case 'stable': + exeName = 'Code.exe'; + break; + case 'insider': + exeName = 'Code - Insiders.exe'; + break; + case 'exploration': + exeName = 'Code - Exploration.exe'; + break; + } + filePath = path.join(dir, exeName); + break; + } + } + + if (!filePath || !fs.existsSync(filePath)) { + this.error(`Desktop entry point does not exist: ${filePath}`); + } + + return filePath; + } + + /** + * Returns the entry point executable for the VS Code CLI in the specified directory. + * @param dir The directory containing unpacked CLI files. + * @returns The path to the CLI entry point executable. + */ + public getCliEntryPoint(dir: string): string { + let filename: string; + switch (this.options.quality) { + case 'stable': + filename = 'code'; + break; + case 'insider': + filename = 'code-insiders'; + break; + case 'exploration': + filename = 'code-exploration'; + break; + } + + if (os.platform() === 'win32') { + filename += '.exe'; + } + + const entryPoint = path.join(dir, filename); + if (!fs.existsSync(entryPoint)) { + this.error(`CLI entry point does not exist: ${entryPoint}`); + } + + return entryPoint; + } + + /** + * Returns the entry point executable for the VS Code server in the specified directory. + * @param dir The directory containing unpacked server files. + * @param forWsl If true, returns the Linux entry point (for running in WSL on Windows). + * @returns The path to the server entry point executable. + */ + public getServerEntryPoint(dir: string, forWsl = false): string { + let filename: string; + switch (this.options.quality) { + case 'stable': + filename = 'code-server'; + break; + case 'insider': + filename = 'code-server-insiders'; + break; + case 'exploration': + filename = 'code-server-exploration'; + break; + } + + if (os.platform() === 'win32' && !forWsl) { + filename += '.cmd'; + } + + const entryPoint = path.join(this.getFirstSubdirectory(dir), 'bin', filename); + if (!fs.existsSync(entryPoint)) { + this.error(`Server entry point does not exist: ${entryPoint}`); + } + + return entryPoint; + } + + /** + * Returns the first subdirectory within the specified directory. + */ + public getFirstSubdirectory(dir: string): string { + const subDir = fs.readdirSync(dir, { withFileTypes: true }).filter(o => o.isDirectory()).at(0)?.name; + if (!subDir) { + this.error(`No subdirectories found in directory: ${dir}`); + } + return path.join(dir, subDir); + } + + /** + * Creates a portable data directory in the specified unpacked VS Code directory. + * @param dir The directory where VS Code was unpacked. + * @returns The path to the created portable data directory. + */ + public createPortableDataDir(dir: string): string { + const dataDir = path.join(dir, os.platform() === 'darwin' ? 'code-portable-data' : 'data'); + + this.log(`Creating portable data directory: ${dataDir}`); + fs.mkdirSync(dataDir, { recursive: true }); + this.log(`Created portable data directory: ${dataDir}`); + + return dataDir; + } + + /** + * Launches a web browser for UI testing. + * @returns The launched Browser instance. + */ + public async launchBrowser(): Promise { + this.log(`Launching web browser`); + const headless = this.options.headlessBrowser; + switch (os.platform()) { + case 'darwin': { + return await webkit.launch({ headless }); + } + case 'win32': { + const executablePath = process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] ?? 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe'; + this.log(`Using Chromium executable at: ${executablePath}`); + return await chromium.launch({ headless, executablePath }); + } + case 'linux': + default: { + const executablePath = process.env['PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH'] ?? '/usr/bin/chromium-browser'; + this.log(`Using Chromium executable at: ${executablePath}`); + return await chromium.launch({ + headless, + executablePath, + args: [ + '--disable-gpu', + '--disable-gpu-compositing', + '--disable-software-rasterizer', + '--no-zygote', + ] + }); + } + } + } + + /** + * Awaits a page promise and sets the default timeout. + * @param pagePromise The promise that resolves to a Page. + * @returns The page with the timeout configured. + */ + public async getPage(pagePromise: Promise): Promise { + const page = await pagePromise; + page.setDefaultTimeout(3 * 60 * 1000); + return page; + } + + /** + * Constructs a web server URL with optional token and folder parameters. + * @param port The port number of the web server. + * @param token The optional authentication token. + * @param folder The optional workspace folder path to open. + * @returns The constructed web server URL. + */ + public getWebServerUrl(port: string, token?: string, folder?: string): URL { + const url = new URL(`http://localhost:${port}`); + if (token) { + url.searchParams.set('tkn', token); + } + if (folder) { + folder = folder.replaceAll('\\', '/'); + if (!folder.startsWith('/')) { + folder = `/${folder}`; + } + url.searchParams.set('folder', folder); + } + return url; + } + + /** + * Returns the tunnel URL for the VS Code server. + * @param baseUrl The base URL for *vscode.dev/tunnel connection. + * @param workspaceDir Optional folder path to open + * @returns The tunnel URL with folder in pathname. + */ + public getTunnelUrl(baseUrl: string, workspaceDir?: string): string { + const url = new URL(baseUrl); + url.searchParams.set('vscode-version', this.options.commit); + if (workspaceDir) { + let folder = workspaceDir.replaceAll('\\', '/'); + if (!folder.startsWith('/')) { + folder = `/${folder}`; + } + url.pathname = url.pathname.replace(/\/+$/, '') + folder; + } + return url.toString(); + } + + /** + * Returns a random alphanumeric token of length 10. + */ + public getRandomToken(): string { + return Array.from({ length: 10 }, () => Math.floor(Math.random() * 36).toString(36)).join(''); + } + + /** + * Returns a unique port number, starting from 3010 and incrementing. + */ + public getUniquePort(): string { + return String(this.nextPort++); + } + + /** + * Returns the default WSL server extensions directory path. + * @returns The path to the extensions directory (e.g., '~/.vscode-server-insiders/extensions'). + */ + public getWslServerExtensionsDir(): string { + let serverDir: string; + switch (this.options.quality) { + case 'stable': + serverDir = '.vscode-server'; + break; + case 'insider': + serverDir = '.vscode-server-insiders'; + break; + case 'exploration': + serverDir = '.vscode-server-exploration'; + break; + } + return `~/${serverDir}/extensions`; + } + + /** + * Runs a VS Code command-line application (such as server or CLI). + * @param name The name of the app as it will appear in logs. + * @param command Command to run. + * @param args Arguments for the command. + * @param onLine Callback to handle output lines. + */ + public async runCliApp(name: string, command: string, args: string[], onLine: (text: string) => Promise) { + this.log(`Starting ${name} with command line: ${command} ${args.join(' ')}`); + + const app = spawn(command, args, { + shell: /\.(sh|cmd)$/.test(command), + detached: !this.capabilities.has('windows'), + stdio: ['ignore', 'pipe', 'pipe'] + }); + + try { + await new Promise((resolve, reject) => { + app.stderr.on('data', (data) => { + const text = `[${name}] ${data.toString().trim()}`; + if (/ECONNRESET/.test(text)) { + this.log(text); + } else { + reject(new Error(text)); + } + }); + + let terminated = false; + app.stdout.on('data', (data) => { + const text = data.toString().trim(); + if (/\berror\b/.test(text)) { + reject(new Error(`[${name}] ${text}`)); + } + + for (const line of text.split('\n')) { + this.log(`[${name}] ${line}`); + onLine(line).then((result) => { + if (terminated = !!result) { + this.log(`Terminating ${name} process`); + resolve(); + } + }).catch(reject); + } + }); + + app.on('error', reject); + app.on('exit', (code) => { + if (code === 0) { + resolve(); + } else if (!terminated) { + reject(new Error(`[${name}] Exited with code ${code}`)); + } + }); + }); + } finally { + this.killProcessTree(app.pid!); + } + } +} diff --git a/test/sanity/src/desktop.test.ts b/test/sanity/src/desktop.test.ts new file mode 100644 index 00000000000..8a9b57e6dc3 --- /dev/null +++ b/test/sanity/src/desktop.test.ts @@ -0,0 +1,251 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import path from 'path'; +import { _electron } from 'playwright'; +import { TestContext } from './context.js'; +import { UITest } from './uiTest.js'; + +export function setup(context: TestContext) { + context.test('desktop-darwin-x64', ['darwin', 'x64', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('darwin'); + context.validateAllCodesignSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + } + }); + + context.test('desktop-darwin-arm64', ['darwin', 'arm64', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('darwin-arm64'); + context.validateAllCodesignSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + } + }); + + context.test('desktop-darwin-universal', ['darwin', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('darwin-universal'); + context.validateAllCodesignSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + } + }); + + context.test('desktop-darwin-x64-dmg', ['darwin', 'x64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('darwin-x64-dmg'); + context.validateCodesignSignature(packagePath); + if (!context.options.downloadOnly) { + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + context.unmountDmg(dir); + } + }); + + context.test('desktop-darwin-arm64-dmg', ['darwin', 'arm64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('darwin-arm64-dmg'); + context.validateCodesignSignature(packagePath); + if (!context.options.downloadOnly) { + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + context.unmountDmg(dir); + } + }); + + context.test('desktop-darwin-universal-dmg', ['darwin', 'desktop'], async () => { + const packagePath = await context.downloadTarget('darwin-universal-dmg'); + context.validateCodesignSignature(packagePath); + if (!context.options.downloadOnly) { + const dir = context.mountDmg(packagePath); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + await testDesktopApp(entryPoint); + context.unmountDmg(dir); + } + }); + + context.test('desktop-linux-arm64', ['linux', 'arm64', 'desktop'], async () => { + let dir = await context.downloadAndUnpack('linux-arm64'); + if (!context.options.downloadOnly) { + dir = context.getFirstSubdirectory(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('desktop-linux-armhf', ['linux', 'arm32', 'desktop'], async () => { + let dir = await context.downloadAndUnpack('linux-armhf'); + if (!context.options.downloadOnly) { + dir = context.getFirstSubdirectory(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('desktop-linux-deb-arm64', ['linux', 'arm64', 'deb', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-deb-arm64'); + if (!context.options.downloadOnly) { + const entryPoint = context.installDeb(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallDeb(); + } + }); + + context.test('desktop-linux-deb-armhf', ['linux', 'arm32', 'deb', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-deb-armhf'); + if (!context.options.downloadOnly) { + const entryPoint = context.installDeb(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallDeb(); + } + }); + + context.test('desktop-linux-deb-x64', ['linux', 'x64', 'deb', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-deb-x64'); + if (!context.options.downloadOnly) { + const entryPoint = context.installDeb(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallDeb(); + } + }); + + context.test('desktop-linux-rpm-arm64', ['linux', 'arm64', 'rpm', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-rpm-arm64'); + if (!context.options.downloadOnly) { + const entryPoint = context.installRpm(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallRpm(); + } + }); + + context.test('desktop-linux-rpm-armhf', ['linux', 'arm32', 'rpm', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-rpm-armhf'); + if (!context.options.downloadOnly) { + const entryPoint = context.installRpm(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallRpm(); + } + }); + + context.test('desktop-linux-rpm-x64', ['linux', 'x64', 'rpm', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-rpm-x64'); + if (!context.options.downloadOnly) { + const entryPoint = context.installRpm(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallRpm(); + } + }); + + context.test('desktop-linux-snap-x64', ['linux', 'x64', 'snap', 'desktop'], async () => { + const packagePath = await context.downloadTarget('linux-snap-x64'); + if (!context.options.downloadOnly) { + const entryPoint = context.installSnap(packagePath); + await testDesktopApp(entryPoint); + await context.uninstallSnap(); + } + }); + + context.test('desktop-linux-x64', ['linux', 'x64', 'desktop'], async () => { + let dir = await context.downloadAndUnpack('linux-x64'); + if (!context.options.downloadOnly) { + dir = context.getFirstSubdirectory(dir); + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('desktop-win32-arm64', ['windows', 'arm64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('win32-arm64'); + context.validateAuthenticodeSignature(packagePath); + if (!context.options.downloadOnly) { + const entryPoint = context.installWindowsApp('system', packagePath); + context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); + await testDesktopApp(entryPoint); + await context.uninstallWindowsApp('system'); + } + }); + + context.test('desktop-win32-arm64-archive', ['windows', 'arm64', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('win32-arm64-archive'); + context.validateAllAuthenticodeSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('desktop-win32-arm64-user', ['windows', 'arm64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('win32-arm64-user'); + context.validateAuthenticodeSignature(packagePath); + if (!context.options.downloadOnly) { + const entryPoint = context.installWindowsApp('user', packagePath); + context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); + await testDesktopApp(entryPoint); + await context.uninstallWindowsApp('user'); + } + }); + + context.test('desktop-win32-x64', ['windows', 'x64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('win32-x64'); + context.validateAuthenticodeSignature(packagePath); + if (!context.options.downloadOnly) { + const entryPoint = context.installWindowsApp('system', packagePath); + context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); + await testDesktopApp(entryPoint); + await context.uninstallWindowsApp('system'); + } + }); + + context.test('desktop-win32-x64-archive', ['windows', 'x64', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('win32-x64-archive'); + context.validateAllAuthenticodeSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('desktop-win32-x64-user', ['windows', 'x64', 'desktop'], async () => { + const packagePath = await context.downloadTarget('win32-x64-user'); + context.validateAuthenticodeSignature(packagePath); + if (!context.options.downloadOnly) { + const entryPoint = context.installWindowsApp('user', packagePath); + context.validateAllAuthenticodeSignatures(path.dirname(entryPoint)); + await testDesktopApp(entryPoint); + await context.uninstallWindowsApp('user'); + } + }); + + async function testDesktopApp(entryPoint: string, dataDir?: string) { + const test = new UITest(context, dataDir); + const args = dataDir ? [] : [ + '--extensions-dir', test.extensionsDir, + '--user-data-dir', test.userDataDir, + ]; + args.push(test.workspaceDir); + + context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`); + const app = await _electron.launch({ executablePath: entryPoint, args }); + const window = await context.getPage(app.firstWindow()); + + await test.run(window); + + context.log('Closing the application'); + await app.close(); + + test.validate(); + } +} diff --git a/test/sanity/src/detectors.ts b/test/sanity/src/detectors.ts new file mode 100644 index 00000000000..ed1cc693099 --- /dev/null +++ b/test/sanity/src/detectors.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 fs from 'fs'; +import os from 'os'; +import { webkit } from 'playwright'; + +/** + * The capabilities of the current environment. + */ +export type Capability = + | 'linux' | 'darwin' | 'windows' | 'alpine' + | 'x64' | 'arm64' | 'arm32' + | 'deb' | 'rpm' | 'snap' + | 'desktop' + | 'browser' + | 'wsl' + | 'github-account'; + +/** + * Detect the capabilities of the current environment. + */ +export function detectCapabilities(): ReadonlySet { + const capabilities = new Set(); + detectOS(capabilities); + detectArch(capabilities); + detectPackageManagers(capabilities); + detectDesktop(capabilities); + detectBrowser(capabilities); + detectWSL(capabilities); + detectGitHubAccount(capabilities); + return capabilities; +} + +/** + * Detect the operating system. + */ +function detectOS(capabilities: Set) { + switch (os.platform()) { + case 'linux': + if (fs.existsSync('/etc/alpine-release')) { + capabilities.add('alpine'); + } else { + capabilities.add('linux'); + } + break; + case 'darwin': + capabilities.add('darwin'); + break; + case 'win32': + capabilities.add('windows'); + break; + default: + throw new Error(`Unsupported platform: ${os.platform()}`); + } +} + +/** + * Detect the architecture. + */ +function detectArch(capabilities: Set) { + let arch = os.arch(); + + if (os.platform() === 'win32') { + const winArch = process.env.PROCESSOR_ARCHITEW6432 || process.env.PROCESSOR_ARCHITECTURE; + if (winArch === 'ARM64') { + arch = 'arm64'; + } else if (winArch === 'AMD64') { + arch = 'x64'; + } + } + + switch (arch) { + case 'x64': + capabilities.add('x64'); + break; + case 'arm64': + capabilities.add('arm64'); + break; + case 'arm': + capabilities.add('arm32'); + break; + default: + throw new Error(`Unsupported architecture: ${arch}`); + } +} + +/** + * Detect the package managers. + */ +function detectPackageManagers(capabilities: Set) { + if (os.platform() !== 'linux') { + return; + } + if (fs.existsSync('/usr/bin/dpkg')) { + capabilities.add('deb'); + } + if (fs.existsSync('/usr/bin/dnf') || fs.existsSync('/usr/bin/yum')) { + capabilities.add('rpm'); + } + if (fs.existsSync('/run/snapd.socket')) { + capabilities.add('snap'); + } +} + +/** + * Detect if a desktop environment is available. + */ +function detectDesktop(capabilities: Set) { + if (os.platform() !== 'linux' || !!process.env.DISPLAY) { + capabilities.add('desktop'); + } +} + +/** + * Detect if a browser environment is available. + */ +function detectBrowser(capabilities: Set) { + switch (os.platform()) { + case 'linux': { + const path = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; + if (path && fs.existsSync(path)) { + capabilities.add('browser'); + } + break; + } + case 'darwin': { + if (fs.existsSync(webkit.executablePath())) { + capabilities.add('browser'); + } + break; + } + case 'win32': { + const path = + process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH ?? + `${process.env['ProgramFiles(x86)']}\\Microsoft\\Edge\\Application\\msedge.exe`; + + if (fs.existsSync(path)) { + capabilities.add('browser'); + } + break; + } + } +} + +/** + * Detect if WSL is available on Windows. + */ +function detectWSL(capabilities: Set) { + if (os.platform() === 'win32') { + const wslPath = `${process.env.SystemRoot}\\System32\\wsl.exe`; + if (fs.existsSync(wslPath)) { + capabilities.add('wsl'); + } + } +} + +/** + * Detect if GitHub account and password are available in the environment. + */ +function detectGitHubAccount(capabilities: Set) { + if (process.env.GITHUB_ACCOUNT && process.env.GITHUB_PASSWORD) { + capabilities.add('github-account'); + } +} + diff --git a/test/sanity/src/githubAuth.ts b/test/sanity/src/githubAuth.ts new file mode 100644 index 00000000000..d7576d761cd --- /dev/null +++ b/test/sanity/src/githubAuth.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Browser, Page } from 'playwright'; +import { TestContext } from './context.js'; + +/** + * Handles GitHub authentication flows in the browser. + */ +export class GitHubAuth { + // private readonly username = process.env.GITHUB_ACCOUNT; + // private readonly password = process.env.GITHUB_PASSWORD; + + public constructor(private readonly context: TestContext) { } + + /** + * Runs GitHub device authentication flow in a browser. + * @param browser Browser to use. + * @param code Device authentication code to use. + */ + public async runDeviceCodeFlow(browser: Browser, code: string) { + this.context.log(`Running GitHub device flow with code ${code}`); + const page = await browser.newPage(); + await page.goto('https://github.com/login/device'); + } + + /** + * Runs GitHub user authentication flow in the browser. + * @param page Authentication page. + */ + public async runUserWebFlow(page: Page) { + this.context.log(`Running GitHub browser flow at ${page.url()}`); + } +} diff --git a/test/sanity/src/index.ts b/test/sanity/src/index.ts new file mode 100644 index 00000000000..6439018a9b6 --- /dev/null +++ b/test/sanity/src/index.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import fs from 'fs'; +import minimist from 'minimist'; +import Mocha, { MochaOptions } from 'mocha'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const options = minimist(process.argv.slice(2), { + string: ['fgrep', 'grep', 'test-results', 'timeout'], + boolean: ['help'], + alias: { fgrep: 'f', grep: 'g', help: 'h', 'test-results': 't' }, +}); + +if (options.help) { + console.info(`Usage: node ${path.basename(process.argv[1])} [options]`); + console.info('Options:'); + console.info(' --commit, -c The commit to test (required)'); + console.info(` --quality, -q The quality to test (required, "stable", "insider" or "exploration")`); + console.info(' --no-cleanup Do not cleanup downloaded files after each test'); + console.info(' --no-signing-check Skip Authenticode and codesign signature checks'); + console.info(' --no-headless Run tests with a visible UI (desktop tests only)'); + console.info(' --no-detection Enable all tests regardless of platform and skip executable runs'); + console.info(' --grep, -g Only run tests matching the given '); + console.info(' --fgrep, -f Only run tests containing the given '); + console.info(' --test-results, -t Output test results in JUnit format to the specified path'); + console.info(' --timeout Set the test-case timeout in seconds (default: 600 seconds)'); + console.info(' --verbose, -v Enable verbose logging'); + console.info(' --help, -h Show this help message'); + process.exit(0); +} + +const testResults = options['test-results']; +const mochaOptions: MochaOptions = { + color: true, + timeout: (options.timeout ?? 10 * 60) * 1000, + slow: 3 * 60 * 1000, + grep: options.grep, + fgrep: options.fgrep, + reporter: testResults ? 'mocha-junit-reporter' : undefined, + reporterOptions: testResults ? { mochaFile: testResults, outputs: true } : undefined, +}; + +if (testResults) { + fs.mkdirSync(path.dirname(testResults), { recursive: true }); +} + +const mocha = new Mocha(mochaOptions); +mocha.addFile(fileURLToPath(new URL('./main.js', import.meta.url))); +await mocha.loadFilesAsync(); +mocha.run(failures => { + process.exitCode = failures > 0 ? 1 : 0; + // Force exit to prevent hanging on open handles (background processes, timers, etc.) + setTimeout(() => process.exit(process.exitCode), 1000); +}); diff --git a/test/sanity/src/main.ts b/test/sanity/src/main.ts new file mode 100644 index 00000000000..cf3a7780350 --- /dev/null +++ b/test/sanity/src/main.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import minimist from 'minimist'; +import os from 'os'; +import { setup as setupCliTests } from './cli.test.js'; +import { TestContext } from './context.js'; +import { setup as setupDesktopTests } from './desktop.test.js'; +import { setup as setupServerTests } from './server.test.js'; +import { setup as setupServerWebTests } from './serverWeb.test.js'; +import { setup as setupWSLTests } from './wsl.test.js'; + +const options = minimist(process.argv.slice(2), { + string: ['commit', 'quality'], + boolean: ['cleanup', 'verbose', 'signing-check', 'headless', 'detection'], + alias: { commit: 'c', quality: 'q', verbose: 'v' }, + default: { cleanup: true, verbose: false, 'signing-check': true, headless: true, 'detection': true }, +}); + +if (!options.commit) { + throw new Error('--commit is required'); +} + +if (!options.quality) { + throw new Error('--quality is required'); +} + +const context = new TestContext({ + quality: options.quality, + commit: options.commit, + verbose: options.verbose, + cleanup: options.cleanup, + checkSigning: options['signing-check'], + headlessBrowser: options.headless, + downloadOnly: !options['detection'], +}); + +context.log(`Arguments: ${process.argv.slice(2).join(' ')}`); +context.log(`Platform: ${os.platform()}, Architecture: ${os.arch()}`); +context.log(`Capabilities: ${Array.from(context.capabilities).join(', ')}`); + +beforeEach(function () { + context.consoleOutputs = []; + (this.currentTest! as { consoleOutputs?: string[] }).consoleOutputs = context.consoleOutputs; +}); + +setupCliTests(context); +setupDesktopTests(context); +setupServerTests(context); +setupServerWebTests(context); +setupWSLTests(context); diff --git a/test/sanity/src/server.test.ts b/test/sanity/src/server.test.ts new file mode 100644 index 00000000000..ff2384f5c9a --- /dev/null +++ b/test/sanity/src/server.test.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TestContext } from './context.js'; + +export function setup(context: TestContext) { + context.test('server-alpine-arm64', ['alpine', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('server-alpine-arm64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-alpine-x64', ['alpine', 'x64'], async () => { + const dir = await context.downloadAndUnpack('server-linux-alpine'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-darwin-arm64', ['darwin', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('server-darwin-arm64'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-darwin-x64', ['darwin', 'x64'], async () => { + const dir = await context.downloadAndUnpack('server-darwin'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-linux-arm64', ['linux', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-linux-armhf', ['linux', 'arm32'], async () => { + const dir = await context.downloadAndUnpack('server-linux-armhf'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-linux-x64', ['linux', 'x64'], async () => { + const dir = await context.downloadAndUnpack('server-linux-x64'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-win32-arm64', ['windows', 'arm64'], async () => { + const dir = await context.downloadAndUnpack('server-win32-arm64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-win32-x64', ['windows', 'x64'], async () => { + const dir = await context.downloadAndUnpack('server-win32-x64'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + async function testServer(entryPoint: string) { + if (context.options.downloadOnly) { + return; + } + + await context.runCliApp('Server', entryPoint, + [ + '--accept-server-license-terms', + '--connection-token', context.getRandomToken(), + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createTempDir(), + '--extensions-dir', context.createTempDir() + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } + + const url = new URL('version', context.getWebServerUrl(port)).toString(); + + context.log(`Fetching version from ${url}`); + const response = await context.fetchNoErrors(url); + const version = await response.text(); + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); + + return true; + } + ); + } +} diff --git a/test/sanity/src/serverWeb.test.ts b/test/sanity/src/serverWeb.test.ts new file mode 100644 index 00000000000..89084cb6c6c --- /dev/null +++ b/test/sanity/src/serverWeb.test.ts @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TestContext } from './context.js'; +import { UITest } from './uiTest.js'; + +export function setup(context: TestContext) { + context.test('server-web-alpine-arm64', ['alpine', 'arm64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-alpine-arm64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-alpine-x64', ['alpine', 'x64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-alpine-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-darwin-arm64', ['darwin', 'arm64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-darwin-arm64-web'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-darwin-x64', ['darwin', 'x64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-darwin-web'); + context.validateAllCodesignSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-linux-arm64', ['linux', 'arm64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-linux-armhf', ['linux', 'arm32', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-armhf-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-linux-x64', ['linux', 'x64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-x64-web'); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-win32-arm64', ['windows', 'arm64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-win32-arm64-web'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + context.test('server-web-win32-x64', ['windows', 'x64', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-win32-x64-web'); + context.validateAllAuthenticodeSignatures(dir); + const entryPoint = context.getServerEntryPoint(dir); + await testServer(entryPoint); + }); + + async function testServer(entryPoint: string) { + if (context.options.downloadOnly) { + return; + } + + const token = context.getRandomToken(); + const test = new UITest(context); + await context.runCliApp('Server', entryPoint, + [ + '--accept-server-license-terms', + '--port', context.getUniquePort(), + '--connection-token', token, + '--server-data-dir', context.createTempDir(), + '--extensions-dir', test.extensionsDir, + '--user-data-dir', test.userDataDir + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } + + const url = context.getWebServerUrl(port, token, test.workspaceDir).toString(); + const browser = await context.launchBrowser(); + const page = await context.getPage(browser.newPage()); + + context.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: 'networkidle' }); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; + } + ); + } +} diff --git a/test/sanity/src/uiTest.ts b/test/sanity/src/uiTest.ts new file mode 100644 index 00000000000..cf06f013025 --- /dev/null +++ b/test/sanity/src/uiTest.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; +import { Page } from 'playwright'; +import { TestContext } from './context.js'; + +/** + * UI Test helper class to perform common UI actions and verifications. + */ +export class UITest { + private _extensionsDir: string | undefined; + private _workspaceDir: string | undefined; + private _userDataDir: string | undefined; + + constructor( + protected readonly context: TestContext, + dataDir?: string + ) { + if (dataDir) { + this._extensionsDir = path.join(dataDir, 'extensions'); + this._userDataDir = path.join(dataDir, 'user-data'); + } + } + + /** + * The directory where extensions are installed. + */ + public get extensionsDir(): string { + return this._extensionsDir ??= this.context.createTempDir(); + } + + /** + * The workspace directory used for testing. + */ + public get workspaceDir(): string { + return this._workspaceDir ??= this.context.createTempDir(); + } + + /** + * The user data directory used for testing. + */ + public get userDataDir(): string { + return this._userDataDir ??= this.context.createTempDir(); + } + + /** + * Run the UI test actions. + */ + public async run(page: Page) { + await this.dismissWorkspaceTrustDialog(page); + await this.createTextFile(page); + await this.installExtension(page); + } + + /** + * Validate the results of the UI test actions. + */ + public validate() { + this.verifyTextFileCreated(); + this.verifyExtensionInstalled(); + } + + /** + * Dismiss the workspace trust dialog. + */ + private async dismissWorkspaceTrustDialog(page: Page) { + this.context.log('Dismissing workspace trust dialog'); + await page.getByText('Yes, I trust the authors').click(); + await page.waitForTimeout(500); + } + + /** + * Run a command from the command palette. + */ + private async runCommand(page: Page, command: string) { + this.context.log(`Running command: ${command}`); + await page.keyboard.press('F1'); + await page.getByPlaceholder(/^Type the name of a command/).fill(`>${command}`); + await page.locator('span.monaco-highlighted-label', { hasText: new RegExp(`^${command}$`) }).click(); + await page.waitForTimeout(1000); + } + + /** + * Create a new text file in the editor with some content and save it. + */ + private async createTextFile(page: Page) { + await this.runCommand(page, 'View: Show Explorer'); + + this.context.log('Clicking New File button'); + await page.getByLabel('New File...').click(); + + this.context.log('Typing file name'); + await page.getByRole('textbox', { name: /^Type file name/ }).fill('helloWorld.txt'); + await page.keyboard.press('Enter'); + + this.context.log('Focusing the code editor'); + await page.getByText(/Start typing/).focus(); + + this.context.log('Typing some content into the file'); + await page.keyboard.type('Hello, World!'); + + await this.runCommand(page, 'File: Save'); + } + + /** + * Verify that the text file was created with the expected content. + */ + protected verifyTextFileCreated() { + this.context.log('Verifying file contents'); + const filePath = `${this.workspaceDir}/helloWorld.txt`; + const fileContents = fs.readFileSync(filePath, 'utf-8'); + assert.strictEqual(fileContents, 'Hello, World!', 'File contents do not match expected value'); + } + + /** + * Install GitHub Pull Requests extension from the Extensions view. + */ + private async installExtension(page: Page) { + await this.runCommand(page, 'View: Show Extensions'); + + this.context.log('Typing extension name to search for'); + await page.getByText('Search Extensions in Marketplace').focus(); + await page.keyboard.type('GitHub Pull Requests'); + + this.context.log('Clicking Install on the first extension in the list'); + await page.locator('.extension-list-item').getByText(/^GitHub Pull Requests$/).waitFor(); + await page.locator('.extension-action:not(.disabled)', { hasText: /Install/ }).first().click(); + await page.waitForTimeout(1000); + + this.context.log('Waiting for extension to be installed'); + await page.locator('.extension-action:not(.disabled)', { hasText: /Uninstall/ }).waitFor(); + } + + /** + * Verify that the GitHub Pull Requests extension is installed. + */ + protected verifyExtensionInstalled() { + this.context.log('Verifying extension is installed'); + const extensions = fs.readdirSync(this.extensionsDir); + const hasExtension = extensions.some(ext => ext.startsWith('github.vscode-pull-request-github')); + assert.strictEqual(hasExtension, true, 'GitHub Pull Requests extension is not installed'); + } +} diff --git a/test/sanity/src/wsl.test.ts b/test/sanity/src/wsl.test.ts new file mode 100644 index 00000000000..cd54740ac98 --- /dev/null +++ b/test/sanity/src/wsl.test.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { _electron } from 'playwright'; +import { TestContext } from './context.js'; +import { UITest } from './uiTest.js'; + +export function setup(context: TestContext) { + context.test('wsl-server-arm64', ['windows', 'arm64', 'wsl'], async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64'); + const entryPoint = context.getServerEntryPoint(dir, true); + await testServer(entryPoint); + }); + + context.test('wsl-server-x64', ['windows', 'x64', 'wsl'], async () => { + const dir = await context.downloadAndUnpack('server-linux-x64'); + const entryPoint = context.getServerEntryPoint(dir, true); + await testServer(entryPoint); + }); + + context.test('wsl-server-web-arm64', ['windows', 'arm64', 'wsl', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-arm64-web'); + const entryPoint = context.getServerEntryPoint(dir, true); + await testServerWeb(entryPoint); + }); + + context.test('wsl-server-web-x64', ['windows', 'x64', 'wsl', 'browser'], async () => { + const dir = await context.downloadAndUnpack('server-linux-x64-web'); + const entryPoint = context.getServerEntryPoint(dir, true); + await testServerWeb(entryPoint); + }); + + context.test('wsl-desktop-arm64', ['windows', 'arm64', 'wsl', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('win32-arm64-archive'); + context.validateAllAuthenticodeSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + context.test('wsl-desktop-x64', ['windows', 'x64', 'wsl', 'desktop'], async () => { + const dir = await context.downloadAndUnpack('win32-x64-archive'); + context.validateAllAuthenticodeSignatures(dir); + if (!context.options.downloadOnly) { + const entryPoint = context.getDesktopEntryPoint(dir); + const dataDir = context.createPortableDataDir(dir); + await testDesktopApp(entryPoint, dataDir); + } + }); + + async function testServer(entryPoint: string) { + if (context.options.downloadOnly) { + return; + } + + await context.runCliApp('WSL Server', 'wsl', + [ + context.toWslPath(entryPoint), + '--accept-server-license-terms', + '--connection-token', context.getRandomToken(), + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createWslTempDir(), + '--extensions-dir', context.createWslTempDir(), + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return; + } + + const url = new URL('version', context.getWebServerUrl(port)).toString(); + + context.log(`Fetching version from ${url}`); + const response = await context.fetchNoErrors(url); + const version = await response.text(); + assert.strictEqual(version, context.options.commit, `Expected commit ${context.options.commit} but got ${version}`); + + return true; + } + ); + } + + async function testServerWeb(entryPoint: string) { + if (context.options.downloadOnly) { + return; + } + + const wslWorkspaceDir = context.createWslTempDir(); + const wslExtensionsDir = context.createWslTempDir(); + const token = context.getRandomToken(); + const test = new WslUITest(context, undefined, wslWorkspaceDir, wslExtensionsDir); + + await context.runCliApp('WSL Server', 'wsl', + [ + context.toWslPath(entryPoint), + '--accept-server-license-terms', + '--connection-token', token, + '--host', '0.0.0.0', + '--port', context.getUniquePort(), + '--server-data-dir', context.createWslTempDir(), + '--extensions-dir', wslExtensionsDir, + '--user-data-dir', context.createWslTempDir(), + ], + async (line) => { + const port = /Extension host agent listening on (\d+)/.exec(line)?.[1]; + if (!port) { + return false; + } + + const url = context.getWebServerUrl(port, token, wslWorkspaceDir).toString(); + const browser = await context.launchBrowser(); + const page = await context.getPage(browser.newPage()); + + context.log(`Navigating to ${url}`); + await page.goto(url, { waitUntil: 'networkidle' }); + + context.log('Waiting for the workbench to load'); + await page.waitForSelector('.monaco-workbench'); + + await test.run(page); + + context.log('Closing browser'); + await browser.close(); + + test.validate(); + return true; + } + ); + } + + async function testDesktopApp(entryPoint: string, dataDir: string) { + const wslExtensionsDir = context.getWslServerExtensionsDir(); + context.deleteWslDir(wslExtensionsDir); + + const wslWorkspaceDir = context.createWslTempDir(); + const wslDistro = context.getDefaultWslDistro(); + const test = new WslUITest(context, dataDir, wslWorkspaceDir, wslExtensionsDir); + + const args = [ + '--extensions-dir', context.createTempDir(), + '--user-data-dir', test.userDataDir, + '--folder-uri', `vscode-remote://wsl+${wslDistro}${wslWorkspaceDir}`, + ]; + + context.log(`Starting VS Code ${entryPoint} with args ${args.join(' ')}`); + const app = await _electron.launch({ executablePath: entryPoint, args }); + const window = await context.getPage(app.firstWindow()); + + context.log('Installing WSL extension'); + await window.getByRole('button', { name: 'Install and Reload' }).click(); + + context.log('Waiting for WSL connection'); + await window.getByText(/WSL/).waitFor(); + + await test.run(window); + + context.log('Closing the application'); + await app.close(); + + test.validate(); + } +} + +/** + * UI Test subclass for WSL that validates files in WSL filesystem. + */ +class WslUITest extends UITest { + constructor( + context: TestContext, + dataDir: string | undefined, + private readonly wslWorkspaceDir: string, + private readonly wslExtensionsDir: string + ) { + super(context, dataDir); + } + + protected override verifyTextFileCreated() { + this.context.log('Verifying file contents in WSL'); + const result = this.context.runNoErrors('wsl', 'cat', `${this.wslWorkspaceDir}/helloWorld.txt`); + assert.strictEqual(result.stdout.trim(), 'Hello, World!', 'File contents in WSL do not match expected value'); + } + + protected override verifyExtensionInstalled() { + this.context.log(`Verifying extension is installed in WSL at ${this.wslExtensionsDir}`); + const result = this.context.runNoErrors('wsl', 'ls', this.wslExtensionsDir); + const hasExtension = result.stdout.split('\n').some(ext => ext.startsWith('github.vscode-pull-request-github')); + assert.strictEqual(hasExtension, true, 'GitHub Pull Requests extension is not installed in WSL'); + } +} diff --git a/test/sanity/tsconfig.json b/test/sanity/tsconfig.json new file mode 100644 index 00000000000..74c4bd9b927 --- /dev/null +++ b/test/sanity/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "ES2022", + "moduleResolution": "bundler", + "noImplicitAny": true, + "removeComments": false, + "preserveConstEnums": true, + "target": "es2024", + "strict": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "rootDir": "./src", + "outDir": "out", + "sourceMap": true, + "skipLibCheck": true, + "esModuleInterop": true, + "lib": [ + "esnext", + "dom" + ] + }, + "exclude": [ + "node_modules" + ] +} diff --git a/test/smoke/package-lock.json b/test/smoke/package-lock.json index f9d09bbed5e..39e7b7172bc 100644 --- a/test/smoke/package-lock.json +++ b/test/smoke/package-lock.json @@ -16,7 +16,7 @@ "@types/ncp": "2.0.1", "@types/node": "22.x", "@types/node-fetch": "^2.5.10", - "npm-run-all": "^4.1.5" + "npm-run-all2": "^8.0.4" } }, "node_modules/@types/ncp": { @@ -48,83 +48,26 @@ "form-data": "^3.0.0" } }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k= sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, - "node_modules/balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c= sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=4" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" + "node": ">= 0.4" } }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -137,200 +80,124 @@ "node": ">= 0.8" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, "engines": { - "node": ">=4.8" + "node": ">=0.4.0" } }, - "node_modules/define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "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": { - "object-keys": "^1.0.12" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, "engines": { "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": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk= sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "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.0" + "node": ">= 0.4" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, - "node_modules/es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "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-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "es-errors": "^1.3.0" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "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": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" } }, "node_modules/form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz", + "integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - }, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/get-intrinsic": { + "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": { - "function-bind": "^1.1.1" + "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", + "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.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0= sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -338,23 +205,26 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "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": { - "has": "^1.0.3" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">= 0.4" } }, - "node_modules/is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "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" }, @@ -362,11 +232,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==", + "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" }, @@ -374,13 +245,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "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.1" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -389,19 +261,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.1" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, "node_modules/isexe": { @@ -410,25 +280,24 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/load-json-file": { + "node_modules/json-parse-even-better-errors": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs= sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "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": ">=4" + "node": ">= 0.4" } }, "node_modules/memorystream": { @@ -441,38 +310,28 @@ } }, "node_modules/mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "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.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "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.48.0" + "mime-db": "1.52.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -481,12 +340,6 @@ "ncp": "bin/ncp" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -506,124 +359,103 @@ } } }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "node_modules/npm-run-all2": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-8.0.4.tgz", + "integrity": "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" + "picomatch": "^4.0.2", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" }, "bin": { "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js" }, "engines": { - "node": ">= 4" + "node": "^20.5.0 || >=22.0.0", + "npm": ">= 10" } }, - "node_modules/object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==", + "node_modules/npm-run-all2/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, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "node_modules/npm-run-all2/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/npm-run-all2/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "node_modules/npm-run-all2/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/pidtree": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", - "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "node_modules/npm-run-all2/node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", "dev": true, + "license": "MIT", "bin": { "pidtree": "bin/pidtree.js" }, @@ -631,173 +463,87 @@ "node": ">=0.10" } }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "node_modules/npm-run-all2/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/read-pkg": { + "node_modules/npm-run-all2/node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "dependencies": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "isexe": "^3.1.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, "bin": { - "semver": "bin/semver" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" + "node-which": "bin/which.js" }, "engines": { - "node": ">=0.10.0" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", - "dev": true - }, - "node_modules/spdx-correct": { + "node_modules/npm-run-all2/node_modules/which/node_modules/isexe": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "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, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true - }, - "node_modules/string.prototype.padend": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.1.tgz", - "integrity": "sha512-eCzTASPnoCr5Ht+Vn1YXgm8SB015hHKgEIMu9Nr9bQmLhRBxKRfmzSj/IQsxDFc8JInJDDFA0qXwK+xxI7wDkg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, + "license": "ISC", "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=16" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", - "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", - "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "node_modules/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", "dev": true, + "license": "ISC", "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "engines": { - "node": ">=4" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true }, "node_modules/tr46": { "version": "0.0.3", @@ -811,16 +557,6 @@ "dev": true, "license": "MIT" }, - "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, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -834,18 +570,6 @@ "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } } } } diff --git a/test/smoke/package.json b/test/smoke/package.json index 13963992228..ce71df0522c 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -7,7 +7,7 @@ "compile": "cd ../automation && npm run compile && cd ../smoke && node ../../node_modules/typescript/bin/tsc", "watch-automation": "cd ../automation && npm run watch", "watch-smoke": "node ../../node_modules/typescript/bin/tsc --watch --preserveWatchOutput", - "watch": "npm-run-all -lp watch-automation watch-smoke", + "watch": "npm-run-all2 -lp watch-automation watch-smoke", "mocha": "node ../node_modules/mocha/bin/mocha" }, "dependencies": { @@ -18,6 +18,6 @@ "@types/ncp": "2.0.1", "@types/node": "22.x", "@types/node-fetch": "^2.5.10", - "npm-run-all": "^4.1.5" + "npm-run-all2": "^8.0.4" } } diff --git a/test/smoke/src/areas/accessibility/accessibility.test.ts b/test/smoke/src/areas/accessibility/accessibility.test.ts new file mode 100644 index 00000000000..9472218a82f --- /dev/null +++ b/test/smoke/src/areas/accessibility/accessibility.test.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Application, Logger } from '../../../../automation'; +import { installAllHandlers } from '../../utils'; + +export function setup(logger: Logger, opts: { web?: boolean }) { + describe.skip('Accessibility', function () { + + // Increase timeout for accessibility scans + this.timeout(30 * 1000); + + // Retry tests to minimize flakiness + this.retries(2); + + // Shared before/after handling + installAllHandlers(logger); + + let app: Application; + + before(async function () { + app = this.app as Application; + }); + + describe('Workbench', function () { + + (opts.web ? it.skip : it)('workbench has no accessibility violations', async function () { + // Wait for workbench to be fully loaded + await app.code.waitForElement('.monaco-workbench'); + + await app.code.driver.assertNoAccessibilityViolations({ + selector: '.monaco-workbench', + excludeRules: { + // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect + 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'], + // Monaco lists use aria-multiselectable on role="list" and aria-setsize/aria-posinset/aria-selected on role="dialog" rows + // These violations appear intermittently when notification lists or other dynamic lists are visible + // Note: patterns match against HTML string, not CSS selectors, so no leading dots + 'aria-allowed-attr': ['monaco-list', 'monaco-list-row'] + } + }); + }); + + it('activity bar has no accessibility violations', async function () { + await app.code.waitForElement('.activitybar'); + + await app.code.driver.assertNoAccessibilityViolations({ + selector: '.activitybar' + }); + }); + + it('sidebar has no accessibility violations', async function () { + await app.code.waitForElement('.sidebar'); + + await app.code.driver.assertNoAccessibilityViolations({ + selector: '.sidebar' + }); + }); + + it('status bar has no accessibility violations', async function () { + await app.code.waitForElement('.statusbar'); + + await app.code.driver.assertNoAccessibilityViolations({ + selector: '.statusbar' + }); + }); + }); + + // Chat is not available in web mode + if (!opts.web) { + describe('Chat', function () { + + it('chat panel has no accessibility violations', async function () { + // Open chat panel + await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); + + // Wait for chat view to be visible + await app.code.waitForElement('div[id="workbench.panel.chat"]'); + + await app.code.driver.assertNoAccessibilityViolations({ + selector: 'div[id="workbench.panel.chat"]', + excludeRules: { + // Links in chat welcome view show underline on hover/focus which axe-core static analysis cannot detect + 'link-in-text-block': ['command:workbench.action.chat.generateInstructions'] + } + }); + }); + }); + } + }); +} diff --git a/test/smoke/src/areas/chat/chatAnonymous.test.ts b/test/smoke/src/areas/chat/chatAnonymous.test.ts new file mode 100644 index 00000000000..b2d178f18b2 --- /dev/null +++ b/test/smoke/src/areas/chat/chatAnonymous.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Application, Logger } from '../../../../automation'; +import { installAllHandlers } from '../../utils'; + +export function setup(logger: Logger) { + describe('Chat Anonymous', () => { + + // Shared before/after handling + installAllHandlers(logger); + + it('can send a chat message with anonymous access', async function () { + const app = this.app as Application; + + // Enable anonymous access + await app.workbench.settingsEditor.addUserSetting('chat.allowAnonymousAccess', 'true'); + + // Open chat view + await app.workbench.quickaccess.runCommand('workbench.action.chat.open'); + + // Wait for chat view to be visible + await app.workbench.chat.waitForChatView(); + + // Send a message + await app.workbench.chat.sendMessage('Hello'); + + // Wait for a response to complete + await app.workbench.chat.waitForResponse(); + + // Wait for model name to appear in footer + await app.workbench.chat.waitForModelInFooter('GPT-5 mini'); + }); + }); +} diff --git a/test/smoke/src/areas/languages/languages.test.ts b/test/smoke/src/areas/languages/languages.test.ts index 9ec05b0c9e2..3db5c7c9894 100644 --- a/test/smoke/src/areas/languages/languages.test.ts +++ b/test/smoke/src/areas/languages/languages.test.ts @@ -15,6 +15,7 @@ export function setup(logger: Logger) { it('verifies quick outline (js)', async function () { const app = this.app as Application; + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'bin', 'www')); await app.workbench.quickaccess.openQuickOutline(); @@ -24,6 +25,7 @@ export function setup(logger: Logger) { it('verifies quick outline (css)', async function () { const app = this.app as Application; + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css')); await app.workbench.quickaccess.openQuickOutline(); @@ -33,6 +35,7 @@ export function setup(logger: Logger) { it('verifies problems view (css)', async function () { const app = this.app as Application; + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css')); await app.workbench.editor.waitForTypeInEditor('style.css', '.foo{}'); @@ -45,6 +48,7 @@ export function setup(logger: Logger) { it('verifies settings (css)', async function () { const app = this.app as Application; + await app.workbench.settingsEditor.addUserSetting('css.lint.emptyRules', '"error"'); await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'public', 'stylesheets', 'style.css')); diff --git a/test/smoke/src/areas/multiroot/multiroot.test.ts b/test/smoke/src/areas/multiroot/multiroot.test.ts index cedbac51e7a..f48f1cad1b7 100644 --- a/test/smoke/src/areas/multiroot/multiroot.test.ts +++ b/test/smoke/src/areas/multiroot/multiroot.test.ts @@ -46,6 +46,9 @@ export function setup(logger: Logger) { // Shared before/after handling installAllHandlers(logger, opts => { + if (!opts.workspacePath) { + throw new Error('Multiroot tests require a workspace to be open'); + } const workspacePath = createWorkspaceFile(opts.workspacePath); return { ...opts, workspacePath }; }); diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts index 6763c33ff79..39fc1e339f5 100644 --- a/test/smoke/src/areas/notebook/notebook.test.ts +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -21,6 +21,7 @@ export function setup(logger: Logger) { after(async function () { const app = this.app as Application; + cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder }); cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }); }); @@ -67,12 +68,15 @@ export function setup(logger: Logger) { it.skip('moves focus as it inserts/deletes a cell', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); + await app.workbench.notebook.focusFirstCell(); + await app.workbench.notebook.insertNotebookCell('code'); + await app.workbench.notebook.waitForActiveCellEditorContents(''); + await app.workbench.notebook.waitForTypeInEditor('# added cell'); + await app.workbench.notebook.focusFirstCell(); await app.workbench.notebook.insertNotebookCell('code'); await app.workbench.notebook.waitForActiveCellEditorContents(''); - await app.workbench.notebook.stopEditingCell(); await app.workbench.notebook.deleteActiveCell(); - await app.workbench.notebook.editCell(); - await app.workbench.notebook.waitForTypeInEditor('## hello2!'); + await app.workbench.notebook.waitForActiveCellEditorContents('# added cell'); }); it.skip('moves focus in and out of output', async function () { // TODO@rebornix https://github.com/microsoft/vscode/issues/139270 diff --git a/test/smoke/src/areas/search/search.test.ts b/test/smoke/src/areas/search/search.test.ts index f635ad827df..40db1cb07c0 100644 --- a/test/smoke/src/areas/search/search.test.ts +++ b/test/smoke/src/areas/search/search.test.ts @@ -15,6 +15,7 @@ export function setup(logger: Logger) { after(function () { const app = this.app as Application; + retry(async () => cp.execSync('git checkout . --quiet', { cwd: app.workspacePathOrFolder }), 0, 5); retry(async () => cp.execSync('git reset --hard HEAD --quiet', { cwd: app.workspacePathOrFolder }), 0, 5); }); diff --git a/test/smoke/src/areas/statusbar/statusbar.test.ts b/test/smoke/src/areas/statusbar/statusbar.test.ts index ccfbeb5772f..edf594ad7e9 100644 --- a/test/smoke/src/areas/statusbar/statusbar.test.ts +++ b/test/smoke/src/areas/statusbar/statusbar.test.ts @@ -15,6 +15,7 @@ export function setup(logger: Logger) { it('verifies presence of all default status bar elements', async function () { const app = this.app as Application; + await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.BRANCH_STATUS); await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.SYNC_STATUS); await app.workbench.statusbar.waitForStatusbarElement(StatusBarElement.PROBLEMS_STATUS); @@ -29,6 +30,7 @@ export function setup(logger: Logger) { it(`verifies that 'quick input' opens when clicking on status bar elements`, async function () { const app = this.app as Application; + await app.workbench.statusbar.clickOn(StatusBarElement.BRANCH_STATUS); await app.workbench.quickinput.waitForQuickInputOpened(); await app.workbench.quickinput.closeQuickInput(); @@ -56,6 +58,7 @@ export function setup(logger: Logger) { it(`verifies if changing EOL is reflected in the status bar`, async function () { const app = this.app as Application; + await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'readme.md')); await app.workbench.statusbar.clickOn(StatusBarElement.EOL_STATUS); diff --git a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts index 7443c02b30a..c6d4a89c496 100644 --- a/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts +++ b/test/smoke/src/areas/terminal/terminal-shellIntegration.test.ts @@ -101,7 +101,7 @@ export function setup(options?: { skipSuite: boolean }) { // Use the simplest profile to get as little process interaction as possible await terminal.createEmptyTerminal(); // Erase all content and reset cursor to top - await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('H')}`); + await terminal.runCommandWithValue(TerminalCommandIdWithValue.WriteDataToTerminal, `${csi('2J')}${csi('3J')}${csi('H')}`); } describe('VS Code sequences', () => { diff --git a/test/smoke/src/areas/workbench/data-loss.test.ts b/test/smoke/src/areas/workbench/data-loss.test.ts index 3e27f1acba9..3fb16b61c74 100644 --- a/test/smoke/src/areas/workbench/data-loss.test.ts +++ b/test/smoke/src/areas/workbench/data-loss.test.ts @@ -27,6 +27,7 @@ export function setup(ensureStableCode: () => { stableCodePath: string | undefin }); await app.start(); + // Open 3 editors await app.workbench.quickaccess.openFile(join(app.workspacePathOrFolder, 'bin', 'www')); await app.workbench.quickaccess.runCommand('View: Keep Editor'); diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index ffb09e1217d..f82e297eaf5 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -8,7 +8,7 @@ import { gracefulify } from 'graceful-fs'; import * as cp from 'child_process'; import * as path from 'path'; import * as os from 'os'; -import * as minimist from 'minimist'; +import minimist from 'minimist'; import * as vscodetest from '@vscode/test-electron'; import fetch from 'node-fetch'; import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion, ApplicationOptions } from '../../automation'; @@ -27,6 +27,8 @@ import { setup as setupLaunchTests } from './areas/workbench/launch.test'; import { setup as setupTerminalTests } from './areas/terminal/terminal.test'; import { setup as setupTaskTests } from './areas/task/task.test'; import { setup as setupChatTests } from './areas/chat/chat.test'; +import { setup as setupChatAnonymousTests } from './areas/chat/chatAnonymous.test'; +import { setup as setupAccessibilityTests } from './areas/accessibility/accessibility.test'; const rootPath = path.join(__dirname, '..', '..', '..'); @@ -405,4 +407,6 @@ describe(`VSCode Smoke Tests (${opts.web ? 'Web' : 'Electron'})`, () => { if (!opts.web && !opts.remote && quality !== Quality.Dev && quality !== Quality.OSS) { setupLocalizationTests(logger); } if (!opts.web && !opts.remote) { setupLaunchTests(logger); } if (!opts.web) { setupChatTests(logger); } + if (!opts.web && quality === Quality.Insiders) { setupChatAnonymousTests(logger); } + setupAccessibilityTests(logger, opts); }); diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index ee1ac564720..4cc13e41476 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -65,7 +65,9 @@ mocha.run(failures => { ################################################################### # # # Logs are attached as build artefact and can be downloaded # -# from the build Summary page (Summary -> Artifacts) # +# from the build Summary page: # +# - click on "Summary" in the top left corner # +# - scroll all the way down to "Artifacts" # # # # Please also scan through attached crash logs in case the # # failure was caused by a native crash. # diff --git a/test/smoke/tsconfig.json b/test/smoke/tsconfig.json index 0d77fe42693..74fa2635bec 100644 --- a/test/smoke/tsconfig.json +++ b/test/smoke/tsconfig.json @@ -8,6 +8,7 @@ "strict": true, "noUnusedParameters": false, "noUnusedLocals": true, + "rootDir": "./src", "outDir": "out", "sourceMap": true, "skipLibCheck": true, diff --git a/test/unit/electron/renderer.js b/test/unit/electron/renderer.js index 23f66cc2fe5..718cf9dd8d7 100644 --- a/test/unit/electron/renderer.js +++ b/test/unit/electron/renderer.js @@ -304,7 +304,7 @@ async function loadTests(opts) { const msg = []; for (const error of errors) { console.error(`Error: Test run should not have unexpected errors:\n${error}`); - msg.push(String(error)) + msg.push(String(error)); } assert.ok(false, `Error: Test run should not have unexpected errors:\n${msg.join('\n')}`); } @@ -464,7 +464,7 @@ async function runTests(opts) { await loadTests(opts); const runner = mocha.run(async () => { - await createCoverageReport(opts) + await createCoverageReport(opts); ipcRenderer.send('all done'); }); diff --git a/vscode-sync-report-20260201-210437.txt b/vscode-sync-report-20260201-210437.txt new file mode 100644 index 00000000000..61c1ff0c7b5 --- /dev/null +++ b/vscode-sync-report-20260201-210437.txt @@ -0,0 +1,99 @@ + +═══════════════════════════════════════════════════════════ + VS Code Sync Comparison Report + Generated: Sun Feb 1 21:04:39 GMT 2026 +═══════════════════════════════════════════════════════════ + +Current State: + Branch: main + Commit: 079043ba5634493a21a535f53173e21822fe7859 + Merge pull request #43 from OpenCortexIDE/fix/streaming-freeze-local-providers + +VS Code State: + Commit: 77c18b8431d9f97675e98aa8eaeaacb12181a900 + Merge pull request #292151 from microsoft/tyriar/291991 + +Common Ancestor (Merge Base): + Commit: ac4cbdf48759c7d8c3eb91ffe6bb04316e263c57 + Ignore obsolete chat content part type Fix #276094 + Date: 2025-11-11 15:25:32 +0100 + +Divergence Analysis: + Your commits ahead of merge base: 150 + VS Code commits ahead of merge base: 4558 + +Analyzing potential conflicts... + +Files changed in your branch (since merge base): + Total files modified: 335 + +Files changed in VS Code (since merge base): + Total files modified: 2994 + +Potential Conflict Analysis: + Files modified in both branches: 66 + ⚠️ These files may have conflicts: + .github/workflows/pr-darwin-test.yml + .github/workflows/pr-linux-cli-test.yml + .github/workflows/pr-linux-test.yml + .github/workflows/pr-node-modules.yml + .github/workflows/pr-win32-test.yml + .github/workflows/pr.yml + .gitignore + README.md + build/azure-pipelines/linux/setup-env.sh + build/lib/extensions.js + build/lib/extensions.ts + build/lib/inlineMeta.js + build/lib/preLaunch.js + build/lib/preLaunch.ts + build/lib/stylelint/validateVariableNames.js + build/lib/stylelint/validateVariableNames.ts + build/lib/stylelint/vscode-known-variables.json + build/lib/tsb/index.js + build/lib/tsb/index.ts + build/lib/util.js + ... and 46 more files + +CortexIDE-Specific Files (should be safe): + CortexIDE-specific files modified: 199 + +Your Recent Commits (last 10): + 079043ba563 Merge pull request #43 from OpenCortexIDE/fix/streaming-freeze-local-providers + fb86cd5bff1 Bump cortexVersion to 0.0.10 + afaa4a75638 Theme: make chat panel follow selected theme via void var overrides + d497ff6c450 Theme: respect user theme, rounded logo, chat theme vars, CortexIDE Dark theme + 38e44ffcd1b fix: theme switch crash/error when switching theme (e.g. Solarized Dark) + 2d844081f97 fix: streaming freeze on local providers; Windows update preserve user task choices + 980a4bc8c20 Merge pull request #41 from OpenCortexIDE/fix/secret-detection-path-false-positive + abf0a6f9b0a fix: stop falsely redacting paths as AWS Secret Key in system message + 9c96c9c541c Merge pull request #40 from OpenCortexIDE/feature/pollinations-provider + 4dd73771892 fix: Vertex AI empty content error and chat error recovery + +VS Code Recent Commits (last 10): + 77c18b8431d Merge pull request #292151 from microsoft/tyriar/291991 + 377e7d295fe Fix Monaco scroll offset buffer use after destroy + 084facbe335 agent sessions - only render non-default session icons (#292116) + c6cdeb142a2 fix bad styling in attachments for images (#292099) + e6f0ffde9cd Merge pull request #291937 from mjbvz/dev/mjbvz/severe-buzzard + 984af6f3a8f Better wording for Claude agent description (#292076) + 306d939cf91 Fix panel alignment command breaking with maximized auxiliary bar (#292022) + b31c729a39e Add smoke test for anonymous chat access (#291953) + d21910006a9 agent sessions - restore ability to hide sidebar when chat maximised (#292048) + 00e0d96a9cd Merge pull request #292063 from microsoft/tyriar/291952 + +═══════════════════════════════════════════════════════════ + Sync Recommendations +═══════════════════════════════════════════════════════════ + +⚠ CAUTION: Potential conflicts detected + 66 files were modified in both branches. + Review the files listed above before syncing. + + RECOMMENDED APPROACH: + 1. Create a test branch: git checkout -b test-sync-vscode + 2. Try merging: git merge vscode/main + 3. Resolve any conflicts + 4. Test thoroughly + 5. If successful, merge test branch back to main +